9강. 회원가입 과제 구현

0. 학습 목표
→ 8강 회원가입 과제를 실제 코드로 구현하고, 회원가입부터 로그인, 프로필 수정까지 전체 흐름을 확인합니다.
이번 글에서 다룰 내용
이번 글에서는 8강 과제였던 회원가입 기능을 실제 코드로 구현합니다.
회원가입 View에서 새 회원 정보를 입력하면, 비밀번호는 그대로 저장하지 않고 password_hash로 변환해 저장합니다.
또한 회원 정보를 한 명만 저장하는 구조가 아니라, members 리스트에 여러 회원을 저장하는 구조로 만듭니다.

| 구분 | 내용 |
| 핵심 개념 | 회원가입 정보를 member.json에 저장하고, 비밀번호는 password_hash로 관리합니다. |
| 구현 파일 | main.py, password_utils.py, member_storage.py, member_model.py, tab_window.py |
| 최종 목표 | 회원가입한 계정으로 로그인하고, 현재 로그인한 회원의 프로필만 수정되도록 구현합니다. |
이번 단계의 핵심: View는 입력과 출력만 담당하고, 회원가입·로그인·프로필 수정 처리는 MemberModel이 담당해야 합니다.
1. 완성 프로젝트 구조 확인하기
→ 구현을 시작하기 전에 필요한 파일과 역할을 먼저 확인합니다.
1.1 프로젝트 구조
이번 구현에서는 파일을 5개로 나눕니다.
화면, 모델, 저장소, 비밀번호 해시 기능을 분리하면 코드 역할이 더 명확해집니다.
# 프로젝트 구조
signup_login_hash_project/
├── main.py
├── password_utils.py
├── member_storage.py
├── member_model.py
└── tab_window.py
| 파일 | 역할 |
| main.py | QApplication을 만들고 프로그램을 실행합니다. |
| password_utils.py | 비밀번호를 해시값으로 변환합니다. |
| member_storage.py | member.json 파일을 저장하고 불러옵니다. |
| member_model.py | 회원가입, 로그인, 프로필 수정을 처리합니다. |
| tab_window.py | QTabWidget과 각 View 화면을 구성합니다. |
1.2 최종 member.json 구조
회원 정보는 한 명의 딕셔너리로 저장하지 않습니다.
여러 회원을 저장할 수 있도록 members 리스트 구조를 사용합니다.
# member.json 예시
{
"members": [
{
"id": "test",
"password_hash": "03ac674216f3e15c...",
"name": "홍길동",
"email": "test@test.com",
"phone": "010-1234-5678"
},
{
"id": "kim",
"password_hash": "a8b3f2...",
"name": "김철수",
"email": "kim@test.com",
"phone": "010-9999-8888"
}
]
}
핵심 확인: 회원가입을 하면 새 회원 딕셔너리가 members 리스트에 추가되어야 합니다.
2. 비밀번호 해시 함수 구현하기
→ 비밀번호를 그대로 저장하지 않기 위해 hash_password() 함수를 먼저 만듭니다.
2.1 password_utils.py 작성하기
먼저 password_utils.py 파일을 만듭니다.
이 파일에는 비밀번호를 해시값으로 바꾸는 함수만 넣습니다.
import hashlib
def hash_password(password):
"""입력한 비밀번호를 SHA-256 해시 문자열로 변환합니다."""
return hashlib.sha256(password.encode("utf-8")).hexdigest()
2.2 코드 흐름
비밀번호가 1234라면, 그대로 저장하지 않고 해시값으로 바꿉니다.
# 비밀번호 해시 변환 흐름
"1234"
↓
hash_password()
↓
"03ac674216f3e15c..."
| 코드 | 설명 |
| encode("utf-8") | 문자열 비밀번호를 바이트 데이터로 바꿉니다. |
| hashlib.sha256() | SHA-256 방식으로 해시값을 계산합니다. |
| hexdigest() | 해시 결과를 문자열 형태로 반환합니다. |
주의할 점: 이번 예제의 SHA-256은 학습용입니다. 실제 서비스에서는 salt와 비밀번호 저장 전용 해시 알고리즘을 사용해야 합니다.
3. 회원 정보 저장소 구현하기
→ member.json 파일을 저장하고 불러오는 MemberStorage를 만듭니다.
3.1 member_storage.py 작성하기
이제 member_storage.py 파일을 만듭니다.
MemberStorage는 화면을 모르고, 오직 파일 저장과 불러오기만 담당합니다.
import json
from pathlib import Path
class MemberStorage:
"""회원 목록을 member.json 파일에 저장하고 불러오는 클래스"""
def __init__(self, file_path="member.json"):
self.file_path = Path(file_path)
def default_data(self):
return {
"members": []
}
def load_data(self):
if not self.file_path.exists():
data = self.default_data()
self.save_data(data)
return data
try:
with self.file_path.open("r", encoding="utf-8") as file:
data = json.load(file)
except json.JSONDecodeError:
data = self.default_data()
self.save_data(data)
return data
if "members" not in data:
data = self.default_data()
self.save_data(data)
return data
def save_data(self, data):
with self.file_path.open("w", encoding="utf-8") as file:
json.dump(data, file, ensure_ascii=False, indent=4)
3.2 MemberStorage 코드 역할
| 메서드 | 역할 |
| default_data() | member.json이 없을 때 사용할 기본 구조를 만듭니다. |
| load_data() | member.json 파일에서 회원 목록을 불러옵니다. |
| save_data() | 회원 목록을 member.json 파일에 저장합니다. |
# 처음 실행 시 생성되는 member.json
{
"members": []
}
핵심 확인: 회원가입을 하기 전에는 members 리스트가 비어 있고, 회원가입을 하면 이 리스트에 회원 딕셔너리가 추가됩니다.
4. 회원 Model 구현하기
→ 회원가입, 로그인, 프로필 수정 기능을 담당하는 MemberModel을 구현합니다.

4.1 member_model.py 작성하기
MemberModel은 이번 과제의 핵심입니다.
회원가입 View, 로그인 View, 메인화면 View, 프로필 View는 모두 같은 MemberModel을 사용합니다.
from member_storage import MemberStorage
from password_utils import hash_password
class MemberModel:
"""회원가입, 로그인, 프로필 수정을 담당하는 Model"""
def __init__(self):
self.storage = MemberStorage()
self.data = self.storage.load_data()
self.current_member_id = None
def is_duplicate_id(self, user_id):
for member in self.data["members"]:
if member["id"] == user_id:
return True
return False
def register_member(self, user_id, password, password_confirm, name, email, phone):
if not user_id or not password or not password_confirm or not name or not email or not phone:
return False, "모든 값을 입력하세요."
if password != password_confirm:
return False, "비밀번호가 서로 다릅니다."
if self.is_duplicate_id(user_id):
return False, "이미 사용 중인 아이디입니다."
new_member = {
"id": user_id,
"password_hash": hash_password(password),
"name": name,
"email": email,
"phone": phone,
}
self.data["members"].append(new_member)
self.storage.save_data(self.data)
return True, "회원가입이 완료되었습니다."
def check_login(self, user_id, password):
input_password_hash = hash_password(password)
for member in self.data["members"]:
if member["id"] == user_id and member["password_hash"] == input_password_hash:
self.current_member_id = user_id
return True
return False
def get_current_member(self):
if self.current_member_id is None:
return None
for member in self.data["members"]:
if member["id"] == self.current_member_id:
return member
return None
def get_member_info(self):
member = self.get_current_member()
if member is None:
return None
return {
"id": member["id"],
"name": member["name"],
"email": member["email"],
"phone": member["phone"],
}
def update_profile(self, name, email, phone):
member = self.get_current_member()
if member is None:
return False
member["name"] = name
member["email"] = email
member["phone"] = phone
self.storage.save_data(self.data)
return True
4.2 회원가입 처리 흐름
register_member()는 회원가입 버튼을 눌렀을 때 호출됩니다.
# register_member() 실행 흐름
빈 값 검사
↓
비밀번호 확인 검사
↓
아이디 중복 검사
↓
비밀번호 해시 변환
↓
members 리스트에 새 회원 추가
↓
member.json 파일에 저장
새 회원을 만들 때 비밀번호는 password_hash로 저장합니다.
new_member = {
"id": user_id,
"password_hash": hash_password(password),
"name": name,
"email": email,
"phone": phone,
}
4.3 로그인 처리 흐름
check_login()은 입력한 ID와 PW가 members 목록 안의 회원 정보와 일치하는지 확인합니다.
input_password_hash = hash_password(password)
사용자가 입력한 비밀번호를 먼저 해시로 바꿉니다.
if member["id"] == user_id and member["password_hash"] == input_password_hash:
self.current_member_id = user_id
return True
ID와 password_hash가 모두 일치하면 로그인 성공입니다.
그리고 current_member_id에 현재 로그인한 회원의 ID를 저장합니다.
4.4 프로필 수정 흐름
프로필 수정은 전체 회원이 아니라 현재 로그인한 회원에게만 적용되어야 합니다.
member = self.get_current_member()
먼저 현재 로그인한 회원을 찾습니다.
member["name"] = name
member["email"] = email
member["phone"] = phone
self.storage.save_data(self.data)
그 회원의 이름, 이메일, 전화번호만 수정하고 다시 파일에 저장합니다.
핵심 확인: current_member_id를 저장해야 로그인한 회원이 누구인지 알 수 있고, 프로필 수정도 그 회원에게만 적용할 수 있습니다.
5. 화면 코드 구현하기
→ QTabWidget에 로그인, 회원가입, 메인화면, 프로필 View를 연결합니다.
5.1 tab_window.py 전체 코드
이제 화면을 구성하는 tab_window.py를 작성합니다.
코드가 길지만 View별 역할을 나누어 보면 어렵지 않습니다.
from PySide6.QtWidgets import (
QWidget,
QTabWidget,
QLabel,
QLineEdit,
QPushButton,
QVBoxLayout,
QHBoxLayout,
)
class LoginView(QWidget):
def __init__(self, member_model, tab_widget, main_view, profile_view):
super().__init__()
self.member_model = member_model
self.tab_widget = tab_widget
self.main_view = main_view
self.profile_view = profile_view
self.id_input = QLineEdit()
self.id_input.setPlaceholderText("아이디를 입력하세요")
self.pw_input = QLineEdit()
self.pw_input.setPlaceholderText("비밀번호를 입력하세요")
self.pw_input.setEchoMode(QLineEdit.EchoMode.Password)
self.login_button = QPushButton("로그인")
self.login_button.clicked.connect(self.login)
self.message_label = QLabel("로그인 정보를 입력하세요.")
layout = QVBoxLayout()
layout.addWidget(QLabel("[로그인]"))
layout.addWidget(QLabel("아이디"))
layout.addWidget(self.id_input)
layout.addWidget(QLabel("비밀번호"))
layout.addWidget(self.pw_input)
layout.addWidget(self.login_button)
layout.addWidget(self.message_label)
self.setLayout(layout)
def login(self):
user_id = self.id_input.text()
password = self.pw_input.text()
if self.member_model.check_login(user_id, password):
self.message_label.setText("로그인 성공!")
self.main_view.refresh()
self.profile_view.refresh()
self.tab_widget.setCurrentIndex(2)
else:
self.message_label.setText("로그인 실패! 아이디 또는 비밀번호를 확인하세요.")
class SignupView(QWidget):
def __init__(self, member_model, tab_widget):
super().__init__()
self.member_model = member_model
self.tab_widget = tab_widget
self.id_input = QLineEdit()
self.id_input.setPlaceholderText("아이디")
self.pw_input = QLineEdit()
self.pw_input.setPlaceholderText("비밀번호")
self.pw_input.setEchoMode(QLineEdit.EchoMode.Password)
self.pw_confirm_input = QLineEdit()
self.pw_confirm_input.setPlaceholderText("비밀번호 확인")
self.pw_confirm_input.setEchoMode(QLineEdit.EchoMode.Password)
self.name_input = QLineEdit()
self.name_input.setPlaceholderText("이름")
self.email_input = QLineEdit()
self.email_input.setPlaceholderText("이메일")
self.phone_input = QLineEdit()
self.phone_input.setPlaceholderText("전화번호")
self.signup_button = QPushButton("회원가입")
self.signup_button.clicked.connect(self.signup)
self.message_label = QLabel("회원가입 정보를 입력하세요.")
layout = QVBoxLayout()
layout.addWidget(QLabel("[회원가입]"))
layout.addWidget(QLabel("아이디"))
layout.addWidget(self.id_input)
layout.addWidget(QLabel("비밀번호"))
layout.addWidget(self.pw_input)
layout.addWidget(QLabel("비밀번호 확인"))
layout.addWidget(self.pw_confirm_input)
layout.addWidget(QLabel("이름"))
layout.addWidget(self.name_input)
layout.addWidget(QLabel("이메일"))
layout.addWidget(self.email_input)
layout.addWidget(QLabel("전화번호"))
layout.addWidget(self.phone_input)
layout.addWidget(self.signup_button)
layout.addWidget(self.message_label)
self.setLayout(layout)
def signup(self):
user_id = self.id_input.text()
password = self.pw_input.text()
password_confirm = self.pw_confirm_input.text()
name = self.name_input.text()
email = self.email_input.text()
phone = self.phone_input.text()
success, message = self.member_model.register_member(
user_id,
password,
password_confirm,
name,
email,
phone,
)
self.message_label.setText(message)
if success:
self.clear_inputs()
self.tab_widget.setCurrentIndex(0)
def clear_inputs(self):
self.id_input.clear()
self.pw_input.clear()
self.pw_confirm_input.clear()
self.name_input.clear()
self.email_input.clear()
self.phone_input.clear()
class MainView(QWidget):
def __init__(self, member_model):
super().__init__()
self.member_model = member_model
self.title_label = QLabel("[메인화면] 로그인한 회원 정보")
self.id_label = QLabel("아이디:")
self.name_label = QLabel("이름:")
self.email_label = QLabel("이메일:")
self.phone_label = QLabel("전화번호:")
layout = QVBoxLayout()
layout.addWidget(self.title_label)
layout.addWidget(self.id_label)
layout.addWidget(self.name_label)
layout.addWidget(self.email_label)
layout.addWidget(self.phone_label)
self.setLayout(layout)
def refresh(self):
member = self.member_model.get_member_info()
if member is None:
return
self.id_label.setText(f"아이디: {member['id']}")
self.name_label.setText(f"이름: {member['name']}")
self.email_label.setText(f"이메일: {member['email']}")
self.phone_label.setText(f"전화번호: {member['phone']}")
class ProfileView(QWidget):
def __init__(self, member_model, main_view):
super().__init__()
self.member_model = member_model
self.main_view = main_view
self.id_label = QLabel("아이디:")
self.name_input = QLineEdit()
self.email_input = QLineEdit()
self.phone_input = QLineEdit()
self.update_button = QPushButton("수정하기")
self.update_button.clicked.connect(self.update_profile)
self.message_label = QLabel("프로필 정보를 확인하거나 수정하세요.")
layout = QVBoxLayout()
layout.addWidget(QLabel("[프로필]"))
layout.addWidget(self.id_label)
name_layout = QHBoxLayout()
name_layout.addWidget(QLabel("이름"))
name_layout.addWidget(self.name_input)
email_layout = QHBoxLayout()
email_layout.addWidget(QLabel("이메일"))
email_layout.addWidget(self.email_input)
phone_layout = QHBoxLayout()
phone_layout.addWidget(QLabel("전화번호"))
phone_layout.addWidget(self.phone_input)
layout.addLayout(name_layout)
layout.addLayout(email_layout)
layout.addLayout(phone_layout)
layout.addWidget(self.update_button)
layout.addWidget(self.message_label)
self.setLayout(layout)
def refresh(self):
member = self.member_model.get_member_info()
if member is None:
return
self.id_label.setText(f"아이디: {member['id']}")
self.name_input.setText(member["name"])
self.email_input.setText(member["email"])
self.phone_input.setText(member["phone"])
def update_profile(self):
name = self.name_input.text()
email = self.email_input.text()
phone = self.phone_input.text()
success = self.member_model.update_profile(name, email, phone)
if success:
self.refresh()
self.main_view.refresh()
self.message_label.setText("프로필 정보가 수정되었습니다.")
else:
self.message_label.setText("프로필 정보를 수정할 수 없습니다.")
class TabWindow(QWidget):
def __init__(self, member_model):
super().__init__()
self.setWindowTitle("회원가입 구현 과제 정답")
self.resize(460, 420)
self.member_model = member_model
self.tab_widget = QTabWidget()
self.main_view = MainView(self.member_model)
self.profile_view = ProfileView(self.member_model, self.main_view)
self.login_view = LoginView(
self.member_model,
self.tab_widget,
self.main_view,
self.profile_view,
)
self.signup_view = SignupView(self.member_model, self.tab_widget)
self.tab_widget.addTab(self.login_view, "로그인")
self.tab_widget.addTab(self.signup_view, "회원가입")
self.tab_widget.addTab(self.main_view, "메인화면")
self.tab_widget.addTab(self.profile_view, "프로필")
layout = QVBoxLayout()
layout.addWidget(self.tab_widget)
self.setLayout(layout)
5.2 탭 인덱스 확인하기
이번 프로그램의 탭 순서는 아래와 같습니다.
| 탭 인덱스 | 탭 이름 |
| 0 | 로그인 |
| 1 | 회원가입 |
| 2 | 메인화면 |
| 3 | 프로필 |
회원가입 성공 후에는 로그인 탭으로 이동합니다.
self.tab_widget.setCurrentIndex(0)
로그인 성공 후에는 메인화면 탭으로 이동합니다.
self.tab_widget.setCurrentIndex(2)
주의할 점: 탭 순서가 바뀌면 setCurrentIndex()의 숫자도 함께 바뀌어야 합니다.
6. main.py에서 프로그램 실행하기
→ QApplication을 만들고 TabWindow를 실행합니다.
6.1 main.py 작성하기
마지막으로 main.py를 작성합니다.
import sys
from PySide6.QtWidgets import QApplication
from member_model import MemberModel
from tab_window import TabWindow
app = QApplication(sys.argv)
member_model = MemberModel()
window = TabWindow(member_model)
window.show()
sys.exit(app.exec())
6.2 실행 방법
터미널에서 프로젝트 폴더로 이동한 뒤 아래 명령어를 실행합니다.
python main.py
처음 실행하면 프로젝트 폴더 안에 member.json 파일이 자동으로 생성됩니다.
# 처음 생성되는 member.json
{
"members": []
}
확인: member.json 파일이 생성되지 않는다면 프로젝트 실행 위치와 파일 쓰기 권한을 확인해야 합니다.
7. 실행 결과 확인하기
→ 회원가입, 로그인, 프로필 수정 결과가 올바르게 동작하는지 확인합니다.

7.1 회원가입 성공 테스트
회원가입 탭에서 아래처럼 입력합니다.
| 항목 | 입력값 |
| 아이디 | test |
| 비밀번호 | 1234 |
| 비밀번호 확인 | 1234 |
| 이름 | 홍길동 |
| 이메일 | test@test.com |
| 전화번호 | 010-1234-5678 |
회원가입에 성공하면 로그인 탭으로 이동합니다.
member.json 파일을 열어 보면 아래처럼 새 회원이 추가되어 있어야 합니다.
# 회원가입 후 member.json 예시
{
"members": [
{
"id": "test",
"password_hash": "03ac674216f3e15c...",
"name": "홍길동",
"email": "test@test.com",
"phone": "010-1234-5678"
}
]
}
7.2 회원가입 실패 테스트
아래 상황에서는 회원가입이 실패해야 합니다.
| 상황 | 출력 메시지 |
| 빈 값이 있음 | 모든 값을 입력하세요. |
| 비밀번호와 비밀번호 확인이 다름 | 비밀번호가 서로 다릅니다. |
| 이미 사용 중인 아이디 | 이미 사용 중인 아이디입니다. |
7.3 로그인 성공 테스트
로그인 탭에서 회원가입한 계정으로 로그인합니다.
| 항목 | 입력값 |
| 아이디 | test |
| 비밀번호 | 1234 |
로그인에 성공하면 메인화면 탭으로 이동합니다.
# 메인화면 출력 예시
[메인화면] 로그인한 회원 정보
아이디: test
이름: 홍길동
이메일: test@test.com
전화번호: 010-1234-5678
7.4 프로필 수정 테스트
프로필 탭에서 이름, 이메일, 전화번호를 수정합니다.
# 수정 입력 예시
이름: 김철수
이메일: kim@test.com
전화번호: 010-9999-8888
수정하기 버튼을 누르면 현재 로그인한 회원 정보만 수정되어야 합니다.
수정 후 member.json은 아래처럼 바뀝니다.
# 프로필 수정 후 member.json 예시
{
"members": [
{
"id": "test",
"password_hash": "03ac674216f3e15c...",
"name": "김철수",
"email": "kim@test.com",
"phone": "010-9999-8888"
}
]
}
핵심 확인: 여러 회원이 저장되어 있어도 프로필 수정은 현재 로그인한 회원에게만 적용되어야 합니다.
8. 기존 방식과 새 방식 비교하며 정리하기
→ 회원 1명 저장 방식과 여러 회원 가입 방식의 차이를 정리합니다.
8.1 기존 방식
기존 방식은 미리 만들어진 회원 1명만 사용하는 구조였습니다.
# 기존 방식
member.json
└── 회원 1명 정보만 저장
로그인
└── 미리 저장된 회원만 로그인 가능
8.2 새 방식
새 방식은 회원가입을 통해 여러 회원을 추가할 수 있는 구조입니다.
# 새 방식
member.json
└── members 리스트에 여러 회원 저장
회원가입
└── 새 회원 추가
로그인
└── members 목록에서 일치하는 회원 찾기
프로필 수정
└── 현재 로그인한 회원만 수정
| 구분 | 기존 방식 | 새 방식 |
| 회원 수 | 회원 1명 중심 | 여러 회원 저장 가능 |
| 회원 추가 | 코드 또는 파일을 직접 수정해야 함 | 회원가입 View에서 추가 가능 |
| 비밀번호 저장 | pw 또는 password_hash 구조를 직접 준비 | 회원가입 시 password_hash로 자동 저장 |
| 프로필 수정 | 회원 1명만 고려 | 현재 로그인한 회원만 찾아서 수정 |
8.3 최종 정리
이번 글에서 구현한 내용을 정리하면 다음과 같습니다.
| 핵심 내용 | 정리 |
| 회원가입 View | 새 회원 정보를 입력받습니다. |
| MemberModel | 회원가입, 로그인, 프로필 수정을 처리합니다. |
| MemberStorage | members 목록을 member.json 파일에 저장합니다. |
| password_hash | 비밀번호를 그대로 저장하지 않고 해시값으로 저장합니다. |
| current_member_id | 현재 로그인한 회원을 구분하는 기준입니다. |
기억할 문장: 회원가입은 members 목록에 새 회원을 추가하는 기능이고, 로그인과 프로필 수정은 현재 로그인한 회원을 기준으로 처리해야 합니다.
8.4 프로젝트 파일
참고. 공식 문서로 확인하기
→ PySide6와 Python 파일 저장, 해시 기능의 공식 문서를 확인합니다.
참고 문서
이번 글에서 사용한 주요 기능은 아래 공식 문서에서 확인할 수 있습니다.
- PySide6 QTabWidget 공식 문서
- PySide6 QLineEdit 공식 문서
- Python json 공식 문서
- Python hashlib 공식 문서
- Python pathlib 공식 문서
참고: 이번 강의에서는 학습을 위해 SHA-256을 사용했습니다. 실제 서비스에서는 salt와 bcrypt, scrypt, argon2 같은 비밀번호 저장 전용 방식을 사용하는 것이 좋습니다.