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을 구현합니다.

더보기
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 프로젝트  파일

659.zip
0.01MB

 

참고. 공식 문서로 확인하기

→ PySide6와 Python 파일 저장, 해시 기능의 공식 문서를 확인합니다.

더보기

참고 문서

이번 글에서 사용한 주요 기능은 아래 공식 문서에서 확인할 수 있습니다.

참고: 이번 강의에서는 학습을 위해 SHA-256을 사용했습니다. 실제 서비스에서는 salt와 bcrypt, scrypt, argon2 같은 비밀번호 저장 전용 방식을 사용하는 것이 좋습니다.