<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>BasicLike</title>
    <link>https://basiclike.tistory.com/</link>
    <description>어? 나 프로그래밍 좋아하네?</description>
    <language>ko</language>
    <pubDate>Fri, 5 Jun 2026 13:39:20 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>BasicLike</managingEditor>
    <image>
      <title>BasicLike</title>
      <url>https://tistory1.daumcdn.net/tistory/5953506/attach/07d087d2509e41689b1d18a2eb4a1dd3</url>
      <link>https://basiclike.tistory.com</link>
    </image>
    <item>
      <title>9강. 회원가입 과제 구현</title>
      <link>https://basiclike.tistory.com/786</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;436&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Z7PWW/dJMcabRTzds/AHqnqAjxZDW64iAXzUiOZK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Z7PWW/dJMcabRTzds/AHqnqAjxZDW64iAXzUiOZK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Z7PWW/dJMcabRTzds/AHqnqAjxZDW64iAXzUiOZK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FZ7PWW%2FdJMcabRTzds%2FAHqnqAjxZDW64iAXzUiOZK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;436&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;436&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h1&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;0.&lt;/span&gt; 학습 목표&lt;/h1&gt;
&lt;p style=&quot;color: #9d9d9d;&quot; data-ke-size=&quot;size14&quot;&gt;&amp;rarr; 8강 회원가입 과제를 실제 코드로 구현하고, 회원가입부터 로그인, 프로필 수정까지 전체 흐름을 확인합니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot; data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;이번 글에서 다룰 내용&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글에서는 8강 과제였던 &lt;b&gt;회원가입 기능&lt;/b&gt;을 실제 코드로 구현합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회원가입 View에서 새 회원 정보를 입력하면, 비밀번호는 그대로 저장하지 않고 &lt;b&gt;password_hash&lt;/b&gt;로 변환해 저장합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 회원 정보를 한 명만 저장하는 구조가 아니라, &lt;b&gt;members 리스트&lt;/b&gt;에 여러 회원을 저장하는 구조로 만듭니다.&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1491&quot; data-origin-height=&quot;1055&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cnDPKR/dJMcag6LXUd/gyEIQxdFsbzi7wwbOlpKy0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cnDPKR/dJMcag6LXUd/gyEIQxdFsbzi7wwbOlpKy0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cnDPKR/dJMcag6LXUd/gyEIQxdFsbzi7wwbOlpKy0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcnDPKR%2FdJMcag6LXUd%2FgyEIQxdFsbzi7wwbOlpKy0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1491&quot; height=&quot;1055&quot; data-origin-width=&quot;1491&quot; data-origin-height=&quot;1055&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;table style=&quot;border-collapse: collapse; width: 100%; margin-top: 15px; margin-bottom: 20px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;background-color: #f5f8ff;&quot;&gt;
&lt;td style=&quot;width: 21.5648%; padding: 10px;&quot;&gt;&lt;b&gt;구분&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 78.4352%; padding: 10px;&quot;&gt;&lt;b&gt;내용&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px; width: 21.5648%;&quot;&gt;핵심 개념&lt;/td&gt;
&lt;td style=&quot;padding: 10px; width: 78.4352%;&quot;&gt;회원가입 정보를 member.json에 저장하고, 비밀번호는 password_hash로 관리합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px; width: 21.5648%;&quot;&gt;구현 파일&lt;/td&gt;
&lt;td style=&quot;padding: 10px; width: 78.4352%;&quot;&gt;main.py, password_utils.py, member_storage.py, member_model.py, tab_window.py&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px; width: 21.5648%;&quot;&gt;최종 목표&lt;/td&gt;
&lt;td style=&quot;padding: 10px; width: 78.4352%;&quot;&gt;회원가입한 계정으로 로그인하고, 현재 로그인한 회원의 프로필만 수정되도록 구현합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;div style=&quot;background-color: #f8fbff; border: 1px solid #d8e8ff; border-radius: 10px; padding: 18px; margin: 25px 0;&quot;&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;이번 단계의 핵심:&lt;/b&gt; View는 입력과 출력만 담당하고, 회원가입&amp;middot;로그인&amp;middot;프로필 수정 처리는 MemberModel이 담당해야 합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h1&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;1.&lt;/span&gt; 완성 프로젝트 구조 확인하기&lt;/h1&gt;
&lt;p style=&quot;color: #9d9d9d;&quot; data-ke-size=&quot;size14&quot;&gt;&amp;rarr; 구현을 시작하기 전에 필요한 파일과 역할을 먼저 확인합니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot; data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1.1 프로젝트 구조&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 구현에서는 파일을 5개로 나눕니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;화면, 모델, 저장소, 비밀번호 해시 기능을 분리하면 코드 역할이 더 명확해집니다.&lt;/p&gt;
&lt;pre class=&quot;vim&quot; data-ke-language=&quot;text&quot;&gt;&lt;code&gt;# 프로젝트 구조

signup_login_hash_project/
    ├── main.py
    ├── password_utils.py
    ├── member_storage.py
    ├── member_model.py
    └── tab_window.py&lt;/code&gt;&lt;/pre&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; margin-top: 15px; margin-bottom: 20px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;background-color: #f5f8ff;&quot;&gt;
&lt;td style=&quot;width: 30%; padding: 10px;&quot;&gt;&lt;b&gt;파일&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 70%; padding: 10px;&quot;&gt;&lt;b&gt;역할&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;main.py&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;QApplication을 만들고 프로그램을 실행합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;password_utils.py&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;비밀번호를 해시값으로 변환합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;member_storage.py&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;member.json 파일을 저장하고 불러옵니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;member_model.py&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;회원가입, 로그인, 프로필 수정을 처리합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;tab_window.py&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;QTabWidget과 각 View 화면을 구성합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1.2 최종 member.json 구조&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회원 정보는 한 명의 딕셔너리로 저장하지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 회원을 저장할 수 있도록 &lt;b&gt;members 리스트&lt;/b&gt; 구조를 사용합니다.&lt;/p&gt;
&lt;pre class=&quot;perl&quot; data-ke-language=&quot;text&quot;&gt;&lt;code&gt;# member.json 예시

{
    &quot;members&quot;: [
        {
            &quot;id&quot;: &quot;test&quot;,
            &quot;password_hash&quot;: &quot;03ac674216f3e15c...&quot;,
            &quot;name&quot;: &quot;홍길동&quot;,
            &quot;email&quot;: &quot;test@test.com&quot;,
            &quot;phone&quot;: &quot;010-1234-5678&quot;
        },
        {
            &quot;id&quot;: &quot;kim&quot;,
            &quot;password_hash&quot;: &quot;a8b3f2...&quot;,
            &quot;name&quot;: &quot;김철수&quot;,
            &quot;email&quot;: &quot;kim@test.com&quot;,
            &quot;phone&quot;: &quot;010-9999-8888&quot;
        }
    ]
}&lt;/code&gt;&lt;/pre&gt;
&lt;div style=&quot;background-color: #f8fbff; border: 1px solid #d8e8ff; border-radius: 10px; padding: 18px; margin: 25px 0;&quot;&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;핵심 확인:&lt;/b&gt; 회원가입을 하면 새 회원 딕셔너리가 members 리스트에 추가되어야 합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h1&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;2.&lt;/span&gt; 비밀번호 해시 함수 구현하기&lt;/h1&gt;
&lt;p style=&quot;color: #9d9d9d;&quot; data-ke-size=&quot;size14&quot;&gt;&amp;rarr; 비밀번호를 그대로 저장하지 않기 위해 hash_password() 함수를 먼저 만듭니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot; data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2.1 password_utils.py 작성하기&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 &lt;b&gt;password_utils.py&lt;/b&gt; 파일을 만듭니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 파일에는 비밀번호를 해시값으로 바꾸는 함수만 넣습니다.&lt;/p&gt;
&lt;pre class=&quot;python&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;import hashlib


def hash_password(password):
    &quot;&quot;&quot;입력한 비밀번호를 SHA-256 해시 문자열로 변환합니다.&quot;&quot;&quot;
    return hashlib.sha256(password.encode(&quot;utf-8&quot;)).hexdigest()&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2.2 코드 흐름&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비밀번호가 &lt;b&gt;1234&lt;/b&gt;라면, 그대로 저장하지 않고 해시값으로 바꿉니다.&lt;/p&gt;
&lt;pre class=&quot;vala&quot; data-ke-language=&quot;text&quot;&gt;&lt;code&gt;# 비밀번호 해시 변환 흐름

&quot;1234&quot;
    &amp;darr;
hash_password()
    &amp;darr;
&quot;03ac674216f3e15c...&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; margin-top: 15px; margin-bottom: 20px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;background-color: #f5f8ff;&quot;&gt;
&lt;td style=&quot;width: 35%; padding: 10px;&quot;&gt;&lt;b&gt;코드&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 65%; padding: 10px;&quot;&gt;&lt;b&gt;설명&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;encode(&quot;utf-8&quot;)&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;문자열 비밀번호를 바이트 데이터로 바꿉니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;hashlib.sha256()&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;SHA-256 방식으로 해시값을 계산합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;hexdigest()&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;해시 결과를 문자열 형태로 반환합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;div style=&quot;background-color: #f8fbff; border: 1px solid #d8e8ff; border-radius: 10px; padding: 18px; margin: 25px 0;&quot;&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;주의할 점:&lt;/b&gt; 이번 예제의 SHA-256은 학습용입니다. 실제 서비스에서는 salt와 비밀번호 저장 전용 해시 알고리즘을 사용해야 합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h1&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;3.&lt;/span&gt; 회원 정보 저장소 구현하기&lt;/h1&gt;
&lt;p style=&quot;color: #9d9d9d;&quot; data-ke-size=&quot;size14&quot;&gt;&amp;rarr; member.json 파일을 저장하고 불러오는 MemberStorage를 만듭니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot; data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;3.1 member_storage.py 작성하기&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 &lt;b&gt;member_storage.py&lt;/b&gt; 파일을 만듭니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MemberStorage는 화면을 모르고, 오직 파일 저장과 불러오기만 담당합니다.&lt;/p&gt;
&lt;pre class=&quot;python&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;import json
from pathlib import Path


class MemberStorage:
    &quot;&quot;&quot;회원 목록을 member.json 파일에 저장하고 불러오는 클래스&quot;&quot;&quot;

    def __init__(self, file_path=&quot;member.json&quot;):
        self.file_path = Path(file_path)

    def default_data(self):
        return {
            &quot;members&quot;: []
        }

    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(&quot;r&quot;, encoding=&quot;utf-8&quot;) as file:
                data = json.load(file)
        except json.JSONDecodeError:
            data = self.default_data()
            self.save_data(data)
            return data

        if &quot;members&quot; not in data:
            data = self.default_data()
            self.save_data(data)

        return data

    def save_data(self, data):
        with self.file_path.open(&quot;w&quot;, encoding=&quot;utf-8&quot;) as file:
            json.dump(data, file, ensure_ascii=False, indent=4)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;3.2 MemberStorage 코드 역할&lt;/b&gt;&lt;/h2&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; margin-top: 15px; margin-bottom: 20px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;background-color: #f5f8ff;&quot;&gt;
&lt;td style=&quot;width: 30%; padding: 10px;&quot;&gt;&lt;b&gt;메서드&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 70%; padding: 10px;&quot;&gt;&lt;b&gt;역할&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;default_data()&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;member.json이 없을 때 사용할 기본 구조를 만듭니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;load_data()&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;member.json 파일에서 회원 목록을 불러옵니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;save_data()&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;회원 목록을 member.json 파일에 저장합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre class=&quot;vala&quot; data-ke-language=&quot;text&quot;&gt;&lt;code&gt;# 처음 실행 시 생성되는 member.json

{
    &quot;members&quot;: []
}&lt;/code&gt;&lt;/pre&gt;
&lt;div style=&quot;background-color: #f8fbff; border: 1px solid #d8e8ff; border-radius: 10px; padding: 18px; margin: 25px 0;&quot;&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;핵심 확인:&lt;/b&gt; 회원가입을 하기 전에는 members 리스트가 비어 있고, 회원가입을 하면 이 리스트에 회원 딕셔너리가 추가됩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h1&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;4.&lt;/span&gt; 회원 Model 구현하기&lt;/h1&gt;
&lt;p style=&quot;color: #9d9d9d;&quot; data-ke-size=&quot;size14&quot;&gt;&amp;rarr; 회원가입, 로그인, 프로필 수정 기능을 담당하는 MemberModel을 구현합니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot; data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1672&quot; data-origin-height=&quot;941&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/oNeLa/dJMcacQO34w/s5pjAKH7UBaxy5BZYfrfE0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/oNeLa/dJMcacQO34w/s5pjAKH7UBaxy5BZYfrfE0/img.png&quot; data-alt=&quot;MemberModel이 회원가입, 로그인, 프로필 수정을 처리하는 구조&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/oNeLa/dJMcacQO34w/s5pjAKH7UBaxy5BZYfrfE0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FoNeLa%2FdJMcacQO34w%2Fs5pjAKH7UBaxy5BZYfrfE0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1672&quot; height=&quot;941&quot; data-origin-width=&quot;1672&quot; data-origin-height=&quot;941&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;MemberModel이 회원가입, 로그인, 프로필 수정을 처리하는 구조&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;4.1 member_model.py 작성하기&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MemberModel은 이번 과제의 핵심입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회원가입 View, 로그인 View, 메인화면 View, 프로필 View는 모두 같은 MemberModel을 사용합니다.&lt;/p&gt;
&lt;pre class=&quot;python&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;from member_storage import MemberStorage
from password_utils import hash_password


class MemberModel:
    &quot;&quot;&quot;회원가입, 로그인, 프로필 수정을 담당하는 Model&quot;&quot;&quot;

    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[&quot;members&quot;]:
            if member[&quot;id&quot;] == 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, &quot;모든 값을 입력하세요.&quot;

        if password != password_confirm:
            return False, &quot;비밀번호가 서로 다릅니다.&quot;

        if self.is_duplicate_id(user_id):
            return False, &quot;이미 사용 중인 아이디입니다.&quot;

        new_member = {
            &quot;id&quot;: user_id,
            &quot;password_hash&quot;: hash_password(password),
            &quot;name&quot;: name,
            &quot;email&quot;: email,
            &quot;phone&quot;: phone,
        }

        self.data[&quot;members&quot;].append(new_member)
        self.storage.save_data(self.data)

        return True, &quot;회원가입이 완료되었습니다.&quot;

    def check_login(self, user_id, password):
        input_password_hash = hash_password(password)

        for member in self.data[&quot;members&quot;]:
            if member[&quot;id&quot;] == user_id and member[&quot;password_hash&quot;] == 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[&quot;members&quot;]:
            if member[&quot;id&quot;] == 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 {
            &quot;id&quot;: member[&quot;id&quot;],
            &quot;name&quot;: member[&quot;name&quot;],
            &quot;email&quot;: member[&quot;email&quot;],
            &quot;phone&quot;: member[&quot;phone&quot;],
        }

    def update_profile(self, name, email, phone):
        member = self.get_current_member()

        if member is None:
            return False

        member[&quot;name&quot;] = name
        member[&quot;email&quot;] = email
        member[&quot;phone&quot;] = phone

        self.storage.save_data(self.data)

        return True&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;4.2 회원가입 처리 흐름&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;register_member()는 회원가입 버튼을 눌렀을 때 호출됩니다.&lt;/p&gt;
&lt;pre class=&quot;mipsasm&quot; data-ke-language=&quot;text&quot;&gt;&lt;code&gt;# register_member() 실행 흐름

빈 값 검사
    &amp;darr;
비밀번호 확인 검사
    &amp;darr;
아이디 중복 검사
    &amp;darr;
비밀번호 해시 변환
    &amp;darr;
members 리스트에 새 회원 추가
    &amp;darr;
member.json 파일에 저장&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;새 회원을 만들 때 비밀번호는 password_hash로 저장합니다.&lt;/p&gt;
&lt;pre class=&quot;haxe&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;new_member = {
    &quot;id&quot;: user_id,
    &quot;password_hash&quot;: hash_password(password),
    &quot;name&quot;: name,
    &quot;email&quot;: email,
    &quot;phone&quot;: phone,
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;4.3 로그인 처리 흐름&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;check_login()은 입력한 ID와 PW가 members 목록 안의 회원 정보와 일치하는지 확인합니다.&lt;/p&gt;
&lt;pre class=&quot;ini&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;input_password_hash = hash_password(password)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자가 입력한 비밀번호를 먼저 해시로 바꿉니다.&lt;/p&gt;
&lt;pre class=&quot;fsharp&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;if member[&quot;id&quot;] == user_id and member[&quot;password_hash&quot;] == input_password_hash:
    self.current_member_id = user_id
    return True&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ID와 password_hash가 모두 일치하면 로그인 성공입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 current_member_id에 현재 로그인한 회원의 ID를 저장합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;4.4 프로필 수정 흐름&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로필 수정은 전체 회원이 아니라 현재 로그인한 회원에게만 적용되어야 합니다.&lt;/p&gt;
&lt;pre class=&quot;ini&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;member = self.get_current_member()&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 현재 로그인한 회원을 찾습니다.&lt;/p&gt;
&lt;pre class=&quot;fsharp&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;member[&quot;name&quot;] = name
member[&quot;email&quot;] = email
member[&quot;phone&quot;] = phone

self.storage.save_data(self.data)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 회원의 이름, 이메일, 전화번호만 수정하고 다시 파일에 저장합니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #f8fbff; border: 1px solid #d8e8ff; border-radius: 10px; padding: 18px; margin: 25px 0;&quot;&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;핵심 확인:&lt;/b&gt; current_member_id를 저장해야 로그인한 회원이 누구인지 알 수 있고, 프로필 수정도 그 회원에게만 적용할 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h1&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;5.&lt;/span&gt; 화면 코드 구현하기&lt;/h1&gt;
&lt;p style=&quot;color: #9d9d9d;&quot; data-ke-size=&quot;size14&quot;&gt;&amp;rarr; QTabWidget에 로그인, 회원가입, 메인화면, 프로필 View를 연결합니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot; data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;5.1 tab_window.py 전체 코드&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 화면을 구성하는 &lt;b&gt;tab_window.py&lt;/b&gt;를 작성합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드가 길지만 View별 역할을 나누어 보면 어렵지 않습니다.&lt;/p&gt;
&lt;pre class=&quot;python&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;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(&quot;아이디를 입력하세요&quot;)

        self.pw_input = QLineEdit()
        self.pw_input.setPlaceholderText(&quot;비밀번호를 입력하세요&quot;)
        self.pw_input.setEchoMode(QLineEdit.EchoMode.Password)

        self.login_button = QPushButton(&quot;로그인&quot;)
        self.login_button.clicked.connect(self.login)

        self.message_label = QLabel(&quot;로그인 정보를 입력하세요.&quot;)

        layout = QVBoxLayout()
        layout.addWidget(QLabel(&quot;[로그인]&quot;))
        layout.addWidget(QLabel(&quot;아이디&quot;))
        layout.addWidget(self.id_input)
        layout.addWidget(QLabel(&quot;비밀번호&quot;))
        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(&quot;로그인 성공!&quot;)

            self.main_view.refresh()
            self.profile_view.refresh()

            self.tab_widget.setCurrentIndex(2)
        else:
            self.message_label.setText(&quot;로그인 실패! 아이디 또는 비밀번호를 확인하세요.&quot;)


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(&quot;아이디&quot;)

        self.pw_input = QLineEdit()
        self.pw_input.setPlaceholderText(&quot;비밀번호&quot;)
        self.pw_input.setEchoMode(QLineEdit.EchoMode.Password)

        self.pw_confirm_input = QLineEdit()
        self.pw_confirm_input.setPlaceholderText(&quot;비밀번호 확인&quot;)
        self.pw_confirm_input.setEchoMode(QLineEdit.EchoMode.Password)

        self.name_input = QLineEdit()
        self.name_input.setPlaceholderText(&quot;이름&quot;)

        self.email_input = QLineEdit()
        self.email_input.setPlaceholderText(&quot;이메일&quot;)

        self.phone_input = QLineEdit()
        self.phone_input.setPlaceholderText(&quot;전화번호&quot;)

        self.signup_button = QPushButton(&quot;회원가입&quot;)
        self.signup_button.clicked.connect(self.signup)

        self.message_label = QLabel(&quot;회원가입 정보를 입력하세요.&quot;)

        layout = QVBoxLayout()
        layout.addWidget(QLabel(&quot;[회원가입]&quot;))
        layout.addWidget(QLabel(&quot;아이디&quot;))
        layout.addWidget(self.id_input)
        layout.addWidget(QLabel(&quot;비밀번호&quot;))
        layout.addWidget(self.pw_input)
        layout.addWidget(QLabel(&quot;비밀번호 확인&quot;))
        layout.addWidget(self.pw_confirm_input)
        layout.addWidget(QLabel(&quot;이름&quot;))
        layout.addWidget(self.name_input)
        layout.addWidget(QLabel(&quot;이메일&quot;))
        layout.addWidget(self.email_input)
        layout.addWidget(QLabel(&quot;전화번호&quot;))
        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(&quot;[메인화면] 로그인한 회원 정보&quot;)
        self.id_label = QLabel(&quot;아이디:&quot;)
        self.name_label = QLabel(&quot;이름:&quot;)
        self.email_label = QLabel(&quot;이메일:&quot;)
        self.phone_label = QLabel(&quot;전화번호:&quot;)

        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&quot;아이디: {member['id']}&quot;)
        self.name_label.setText(f&quot;이름: {member['name']}&quot;)
        self.email_label.setText(f&quot;이메일: {member['email']}&quot;)
        self.phone_label.setText(f&quot;전화번호: {member['phone']}&quot;)


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(&quot;아이디:&quot;)

        self.name_input = QLineEdit()
        self.email_input = QLineEdit()
        self.phone_input = QLineEdit()

        self.update_button = QPushButton(&quot;수정하기&quot;)
        self.update_button.clicked.connect(self.update_profile)

        self.message_label = QLabel(&quot;프로필 정보를 확인하거나 수정하세요.&quot;)

        layout = QVBoxLayout()
        layout.addWidget(QLabel(&quot;[프로필]&quot;))
        layout.addWidget(self.id_label)

        name_layout = QHBoxLayout()
        name_layout.addWidget(QLabel(&quot;이름&quot;))
        name_layout.addWidget(self.name_input)

        email_layout = QHBoxLayout()
        email_layout.addWidget(QLabel(&quot;이메일&quot;))
        email_layout.addWidget(self.email_input)

        phone_layout = QHBoxLayout()
        phone_layout.addWidget(QLabel(&quot;전화번호&quot;))
        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&quot;아이디: {member['id']}&quot;)
        self.name_input.setText(member[&quot;name&quot;])
        self.email_input.setText(member[&quot;email&quot;])
        self.phone_input.setText(member[&quot;phone&quot;])

    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(&quot;프로필 정보가 수정되었습니다.&quot;)
        else:
            self.message_label.setText(&quot;프로필 정보를 수정할 수 없습니다.&quot;)


class TabWindow(QWidget):
    def __init__(self, member_model):
        super().__init__()

        self.setWindowTitle(&quot;회원가입 구현 과제 정답&quot;)
        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, &quot;로그인&quot;)
        self.tab_widget.addTab(self.signup_view, &quot;회원가입&quot;)
        self.tab_widget.addTab(self.main_view, &quot;메인화면&quot;)
        self.tab_widget.addTab(self.profile_view, &quot;프로필&quot;)

        layout = QVBoxLayout()
        layout.addWidget(self.tab_widget)

        self.setLayout(layout)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;5.2 탭 인덱스 확인하기&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 프로그램의 탭 순서는 아래와 같습니다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; margin-top: 15px; margin-bottom: 20px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;background-color: #f5f8ff;&quot;&gt;
&lt;td style=&quot;width: 30%; padding: 10px;&quot;&gt;&lt;b&gt;탭 인덱스&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 70%; padding: 10px;&quot;&gt;&lt;b&gt;탭 이름&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;0&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;로그인&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;1&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;회원가입&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;2&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;메인화면&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;3&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;프로필&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회원가입 성공 후에는 로그인 탭으로 이동합니다.&lt;/p&gt;
&lt;pre class=&quot;css&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;self.tab_widget.setCurrentIndex(0)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그인 성공 후에는 메인화면 탭으로 이동합니다.&lt;/p&gt;
&lt;pre class=&quot;css&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;self.tab_widget.setCurrentIndex(2)&lt;/code&gt;&lt;/pre&gt;
&lt;div style=&quot;background-color: #f8fbff; border: 1px solid #d8e8ff; border-radius: 10px; padding: 18px; margin: 25px 0;&quot;&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;주의할 점:&lt;/b&gt; 탭 순서가 바뀌면 setCurrentIndex()의 숫자도 함께 바뀌어야 합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h1&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;6.&lt;/span&gt; main.py에서 프로그램 실행하기&lt;/h1&gt;
&lt;p style=&quot;color: #9d9d9d;&quot; data-ke-size=&quot;size14&quot;&gt;&amp;rarr; QApplication을 만들고 TabWindow를 실행합니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot; data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;6.1 main.py 작성하기&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막으로 &lt;b&gt;main.py&lt;/b&gt;를 작성합니다.&lt;/p&gt;
&lt;pre class=&quot;python&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;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())&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;6.2 실행 방법&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;터미널에서 프로젝트 폴더로 이동한 뒤 아래 명령어를 실행합니다.&lt;/p&gt;
&lt;pre class=&quot;vim&quot; data-ke-language=&quot;text&quot;&gt;&lt;code&gt;python main.py&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음 실행하면 프로젝트 폴더 안에 member.json 파일이 자동으로 생성됩니다.&lt;/p&gt;
&lt;pre class=&quot;vala&quot; data-ke-language=&quot;text&quot;&gt;&lt;code&gt;# 처음 생성되는 member.json

{
    &quot;members&quot;: []
}&lt;/code&gt;&lt;/pre&gt;
&lt;div style=&quot;background-color: #f8fbff; border: 1px solid #d8e8ff; border-radius: 10px; padding: 18px; margin: 25px 0;&quot;&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;확인:&lt;/b&gt; member.json 파일이 생성되지 않는다면 프로젝트 실행 위치와 파일 쓰기 권한을 확인해야 합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h1&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;7.&lt;/span&gt; 실행 결과 확인하기&lt;/h1&gt;
&lt;p style=&quot;color: #9d9d9d;&quot; data-ke-size=&quot;size14&quot;&gt;&amp;rarr; 회원가입, 로그인, 프로필 수정 결과가 올바르게 동작하는지 확인합니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot; data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1491&quot; data-origin-height=&quot;1055&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bJe0tN/dJMcaijdTVF/TfrLoCsktmrV04bWOc8Xo0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bJe0tN/dJMcaijdTVF/TfrLoCsktmrV04bWOc8Xo0/img.png&quot; data-alt=&quot;회원가입 성공, 로그인 성공, 프로필 수정 성공 결과 흐름&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bJe0tN/dJMcaijdTVF/TfrLoCsktmrV04bWOc8Xo0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbJe0tN%2FdJMcaijdTVF%2FTfrLoCsktmrV04bWOc8Xo0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1491&quot; height=&quot;1055&quot; data-origin-width=&quot;1491&quot; data-origin-height=&quot;1055&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;회원가입 성공, 로그인 성공, 프로필 수정 성공 결과 흐름&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;7.1 회원가입 성공 테스트&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회원가입 탭에서 아래처럼 입력합니다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; margin-top: 15px; margin-bottom: 20px; height: 151px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;background-color: #f5f8ff; height: 21px;&quot;&gt;
&lt;td style=&quot;width: 30%; padding: 10px; height: 21px;&quot;&gt;&lt;b&gt;항목&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 70%; padding: 10px; height: 21px;&quot;&gt;&lt;b&gt;입력값&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;padding: 10px; height: 21px;&quot;&gt;아이디&lt;/td&gt;
&lt;td style=&quot;padding: 10px; height: 21px;&quot;&gt;test&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;padding: 10px; height: 21px;&quot;&gt;비밀번호&lt;/td&gt;
&lt;td style=&quot;padding: 10px; height: 21px;&quot;&gt;1234&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;padding: 10px; height: 21px;&quot;&gt;비밀번호 확인&lt;/td&gt;
&lt;td style=&quot;padding: 10px; height: 21px;&quot;&gt;1234&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;padding: 10px; height: 21px;&quot;&gt;이름&lt;/td&gt;
&lt;td style=&quot;padding: 10px; height: 21px;&quot;&gt;홍길동&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 25px;&quot;&gt;
&lt;td style=&quot;padding: 10px; height: 25px;&quot;&gt;이메일&lt;/td&gt;
&lt;td style=&quot;padding: 10px; height: 25px;&quot;&gt;test@test.com&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;padding: 10px; height: 21px;&quot;&gt;전화번호&lt;/td&gt;
&lt;td style=&quot;padding: 10px; height: 21px;&quot;&gt;010-1234-5678&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회원가입에 성공하면 로그인 탭으로 이동합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;member.json 파일을 열어 보면 아래처럼 새 회원이 추가되어 있어야 합니다.&lt;/p&gt;
&lt;pre class=&quot;perl&quot; data-ke-language=&quot;text&quot;&gt;&lt;code&gt;# 회원가입 후 member.json 예시

{
    &quot;members&quot;: [
        {
            &quot;id&quot;: &quot;test&quot;,
            &quot;password_hash&quot;: &quot;03ac674216f3e15c...&quot;,
            &quot;name&quot;: &quot;홍길동&quot;,
            &quot;email&quot;: &quot;test@test.com&quot;,
            &quot;phone&quot;: &quot;010-1234-5678&quot;
        }
    ]
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;7.2 회원가입 실패 테스트&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 상황에서는 회원가입이 실패해야 합니다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; margin-top: 15px; margin-bottom: 20px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;background-color: #f5f8ff;&quot;&gt;
&lt;td style=&quot;width: 35%; padding: 10px;&quot;&gt;&lt;b&gt;상황&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 65%; padding: 10px;&quot;&gt;&lt;b&gt;출력 메시지&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;빈 값이 있음&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;모든 값을 입력하세요.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;비밀번호와 비밀번호 확인이 다름&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;비밀번호가 서로 다릅니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;이미 사용 중인 아이디&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;이미 사용 중인 아이디입니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;7.3 로그인 성공 테스트&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그인 탭에서 회원가입한 계정으로 로그인합니다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; margin-top: 15px; margin-bottom: 20px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;background-color: #f5f8ff;&quot;&gt;
&lt;td style=&quot;width: 30%; padding: 10px;&quot;&gt;&lt;b&gt;항목&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 70%; padding: 10px;&quot;&gt;&lt;b&gt;입력값&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;아이디&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;test&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;비밀번호&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;1234&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그인에 성공하면 메인화면 탭으로 이동합니다.&lt;/p&gt;
&lt;pre class=&quot;makefile&quot; data-ke-language=&quot;text&quot;&gt;&lt;code&gt;# 메인화면 출력 예시

[메인화면] 로그인한 회원 정보

아이디: test
이름: 홍길동
이메일: test@test.com
전화번호: 010-1234-5678&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;7.4 프로필 수정 테스트&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로필 탭에서 이름, 이메일, 전화번호를 수정합니다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot; data-ke-language=&quot;text&quot;&gt;&lt;code&gt;# 수정 입력 예시

이름: 김철수
이메일: kim@test.com
전화번호: 010-9999-8888&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수정하기 버튼을 누르면 현재 로그인한 회원 정보만 수정되어야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수정 후 member.json은 아래처럼 바뀝니다.&lt;/p&gt;
&lt;pre class=&quot;perl&quot; data-ke-language=&quot;text&quot;&gt;&lt;code&gt;# 프로필 수정 후 member.json 예시

{
    &quot;members&quot;: [
        {
            &quot;id&quot;: &quot;test&quot;,
            &quot;password_hash&quot;: &quot;03ac674216f3e15c...&quot;,
            &quot;name&quot;: &quot;김철수&quot;,
            &quot;email&quot;: &quot;kim@test.com&quot;,
            &quot;phone&quot;: &quot;010-9999-8888&quot;
        }
    ]
}&lt;/code&gt;&lt;/pre&gt;
&lt;div style=&quot;background-color: #f8fbff; border: 1px solid #d8e8ff; border-radius: 10px; padding: 18px; margin: 25px 0;&quot;&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;핵심 확인:&lt;/b&gt; 여러 회원이 저장되어 있어도 프로필 수정은 현재 로그인한 회원에게만 적용되어야 합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h1&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;8.&lt;/span&gt; 기존 방식과 새 방식 비교하며 정리하기&lt;/h1&gt;
&lt;p style=&quot;color: #9d9d9d;&quot; data-ke-size=&quot;size14&quot;&gt;&amp;rarr; 회원 1명 저장 방식과 여러 회원 가입 방식의 차이를 정리합니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot; data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;8.1 기존 방식&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 방식은 미리 만들어진 회원 1명만 사용하는 구조였습니다.&lt;/p&gt;
&lt;pre class=&quot;mipsasm&quot; data-ke-language=&quot;text&quot;&gt;&lt;code&gt;# 기존 방식

member.json
    └── 회원 1명 정보만 저장

로그인
    └── 미리 저장된 회원만 로그인 가능&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;8.2 새 방식&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;새 방식은 회원가입을 통해 여러 회원을 추가할 수 있는 구조입니다.&lt;/p&gt;
&lt;pre class=&quot;mipsasm&quot; data-ke-language=&quot;text&quot;&gt;&lt;code&gt;# 새 방식

member.json
    └── members 리스트에 여러 회원 저장

회원가입
    └── 새 회원 추가

로그인
    └── members 목록에서 일치하는 회원 찾기

프로필 수정
    └── 현재 로그인한 회원만 수정&lt;/code&gt;&lt;/pre&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; margin-top: 15px; margin-bottom: 20px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;background-color: #f5f8ff;&quot;&gt;
&lt;td style=&quot;width: 20%; padding: 10px;&quot;&gt;&lt;b&gt;구분&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 40%; padding: 10px;&quot;&gt;&lt;b&gt;기존 방식&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 40%; padding: 10px;&quot;&gt;&lt;b&gt;새 방식&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;회원 수&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;회원 1명 중심&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;여러 회원 저장 가능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;회원 추가&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;코드 또는 파일을 직접 수정해야 함&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;회원가입 View에서 추가 가능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;비밀번호 저장&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;pw 또는 password_hash 구조를 직접 준비&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;회원가입 시 password_hash로 자동 저장&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;프로필 수정&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;회원 1명만 고려&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;현재 로그인한 회원만 찾아서 수정&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;8.3 최종 정리&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글에서 구현한 내용을 정리하면 다음과 같습니다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; margin-top: 15px; margin-bottom: 20px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;background-color: #f5f8ff;&quot;&gt;
&lt;td style=&quot;width: 30%; padding: 10px;&quot;&gt;&lt;b&gt;핵심 내용&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 70%; padding: 10px;&quot;&gt;&lt;b&gt;정리&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;회원가입 View&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;새 회원 정보를 입력받습니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;MemberModel&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;회원가입, 로그인, 프로필 수정을 처리합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;MemberStorage&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;members 목록을 member.json 파일에 저장합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;password_hash&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;비밀번호를 그대로 저장하지 않고 해시값으로 저장합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;current_member_id&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;현재 로그인한 회원을 구분하는 기준입니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;div style=&quot;background-color: #f8fbff; border: 1px solid #d8e8ff; border-radius: 10px; padding: 18px; margin: 25px 0;&quot;&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;기억할 문장:&lt;/b&gt; 회원가입은 members 목록에 새 회원을 추가하는 기능이고, 로그인과 프로필 수정은 현재 로그인한 회원을 기준으로 처리해야 합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;8.4 프로젝트&amp;nbsp; 파일&lt;/b&gt;&lt;/h2&gt;
&lt;figure class=&quot;fileblock&quot; data-ke-align=&quot;alignCenter&quot;&gt;&lt;a href=&quot;https://blog.kakaocdn.net/dn/pwB2c/dJMcaiwKZZi/jnl9FblrSbbyRVKjWVMOz1/659.zip?attach=1&amp;amp;knm=tfile.zip&quot; class=&quot;&quot;&gt;
    &lt;div class=&quot;image&quot;&gt;&lt;/div&gt;
    &lt;div class=&quot;desc&quot;&gt;&lt;div class=&quot;filename&quot;&gt;&lt;span class=&quot;name&quot;&gt;659.zip&lt;/span&gt;&lt;/div&gt;
&lt;div class=&quot;size&quot;&gt;0.01MB&lt;/div&gt;
&lt;/div&gt;
  &lt;/a&gt;&lt;/figure&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h1&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;참고.&lt;/span&gt; 공식 문서로 확인하기&lt;/h1&gt;
&lt;p style=&quot;color: #9d9d9d;&quot; data-ke-size=&quot;size14&quot;&gt;&amp;rarr; PySide6와 Python 파일 저장, 해시 기능의 공식 문서를 확인합니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot; data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;참고 문서&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글에서 사용한 주요 기능은 아래 공식 문서에서 확인할 수 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://doc.qt.io/qtforpython-6/PySide6/QtWidgets/QTabWidget.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;PySide6 QTabWidget 공식 문서&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://doc.qt.io/qtforpython-6/PySide6/QtWidgets/QLineEdit.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;PySide6 QLineEdit 공식 문서&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.python.org/ko/3/library/json.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Python json 공식 문서&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.python.org/ko/3/library/hashlib.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Python hashlib 공식 문서&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.python.org/ko/3/library/pathlib.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Python pathlib 공식 문서&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div style=&quot;background-color: #f8fbff; border: 1px solid #d8e8ff; border-radius: 10px; padding: 18px; margin: 25px 0;&quot;&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;참고:&lt;/b&gt; 이번 강의에서는 학습을 위해 SHA-256을 사용했습니다. 실제 서비스에서는 salt와 bcrypt, scrypt, argon2 같은 비밀번호 저장 전용 방식을 사용하는 것이 좋습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;</description>
      <category>3. PySide6/3-5. File</category>
      <author>BasicLike</author>
      <guid isPermaLink="true">https://basiclike.tistory.com/786</guid>
      <comments>https://basiclike.tistory.com/786#entry786comment</comments>
      <pubDate>Mon, 1 Jun 2026 14:56:06 +0900</pubDate>
    </item>
    <item>
      <title>8강. 회원가입 과제</title>
      <link>https://basiclike.tistory.com/785</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;436&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Ulsh1/dJMcagZYmVC/ruopyM9QYatQPAq3EZoVlk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Ulsh1/dJMcagZYmVC/ruopyM9QYatQPAq3EZoVlk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Ulsh1/dJMcagZYmVC/ruopyM9QYatQPAq3EZoVlk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FUlsh1%2FdJMcagZYmVC%2FruopyM9QYatQPAq3EZoVlk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;436&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;436&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h1&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;0.&lt;/span&gt; 학습 목표&lt;/h1&gt;
&lt;p style=&quot;color: #9d9d9d;&quot; data-ke-size=&quot;size14&quot;&gt;&amp;rarr; 회원가입 정보를 JSON 파일에 저장하고, 비밀번호는 해시값으로 관리하는 과제를 이해합니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot; data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;이번 과제에서 할 일&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 과제에서는 기존 로그인 프로그램에 &lt;b&gt;회원가입 View&lt;/b&gt;를 추가합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자가 회원가입 화면에서 아이디, 비밀번호, 이름, 이메일, 전화번호를 입력하면 새 회원 정보가 &lt;b&gt;member.json&lt;/b&gt; 파일에 저장되어야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중요한 점은 비밀번호를 그대로 저장하지 않는 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비밀번호는 password_hash 형태로 변환해서 저장해야 합니다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; margin-top: 15px; margin-bottom: 20px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;background-color: #f5f8ff;&quot;&gt;
&lt;td style=&quot;width: 17.7751%; padding: 10px;&quot;&gt;&lt;b&gt;구분&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 82.2249%; padding: 10px;&quot;&gt;&lt;b&gt;내용&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px; width: 17.7751%;&quot;&gt;핵심 개념&lt;/td&gt;
&lt;td style=&quot;padding: 10px; width: 82.2249%;&quot;&gt;회원가입 정보를 파일에 저장하고, 비밀번호는 해시값으로 관리합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px; width: 17.7751%;&quot;&gt;실습 준비&lt;/td&gt;
&lt;td style=&quot;padding: 10px; width: 82.2249%;&quot;&gt;PySide6, QTabWidget, QLineEdit, QPushButton, QLabel, JSON 파일, hashlib를 사용합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px; width: 17.7751%;&quot;&gt;최종 목표&lt;/td&gt;
&lt;td style=&quot;padding: 10px; width: 82.2249%;&quot;&gt;회원가입한 사용자 정보가 member.json에 저장되고, 가입한 ID/PW로 로그인할 수 있도록 구현합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px; width: 17.7751%;&quot;&gt;제출 기한&lt;/td&gt;
&lt;td style=&quot;padding: 10px; width: 82.2249%;&quot;&gt;X월 X일 X요일까지 제출하세요.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1983&quot; data-origin-height=&quot;793&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bT7uPc/dJMcacwyYRE/v4aYRbEpRzcKjP0DmsV5qk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bT7uPc/dJMcacwyYRE/v4aYRbEpRzcKjP0DmsV5qk/img.png&quot; data-alt=&quot;회원가입 View, MemberModel, MemberStorage, member.json이 연결되는 전체 구조&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bT7uPc/dJMcacwyYRE/v4aYRbEpRzcKjP0DmsV5qk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbT7uPc%2FdJMcacwyYRE%2Fv4aYRbEpRzcKjP0DmsV5qk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1983&quot; height=&quot;793&quot; data-origin-width=&quot;1983&quot; data-origin-height=&quot;793&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;회원가입 View, MemberModel, MemberStorage, member.json이 연결되는 전체 구조&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;div style=&quot;background-color: #f8fbff; border: 1px solid #d8e8ff; border-radius: 10px; padding: 18px; margin: 25px 0;&quot;&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;이번 과제의 핵심:&lt;/b&gt; 회원가입으로 새 회원을 추가하고, 비밀번호는 평문이 아니라 password_hash로 저장해야 합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h1&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;1.&lt;/span&gt; 왜 회원가입 기능이 필요할까?&lt;/h1&gt;
&lt;p style=&quot;color: #9d9d9d;&quot; data-ke-size=&quot;size14&quot;&gt;&amp;rarr; 기존 로그인 프로그램은 미리 저장된 회원만 사용할 수 있었기 때문에 새 회원을 추가하는 기능이 필요합니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot; data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1.1 기존 프로그램의 한계&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 프로그램에서는 회원 정보가 미리 준비되어 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 member.json 파일에 test 회원이 이미 들어 있고, 사용자는 그 계정으로만 로그인할 수 있었습니다.&lt;/p&gt;
&lt;pre class=&quot;perl&quot; data-ke-language=&quot;text&quot;&gt;&lt;code&gt;# 기존 member.json 예시

{
    &quot;id&quot;: &quot;test&quot;,
    &quot;password_hash&quot;: &quot;03ac674216f3e15c...&quot;,
    &quot;name&quot;: &quot;홍길동&quot;,
    &quot;email&quot;: &quot;test@test.com&quot;,
    &quot;phone&quot;: &quot;010-1234-5678&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 구조는 로그인과 파일 저장을 이해하기에는 좋습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 새로운 사용자가 직접 회원가입을 할 수 없습니다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; margin-top: 15px; margin-bottom: 20px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;background-color: #f5f8ff;&quot;&gt;
&lt;td style=&quot;width: 30%; padding: 10px;&quot;&gt;&lt;b&gt;상황&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 70%; padding: 10px;&quot;&gt;&lt;b&gt;문제점&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;회원 정보가 1명만 있음&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;test 계정 외에는 로그인할 수 없습니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;회원가입 화면이 없음&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;새 사용자가 직접 계정을 만들 수 없습니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;여러 회원 목록이 없음&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;회원 여러 명을 관리하기 어렵습니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1.2 이번 과제에서 확장할 구조&lt;/b&gt;&lt;/h2&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1672&quot; data-origin-height=&quot;707&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/eA5kuE/dJMcaiQ2GS8/WikumLgYXmShjOkgPWmkik/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/eA5kuE/dJMcaiQ2GS8/WikumLgYXmShjOkgPWmkik/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/eA5kuE/dJMcaiQ2GS8/WikumLgYXmShjOkgPWmkik/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FeA5kuE%2FdJMcaiQ2GS8%2FWikumLgYXmShjOkgPWmkik%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1672&quot; height=&quot;707&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1672&quot; data-origin-height=&quot;707&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p data-ke-size=&quot;size16&quot;&gt;이번 과제에서는 회원 1명만 저장하는 구조를 여러 회원을 저장하는 구조로 확장합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 QTabWidget에 회원가입 View를 추가합니다.&lt;/p&gt;
&lt;pre class=&quot;gauss&quot; data-ke-language=&quot;text&quot;&gt;&lt;code&gt;# 기존 구조

QTabWidget
    ├── 로그인 View
    ├── 메인화면 View
    └── 프로필 View&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;gauss&quot; data-ke-language=&quot;text&quot;&gt;&lt;code&gt;# 새 구조

QTabWidget
    ├── 로그인 View
    ├── 회원가입 View
    ├── 메인화면 View
    └── 프로필 View&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회원가입 View에서 새 회원 정보를 입력하면 MemberModel&lt;sup&gt;모델&lt;/sup&gt;이 회원가입 처리를 담당합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MemberStorage&lt;sup&gt;저장소&lt;/sup&gt;는 변경된 회원 목록을 member.json 파일에 저장합니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #f8fbff; border: 1px solid #d8e8ff; border-radius: 10px; padding: 18px; margin: 25px 0;&quot;&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;문제의 핵심:&lt;/b&gt; 미리 만들어진 회원만 로그인하는 구조에서, 사용자가 직접 회원가입하고 로그인할 수 있는 구조로 확장해야 합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h1&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;2.&lt;/span&gt; 회원가입 화면 구조 설계하기&lt;/h1&gt;
&lt;p style=&quot;color: #9d9d9d;&quot; data-ke-size=&quot;size14&quot;&gt;&amp;rarr; 회원가입 View에 필요한 입력칸과 버튼을 정리합니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot; data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2.1 회원가입 View에 필요한 입력 요소&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회원가입 View에는 새 회원 정보를 입력할 수 있는 입력칸이 필요합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비밀번호는 실수 방지를 위해 한 번 더 입력받습니다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; margin-top: 15px; margin-bottom: 20px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;background-color: #f5f8ff;&quot;&gt;
&lt;td style=&quot;width: 30%; padding: 10px;&quot;&gt;&lt;b&gt;입력 항목&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 70%; padding: 10px;&quot;&gt;&lt;b&gt;설명&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;아이디&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;로그인할 때 사용할 회원 ID입니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;비밀번호&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;로그인할 때 사용할 비밀번호입니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;비밀번호 확인&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;비밀번호를 한 번 더 입력해 같은지 확인합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;이름&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;회원 이름입니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;이메일&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;회원 이메일입니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;전화번호&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;회원 전화번호입니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2.2 회원가입 View 구현 조건&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회원가입 View에는 입력칸과 회원가입 버튼, 결과 메시지 라벨이 있어야 합니다.&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1536&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/MRNKu/dJMcabLeGxM/9ykvJy22yjd6F1PktvYZG0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/MRNKu/dJMcabLeGxM/9ykvJy22yjd6F1PktvYZG0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/MRNKu/dJMcabLeGxM/9ykvJy22yjd6F1PktvYZG0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FMRNKu%2FdJMcabLeGxM%2F9ykvJy22yjd6F1PktvYZG0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1536&quot; height=&quot;1024&quot; data-origin-width=&quot;1536&quot; data-origin-height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;table style=&quot;border-collapse: collapse; width: 100%; margin-top: 15px; margin-bottom: 20px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;background-color: #f5f8ff;&quot;&gt;
&lt;td style=&quot;width: 30%; padding: 10px;&quot;&gt;&lt;b&gt;요소&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 70%; padding: 10px;&quot;&gt;&lt;b&gt;조건&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;ID 입력칸&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;QLineEdit으로 구현합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;PW 입력칸&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;QLineEdit으로 구현하고 비밀번호처럼 보이도록 EchoMode를 설정합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;PW 확인 입력칸&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;PW와 같은지 검사합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;회원가입 버튼&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;clicked.connect(...)로 회원가입 처리 함수와 연결합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;메시지 라벨&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;회원가입 성공 또는 실패 이유를 출력합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;div style=&quot;background-color: #f8fbff; border: 1px solid #d8e8ff; border-radius: 10px; padding: 18px; margin: 25px 0;&quot;&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;핵심 확인:&lt;/b&gt; 회원가입 View는 입력을 받는 역할만 하고, 실제 회원가입 처리는 MemberModel에게 요청해야 합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h1&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;3.&lt;/span&gt; 회원가입 Model 기능 설계하기&lt;/h1&gt;
&lt;p style=&quot;color: #9d9d9d;&quot; data-ke-size=&quot;size14&quot;&gt;&amp;rarr; MemberModel에 회원가입, 중복 검사, 로그인 검증 기능을 추가합니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot; data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;3.1 MemberModel에 필요한 기능&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 과제에서는 MemberModel이 회원가입, 로그인, 프로필 수정을 모두 관리해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단, 화면 코드를 Model 안에 넣으면 안 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MemberModel은 데이터와 기능만 담당해야 합니다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; margin-top: 15px; margin-bottom: 20px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;background-color: #f5f8ff;&quot;&gt;
&lt;td style=&quot;width: 35%; padding: 10px;&quot;&gt;&lt;b&gt;기능&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 65%; padding: 10px;&quot;&gt;&lt;b&gt;설명&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;register_member()&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;새 회원 정보를 members 목록에 추가합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;is_duplicate_id()&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;이미 존재하는 아이디인지 확인합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;check_login()&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;여러 회원 중 ID와 password_hash가 일치하는 회원을 찾습니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;get_member_info()&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;현재 로그인한 회원 정보를 반환합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;update_profile()&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;현재 로그인한 회원의 이름, 이메일, 전화번호를 수정합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;3.2 회원가입 입력 검증 조건&lt;/b&gt;&lt;/h2&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1672&quot; data-origin-height=&quot;941&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bfOdhC/dJMcadB7uJa/0Jwt8tKUlPl8zyJbKkT38K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bfOdhC/dJMcadB7uJa/0Jwt8tKUlPl8zyJbKkT38K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bfOdhC/dJMcadB7uJa/0Jwt8tKUlPl8zyJbKkT38K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbfOdhC%2FdJMcadB7uJa%2F0Jwt8tKUlPl8zyJbKkT38K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1672&quot; height=&quot;941&quot; data-origin-width=&quot;1672&quot; data-origin-height=&quot;941&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p data-ke-size=&quot;size16&quot;&gt;회원가입 버튼을 눌렀을 때 바로 저장하면 안 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 입력값이 올바른지 확인해야 합니다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; margin-top: 15px; margin-bottom: 20px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;background-color: #f5f8ff;&quot;&gt;
&lt;td style=&quot;width: 30%; padding: 10px;&quot;&gt;&lt;b&gt;검증 항목&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 70%; padding: 10px;&quot;&gt;&lt;b&gt;조건&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;빈 값 검사&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;ID, PW, PW 확인, 이름, 이메일, 전화번호가 비어 있으면 가입할 수 없습니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;비밀번호 확인&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;PW와 PW 확인 값이 다르면 가입할 수 없습니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;아이디 중복 검사&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;이미 같은 ID가 있으면 가입할 수 없습니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;비밀번호 해시 저장&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;PW는 그대로 저장하지 않고 password_hash로 저장해야 합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;div style=&quot;background-color: #f8fbff; border: 1px solid #d8e8ff; border-radius: 10px; padding: 18px; margin: 25px 0;&quot;&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;주의할 점:&lt;/b&gt; 비밀번호는 절대 &quot;pw&quot;: &quot;1234&quot; 형태로 저장하지 않고, 반드시 &quot;password_hash&quot;: &quot;...&quot; 형태로 저장해야 합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h1&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;4.&lt;/span&gt; member.json 파일 구조 설계하기&lt;/h1&gt;
&lt;p style=&quot;color: #9d9d9d;&quot; data-ke-size=&quot;size14&quot;&gt;&amp;rarr; 회원 1명 저장 구조를 여러 회원 목록 저장 구조로 확장합니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot; data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1693&quot; data-origin-height=&quot;929&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/u8tM2/dJMcaayKTGb/7N5bHqkyNCtML0KjgylWa1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/u8tM2/dJMcaayKTGb/7N5bHqkyNCtML0KjgylWa1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/u8tM2/dJMcaayKTGb/7N5bHqkyNCtML0KjgylWa1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fu8tM2%2FdJMcaayKTGb%2F7N5bHqkyNCtML0KjgylWa1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1693&quot; height=&quot;929&quot; data-origin-width=&quot;1693&quot; data-origin-height=&quot;929&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;4.1 기존 파일 구조&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 구조는 회원 1명의 정보만 저장했습니다.&lt;/p&gt;
&lt;pre class=&quot;perl&quot; data-ke-language=&quot;text&quot;&gt;&lt;code&gt;# 기존 member.json 구조

{
    &quot;id&quot;: &quot;test&quot;,
    &quot;password_hash&quot;: &quot;03ac674216f3e15c...&quot;,
    &quot;name&quot;: &quot;홍길동&quot;,
    &quot;email&quot;: &quot;test@test.com&quot;,
    &quot;phone&quot;: &quot;010-1234-5678&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 구조는 간단하지만 여러 회원을 저장하기 어렵습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;4.2 새 파일 구조&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 과제에서는 여러 회원을 저장하기 위해 members 목록을 사용합니다.&lt;/p&gt;
&lt;pre class=&quot;perl&quot; data-ke-language=&quot;text&quot;&gt;&lt;code&gt;# 새 member.json 구조

{
    &quot;members&quot;: [
        {
            &quot;id&quot;: &quot;test&quot;,
            &quot;password_hash&quot;: &quot;03ac674216f3e15c...&quot;,
            &quot;name&quot;: &quot;홍길동&quot;,
            &quot;email&quot;: &quot;test@test.com&quot;,
            &quot;phone&quot;: &quot;010-1234-5678&quot;
        },
        {
            &quot;id&quot;: &quot;kim&quot;,
            &quot;password_hash&quot;: &quot;a8b3f2...&quot;,
            &quot;name&quot;: &quot;김철수&quot;,
            &quot;email&quot;: &quot;kim@test.com&quot;,
            &quot;phone&quot;: &quot;010-9999-8888&quot;
        }
    ]
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회원가입을 하면 새 회원 딕셔너리가 members 목록에 추가되어야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그인할 때는 members 목록 안에서 입력한 ID와 password_hash가 일치하는 회원을 찾아야 합니다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; margin-top: 15px; margin-bottom: 20px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;background-color: #f5f8ff;&quot;&gt;
&lt;td style=&quot;width: 30%; padding: 10px;&quot;&gt;&lt;b&gt;항목&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 70%; padding: 10px;&quot;&gt;&lt;b&gt;설명&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;members&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;여러 회원 정보를 담는 리스트입니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;id&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;로그인할 때 사용할 회원 아이디입니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;password_hash&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;비밀번호를 해시로 변환한 값입니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;name, email, phone&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;메인화면과 프로필 화면에 표시할 회원 정보입니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;div style=&quot;background-color: #f8fbff; border: 1px solid #d8e8ff; border-radius: 10px; padding: 18px; margin: 25px 0;&quot;&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;핵심 확인:&lt;/b&gt; 회원가입 과제에서는 회원 정보를 하나의 딕셔너리가 아니라 members 리스트 안에 여러 개의 딕셔너리로 저장해야 합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h1&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;5.&lt;/span&gt; 회원가입부터 로그인까지 동작 흐름 살펴보기&lt;/h1&gt;
&lt;p style=&quot;color: #9d9d9d;&quot; data-ke-size=&quot;size14&quot;&gt;&amp;rarr; 회원가입 성공, 파일 저장, 로그인 성공, 프로필 수정 흐름을 단계별로 정리합니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot; data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1672&quot; data-origin-height=&quot;941&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c9UCbM/dJMcaaer4kG/zZPQXJ23sp9Lvm7BHkO1oK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c9UCbM/dJMcaaer4kG/zZPQXJ23sp9Lvm7BHkO1oK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c9UCbM/dJMcaaer4kG/zZPQXJ23sp9Lvm7BHkO1oK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc9UCbM%2FdJMcaaer4kG%2FzZPQXJ23sp9Lvm7BHkO1oK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1672&quot; height=&quot;941&quot; data-origin-width=&quot;1672&quot; data-origin-height=&quot;941&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;5.1 회원가입 성공 흐름&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회원가입 버튼을 눌렀을 때 성공하면 다음 순서로 동작해야 합니다.&lt;/p&gt;
&lt;pre class=&quot;gauss&quot; data-ke-language=&quot;text&quot;&gt;&lt;code&gt;# 회원가입 성공 흐름

회원가입 View에서 정보 입력
    &amp;darr;
회원가입 버튼 클릭
    &amp;darr;
빈 값 검사
    &amp;darr;
비밀번호와 비밀번호 확인 비교
    &amp;darr;
아이디 중복 검사
    &amp;darr;
비밀번호를 password_hash로 변환
    &amp;darr;
members 목록에 새 회원 추가
    &amp;darr;
member.json 파일에 저장
    &amp;darr;
회원가입 성공 메시지 출력
    &amp;darr;
로그인 View 탭으로 이동&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회원가입이 성공하면 로그인 View로 이동하도록 구현하면 사용자가 바로 로그인할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;5.2 회원가입 실패 흐름&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;입력값이 올바르지 않으면 회원가입을 진행하지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실패 이유를 메시지로 출력하고 회원가입 View에 그대로 머무릅니다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; margin-top: 15px; margin-bottom: 20px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;background-color: #f5f8ff;&quot;&gt;
&lt;td style=&quot;width: 35%; padding: 10px;&quot;&gt;&lt;b&gt;실패 상황&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 65%; padding: 10px;&quot;&gt;&lt;b&gt;출력 메시지 예시&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;빈 값이 있음&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;모든 값을 입력하세요.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;비밀번호 확인 불일치&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;비밀번호가 서로 다릅니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;아이디 중복&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;이미 사용 중인 아이디입니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;5.3 가입한 회원으로 로그인하는 흐름&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회원가입이 끝나면 가입한 ID와 PW로 로그인할 수 있어야 합니다.&lt;/p&gt;
&lt;pre class=&quot;gauss&quot; data-ke-language=&quot;text&quot;&gt;&lt;code&gt;# 가입한 회원 로그인 흐름

로그인 View에서 ID, PW 입력
    &amp;darr;
로그인 버튼 클릭
    &amp;darr;
입력한 PW를 hash_password()로 변환
    &amp;darr;
members 목록에서 같은 ID 찾기
    &amp;darr;
저장된 password_hash와 입력 PW 해시값 비교
    &amp;darr;
일치하면 로그인 성공
    &amp;darr;
현재 로그인한 회원 정보를 저장
    &amp;darr;
메인화면 View 탭으로 이동&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그인에 성공하면 현재 로그인한 회원 정보를 기준으로 메인화면과 프로필 화면이 갱신되어야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;5.4 프로필 수정 흐름&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로필 수정은 전체 회원이 아니라 현재 로그인한 회원에게만 적용되어야 합니다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot; data-ke-language=&quot;text&quot;&gt;&lt;code&gt;# 프로필 수정 흐름

프로필 View에서 이름, 이메일, 전화번호 수정
    &amp;darr;
수정하기 버튼 클릭
    &amp;darr;
현재 로그인한 회원 찾기
    &amp;darr;
해당 회원의 name, email, phone 수정
    &amp;darr;
member.json 파일에 저장
    &amp;darr;
메인화면 View와 프로필 View 갱신&lt;/code&gt;&lt;/pre&gt;
&lt;div style=&quot;background-color: #f8fbff; border: 1px solid #d8e8ff; border-radius: 10px; padding: 18px; margin: 25px 0;&quot;&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;중요:&lt;/b&gt; 여러 회원을 저장할 때는 프로필 수정이 모든 회원에게 적용되지 않도록, 현재 로그인한 회원만 찾아서 수정해야 합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h1&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;6.&lt;/span&gt; 구현 조건과 제출 전 체크리스트 정리하기&lt;/h1&gt;
&lt;p style=&quot;color: #9d9d9d;&quot; data-ke-size=&quot;size14&quot;&gt;&amp;rarr; 과제에서 반드시 지켜야 할 조건과 제출 전 확인 항목을 정리합니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot; data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;6.1 핵심 구현 조건&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반드시 다음 조건을 지켜서 구현하세요.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; margin-top: 15px; margin-bottom: 20px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;background-color: #f5f8ff;&quot;&gt;
&lt;td style=&quot;width: 15%; padding: 10px;&quot;&gt;&lt;b&gt;번호&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 85%; padding: 10px;&quot;&gt;&lt;b&gt;조건&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;1&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;QTabWidget에 회원가입 View를 추가합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;2&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;회원가입 View에서 ID, PW, PW 확인, 이름, 이메일, 전화번호를 입력받습니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;3&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;회원가입 시 빈 값 검사를 수행합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;4&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;PW와 PW 확인 값이 같은지 검사합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;5&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;이미 존재하는 ID는 가입할 수 없도록 중복 검사를 합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;6&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;비밀번호는 그대로 저장하지 않고 password_hash로 저장합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;7&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;member.json에는 members 리스트 구조로 여러 회원을 저장합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;8&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;회원가입한 ID/PW로 로그인할 수 있어야 합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;9&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;로그인 성공 시 메인화면과 프로필 화면에 현재 로그인한 회원 정보가 출력되어야 합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;10&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;프로필 수정 시 현재 로그인한 회원 정보만 변경되어야 합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;6.2 권장 프로젝트 구조&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트 구조는 아래처럼 구성하는 것을 권장합니다.&lt;/p&gt;
&lt;pre class=&quot;vim&quot; data-ke-language=&quot;text&quot;&gt;&lt;code&gt;# 권장 프로젝트 구조

signup_login_hash_project/
    ├── main.py
    ├── member_model.py
    ├── member_storage.py
    ├── password_utils.py
    └── tab_window.py&lt;/code&gt;&lt;/pre&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; margin-top: 15px; margin-bottom: 20px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;background-color: #f5f8ff;&quot;&gt;
&lt;td style=&quot;width: 30%; padding: 10px;&quot;&gt;&lt;b&gt;파일&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 70%; padding: 10px;&quot;&gt;&lt;b&gt;역할&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;main.py&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;QApplication을 만들고 프로그램을 실행합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;member_model.py&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;회원가입, 로그인 검사, 프로필 수정을 담당합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;member_storage.py&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;member.json 파일 저장과 불러오기를 담당합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;password_utils.py&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;비밀번호를 password_hash로 변환하는 함수를 담당합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;tab_window.py&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;QTabWidget과 각 View 화면 구성을 담당합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;6.3 제출 전 확인 사항&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제출하기 전에 아래 항목을 직접 확인하세요.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; margin-top: 15px; margin-bottom: 20px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;background-color: #f5f8ff;&quot;&gt;
&lt;td style=&quot;width: 80%; padding: 10px;&quot;&gt;&lt;b&gt;확인 항목&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 20%; padding: 10px;&quot;&gt;&lt;b&gt;완료&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;회원가입 View가 추가되었는가?&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;□&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;ID, PW, PW 확인, 이름, 이메일, 전화번호 입력칸이 있는가?&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;□&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;빈 값이면 회원가입이 되지 않는가?&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;□&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;PW와 PW 확인이 다르면 회원가입이 되지 않는가?&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;□&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;중복 ID이면 회원가입이 되지 않는가?&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;□&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;비밀번호가 password_hash로 저장되는가?&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;□&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;member.json에 새 회원이 members 목록으로 추가되는가?&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;□&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;가입한 회원으로 로그인할 수 있는가?&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;□&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;로그인 후 메인화면과 프로필 화면에 해당 회원 정보가 보이는가?&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;□&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;프로필 수정 시 현재 로그인한 회원 정보만 수정되는가?&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;□&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;View마다 회원 정보를 따로 저장하지 않고 하나의 MemberModel을 공유하는가?&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;□&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;6.4 선택 구현과 심화 구현&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본 구현을 완료했다면 아래 기능을 추가로 구현해 볼 수 있습니다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; margin-top: 15px; margin-bottom: 20px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;background-color: #f5f8ff;&quot;&gt;
&lt;td style=&quot;width: 25%; padding: 10px;&quot;&gt;&lt;b&gt;구분&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 75%; padding: 10px;&quot;&gt;&lt;b&gt;내용&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;선택 구현&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;회원가입 성공 후 입력칸 초기화, 로그인 탭 자동 이동, 오류 메시지 자세히 표시&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;심화 구현&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;로그아웃 기능, 회원 탈퇴 기능, 비밀번호 변경 기능, salt를 추가한 비밀번호 해시 저장&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;div style=&quot;background-color: #f8fbff; border: 1px solid #d8e8ff; border-radius: 10px; padding: 18px; margin: 25px 0;&quot;&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;기억할 문장:&lt;/b&gt; 회원가입 기능은 새 회원을 추가하는 기능이고, 비밀번호는 그대로 저장하지 않고 password_hash로 저장해야 합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h1&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;참고.&lt;/span&gt; 공식 문서로 확인하기&lt;/h1&gt;
&lt;p style=&quot;color: #9d9d9d;&quot; data-ke-size=&quot;size14&quot;&gt;&amp;rarr; QTabWidget, JSON 저장, hashlib 사용법은 공식 문서에서 확인할 수 있습니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot; data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;참고 문서&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 과제에서 사용하는 주요 개념은 아래 공식 문서에서 더 자세히 확인할 수 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://doc.qt.io/qtforpython-6/PySide6/QtWidgets/QTabWidget.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;PySide6 QTabWidget 공식 문서&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://doc.qt.io/qtforpython-6/PySide6/QtWidgets/QLineEdit.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;PySide6 QLineEdit 공식 문서&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.python.org/ko/3/library/json.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Python json 공식 문서&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.python.org/ko/3/library/hashlib.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Python hashlib 공식 문서&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div style=&quot;background-color: #f8fbff; border: 1px solid #d8e8ff; border-radius: 10px; padding: 18px; margin: 25px 0;&quot;&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;참고:&lt;/b&gt; 이번 과제에서는 학습용으로 SHA-256 해시를 사용할 수 있습니다. 실제 서비스에서는 salt와 bcrypt, scrypt, argon2 같은 비밀번호 저장 전용 방식을 검토해야 합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;</description>
      <category>3. PySide6/3-5. File</category>
      <author>BasicLike</author>
      <guid isPermaLink="true">https://basiclike.tistory.com/785</guid>
      <comments>https://basiclike.tistory.com/785#entry785comment</comments>
      <pubDate>Mon, 1 Jun 2026 14:38:50 +0900</pubDate>
    </item>
    <item>
      <title>7강. 비밀번호를 해시로 저장하고 로그인 검증하기</title>
      <link>https://basiclike.tistory.com/784</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;436&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lo3WI/dJMcabYJEak/ukOYmjweuuT4PuW8bFU9KK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lo3WI/dJMcabYJEak/ukOYmjweuuT4PuW8bFU9KK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lo3WI/dJMcabYJEak/ukOYmjweuuT4PuW8bFU9KK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Flo3WI%2FdJMcabYJEak%2FukOYmjweuuT4PuW8bFU9KK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;436&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;436&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h1&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;0.&lt;/span&gt; 학습 목표&lt;/h1&gt;
&lt;p style=&quot;color: #9d9d9d;&quot; data-ke-size=&quot;size14&quot;&gt;&amp;rarr; 비밀번호를 평문으로 저장하지 않고, 해시값으로 저장한 뒤 로그인할 때 안전하게 비교하는 구조를 학습합니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot; data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;이번 글에서 다룰 내용&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글에서는 로그인 회원 정보 중 &lt;b&gt;비밀번호를 그대로 저장하지 않는 방법&lt;/b&gt;을 학습합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;6강에서는 회원 정보를 &lt;b&gt;member.json&lt;/b&gt; 파일에 저장하고 다시 불러오는 구조를 만들었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 비밀번호가 &lt;b&gt;1234&lt;/b&gt;처럼 그대로 파일에 저장되면 누구나 파일을 열어 비밀번호를 확인할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 이번 강의에서는 비밀번호를 Hash&lt;sup&gt;해시&lt;/sup&gt; 값으로 바꾼 뒤 저장합니다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; margin-top: 15px; margin-bottom: 20px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;background-color: #f5f8ff;&quot;&gt;
&lt;td style=&quot;width: 30%; padding: 10px;&quot;&gt;&lt;b&gt;구분&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 70%; padding: 10px;&quot;&gt;&lt;b&gt;내용&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;핵심 개념&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;비밀번호를 평문으로 저장하지 않고 해시값으로 저장합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;실습 준비&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;Python의 hashlib 모듈, 기존 member.json 저장 구조, PySide6 로그인 화면&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;최종 목표&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;member.json에는 pw 대신 password_hash를 저장하고, 로그인할 때 입력 비밀번호를 해시로 바꿔 비교합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1693&quot; data-origin-height=&quot;929&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/d0ETwl/dJMcaaZMM9I/2fj5X27YXSQfdgvrjEgzgk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/d0ETwl/dJMcaaZMM9I/2fj5X27YXSQfdgvrjEgzgk/img.png&quot; data-alt=&quot;평문 비밀번호 저장 방식과 해시 저장 방식 비교&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/d0ETwl/dJMcaaZMM9I/2fj5X27YXSQfdgvrjEgzgk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fd0ETwl%2FdJMcaaZMM9I%2F2fj5X27YXSQfdgvrjEgzgk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1693&quot; height=&quot;929&quot; data-origin-width=&quot;1693&quot; data-origin-height=&quot;929&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;평문 비밀번호 저장 방식과 해시 저장 방식 비교&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;div style=&quot;background-color: #f8fbff; border: 1px solid #d8e8ff; border-radius: 10px; padding: 18px; margin: 25px 0;&quot;&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;이번 단계의 핵심:&lt;/b&gt; 비밀번호는 다시 꺼내서 보여주는 값이 아니라, 입력값과 비교하기 위한 해시값으로 저장해야 합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h1&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;1.&lt;/span&gt; 비밀번호를 그대로 저장하면 왜 위험할까?&lt;/h1&gt;
&lt;p style=&quot;color: #9d9d9d;&quot; data-ke-size=&quot;size14&quot;&gt;&amp;rarr; member.json 파일에 비밀번호가 그대로 보이는 문제를 확인합니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot; data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1.1 6강에서 만든 member.json 구조&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;6강에서는 회원 정보를 member.json 파일에 저장했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 현재 구조에서는 비밀번호가 아래처럼 그대로 보일 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;perl&quot; data-ke-language=&quot;text&quot;&gt;&lt;code&gt;# 기존 member.json

{
    &quot;id&quot;: &quot;test&quot;,
    &quot;pw&quot;: &quot;1234&quot;,
    &quot;name&quot;: &quot;홍길동&quot;,
    &quot;email&quot;: &quot;test@test.com&quot;,
    &quot;phone&quot;: &quot;010-1234-5678&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 저장된 비밀번호를 &lt;b&gt;평문 비밀번호&lt;/b&gt;라고 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;평문은 사람이 바로 읽을 수 있는 원래 문장이나 값을 의미합니다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; margin-top: 15px; margin-bottom: 20px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;background-color: #f5f8ff;&quot;&gt;
&lt;td style=&quot;width: 30%; padding: 10px;&quot;&gt;&lt;b&gt;저장 방식&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 70%; padding: 10px;&quot;&gt;&lt;b&gt;문제점&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;&quot;pw&quot;: &quot;1234&quot;&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;파일을 열면 비밀번호가 그대로 보입니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;코드 안에 비밀번호 작성&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;코드를 보는 사람이 비밀번호를 알 수 있습니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;파일에 비밀번호 그대로 저장&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;파일이 노출되면 비밀번호도 함께 노출됩니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1.2 암호화와 해시의 차이&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비밀번호를 안전하게 저장한다고 할 때 흔히 &amp;ldquo;암호화&amp;rdquo;라는 말을 사용합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 로그인 비밀번호 저장에서는 보통 Encryption&lt;sup&gt;암호화&lt;/sup&gt;보다 해시 방식을 사용합니다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; margin-top: 15px; margin-bottom: 20px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;background-color: #f5f8ff;&quot;&gt;
&lt;td style=&quot;width: 25%; padding: 10px;&quot;&gt;&lt;b&gt;구분&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 75%; padding: 10px;&quot;&gt;&lt;b&gt;설명&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;암호화&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;비밀번호를 암호문으로 바꾸고, 필요하면 다시 원래 비밀번호로 복호화할 수 있습니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;해시&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;비밀번호를 해시값으로 바꾸고, 원래 비밀번호로 되돌리기 어렵습니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre class=&quot;vala&quot; data-ke-language=&quot;text&quot;&gt;&lt;code&gt;# 암호화 개념

비밀번호
    &amp;darr;
암호문
    &amp;darr;
다시 원래 비밀번호로 복호화 가능&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;vala&quot; data-ke-language=&quot;text&quot;&gt;&lt;code&gt;# 해시 개념

비밀번호
    &amp;darr;
해시값
    &amp;darr;
원래 비밀번호로 되돌리기 어려움&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1.3 이번 강의에서 바꿀 저장 구조&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 강의에서는 member.json에 &lt;b&gt;pw&lt;/b&gt;를 그대로 저장하지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대신 &lt;b&gt;password_hash&lt;/b&gt;라는 이름으로 해시값을 저장합니다.&lt;/p&gt;
&lt;pre class=&quot;perl&quot; data-ke-language=&quot;text&quot;&gt;&lt;code&gt;# 새 member.json 구조 예시

{
    &quot;id&quot;: &quot;test&quot;,
    &quot;password_hash&quot;: &quot;03ac674216f3e15c...&quot;,
    &quot;name&quot;: &quot;홍길동&quot;,
    &quot;email&quot;: &quot;test@test.com&quot;,
    &quot;phone&quot;: &quot;010-1234-5678&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그인할 때는 사용자가 입력한 비밀번호를 다시 해시값으로 바꿉니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 저장된 password_hash와 비교합니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #f8fbff; border: 1px solid #d8e8ff; border-radius: 10px; padding: 18px; margin: 25px 0;&quot;&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;문제의 핵심:&lt;/b&gt; 비밀번호 원문을 저장하지 않고, 비밀번호를 변환한 해시값만 저장해야 합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h1&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;2.&lt;/span&gt; 비밀번호를 해시로 바꾸는 함수 만들기&lt;/h1&gt;
&lt;p style=&quot;color: #9d9d9d;&quot; data-ke-size=&quot;size14&quot;&gt;&amp;rarr; hashlib를 사용해 입력 비밀번호를 해시 문자열로 변환합니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot; data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2.1 프로젝트 구조 수정하기&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;6강에서 만든 파일 저장 구조를 그대로 사용합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 강의에서는 비밀번호 해시 처리를 담당하는 &lt;b&gt;password_utils.py&lt;/b&gt; 파일만 추가합니다.&lt;/p&gt;
&lt;pre class=&quot;vim&quot; data-ke-language=&quot;text&quot;&gt;&lt;code&gt;# 프로젝트 구조

tab_login_file_storage/
    ├── main.py
    ├── member_model.py
    ├── member_storage.py
    ├── password_utils.py
    └── tab_window.py&lt;/code&gt;&lt;/pre&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; margin-top: 15px; margin-bottom: 20px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;background-color: #f5f8ff;&quot;&gt;
&lt;td style=&quot;width: 30%; padding: 10px;&quot;&gt;&lt;b&gt;파일&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 70%; padding: 10px;&quot;&gt;&lt;b&gt;역할&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;password_utils.py&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;비밀번호를 해시값으로 바꾸는 함수를 담당합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;member_storage.py&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;member.json 파일 저장과 불러오기를 담당합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;member_model.py&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;로그인 검사와 프로필 수정을 담당합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;tab_window.py&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;화면과 버튼 동작을 담당합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2.2 password_utils.py 작성하기&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 비밀번호를 해시값으로 바꾸는 함수를 만듭니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Python의 hashlib 모듈을 사용하면 문자열을 해시값으로 바꿀 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;python&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;import hashlib


def hash_password(password):
    &quot;&quot;&quot;입력한 비밀번호를 SHA-256 해시 문자열로 변환합니다.&quot;&quot;&quot;
    return hashlib.sha256(password.encode(&quot;utf-8&quot;)).hexdigest()&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 함수는 사용자가 입력한 비밀번호를 UTF-8 바이트로 바꾼 뒤 SHA-256 해시값으로 변환합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막의 hexdigest()는 해시 결과를 사람이 읽을 수 있는 16진수 문자열로 바꿉니다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; margin-top: 15px; margin-bottom: 20px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;background-color: #f5f8ff;&quot;&gt;
&lt;td style=&quot;width: 40%; padding: 10px;&quot;&gt;&lt;b&gt;코드&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 60%; padding: 10px;&quot;&gt;&lt;b&gt;역할&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;password.encode(&quot;utf-8&quot;)&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;문자열 비밀번호를 바이트 데이터로 바꿉니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;hashlib.sha256(...)&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;SHA-256 해시 계산을 수행합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;hexdigest()&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;해시 결과를 16진수 문자열로 반환합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2.3 해시 결과 확인하기&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 비밀번호가 1234라면 아래처럼 해시값으로 바뀝니다.&lt;/p&gt;
&lt;pre class=&quot;coffeescript&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;from password_utils import hash_password

print(hash_password(&quot;1234&quot;))&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행 결과는 아래처럼 긴 문자열입니다.&lt;/p&gt;
&lt;pre class=&quot;llvm&quot; data-ke-language=&quot;text&quot;&gt;&lt;code&gt;03ac674216f3e15c761ee1a5e255f067953623c8b388b4459e13f978d7c846f4&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 member.json에는 1234를 그대로 저장하지 않고, 위와 같은 해시 문자열을 저장할 수 있습니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #f8fbff; border: 1px solid #d8e8ff; border-radius: 10px; padding: 18px; margin: 25px 0;&quot;&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;핵심 확인:&lt;/b&gt; 같은 비밀번호는 같은 해시값으로 바뀝니다. 그래서 로그인할 때 입력한 비밀번호를 다시 해시로 바꿔 저장된 해시값과 비교할 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h1&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;3.&lt;/span&gt; member.json에 password_hash 저장하기&lt;/h1&gt;
&lt;p style=&quot;color: #9d9d9d;&quot; data-ke-size=&quot;size14&quot;&gt;&amp;rarr; 기존 pw 저장 구조를 password_hash 저장 구조로 변경합니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot; data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;3.1 member_storage.py 수정하기&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 기본 회원 정보를 만들 때 pw 대신 password_hash를 저장하도록 바꿉니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 6강에서 만든 member.json 파일에 pw가 남아 있을 수도 있으므로, 기존 pw 값을 password_hash로 바꿔주는 처리도 함께 넣습니다.&lt;/p&gt;
&lt;pre class=&quot;python&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;import json

from PySide6.QtCore import QFile, QIODevice, QTextStream

from password_utils import hash_password


class MemberStorage:
    &quot;&quot;&quot;회원 정보를 JSON 파일에 저장하고 불러오는 클래스&quot;&quot;&quot;

    def __init__(self, file_path=&quot;member.json&quot;):
        self.file_path = file_path

    def default_member(self):
        return {
            &quot;id&quot;: &quot;test&quot;,
            &quot;password_hash&quot;: hash_password(&quot;1234&quot;),
            &quot;name&quot;: &quot;홍길동&quot;,
            &quot;email&quot;: &quot;test@test.com&quot;,
            &quot;phone&quot;: &quot;010-1234-5678&quot;,
        }

    def load_member(self):
        if not QFile.exists(self.file_path):
            member = self.default_member()
            self.save_member(member)
            return member

        file = QFile(self.file_path)

        if not file.open(QIODevice.OpenModeFlag.ReadOnly | QIODevice.OpenModeFlag.Text):
            return self.default_member()

        stream = QTextStream(file)
        json_text = stream.readAll()
        file.close()

        try:
            member = json.loads(json_text)
        except json.JSONDecodeError:
            member = self.default_member()
            self.save_member(member)
            return member

        member = self.convert_plain_password(member)
        self.save_member(member)

        return member

    def convert_plain_password(self, member):
        if &quot;pw&quot; in member and &quot;password_hash&quot; not in member:
            member[&quot;password_hash&quot;] = hash_password(member[&quot;pw&quot;])
            del member[&quot;pw&quot;]

        return member

    def save_member(self, member):
        file = QFile(self.file_path)

        if not file.open(QIODevice.OpenModeFlag.WriteOnly | QIODevice.OpenModeFlag.Text):
            return False

        stream = QTextStream(file)
        stream &amp;lt;&amp;lt; json.dumps(member, ensure_ascii=False, indent=4)
        file.close()

        return True&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;3.2 default_member() 변경 확인&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존에는 기본 회원 정보에 pw가 들어 있었습니다.&lt;/p&gt;
&lt;pre class=&quot;vala&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;# 기존 방식

&quot;pw&quot;: &quot;1234&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제는 password_hash에 해시값을 저장합니다.&lt;/p&gt;
&lt;pre class=&quot;vala&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;# 새 방식

&quot;password_hash&quot;: hash_password(&quot;1234&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 member.json 파일에는 비밀번호 1234가 직접 저장되지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;3.3 기존 pw를 password_hash로 바꾸는 이유&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이미 6강을 실행했다면 프로젝트 폴더에 기존 member.json 파일이 있을 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 파일에는 아직 pw가 남아 있을 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;perl&quot; data-ke-language=&quot;text&quot;&gt;&lt;code&gt;# 기존 파일에 남아 있을 수 있는 구조

{
    &quot;id&quot;: &quot;test&quot;,
    &quot;pw&quot;: &quot;1234&quot;,
    &quot;name&quot;: &quot;홍길동&quot;,
    &quot;email&quot;: &quot;test@test.com&quot;,
    &quot;phone&quot;: &quot;010-1234-5678&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 load_member()에서 파일을 읽은 뒤 convert_plain_password()를 호출합니다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;member = self.convert_plain_password(member)
self.save_member(member)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 코드는 기존 pw 값을 password_hash로 바꾸고, 다시 member.json 파일에 저장합니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #f8fbff; border: 1px solid #d8e8ff; border-radius: 10px; padding: 18px; margin: 25px 0;&quot;&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;주의할 점:&lt;/b&gt; 기존 member.json 파일이 남아 있으면 새 코드만 작성해도 파일 내용이 바로 바뀌지 않을 수 있습니다. 그래서 기존 pw를 password_hash로 변환하는 처리를 넣어 줍니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h1&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;4.&lt;/span&gt; 로그인할 때 해시값으로 비교하기&lt;/h1&gt;
&lt;p style=&quot;color: #9d9d9d;&quot; data-ke-size=&quot;size14&quot;&gt;&amp;rarr; 입력한 비밀번호를 해시로 바꾼 뒤 저장된 password_hash와 비교합니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot; data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;4.1 member_model.py 수정하기&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 MemberModel의 로그인 검사 방식을 바꿉니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;입력한 비밀번호를 그대로 비교하지 않고, 해시값으로 바꾼 뒤 비교합니다.&lt;/p&gt;
&lt;pre class=&quot;python&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;from member_storage import MemberStorage
from password_utils import hash_password


class MemberModel:
    &quot;&quot;&quot;회원 정보 관리, 로그인 검사, 프로필 수정을 담당하는 클래스&quot;&quot;&quot;

    def __init__(self):
        self.storage = MemberStorage()
        self.member = self.storage.load_member()
        self.is_logged_in = False

    def check_login(self, user_id, user_pw):
        input_password_hash = hash_password(user_pw)

        if user_id == self.member[&quot;id&quot;] and input_password_hash == self.member[&quot;password_hash&quot;]:
            self.is_logged_in = True
            return True

        return False

    def get_member_info(self):
        if not self.is_logged_in:
            return None

        return {
            &quot;id&quot;: self.member[&quot;id&quot;],
            &quot;name&quot;: self.member[&quot;name&quot;],
            &quot;email&quot;: self.member[&quot;email&quot;],
            &quot;phone&quot;: self.member[&quot;phone&quot;],
        }

    def update_profile(self, name, email, phone):
        if not self.is_logged_in:
            return False

        self.member[&quot;name&quot;] = name
        self.member[&quot;email&quot;] = email
        self.member[&quot;phone&quot;] = phone

        return self.storage.save_member(self.member)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;4.2 check_login() 코드 흐름&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그인 검사는 아래 순서로 진행됩니다.&lt;/p&gt;
&lt;pre class=&quot;vala&quot; data-ke-language=&quot;text&quot;&gt;&lt;code&gt;# 로그인 검증 흐름

사용자가 ID, PW 입력
    &amp;darr;
입력한 PW를 hash_password()로 해시 변환
    &amp;darr;
입력 ID와 저장 ID 비교
    &amp;darr;
입력 PW 해시값과 저장 password_hash 비교
    &amp;darr;
둘 다 같으면 로그인 성공&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심 코드는 아래 부분입니다.&lt;/p&gt;
&lt;pre class=&quot;ini&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;input_password_hash = hash_password(user_pw)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 코드는 사용자가 입력한 비밀번호를 해시값으로 바꿉니다.&lt;/p&gt;
&lt;pre class=&quot;ini&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;input_password_hash == self.member[&quot;password_hash&quot;]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 코드는 입력 비밀번호의 해시값과 파일에 저장된 해시값이 같은지 비교합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;4.3 tab_window.py는 거의 그대로 사용하기&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;화면 코드인 tab_window.py는 거의 수정하지 않아도 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LoginView는 여전히 사용자가 입력한 ID와 PW를 MemberModel에 전달합니다.&lt;/p&gt;
&lt;pre class=&quot;css&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;if self.member_model.check_login(user_id, user_pw):
    self.message_label.setText(&quot;로그인 성공!&quot;)

    self.main_view.refresh()
    self.profile_view.refresh()

    self.tab_widget.setCurrentIndex(1)
else:
    self.message_label.setText(&quot;로그인 실패! 아이디 또는 비밀번호를 확인하세요.&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LoginView는 비밀번호가 해시로 저장되는지 직접 알 필요가 없습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비밀번호 해시 비교는 MemberModel이 담당합니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #f8fbff; border: 1px solid #d8e8ff; border-radius: 10px; padding: 18px; margin: 25px 0;&quot;&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;핵심 확인:&lt;/b&gt; View는 입력만 받고, 비밀번호를 어떻게 비교할지는 Model이 처리합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h1&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;5.&lt;/span&gt; 실행 결과 확인하기&lt;/h1&gt;
&lt;p style=&quot;color: #9d9d9d;&quot; data-ke-size=&quot;size14&quot;&gt;&amp;rarr; member.json에 pw가 사라지고 password_hash가 저장되는지 확인합니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot; data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;5.1 실행하기&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;터미널에서 프로젝트 폴더로 이동한 뒤 아래 명령어를 실행합니다.&lt;/p&gt;
&lt;pre class=&quot;vim&quot; data-ke-language=&quot;text&quot;&gt;&lt;code&gt;python main.py&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 member.json에 pw가 있었다면, 프로그램 실행 후 password_hash로 변환되어 다시 저장됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;5.2 member.json 확인하기&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로그램 실행 후 member.json 파일을 열어 봅니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 pw가 없어지고 password_hash가 저장되어 있어야 합니다.&lt;/p&gt;
&lt;pre class=&quot;perl&quot; data-ke-language=&quot;text&quot;&gt;&lt;code&gt;# 변경 후 member.json 예시

{
    &quot;id&quot;: &quot;test&quot;,
    &quot;name&quot;: &quot;홍길동&quot;,
    &quot;email&quot;: &quot;test@test.com&quot;,
    &quot;phone&quot;: &quot;010-1234-5678&quot;,
    &quot;password_hash&quot;: &quot;03ac674216f3e15c761ee1a5e255f067953623c8b388b4459e13f978d7c846f4&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 파일을 열어도 원래 비밀번호인 1234는 보이지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;5.3 로그인 테스트하기&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그인 화면에서 기존과 동일하게 입력합니다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; margin-top: 15px; margin-bottom: 20px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;background-color: #f5f8ff;&quot;&gt;
&lt;td style=&quot;width: 30%; padding: 10px;&quot;&gt;&lt;b&gt;항목&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 70%; padding: 10px;&quot;&gt;&lt;b&gt;입력값&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;아이디&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;test&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;비밀번호&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;1234&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;member.json에는 1234가 없지만 로그인은 성공해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜냐하면 입력한 1234를 다시 해시로 바꾼 뒤, 저장된 password_hash와 비교하기 때문입니다.&lt;/p&gt;
&lt;pre class=&quot;mipsasm&quot; data-ke-language=&quot;text&quot;&gt;&lt;code&gt;# 로그인 성공 흐름

입력 비밀번호: 1234
    &amp;darr;
hash_password(&quot;1234&quot;)
    &amp;darr;
03ac674216f3e15c...
    &amp;darr;
member.json의 password_hash와 비교
    &amp;darr;
같으면 로그인 성공&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;5.4 확인 체크리스트&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 항목을 직접 확인합니다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; margin-top: 15px; margin-bottom: 20px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;background-color: #f5f8ff;&quot;&gt;
&lt;td style=&quot;width: 80%; padding: 10px;&quot;&gt;&lt;b&gt;확인 항목&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 20%; padding: 10px;&quot;&gt;&lt;b&gt;완료&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;password_utils.py 파일을 만들었는가?&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;□&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;hash_password() 함수가 비밀번호를 해시 문자열로 바꾸는가?&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;□&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;member.json에서 pw가 사라지고 password_hash가 저장되는가?&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;□&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;로그인할 때 입력 비밀번호를 해시로 바꿔 비교하는가?&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;□&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;비밀번호 1234로 정상 로그인되는가?&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;□&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;틀린 비밀번호를 입력하면 로그인에 실패하는가?&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;□&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h1&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;6.&lt;/span&gt; 평문 저장 방식과 해시 저장 방식 비교하기&lt;/h1&gt;
&lt;p style=&quot;color: #9d9d9d;&quot; data-ke-size=&quot;size14&quot;&gt;&amp;rarr; 비밀번호를 그대로 저장하는 방식과 해시값으로 저장하는 방식을 비교하며 정리합니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot; data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;6.1 기존 방식&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 방식은 비밀번호를 그대로 저장하는 구조입니다.&lt;/p&gt;
&lt;pre class=&quot;mipsasm&quot; data-ke-language=&quot;text&quot;&gt;&lt;code&gt;# 기존 방식

member.json
    └── &quot;pw&quot;: &quot;1234&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방식은 구현은 쉽지만 파일을 열면 비밀번호가 바로 보입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;6.2 새 방식&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;새 방식은 비밀번호를 해시값으로 바꿔 저장하는 구조입니다.&lt;/p&gt;
&lt;pre class=&quot;mipsasm&quot; data-ke-language=&quot;text&quot;&gt;&lt;code&gt;# 새 방식

입력 비밀번호
    &amp;darr;
hash_password()
    &amp;darr;
password_hash
    &amp;darr;
member.json에 저장&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방식에서는 member.json을 열어도 원래 비밀번호가 바로 보이지 않습니다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; margin-top: 15px; margin-bottom: 20px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;background-color: #f5f8ff;&quot;&gt;
&lt;td style=&quot;width: 20%; padding: 10px;&quot;&gt;&lt;b&gt;구분&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 40%; padding: 10px;&quot;&gt;&lt;b&gt;평문 저장 방식&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 40%; padding: 10px;&quot;&gt;&lt;b&gt;해시 저장 방식&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;저장 값&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;&quot;pw&quot;: &quot;1234&quot;&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;&quot;password_hash&quot;: &quot;03ac...&quot;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;파일 확인&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;비밀번호가 그대로 보입니다.&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;원래 비밀번호가 바로 보이지 않습니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;로그인 비교&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;입력 비밀번호와 저장 비밀번호를 직접 비교합니다.&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;입력 비밀번호를 해시로 바꾼 뒤 password_hash와 비교합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;학습 목적&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;처음 구조 이해에는 쉽습니다.&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;더 안전한 저장 구조를 이해할 수 있습니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;6.3 실제 서비스에서는 더 안전한 방식이 필요합니다&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 강의에서는 학습을 위해 SHA-256을 사용했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 실제 서비스에서는 단순 SHA-256만으로 비밀번호를 저장하는 것은 충분하지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 서비스에서는 Salt&lt;sup&gt;솔트&lt;/sup&gt;를 추가하고, 비밀번호 저장에 특화된 더 느린 해시 알고리즘을 사용하는 것이 일반적입니다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; margin-top: 15px; margin-bottom: 20px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;background-color: #f5f8ff;&quot;&gt;
&lt;td style=&quot;width: 30%; padding: 10px;&quot;&gt;&lt;b&gt;구분&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 70%; padding: 10px;&quot;&gt;&lt;b&gt;설명&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;이번 강의&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;개념 학습을 위해 hashlib.sha256()을 사용합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;실제 서비스&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;salt, bcrypt, scrypt, argon2 같은 비밀번호 저장 전용 방식을 사용하는 것이 좋습니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;6.4 최종 정리&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글에서는 비밀번호를 평문으로 저장하지 않고 해시값으로 저장하는 구조를 만들었습니다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; margin-top: 15px; margin-bottom: 20px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;background-color: #f5f8ff;&quot;&gt;
&lt;td style=&quot;width: 30%; padding: 10px;&quot;&gt;&lt;b&gt;핵심 내용&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 70%; padding: 10px;&quot;&gt;&lt;b&gt;정리&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;문제 파악&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;member.json에 pw가 그대로 저장되면 비밀번호가 노출될 수 있습니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;문제 해결&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;hash_password() 함수를 만들어 비밀번호를 해시값으로 바꿨습니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;저장 구조 변경&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;pw 대신 password_hash를 member.json에 저장했습니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;로그인 검증&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;입력 비밀번호를 해시로 바꾼 뒤 저장된 password_hash와 비교했습니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;주의점&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;실제 서비스에서는 salt와 비밀번호 저장 전용 해시 알고리즘이 필요합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;div style=&quot;background-color: #f8fbff; border: 1px solid #d8e8ff; border-radius: 10px; padding: 18px; margin: 25px 0;&quot;&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;기억할 문장:&lt;/b&gt; 비밀번호는 그대로 저장하지 않고, 입력값을 해시로 바꿔 저장된 해시값과 비교해야 합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;6.4 프로젝트&lt;/b&gt;&lt;b&gt;&amp;nbsp;파일&lt;/b&gt;&lt;/h2&gt;
&lt;figure class=&quot;fileblock&quot; data-ke-align=&quot;alignCenter&quot;&gt;&lt;a href=&quot;https://blog.kakaocdn.net/dn/bR2M3j/dJMcagZYl7k/LOeJhV5ylk5ySYNLzpKZm1/657.zip?attach=1&amp;amp;knm=tfile.zip&quot; class=&quot;&quot;&gt;
    &lt;div class=&quot;image&quot;&gt;&lt;/div&gt;
    &lt;div class=&quot;desc&quot;&gt;&lt;div class=&quot;filename&quot;&gt;&lt;span class=&quot;name&quot;&gt;657.zip&lt;/span&gt;&lt;/div&gt;
&lt;div class=&quot;size&quot;&gt;0.01MB&lt;/div&gt;
&lt;/div&gt;
  &lt;/a&gt;&lt;/figure&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h1&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;참고.&lt;/span&gt; 공식 문서로 확인하기&lt;/h1&gt;
&lt;p style=&quot;color: #9d9d9d;&quot; data-ke-size=&quot;size14&quot;&gt;&amp;rarr; hashlib와 JSON 저장 방식의 자세한 내용은 공식 문서에서 확인합니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot; data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;참고 문서&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글에서 사용한 hashlib, json 모듈은 아래 문서에서 자세히 확인할 수 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.python.org/ko/3/library/hashlib.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Python hashlib 공식 문서&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.python.org/ko/3/library/json.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Python json 공식 문서&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://doc.qt.io/qtforpython-6/PySide6/QtCore/QFile.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;PySide6 QFile 공식 문서&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://doc.qt.io/qtforpython-6/PySide6/QtCore/QTextStream.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;PySide6 QTextStream 공식 문서&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div style=&quot;background-color: #f8fbff; border: 1px solid #d8e8ff; border-radius: 10px; padding: 18px; margin: 25px 0;&quot;&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;참고:&lt;/b&gt; 이번 강의의 SHA-256 방식은 해시 개념을 이해하기 위한 학습용입니다. 실제 서비스에서는 salt와 bcrypt, scrypt, argon2 같은 비밀번호 저장 전용 방식을 검토해야 합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;</description>
      <category>3. PySide6/3-5. File</category>
      <author>BasicLike</author>
      <guid isPermaLink="true">https://basiclike.tistory.com/784</guid>
      <comments>https://basiclike.tistory.com/784#entry784comment</comments>
      <pubDate>Mon, 1 Jun 2026 14:13:41 +0900</pubDate>
    </item>
    <item>
      <title>6강. Model/View 과제 구현</title>
      <link>https://basiclike.tistory.com/783</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;435&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Ys632/dJMcabJsRXg/1Hv1Vk4jou5z1zz6BGCWk0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Ys632/dJMcabJsRXg/1Hv1Vk4jou5z1zz6BGCWk0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Ys632/dJMcabJsRXg/1Hv1Vk4jou5z1zz6BGCWk0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FYs632%2FdJMcabJsRXg%2F1Hv1Vk4jou5z1zz6BGCWk0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;435&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;435&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h1&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;0.&lt;/span&gt; 학습 목표&lt;/h1&gt;
&lt;p style=&quot;color: #9d9d9d;&quot; data-ke-size=&quot;size14&quot;&gt;&amp;rarr; 5강 Model/View 과제를 실제 PySide6 코드로 구현하고, 하나의 Model을 여러 View가 공유하는 흐름을 확인합니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot; data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;이번 강의에서 구현할 내용&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 강의에서는 5강에서 과제로 제시한 &lt;a href=&quot;https://basiclike.tistory.com/783&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;b&gt;Model/View 과제&lt;/b&gt;&lt;/a&gt;를 실제 코드로 구현합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로그램은 &lt;b&gt;QTabWidget&lt;/b&gt;을 사용해서 로그인 화면, 메인화면, 프로필 화면을 탭으로 나누어 보여줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중요한 점은 각 화면이 회원 정보를 따로 저장하지 않고, 하나의 MemberModel&lt;sup&gt;회원 모델&lt;/sup&gt;을 함께 사용한다는 것입니다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; margin-top: 15px; margin-bottom: 20px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;background-color: #f5f8ff;&quot;&gt;
&lt;td style=&quot;width: 20.3423%; padding: 10px;&quot;&gt;&lt;b&gt;구분&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 79.6577%; padding: 10px;&quot;&gt;&lt;b&gt;내용&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px; width: 20.3423%;&quot;&gt;핵심 개념&lt;/td&gt;
&lt;td style=&quot;padding: 10px; width: 79.6577%;&quot;&gt;하나의 MemberModel을 LoginView, MainView, ProfileView가 함께 사용합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px; width: 20.3423%;&quot;&gt;실습 준비&lt;/td&gt;
&lt;td style=&quot;padding: 10px; width: 79.6577%;&quot;&gt;PySide6, QTabWidget, QWidget, QLabel, QLineEdit, QPushButton, QVBoxLayout, QHBoxLayout&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px; width: 20.3423%;&quot;&gt;최종 목표&lt;/td&gt;
&lt;td style=&quot;padding: 10px; width: 79.6577%;&quot;&gt;로그인 후 회원 정보를 출력하고, 프로필 수정 내용이 메인화면에도 반영되도록 구현합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1448&quot; data-origin-height=&quot;1086&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/oNMSs/dJMcaipZlvv/L6dBsMLRIFjmVuGOV61dok/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/oNMSs/dJMcaipZlvv/L6dBsMLRIFjmVuGOV61dok/img.png&quot; data-alt=&quot;하나의 MemberModel을 여러 View가 공유하는 전체 구조&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/oNMSs/dJMcaipZlvv/L6dBsMLRIFjmVuGOV61dok/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FoNMSs%2FdJMcaipZlvv%2FL6dBsMLRIFjmVuGOV61dok%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1448&quot; height=&quot;1086&quot; data-origin-width=&quot;1448&quot; data-origin-height=&quot;1086&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;하나의 MemberModel을 여러 View가 공유하는 전체 구조&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;div style=&quot;background-color: #f8fbff; border: 1px solid #d8e8ff; border-radius: 10px; padding: 18px; margin: 25px 0;&quot;&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;이번 단계의 핵심:&lt;/b&gt; View는 화면을 담당하고, Model은 데이터를 담당합니다. 여러 View가 같은 데이터를 보여줘야 할 때는 데이터를 Model 한 곳에서 관리해야 합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h1&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;1.&lt;/span&gt; 5강 과제 요구사항 다시 확인하기&lt;/h1&gt;
&lt;p style=&quot;color: #9d9d9d;&quot; data-ke-size=&quot;size14&quot;&gt;&amp;rarr; 구현하기 전에 과제에서 요구한 화면 구성과 핵심 조건을 다시 확인합니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot; data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1.1 과제에서 만들어야 하는 화면&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;5강 과제에서는 하나의 프로그램 안에 3개의 탭 화면을 만들어야 했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫 번째 탭은 로그인 화면입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 번째 탭은 로그인한 회원 정보를 보여주는 메인화면입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세 번째 탭은 회원 정보를 확인하고 수정하는 프로필 화면입니다.&lt;/p&gt;
&lt;pre class=&quot;gauss&quot; data-ke-language=&quot;text&quot;&gt;&lt;code&gt;# 탭 화면 구성

QTabWidget
    ├── 로그인 View
    ├── 메인화면 View
    └── 프로필 View&lt;/code&gt;&lt;/pre&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; margin-top: 15px; margin-bottom: 20px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;background-color: #f5f8ff;&quot;&gt;
&lt;td style=&quot;width: 30%; padding: 10px;&quot;&gt;&lt;b&gt;탭 이름&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 70%; padding: 10px;&quot;&gt;&lt;b&gt;역할&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;로그인 View&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;ID와 PW를 입력하고 로그인 성공 여부를 확인합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;메인화면 View&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;로그인한 회원 정보를 출력합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;프로필 View&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;회원 정보를 출력하고 이름, 이메일, 전화번호를 수정합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1.2 이번 구현에서 가장 중요한 조건&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 구현에서 가장 중요한 조건은 회원 정보를 View마다 따로 저장하지 않는 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회원 정보는 MemberModel 하나에만 저장합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 LoginView, MainView, ProfileView가 같은 MemberModel을 사용해야 합니다.&lt;/p&gt;
&lt;pre class=&quot;nginx&quot; data-ke-language=&quot;text&quot;&gt;&lt;code&gt;# 구현해야 하는 구조

MemberModel
    └── 회원 정보 저장

LoginView
    └── MemberModel 사용

MainView
    └── MemberModel 사용

ProfileView
    └── MemberModel 사용&lt;/code&gt;&lt;/pre&gt;
&lt;div style=&quot;background-color: #f8fbff; border: 1px solid #d8e8ff; border-radius: 10px; padding: 18px; margin: 25px 0;&quot;&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;문제의 핵심:&lt;/b&gt; 프로필 View에서 회원 정보를 수정했을 때, 메인화면 View에도 수정된 값이 보여야 합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h1&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;2.&lt;/span&gt; 프로젝트 구조 만들기&lt;/h1&gt;
&lt;p style=&quot;color: #9d9d9d;&quot; data-ke-size=&quot;size14&quot;&gt;&amp;rarr; Model과 View 역할을 구분하기 위해 파일을 나누어 준비합니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot; data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2.1 파일 구조&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 구현에서는 파일을 3개로 나눕니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 한 파일에 모두 작성할 수도 있지만, Model과 View 역할을 구분하기 위해 파일을 나누는 것이 좋습니다.&lt;/p&gt;
&lt;pre class=&quot;vim&quot; data-ke-language=&quot;text&quot;&gt;&lt;code&gt;# 프로젝트 구조

tab_model_view_login/
    ├── main.py
    ├── member_model.py
    └── tab_window.py&lt;/code&gt;&lt;/pre&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; margin-top: 15px; margin-bottom: 20px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;background-color: #f5f8ff;&quot;&gt;
&lt;td style=&quot;width: 30%; padding: 10px;&quot;&gt;&lt;b&gt;파일&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 70%; padding: 10px;&quot;&gt;&lt;b&gt;역할&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;main.py&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;QApplication을 만들고 프로그램을 실행합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;member_model.py&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;회원 정보 저장, 로그인 검사, 프로필 수정을 담당합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;tab_window.py&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;QTabWidget과 LoginView, MainView, ProfileView를 구성합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2.2 파일을 나누는 이유&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;member_model.py에는 회원 데이터와 관련된 코드만 넣습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;tab_window.py에는 화면과 버튼 동작 코드를 넣습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;main.py에는 프로그램 실행 코드만 넣습니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #f8fbff; border: 1px solid #d8e8ff; border-radius: 10px; padding: 18px; margin: 25px 0;&quot;&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;이번 단계의 핵심:&lt;/b&gt; 파일을 나누는 이유는 코드 양을 줄이기 위해서가 아니라, 역할을 분리해서 구조를 이해하기 쉽게 만들기 위해서입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h1&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;3.&lt;/span&gt; MemberModel 구현하기&lt;/h1&gt;
&lt;p style=&quot;color: #9d9d9d;&quot; data-ke-size=&quot;size14&quot;&gt;&amp;rarr; 회원 정보를 저장하고 로그인 검사와 프로필 수정 기능을 담당하는 Model을 만듭니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot; data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;3.1 member_model.py 코드&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 회원 정보를 저장하는 MemberModel을 구현합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 파일에는 화면을 만드는 코드가 들어가지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회원 정보와 관련된 기능만 작성합니다.&lt;/p&gt;
&lt;pre class=&quot;python&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;class MemberModel:
    def __init__(self):
        self.member = {
            &quot;id&quot;: &quot;test&quot;,
            &quot;pw&quot;: &quot;1234&quot;,
            &quot;name&quot;: &quot;홍길동&quot;,
            &quot;email&quot;: &quot;test@test.com&quot;,
            &quot;phone&quot;: &quot;010-1234-5678&quot;,
        }

    def check_login(self, user_id, user_pw):
        return user_id == self.member[&quot;id&quot;] and user_pw == self.member[&quot;pw&quot;]

    def get_member_info(self):
        return {
            &quot;id&quot;: self.member[&quot;id&quot;],
            &quot;name&quot;: self.member[&quot;name&quot;],
            &quot;email&quot;: self.member[&quot;email&quot;],
            &quot;phone&quot;: self.member[&quot;phone&quot;],
        }

    def update_profile(self, name, email, phone):
        self.member[&quot;name&quot;] = name
        self.member[&quot;email&quot;] = email
        self.member[&quot;phone&quot;] = phone&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;3.2 MemberModel 코드 역할&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MemberModel은 회원 정보를 하나의 딕셔너리로 저장합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 예제에서는 미리 만들어진 회원 정보를 사용합니다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; margin-top: 15px; margin-bottom: 20px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;background-color: #f5f8ff;&quot;&gt;
&lt;td style=&quot;width: 30%; padding: 10px;&quot;&gt;&lt;b&gt;메서드&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 70%; padding: 10px;&quot;&gt;&lt;b&gt;역할&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;check_login()&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;입력한 ID와 PW가 저장된 회원 정보와 같은지 확인합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;get_member_info()&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;화면에 출력할 회원 정보를 반환합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;update_profile()&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;프로필 화면에서 입력한 이름, 이메일, 전화번호를 Model에 반영합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 중요한 점은 MainView와 ProfileView가 회원 정보를 직접 만들지 않는다는 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 View는 필요할 때마다 MemberModel의 값을 가져와 화면에 보여줍니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #f8fbff; border: 1px solid #d8e8ff; border-radius: 10px; padding: 18px; margin: 25px 0;&quot;&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;핵심 확인:&lt;/b&gt; 회원 정보는 View 안에 저장하지 않고 MemberModel 안에만 저장합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h1&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;4.&lt;/span&gt; QTabWidget과 View 구현하기&lt;/h1&gt;
&lt;p style=&quot;color: #9d9d9d;&quot; data-ke-size=&quot;size14&quot;&gt;&amp;rarr; 로그인 View, 메인화면 View, 프로필 View를 만들고 하나의 MemberModel에 연결합니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot; data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;4.1 tab_window.py 전체 코드&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 화면을 구성하는 tab_window.py를 작성합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 파일에는 LoginView, MainView, ProfileView, TabWindow가 들어갑니다.&lt;/p&gt;
&lt;pre class=&quot;python&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;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(&quot;아이디를 입력하세요&quot;)

        self.pw_input = QLineEdit()
        self.pw_input.setPlaceholderText(&quot;비밀번호를 입력하세요&quot;)
        self.pw_input.setEchoMode(QLineEdit.EchoMode.Password)

        self.message_label = QLabel(&quot;로그인 정보를 입력하세요.&quot;)

        self.login_button = QPushButton(&quot;로그인&quot;)
        self.login_button.clicked.connect(self.login)

        layout = QVBoxLayout()
        layout.addWidget(QLabel(&quot;[로그인]&quot;))
        layout.addWidget(QLabel(&quot;아이디&quot;))
        layout.addWidget(self.id_input)
        layout.addWidget(QLabel(&quot;비밀번호&quot;))
        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()
        user_pw = self.pw_input.text()

        if self.member_model.check_login(user_id, user_pw):
            self.message_label.setText(&quot;로그인 성공!&quot;)

            self.main_view.refresh()
            self.profile_view.refresh()

            self.tab_widget.setCurrentIndex(1)
        else:
            self.message_label.setText(&quot;로그인 실패! 아이디 또는 비밀번호를 확인하세요.&quot;)


class MainView(QWidget):
    def __init__(self, member_model):
        super().__init__()

        self.member_model = member_model

        self.title_label = QLabel(&quot;[메인화면] 로그인한 회원 정보&quot;)
        self.id_label = QLabel(&quot;아이디:&quot;)
        self.name_label = QLabel(&quot;이름:&quot;)
        self.email_label = QLabel(&quot;이메일:&quot;)
        self.phone_label = QLabel(&quot;전화번호:&quot;)

        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()

        self.id_label.setText(f&quot;아이디: {member['id']}&quot;)
        self.name_label.setText(f&quot;이름: {member['name']}&quot;)
        self.email_label.setText(f&quot;이메일: {member['email']}&quot;)
        self.phone_label.setText(f&quot;전화번호: {member['phone']}&quot;)


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(&quot;아이디:&quot;)

        self.name_input = QLineEdit()
        self.email_input = QLineEdit()
        self.phone_input = QLineEdit()

        self.update_button = QPushButton(&quot;수정하기&quot;)
        self.update_button.clicked.connect(self.update_profile)

        self.message_label = QLabel(&quot;프로필 정보를 확인하거나 수정하세요.&quot;)

        layout = QVBoxLayout()
        layout.addWidget(QLabel(&quot;[프로필]&quot;))
        layout.addWidget(self.id_label)

        name_layout = QHBoxLayout()
        name_layout.addWidget(QLabel(&quot;이름&quot;))
        name_layout.addWidget(self.name_input)

        email_layout = QHBoxLayout()
        email_layout.addWidget(QLabel(&quot;이메일&quot;))
        email_layout.addWidget(self.email_input)

        phone_layout = QHBoxLayout()
        phone_layout.addWidget(QLabel(&quot;전화번호&quot;))
        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()

        self.id_label.setText(f&quot;아이디: {member['id']}&quot;)
        self.name_input.setText(member[&quot;name&quot;])
        self.email_input.setText(member[&quot;email&quot;])
        self.phone_input.setText(member[&quot;phone&quot;])

    def update_profile(self):
        name = self.name_input.text()
        email = self.email_input.text()
        phone = self.phone_input.text()

        self.member_model.update_profile(name, email, phone)

        self.refresh()
        self.main_view.refresh()

        self.message_label.setText(&quot;프로필 정보가 수정되었습니다.&quot;)


class TabWindow(QWidget):
    def __init__(self, member_model):
        super().__init__()

        self.setWindowTitle(&quot;Model/View 과제 구현&quot;)
        self.resize(400, 300)

        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.tab_widget.addTab(self.login_view, &quot;로그인&quot;)
        self.tab_widget.addTab(self.main_view, &quot;메인화면&quot;)
        self.tab_widget.addTab(self.profile_view, &quot;프로필&quot;)

        layout = QVBoxLayout()
        layout.addWidget(self.tab_widget)

        self.setLayout(layout)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;4.2 View들이 같은 Model을 받는 부분&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TabWindow 안에서 MainView, ProfileView, LoginView를 만듭니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 세 View에 모두 같은 self.member_model을 전달합니다.&lt;/p&gt;
&lt;pre class=&quot;armasm&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;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,
)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 코드가 이번 과제의 핵심입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MainView와 ProfileView가 서로 다른 회원 정보를 가지는 것이 아니라, 같은 MemberModel을 바라보게 됩니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #f8fbff; border: 1px dashed #9cc8ff; border-radius: 10px; padding: 16px; margin: 20px 0;&quot;&gt;
&lt;p style=&quot;margin: 0 0 8px 0;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[이미지 삽입 위치]&lt;/b&gt; TabWindow가 하나의 MemberModel을 각 View에 전달하는 구조&lt;/p&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[이미지 생성 제안]&lt;/b&gt; TabWindow 안에서 MemberModel 하나가 생성되고 LoginView, MainView, ProfileView 생성자에 전달되는 과정을 화살표로 보여주는 구조 다이어그램을 만듭니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h1&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;5.&lt;/span&gt; main.py에서 프로그램 실행하기&lt;/h1&gt;
&lt;p style=&quot;color: #9d9d9d;&quot; data-ke-size=&quot;size14&quot;&gt;&amp;rarr; QApplication을 만들고 MemberModel과 TabWindow를 연결해 프로그램을 실행합니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot; data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;5.1 main.py 코드&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막으로 main.py를 작성합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;main.py에서는 QApplication을 만들고, MemberModel과 TabWindow를 생성합니다.&lt;/p&gt;
&lt;pre class=&quot;python&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;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())&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;5.2 실행 명령어&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;터미널에서 프로젝트 폴더로 이동한 뒤 아래 명령어를 실행합니다.&lt;/p&gt;
&lt;pre class=&quot;vim&quot; data-ke-language=&quot;text&quot;&gt;&lt;code&gt;python main.py&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로그램이 실행되면 로그인, 메인화면, 프로필 탭이 있는 창이 열립니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;5.3 실행 전 파일 위치 확인&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파일 이름과 위치가 아래처럼 되어 있는지 확인합니다.&lt;/p&gt;
&lt;pre class=&quot;vim&quot; data-ke-language=&quot;text&quot;&gt;&lt;code&gt;# 실행 전 확인

tab_model_view_login/
    ├── main.py
    ├── member_model.py
    └── tab_window.py&lt;/code&gt;&lt;/pre&gt;
&lt;div style=&quot;background-color: #f8fbff; border: 1px solid #d8e8ff; border-radius: 10px; padding: 18px; margin: 25px 0;&quot;&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;주의할 점:&lt;/b&gt; 파일 이름이 다르면 import 부분에서 오류가 날 수 있습니다. 파일 이름을 main.py, member_model.py, tab_window.py로 맞춰 주세요.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h1&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;6.&lt;/span&gt; 로그인과 프로필 수정 흐름 확인하기&lt;/h1&gt;
&lt;p style=&quot;color: #9d9d9d;&quot; data-ke-size=&quot;size14&quot;&gt;&amp;rarr; 로그인 성공, 로그인 실패, 프로필 수정 후 메인화면 반영 흐름을 확인합니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot; data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;6.1 로그인 성공 흐름&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 로그인 성공 흐름을 확인합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아이디는 test, 비밀번호는 1234를 입력합니다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; margin-top: 15px; margin-bottom: 20px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;background-color: #f5f8ff;&quot;&gt;
&lt;td style=&quot;width: 30%; padding: 10px;&quot;&gt;&lt;b&gt;항목&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 70%; padding: 10px;&quot;&gt;&lt;b&gt;입력값&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;아이디&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;test&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;비밀번호&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;1234&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre class=&quot;reasonml&quot; data-ke-language=&quot;text&quot;&gt;&lt;code&gt;# 로그인 성공 흐름

로그인 View에서 ID, PW 입력
    &amp;darr;
로그인 버튼 클릭
    &amp;darr;
MemberModel.check_login() 호출
    &amp;darr;
로그인 성공
    &amp;darr;
MainView.refresh() 호출
    &amp;darr;
ProfileView.refresh() 호출
    &amp;darr;
메인화면 탭으로 이동&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그인에 성공하면 아래 코드가 실행됩니다.&lt;/p&gt;
&lt;pre class=&quot;css&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;self.main_view.refresh()
self.profile_view.refresh()
self.tab_widget.setCurrentIndex(1)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 코드는 메인화면과 프로필 화면을 최신 회원 정보로 갱신한 뒤, 메인화면 탭으로 이동하는 코드입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;6.2 로그인 실패 흐름&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아이디 또는 비밀번호가 틀리면 탭이 이동하지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그인 실패 메시지만 출력하고 로그인 화면에 그대로 머무릅니다.&lt;/p&gt;
&lt;pre class=&quot;gauss&quot; data-ke-language=&quot;text&quot;&gt;&lt;code&gt;# 로그인 실패 흐름

로그인 View에서 ID, PW 입력
    &amp;darr;
로그인 버튼 클릭
    &amp;darr;
MemberModel.check_login() 호출
    &amp;darr;
ID 또는 PW 불일치
    &amp;darr;
로그인 실패 메시지 출력
    &amp;darr;
로그인 View에 그대로 머무름&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;6.3 프로필 수정 후 메인화면 반영 흐름&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로필 탭에서 이름, 이메일, 전화번호를 수정합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수정하기 버튼을 누르면 ProfileView 안의 update_profile() 함수가 실행됩니다.&lt;/p&gt;
&lt;pre class=&quot;css&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;self.member_model.update_profile(name, email, phone)

self.refresh()
self.main_view.refresh()&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫 번째 줄은 MemberModel의 회원 정보를 수정합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 번째 줄은 프로필 화면을 다시 갱신합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세 번째 줄은 메인화면을 다시 갱신합니다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot; data-ke-language=&quot;text&quot;&gt;&lt;code&gt;# 프로필 수정 흐름

프로필 View에서 이름, 이메일, 전화번호 수정
    &amp;darr;
수정하기 버튼 클릭
    &amp;darr;
MemberModel.update_profile() 호출
    &amp;darr;
MemberModel의 회원 정보 변경
    &amp;darr;
ProfileView.refresh() 호출
    &amp;darr;
MainView.refresh() 호출
    &amp;darr;
메인화면에도 수정된 정보 표시&lt;/code&gt;&lt;/pre&gt;
&lt;div style=&quot;background-color: #f8fbff; border: 1px dashed #9cc8ff; border-radius: 10px; padding: 16px; margin: 20px 0;&quot;&gt;
&lt;p style=&quot;margin: 0 0 8px 0;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[이미지 삽입 위치]&lt;/b&gt; 프로필 수정 후 메인화면이 갱신되는 흐름&lt;/p&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[이미지 생성 제안]&lt;/b&gt; ProfileView에서 수정하기 버튼을 누르면 MemberModel 값이 변경되고 ProfileView와 MainView가 refresh되는 과정을 단계형 흐름도로 표현합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div style=&quot;background-color: #f8fbff; border: 1px solid #d8e8ff; border-radius: 10px; padding: 18px; margin: 25px 0;&quot;&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;핵심 확인:&lt;/b&gt; 프로필에서 수정한 값이 메인화면에도 보이면, 두 View가 같은 MemberModel을 공유하고 있다는 뜻입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h1&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;7.&lt;/span&gt; 실행 결과 확인하기&lt;/h1&gt;
&lt;p style=&quot;color: #9d9d9d;&quot; data-ke-size=&quot;size14&quot;&gt;&amp;rarr; 로그인 성공 결과와 프로필 수정 결과를 직접 확인합니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot; data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;7.1 로그인 성공 후 메인화면 결과&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그인에 성공하면 메인화면 탭으로 이동합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메인화면에는 MemberModel에 저장된 회원 정보가 출력됩니다.&lt;/p&gt;
&lt;pre class=&quot;makefile&quot; data-ke-language=&quot;text&quot;&gt;&lt;code&gt;# 메인화면 출력 예시

[메인화면] 로그인한 회원 정보

아이디: test
이름: 홍길동
이메일: test@test.com
전화번호: 010-1234-5678&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;7.2 프로필 수정 입력 예시&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로필 탭에서 아래처럼 정보를 수정해 봅니다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot; data-ke-language=&quot;text&quot;&gt;&lt;code&gt;# 수정 입력 예시

이름: 김철수
이메일: kim@test.com
전화번호: 010-9999-8888&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수정하기 버튼을 누른 뒤 메인화면 탭으로 돌아가면 아래처럼 변경된 정보가 보여야 합니다.&lt;/p&gt;
&lt;pre class=&quot;makefile&quot; data-ke-language=&quot;text&quot;&gt;&lt;code&gt;# 수정 후 메인화면 결과

[메인화면] 로그인한 회원 정보

아이디: test
이름: 김철수
이메일: kim@test.com
전화번호: 010-9999-8888&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;7.3 제출 전 체크리스트&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 항목을 직접 확인합니다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; margin-top: 15px; margin-bottom: 20px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;background-color: #f5f8ff;&quot;&gt;
&lt;td style=&quot;width: 80%; padding: 10px;&quot;&gt;&lt;b&gt;확인 항목&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 20%; padding: 10px;&quot;&gt;&lt;b&gt;완료&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;로그인 View에 ID, PW 입력칸이 있는가?&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;□&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;로그인 버튼을 누르면 ID/PW 검사가 되는가?&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;□&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;로그인 성공 시 메인화면 View 탭으로 이동하는가?&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;□&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;메인화면 View에 회원 정보가 출력되는가?&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;□&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;프로필 View에도 같은 회원 정보가 출력되는가?&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;□&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;프로필 View에서 이름, 이메일, 전화번호를 수정할 수 있는가?&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;□&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;수정 후 메인화면 View에도 변경된 정보가 보이는가?&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;□&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;회원 정보가 View마다 따로 저장되지 않고 MemberModel을 통해 공유되는가?&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;□&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h1&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;8.&lt;/span&gt; 기존 방식과 Model 공유 방식 비교하며 정리하기&lt;/h1&gt;
&lt;p style=&quot;color: #9d9d9d;&quot; data-ke-size=&quot;size14&quot;&gt;&amp;rarr; View마다 데이터를 따로 저장하는 방식과 하나의 Model을 공유하는 방식을 비교합니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot; data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;8.1 기존 방식&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 방식은 각 View가 자기 데이터를 따로 가지는 구조입니다.&lt;/p&gt;
&lt;pre class=&quot;nginx&quot; data-ke-language=&quot;text&quot;&gt;&lt;code&gt;# 기존 구조

LoginView
    └── 로그인 정보 따로 저장

MainView
    └── 회원 정보 따로 저장

ProfileView
    └── 회원 정보 따로 저장&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 구조에서는 ProfileView에서 정보를 수정해도 MainView의 정보가 바뀌지 않을 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜냐하면 두 View가 서로 다른 데이터를 가지고 있기 때문입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;8.2 Model 공유 방식&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 강의에서 사용한 방식은 하나의 MemberModel을 여러 View가 함께 사용하는 구조입니다.&lt;/p&gt;
&lt;pre class=&quot;nginx&quot; data-ke-language=&quot;text&quot;&gt;&lt;code&gt;# 새 구조

MemberModel
    └── 회원 정보 저장

LoginView
    └── MemberModel 사용

MainView
    └── MemberModel 사용

ProfileView
    └── MemberModel 사용&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 구조에서는 ProfileView가 MemberModel의 값을 수정합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 MainView가 같은 MemberModel을 다시 읽으면 수정된 값이 화면에 표시됩니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #f8fbff; border: 1px dashed #9cc8ff; border-radius: 10px; padding: 16px; margin: 20px 0;&quot;&gt;
&lt;p style=&quot;margin: 0 0 8px 0;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[이미지 삽입 위치]&lt;/b&gt; View마다 데이터를 따로 저장하는 방식과 Model을 공유하는 방식 비교&lt;/p&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[이미지 생성 제안]&lt;/b&gt; 왼쪽에는 LoginView, MainView, ProfileView가 각각 데이터를 따로 가진 구조를 그리고, 오른쪽에는 하나의 MemberModel을 세 View가 공유하는 구조를 좌우 비교 인포그래픽으로 표현합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; margin-top: 15px; margin-bottom: 20px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;background-color: #f5f8ff;&quot;&gt;
&lt;td style=&quot;width: 20%; padding: 10px;&quot;&gt;&lt;b&gt;구분&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 40%; padding: 10px;&quot;&gt;&lt;b&gt;기존 방식&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 40%; padding: 10px;&quot;&gt;&lt;b&gt;Model 공유 방식&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;데이터 위치&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;각 View 안에 흩어져 있음&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;MemberModel 한 곳에 저장됨&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;수정 반영&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;다른 View에 바로 반영하기 어려움&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;Model을 다시 읽으면 반영 가능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;구조 이해&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;처음에는 쉬워 보이지만 점점 복잡해짐&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;역할이 분리되어 유지보수하기 쉬움&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;이번 과제 목표&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;피해야 하는 방식&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;구현해야 하는 방식&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;8.3 최종 정리&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 강의에서 구현한 내용을 정리하면 다음과 같습니다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; margin-top: 15px; margin-bottom: 20px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;background-color: #f5f8ff;&quot;&gt;
&lt;td style=&quot;width: 30%; padding: 10px;&quot;&gt;&lt;b&gt;핵심 내용&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 70%; padding: 10px;&quot;&gt;&lt;b&gt;정리&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;MemberModel&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;회원 정보를 저장하고 로그인 검사, 프로필 수정을 담당합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;LoginView&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;ID와 PW를 입력받고 MemberModel에 로그인 검사를 요청합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;MainView&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;MemberModel에서 회원 정보를 읽어 화면에 출력합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;ProfileView&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;MemberModel의 회원 정보를 수정하고 MainView를 다시 갱신합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;refresh()&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;Model의 최신 값을 다시 읽어 View에 보여주는 역할을 합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;div style=&quot;background-color: #f8fbff; border: 1px solid #d8e8ff; border-radius: 10px; padding: 18px; margin: 25px 0;&quot;&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;기억할 문장:&lt;/b&gt; View는 데이터를 직접 소유하지 않고, Model의 데이터를 읽고 보여주는 역할에 집중해야 합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h1&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;참고.&lt;/span&gt; 공식 문서로 확인하기&lt;/h1&gt;
&lt;p style=&quot;color: #9d9d9d;&quot; data-ke-size=&quot;size14&quot;&gt;&amp;rarr; QTabWidget과 Qt Model/View 개념은 공식 문서에서 추가로 확인할 수 있습니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot; data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;참고 문서&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 강의에서 사용한 주요 개념은 아래 공식 문서에서 더 자세히 확인할 수 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://doc.qt.io/qtforpython-6/PySide6/QtWidgets/QTabWidget.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;PySide6 QTabWidget 공식 문서&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://doc.qt.io/qtforpython-6/overviews/qtwidgets-modelview.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Qt for Python Model/View Programming 공식 문서&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://doc.qt.io/qtforpython-6/PySide6/QtWidgets/QWidget.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;PySide6 QWidget 공식 문서&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div style=&quot;background-color: #f8fbff; border: 1px solid #d8e8ff; border-radius: 10px; padding: 18px; margin: 25px 0;&quot;&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;참고:&lt;/b&gt; 공식 Qt Model/View 구조는 QAbstractItemModel 같은 클래스를 사용하는 더 확장된 구조입니다. 이번 강의에서는 초보자용으로 하나의 Python 클래스인 MemberModel을 여러 View가 공유하는 방식에 집중했습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;</description>
      <category>3. PySide6/3-4. Model - View</category>
      <author>BasicLike</author>
      <guid isPermaLink="true">https://basiclike.tistory.com/783</guid>
      <comments>https://basiclike.tistory.com/783#entry783comment</comments>
      <pubDate>Mon, 1 Jun 2026 13:06:52 +0900</pubDate>
    </item>
    <item>
      <title>5강. 로그인 과제</title>
      <link>https://basiclike.tistory.com/781</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;435&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Ys632/dJMcabJsRXg/1Hv1Vk4jou5z1zz6BGCWk0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Ys632/dJMcabJsRXg/1Hv1Vk4jou5z1zz6BGCWk0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Ys632/dJMcabJsRXg/1Hv1Vk4jou5z1zz6BGCWk0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FYs632%2FdJMcabJsRXg%2F1Hv1Vk4jou5z1zz6BGCWk0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;435&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;435&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h1 style=&quot;background-color: #ffffff; color: #222222; text-align: start;&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;0.&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;학습 목표&lt;/h1&gt;
&lt;p style=&quot;color: #9d9d9d;&quot; data-ke-size=&quot;size14&quot;&gt;&amp;rarr; QTabWidget 기반 화면에서 하나의 회원 Model을 여러 View가 공유하는 과제를 이해합니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot; data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;이번 과제에서 할 일 &lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot;&gt;이번 과제에서는 QTabWidget&lt;/span&gt;큐탭위젯&lt;span style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot;&gt;을 사용해서 탭이 있는 로그인 프로그램을 만듭니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div id=&quot;SE-bb944834-9e07-4a48-9cb8-e989ebb44190&quot; data-a11y-title=&quot;사진&quot; data-compid=&quot;SE-bb944834-9e07-4a48-9cb8-e989ebb44190&quot;&gt;
&lt;div&gt;
&lt;div data-direction=&quot;top&quot; data-compid=&quot;SE-bb944834-9e07-4a48-9cb8-e989ebb44190&quot; data-unitid=&quot;&quot;&gt;
&lt;div&gt;
&lt;div id=&quot;SE-bb944834-9e07-4a48-9cb8-e989ebb44190&quot;&gt;
&lt;div data-direction=&quot;top&quot; data-compid=&quot;&quot; data-unitid=&quot;SE-bb944834-9e07-4a48-9cb8-e989ebb44190&quot;&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;720&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/AIB9a/dJMcaarXEXS/pOQFBs2zIgmpaBX1kpKYh0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/AIB9a/dJMcaarXEXS/pOQFBs2zIgmpaBX1kpKYh0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/AIB9a/dJMcaarXEXS/pOQFBs2zIgmpaBX1kpKYh0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FAIB9a%2FdJMcaarXEXS%2FpOQFBs2zIgmpaBX1kpKYh0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;800&quot; height=&quot;720&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;720&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/div&gt;
&lt;span&gt;&lt;/span&gt;&lt;b&gt; &lt;/b&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;탭은 로그인 View, 메인화면 View, 프로필 View로 나눕니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중요한 점은 각 View가 회원 정보를 따로 저장하지 않는 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하나의 회원 Model&lt;sup&gt;모델&lt;/sup&gt;을 만들고, 여러 View&lt;sup&gt;뷰&lt;/sup&gt;가 같은 Model을 공유해야 합니다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; margin-top: 15px; margin-bottom: 20px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;background-color: #f5f8ff;&quot;&gt;
&lt;td style=&quot;width: 15.6968%; padding: 10px;&quot;&gt;&lt;b&gt;구분&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 84.3032%; padding: 10px;&quot;&gt;&lt;b&gt;내용&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px; width: 15.6968%;&quot;&gt;핵심 개념&lt;/td&gt;
&lt;td style=&quot;padding: 10px; width: 84.3032%;&quot;&gt;회원 Model 하나를 기준으로 로그인 View, 메인화면 View, 프로필 View가 같은 데이터를 공유합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px; width: 15.6968%;&quot;&gt;실습 준비&lt;/td&gt;
&lt;td style=&quot;padding: 10px; width: 84.3032%;&quot;&gt;PySide6, QTabWidget, QWidget, QLabel, QLineEdit, QPushButton, QVBoxLayout, QHBoxLayout을 사용합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px; width: 15.6968%;&quot;&gt;최종 목표&lt;/td&gt;
&lt;td style=&quot;padding: 10px; width: 84.3032%;&quot;&gt;로그인 후 메인화면과 프로필 화면에 같은 회원 정보가 출력되고, 프로필 수정 내용이 메인화면에도 반영되도록 구현합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px; width: 15.6968%;&quot;&gt;제출 기한&lt;/td&gt;
&lt;td style=&quot;padding: 10px; width: 84.3032%;&quot;&gt;&lt;b&gt;X월 X일 월요일까지 제출하세요.&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;div style=&quot;background-color: #f8fbff; border: 1px solid #d8e8ff; border-radius: 10px; padding: 18px; margin: 25px 0;&quot;&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;이번 과제의 핵심:&lt;/b&gt; View마다 회원 정보를 따로 만들지 말고, 하나의 MemberModel을 여러 View가 함께 사용하도록 구현해야 합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h1&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;1.&lt;/span&gt; 왜 하나의 회원 Model을 공유해야 할까?&lt;/h1&gt;
&lt;p style=&quot;color: #9d9d9d;&quot; data-ke-size=&quot;size14&quot;&gt;&amp;rarr; 여러 화면이 같은 회원 정보를 보여주려면 데이터를 한 곳에서 관리해야 합니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot; data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1.1 이번 과제의 상황&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 과제에서는 하나의 프로그램 안에 3개의 탭 화면을 만듭니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫 번째 탭은 로그인 화면입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 번째 탭은 로그인한 회원 정보를 보여주는 메인화면입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세 번째 탭은 회원 정보를 보여주고 수정하는 프로필 화면입니다.&lt;/p&gt;
&lt;pre class=&quot;gauss&quot; data-ke-language=&quot;text&quot;&gt;&lt;code&gt;# 탭 화면 구성

TabLayout
    ├── 로그인 View
    ├── 메인화면 View
    └── 프로필 View&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 중요한 점은 메인화면 View와 프로필 View가 같은 회원 정보를 보여줘야 한다는 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로필 View에서 이름, 이메일, 전화번호를 수정하면 메인화면 View에도 수정된 값이 보여야 합니다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; margin-top: 15px; margin-bottom: 20px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;background-color: #f5f8ff;&quot;&gt;
&lt;td style=&quot;width: 30%; padding: 10px;&quot;&gt;&lt;b&gt;탭 이름&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 70%; padding: 10px;&quot;&gt;&lt;b&gt;역할&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;로그인 View&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;ID, PW를 입력하고 로그인 성공 여부를 확인합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;메인화면 View&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;로그인한 회원 정보를 출력합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;프로필 View&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;로그인한 회원 정보를 출력하고, 이름, 이메일, 전화번호를 수정합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1.2 View마다 데이터를 따로 저장하면 생기는 문제&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 각 View 안에 회원 정보를 따로 넣고 싶을 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 이렇게 만들면 프로필 View에서 정보를 수정해도 메인화면 View의 정보는 바뀌지 않을 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜냐하면 두 View가 서로 다른 데이터를 가지고 있기 때문입니다.&lt;/p&gt;
&lt;pre class=&quot;nginx&quot; data-ke-language=&quot;text&quot;&gt;&lt;code&gt;# 기존 구조

LoginView
    └── 로그인 정보 따로 저장

MainView
    └── 회원 정보 따로 저장

ProfileView
    └── 회원 정보 따로 저장&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 구조에서는 각 View가 자기 데이터를 따로 가지고 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 한 화면에서 데이터를 바꿔도 다른 화면에 바로 반영되기 어렵습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1.3 하나의 Model을 공유하는 구조&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 과제에서는 회원 정보를 MemberModel 하나에만 저장합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 로그인 View, 메인화면 View, 프로필 View가 같은 MemberModel을 바라보도록 만듭니다.&lt;/p&gt;
&lt;pre class=&quot;nginx&quot; data-ke-language=&quot;text&quot;&gt;&lt;code&gt;# 새 구조

MemberModel
    └── 회원 정보 저장

LoginView
    └── MemberModel 사용

MainView
    └── MemberModel 사용

ProfileView
    └── MemberModel 사용&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 만들면 프로필 View에서 Model의 값을 바꿨을 때, 메인화면 View도 같은 Model에서 값을 다시 읽어올 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 여러 화면이 같은 회원 정보를 공유할 수 있습니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #f8fbff; border: 1px solid #d8e8ff; border-radius: 10px; padding: 18px; margin: 25px 0;&quot;&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;문제의 핵심:&lt;/b&gt; 여러 View가 같은 데이터를 보여줘야 한다면, 데이터는 View 안에 흩어두지 말고 하나의 Model에서 관리해야 합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h1&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;2.&lt;/span&gt; Tab 로그인 화면 구조 직접 설계하기&lt;/h1&gt;
&lt;p style=&quot;color: #9d9d9d;&quot; data-ke-size=&quot;size14&quot;&gt;&amp;rarr; QTabWidget으로 3개의 View를 만들고, 회원 Model을 공유하는 구조를 설계합니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot; data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2.1 프로젝트 구조&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트 구조는 자유롭게 만들 수 있지만, 아래 구조를 권장합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 파일을 너무 많이 나누기보다, Model과 View 역할이 구분되도록 작성하는 것이 중요합니다.&lt;/p&gt;
&lt;pre class=&quot;vim&quot; data-ke-language=&quot;text&quot;&gt;&lt;code&gt;# 권장 프로젝트 구조

tab_model_view_login/
    ├── main.py
    ├── member_model.py
    └── tab_window.py&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파일 역할은 다음과 같습니다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; margin-top: 15px; margin-bottom: 20px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;background-color: #f5f8ff;&quot;&gt;
&lt;td style=&quot;width: 30%; padding: 10px;&quot;&gt;&lt;b&gt;파일&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 70%; padding: 10px;&quot;&gt;&lt;b&gt;역할&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;main.py&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;QApplication을 만들고 메인 윈도우를 실행합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;member_model.py&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;회원 정보를 저장하고 로그인 검사, 정보 수정 기능을 담당합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;tab_window.py&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;QTabWidget을 사용해 로그인 View, 메인화면 View, 프로필 View를 구성합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2.2 회원 Model 구현 조건&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 회원 정보를 저장할 MemberModel을 구현합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회원 정보는 View마다 따로 저장하지 않고, MemberModel 하나에만 저장해야 합니다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; margin-top: 15px; margin-bottom: 20px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;background-color: #f5f8ff;&quot;&gt;
&lt;td style=&quot;width: 30%; padding: 10px;&quot;&gt;&lt;b&gt;필드명&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 70%; padding: 10px;&quot;&gt;&lt;b&gt;설명&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;id&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;회원 아이디&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;pw&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;회원 비밀번호&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;name&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;회원 이름&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;email&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;회원 이메일&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;phone&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;회원 전화번호&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예시 회원 정보는 코드 안에 미리 만들어도 됩니다.&lt;/p&gt;
&lt;pre class=&quot;makefile&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;# 예시 회원 정보

member = {
    &quot;id&quot;: &quot;test&quot;,
    &quot;pw&quot;: &quot;1234&quot;,
    &quot;name&quot;: &quot;홍길동&quot;,
    &quot;email&quot;: &quot;test@test.com&quot;,
    &quot;phone&quot;: &quot;010-1234-5678&quot;,
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MemberModel에는 최소한 로그인 검사 기능과 회원 정보 수정 기능이 있어야 합니다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; margin-top: 15px; margin-bottom: 20px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;background-color: #f5f8ff;&quot;&gt;
&lt;td style=&quot;width: 35%; padding: 10px;&quot;&gt;&lt;b&gt;기능&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 65%; padding: 10px;&quot;&gt;&lt;b&gt;설명&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;check_login(id, pw)&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;입력한 ID와 PW가 Model의 정보와 일치하는지 확인합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;get_member_info()&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;메인화면과 프로필 화면에서 출력할 회원 정보를 반환합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;update_profile(name, email, phone)&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;프로필 View에서 수정한 이름, 이메일, 전화번호를 Model에 반영합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2.3 로그인 View 구현 조건&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그인 View에는 ID 입력칸, PW 입력칸, 로그인 버튼이 있어야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그인 버튼을 누르면 입력한 값과 MemberModel의 값을 비교합니다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; margin-top: 15px; margin-bottom: 20px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;background-color: #f5f8ff;&quot;&gt;
&lt;td style=&quot;width: 30%; padding: 10px;&quot;&gt;&lt;b&gt;요소&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 70%; padding: 10px;&quot;&gt;&lt;b&gt;조건&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;ID 입력칸&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;QLineEdit으로 구현합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;PW 입력칸&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;QLineEdit으로 구현하고, 비밀번호처럼 보이도록 EchoMode를 설정할 수 있습니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;로그인 버튼&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;clicked.connect(...)로 로그인 처리 함수와 연결합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그인 성공 조건은 다음과 같습니다.&lt;/p&gt;
&lt;pre class=&quot;nix&quot; data-ke-language=&quot;text&quot;&gt;&lt;code&gt;# 로그인 성공 조건

입력한 ID == 회원 Model의 ID
입력한 PW == 회원 Model의 PW&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그인에 성공하면 다음 동작을 수행합니다.&lt;/p&gt;
&lt;pre class=&quot;gauss&quot; data-ke-language=&quot;text&quot;&gt;&lt;code&gt;# 로그인 성공 후 동작

로그인 성공 메시지 출력
    &amp;darr;
메인화면 View 탭으로 이동
    &amp;darr;
메인화면 View에 회원 정보 출력
    &amp;darr;
프로필 View에도 같은 회원 정보 출력&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그인에 실패하면 로그인 실패 메시지를 출력하고, 로그인 View에 그대로 머무릅니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2.4 메인화면 View 구현 조건&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메인화면 View에는 로그인한 회원 정보가 출력되어야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출력해야 할 정보는 아이디, 이름, 이메일, 전화번호입니다.&lt;/p&gt;
&lt;pre class=&quot;makefile&quot; data-ke-language=&quot;text&quot;&gt;&lt;code&gt;# 메인화면 View 출력 예시

[메인화면] 로그인한 회원 정보

아이디: test
이름: 홍길동
이메일: test@test.com
전화번호: 010-1234-5678&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주의할 점은 메인화면 View가 회원 정보를 직접 새로 만들면 안 된다는 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반드시 MemberModel에 저장된 정보를 가져와서 출력해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2.5 프로필 View 구현 조건&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로필 View에는 로그인한 회원 정보가 출력되어야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 이름, 이메일, 전화번호를 수정할 수 있어야 합니다.&lt;/p&gt;
&lt;pre class=&quot;css&quot; data-ke-language=&quot;text&quot;&gt;&lt;code&gt;# 프로필 View 예시

[프로필]

아이디: test
이름: [홍길동]
이메일: [test@test.com]
전화번호: [010-1234-5678]

[수정하기 버튼]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수정할 수 있는 정보는 다음 3개입니다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; margin-top: 15px; margin-bottom: 20px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;background-color: #f5f8ff;&quot;&gt;
&lt;td style=&quot;width: 30%; padding: 10px;&quot;&gt;&lt;b&gt;수정 항목&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 70%; padding: 10px;&quot;&gt;&lt;b&gt;설명&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;이름&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;회원 이름을 수정합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;이메일&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;회원 이메일을 수정합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;전화번호&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;회원 전화번호를 수정합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아이디와 비밀번호는 수정하지 않아도 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수정 버튼을 누르면 MemberModel의 정보가 변경되어야 합니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #f8fbff; border: 1px solid #d8e8ff; border-radius: 10px; padding: 18px; margin: 25px 0;&quot;&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;핵심 확인:&lt;/b&gt; 프로필 View에서 수정한 값은 프로필 View 안에만 저장하는 것이 아니라 MemberModel에 반영해야 합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h1&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;3.&lt;/span&gt; 로그인부터 정보 수정까지 동작 흐름 살펴보기&lt;/h1&gt;
&lt;p style=&quot;color: #9d9d9d;&quot; data-ke-size=&quot;size14&quot;&gt;&amp;rarr; 로그인 성공, 탭 이동, 프로필 수정, 메인화면 반영 흐름을 단계별로 정리합니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot; data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;3.1 로그인 성공 흐름&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그인 버튼을 눌렀을 때 성공하면 다음 순서로 동작해야 합니다.&lt;/p&gt;
&lt;pre class=&quot;gauss&quot; data-ke-language=&quot;text&quot;&gt;&lt;code&gt;# 로그인 성공 흐름

로그인 View에서 ID, PW 입력
    &amp;darr;
로그인 버튼 클릭
    &amp;darr;
MemberModel의 ID, PW와 비교
    &amp;darr;
로그인 성공 메시지 출력
    &amp;darr;
메인화면 View 탭으로 이동
    &amp;darr;
메인화면 View에 회원 정보 출력
    &amp;darr;
프로필 View에도 회원 정보 출력&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;탭 이동은 QTabWidget의 현재 탭 인덱스를 변경하는 방식으로 구현할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 로그인 탭이 0번, 메인화면 탭이 1번이라면 로그인 성공 후 1번 탭으로 이동하면 됩니다.&lt;/p&gt;
&lt;pre class=&quot;pf&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;# 예시 코드

self.tab_widget.setCurrentIndex(1)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;3.2 로그인 실패 흐름&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그인에 실패하면 탭을 이동하지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그인 실패 메시지를 출력하고, 로그인 View에 그대로 머무릅니다.&lt;/p&gt;
&lt;pre class=&quot;gauss&quot; data-ke-language=&quot;text&quot;&gt;&lt;code&gt;# 로그인 실패 흐름

로그인 View에서 ID, PW 입력
    &amp;darr;
로그인 버튼 클릭
    &amp;darr;
MemberModel의 ID, PW와 비교
    &amp;darr;
ID 또는 PW 불일치
    &amp;darr;
로그인 실패 메시지 출력
    &amp;darr;
로그인 View에 그대로 머무름&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그인 실패 상태에서는 메인화면 View와 프로필 View에 회원 정보를 출력하지 않아도 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;3.3 프로필 수정 후 메인화면 반영 흐름&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로필 View에서 정보를 수정하면 MemberModel의 값이 바뀌어야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 메인화면 View는 그 바뀐 Model 값을 다시 읽어서 출력해야 합니다.&lt;/p&gt;
&lt;pre class=&quot;gauss&quot; data-ke-language=&quot;text&quot;&gt;&lt;code&gt;# 프로필 수정 흐름

프로필 View에서 이름, 이메일, 전화번호 수정
    &amp;darr;
[수정하기] 버튼 클릭
    &amp;darr;
MemberModel.update_profile() 호출
    &amp;darr;
MemberModel의 회원 정보 변경
    &amp;darr;
메인화면 View 출력 내용 갱신
    &amp;darr;
프로필 View 출력 내용 갱신&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 프로필 View에서 아래처럼 수정했다고 가정합니다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot; data-ke-language=&quot;text&quot;&gt;&lt;code&gt;# 수정 입력 예시

이름: 김철수
이메일: kim@test.com
전화번호: 010-9999-8888&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수정 후 메인화면 View에는 아래처럼 변경된 정보가 보여야 합니다.&lt;/p&gt;
&lt;pre class=&quot;makefile&quot; data-ke-language=&quot;text&quot;&gt;&lt;code&gt;# 메인화면 변경 결과 예시

[메인화면] 로그인한 회원 정보

아이디: test
이름: 김철수
이메일: kim@test.com
전화번호: 010-9999-8888&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 결과가 보이면 메인화면 View와 프로필 View가 같은 MemberModel을 공유하고 있다는 뜻입니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #f8fbff; border: 1px solid #d8e8ff; border-radius: 10px; padding: 18px; margin: 25px 0;&quot;&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;주의할 점:&lt;/b&gt; 프로필 View에서 수정한 뒤 메인화면 View가 바뀌지 않는다면, View마다 회원 정보를 따로 저장하고 있는지 확인해야 합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h1&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;4.&lt;/span&gt; 구현 조건과 제출 전 체크리스트 정리하기&lt;/h1&gt;
&lt;p style=&quot;color: #9d9d9d;&quot; data-ke-size=&quot;size14&quot;&gt;&amp;rarr; 과제에서 반드시 지켜야 할 조건과 제출 전 확인 항목을 정리합니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot; data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;4.1 핵심 구현 조건&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반드시 다음 조건을 지켜서 구현하세요.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; margin-top: 15px; margin-bottom: 20px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;background-color: #f5f8ff;&quot;&gt;
&lt;td style=&quot;width: 15%; padding: 10px;&quot;&gt;&lt;b&gt;번호&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 85%; padding: 10px;&quot;&gt;&lt;b&gt;조건&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;1&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;회원 Model을 먼저 구현합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;2&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;로그인 View에서 ID, PW를 입력받습니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;3&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;로그인 성공 시 메인화면 View 탭으로 이동합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;4&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;메인화면 View에서 회원 정보를 출력합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;5&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;프로필 View에서도 같은 회원 정보를 출력합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;6&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;프로필 View에서 회원 정보를 수정할 수 있어야 합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;7&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;프로필 View에서 수정한 정보가 메인화면 View에도 반영되어야 합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;8&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;각 View가 따로 회원 정보를 저장하지 않고, 하나의 회원 Model을 공유해야 합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;4.2 제출 전 확인 사항&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제출하기 전에 아래 항목을 직접 확인하세요.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; margin-top: 15px; margin-bottom: 20px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;background-color: #f5f8ff;&quot;&gt;
&lt;td style=&quot;width: 80%; padding: 10px;&quot;&gt;&lt;b&gt;확인 항목&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 20%; padding: 10px;&quot;&gt;&lt;b&gt;완료&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;로그인 View에 ID, PW 입력칸이 있는가?&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;□&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;로그인 버튼을 누르면 ID/PW 검사가 되는가?&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;□&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;로그인 성공 시 메인화면 View 탭으로 이동하는가?&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;□&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;메인화면 View에 회원 정보가 출력되는가?&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;□&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;프로필 View에도 같은 회원 정보가 출력되는가?&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;□&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;프로필 View에서 이름, 이메일, 전화번호를 수정할 수 있는가?&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;□&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;수정 후 메인화면 View에도 변경된 정보가 보이는가?&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;□&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;회원 정보가 View마다 따로 저장되지 않고 Model을 통해 공유되는가?&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;□&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;4.3 최종 제출 안내&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 과제는 X&lt;b&gt;월 1일 월요일까지 제출&lt;/b&gt;하세요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제출 전에는 반드시 로그인 성공, 로그인 실패, 프로필 수정, 메인화면 반영까지 직접 실행해 봐야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드가 실행되는 것뿐 아니라, Model 하나를 여러 View가 공유하는 구조로 작성했는지도 확인해야 합니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #f8fbff; border: 1px solid #d8e8ff; border-radius: 10px; padding: 18px; margin: 25px 0;&quot;&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;기억할 문장:&lt;/b&gt; 이번 과제의 목표는 화면을 예쁘게 만드는 것이 아니라, 하나의 Model 데이터를 여러 View가 공유하는 구조를 이해하는 것입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h1&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;참고.&lt;/span&gt; 공식 문서와 참고 링크 확인하기&lt;/h1&gt;
&lt;p style=&quot;color: #9d9d9d;&quot; data-ke-size=&quot;size14&quot;&gt;&amp;rarr; QTabWidget과 Model/View 구조를 더 공부할 수 있는 자료를 확인합니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot; data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;참고 문서&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;QTabWidget과 Model/View 구조는 아래 자료를 참고하세요.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://doc.qt.io/qtforpython-6/PySide6/QtWidgets/QTabWidget.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;PySide6 QTabWidget 공식 문서&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://doc.qt.io/qtforpython-6/overviews/qtwidgets-model-view-programming.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Qt for Python Model/View Programming 공식 문서&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://basiclike.tistory.com/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;basiclike.tistory.com 참고 자료&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div style=&quot;background-color: #f8fbff; border: 1px solid #d8e8ff; border-radius: 10px; padding: 18px; margin: 25px 0;&quot;&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;참고:&lt;/b&gt; 공식 Qt Model/View 구조는 QAbstractItemModel 같은 클래스를 사용하는 더 확장된 구조입니다. 이번 과제에서는 초보자용으로 하나의 회원 Model을 여러 View가 공유하는 방식에 집중합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;</description>
      <category>3. PySide6/3-4. Model - View</category>
      <author>BasicLike</author>
      <guid isPermaLink="true">https://basiclike.tistory.com/781</guid>
      <comments>https://basiclike.tistory.com/781#entry781comment</comments>
      <pubDate>Fri, 29 May 2026 16:30:53 +0900</pubDate>
    </item>
    <item>
      <title>4강. Model/View 구조</title>
      <link>https://basiclike.tistory.com/613</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;435&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Ys632/dJMcabJsRXg/1Hv1Vk4jou5z1zz6BGCWk0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Ys632/dJMcabJsRXg/1Hv1Vk4jou5z1zz6BGCWk0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Ys632/dJMcabJsRXg/1Hv1Vk4jou5z1zz6BGCWk0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FYs632%2FdJMcabJsRXg%2F1Hv1Vk4jou5z1zz6BGCWk0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;435&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;435&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;!-- ========================================================= --&gt;&lt;!-- 0. 학습 목표 --&gt;&lt;!-- ========================================================= --&gt;
&lt;h1&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;0.&lt;/span&gt; 학습 목표&lt;/h1&gt;
&lt;p style=&quot;color: #9d9d9d;&quot; data-ke-size=&quot;size14&quot;&gt;&amp;rarr; 직접 만든 LogModel 하나를 여러 View가 함께 사용하는 구조를 학습합니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot; data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 단계에서는 &lt;b&gt;직접 만든 LogModel&lt;sup&gt;로그 모델&lt;/sup&gt;&lt;/b&gt; 하나를 여러 &lt;b&gt;View&lt;sup&gt;뷰&lt;/sup&gt;&lt;/b&gt;가 함께 공유하는 구조를 학습합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지난 단계에서는 QFileSystemModel처럼 &lt;b&gt;Qt가 제공하는 Model과 Signal&lt;/b&gt;을 사용했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에는 로그 문자열을 저장하는 &lt;b&gt;Model 과 Signal 을 직접 만들고&lt;/b&gt;, 이 Model을 세 개의 View에서 함께 사용합니다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; margin-top: 15px; margin-bottom: 20px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;background-color: #f5f8ff;&quot;&gt;
&lt;td style=&quot;width: 25.599%; padding: 10px;&quot;&gt;&lt;b&gt;학습 목표&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 74.401%; padding: 10px;&quot;&gt;&lt;b&gt;내용&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px; width: 25.599%;&quot;&gt;이전 예제의 문제 확인&lt;/td&gt;
&lt;td style=&quot;padding: 10px; width: 74.401%;&quot;&gt;QScrollArea, QListWidget, QTextEdit 내부에 &lt;br /&gt;데이터가 직접 저장되는 구조의 한계를 이해합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px; width: 25.599%;&quot;&gt;LogModel 역할 이해&lt;/td&gt;
&lt;td style=&quot;padding: 10px; width: 74.401%;&quot;&gt;로그 문자열을 한 곳에서 저장하고 관리하는 &lt;br /&gt;공통 데이터 Model을 이해합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px; width: 25.599%;&quot;&gt;logsChanged 시그널 이해&lt;/td&gt;
&lt;td style=&quot;padding: 10px; width: 74.401%;&quot;&gt;Model 데이터가 변경되면 &lt;br /&gt;여러 View에 변경 사실을 알리는 구조를 이해합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px; width: 25.599%;&quot;&gt;3개의 View 공유 구조 이해&lt;/td&gt;
&lt;td style=&quot;padding: 10px; width: 74.401%;&quot;&gt;QScrollArea, QListWidget, QTextEdit이 &lt;br /&gt;하나의 LogModel을 공유하는 흐름을 이해합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px; width: 25.599%;&quot;&gt;Model/View 구조 체감&lt;/td&gt;
&lt;td style=&quot;padding: 10px; width: 74.401%;&quot;&gt;어느 View에서 로그를 추가해도 &lt;br /&gt;모든 View가 같은 데이터를 보여주는 구조를 체감합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;div style=&quot;background-color: #f8fbff; border: 1px solid #d8e8ff; border-radius: 10px; padding: 18px; margin: 25px 0;&quot;&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;이번 단계의 핵심:&lt;/b&gt; 로그 데이터는 LogModel 하나에만 저장되고, QScrollArea, QListWidget, QTextEdit은 같은 LogModel의 데이터를 각자의 방식으로 보여줍니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;!-- ========================================================= --&gt;&lt;!-- 1. 문제 파악 --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;!-- ========================================================= --&gt;
&lt;h1&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;1.&lt;/span&gt; &lt;span style=&quot;color: #ee2323;&quot;&gt;문제 파악&lt;/span&gt;: 이전 스크롤 UI 예제 다시 보기&lt;/h1&gt;
&lt;p style=&quot;color: #9d9d9d;&quot; data-ke-size=&quot;size14&quot;&gt;&amp;rarr; View 안에 데이터가 직접 들어가면 어떤 문제가 생기는지 확인합니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot; data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1.1 지난 예제에서 사용한 세 가지 View&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전 스크롤 UI 예제에서 스크롤 가능한 UI를 만들기 위해 세 가지 방식을 사용했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;QScrollArea, QListWidget, QTextEdit은 모두 데이터를 화면에 보여줄 수 있는 View 역할을 합니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #f8f8f8; border: 1px solid #dddddd; border-radius: 10px; padding: 18px; margin: 25px 0;&quot;&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;참고:&lt;/b&gt; 여기서 말하는 View는 Qt의 엄격한 Model/View 클래스만 의미하지 않습니다.&lt;/p&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;이번 글에서는 데이터를 화면에 보여주는 UI 역할이라는 넓은 의미로 View라고 부릅니다.&lt;/p&gt;
&lt;/div&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; margin-top: 15px; margin-bottom: 20px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;background-color: #f5f8ff;&quot;&gt;
&lt;td style=&quot;width: 30%; padding: 10px;&quot;&gt;&lt;b&gt;View&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 70%; padding: 10px;&quot;&gt;&lt;b&gt;사용 방식&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;QScrollArea 기반 탭&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;QLabel을 만들어 content_layout.addWidget(label)로 직접 추가했습니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;QListWidget 기반 탭&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;self.list_widget.addItem(&quot;항목&quot;)으로 항목을 직접 추가했습니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;QTextEdit 기반 탭&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;self.log_textedit.append(&quot;로그&quot;)로 텍스트를 직접 추가했습니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 이 방식이 간단하고 편해 보입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 View 안에 데이터가 직접 들어간다는 문제가 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1.2 QScrollArea 기반 탭의 문제&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;QScrollArea 기반 탭에서는 QLabel을 직접 만들어 레이아웃에 추가했습니다.&lt;/p&gt;
&lt;pre class=&quot;dockerfile&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;label = QLabel(&quot;로그 1&quot;)
content_layout.addWidget(label)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방식에서는 로그 문자열이 QLabel 안에 들어갑니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 데이터가 별도의 Model에 있는 것이 아니라 GUI 위젯 안에 직접 저장됩니다.&lt;/p&gt;
&lt;pre class=&quot;stylus&quot; data-ke-language=&quot;text&quot;&gt;&lt;code&gt;QScrollArea
    &amp;darr;
content_layout
    &amp;darr;
QLabel(&quot;로그 1&quot;)
QLabel(&quot;로그 2&quot;)
QLabel(&quot;로그 3&quot;)

데이터가 QLabel 안에 들어간 상태&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1.3 QListWidget 기반 탭의 문제&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;QListWidget 기반 탭에서는 addItem()으로 데이터를 추가했습니다.&lt;/p&gt;
&lt;pre class=&quot;css&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;self.list_widget.addItem(&quot;로그 1&quot;)
self.list_widget.addItem(&quot;로그 2&quot;)
self.list_widget.addItem(&quot;로그 3&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방식에서는 로그 데이터가 QListWidget 내부 항목으로 저장됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다른 View에서 같은 로그 데이터를 사용하려면 별도로 데이터를 다시 넘겨야 합니다.&lt;/p&gt;
&lt;pre class=&quot;nginx&quot; data-ke-language=&quot;text&quot;&gt;&lt;code&gt;QListWidget
    &amp;darr;
내부 아이템
    &amp;darr;
&quot;로그 1&quot;
&quot;로그 2&quot;
&quot;로그 3&quot;

데이터가 QListWidget 안에 들어간 상태&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1.4 QTextEdit 기반 탭의 문제&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;QTextEdit 기반 탭에서는 append()로 로그를 추가했습니다.&lt;/p&gt;
&lt;pre class=&quot;css&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;self.log_textedit.append(&quot;로그 1&quot;)
self.log_textedit.append(&quot;로그 2&quot;)
self.log_textedit.append(&quot;로그 3&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방식에서는 텍스트가 QTextEdit 내부 문서에 바로 저장됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;역시 다른 View와 데이터를 공유하기 어렵습니다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot; data-ke-language=&quot;text&quot;&gt;&lt;code&gt;QTextEdit
    &amp;darr;
내부 Document
    &amp;darr;
로그 1
로그 2
로그 3

데이터가 QTextEdit 안에 들어간 상태&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1.5 공통된 근본 문제&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세 방식의 공통 문제는 데이터가 View 안에 저장된다는 점입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, View가 데이터를 보여주는 역할뿐 아니라 데이터를 직접 들고 있는 구조입니다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; margin-top: 15px; margin-bottom: 20px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;background-color: #f5f8ff;&quot;&gt;
&lt;td style=&quot;width: 30%; padding: 10px;&quot;&gt;&lt;b&gt;View&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 35%; padding: 10px;&quot;&gt;&lt;b&gt;데이터 저장 위치&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 35%; padding: 10px;&quot;&gt;&lt;b&gt;문제&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;QScrollArea&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;QLabel 안&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;다른 View와 공유하기 어렵습니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;QListWidget&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;QListWidget 내부 아이템&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;다른 View와 공유하기 어렵습니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;QTextEdit&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;QTextEdit 내부 문서&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;다른 View와 공유하기 어렵습니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;div style=&quot;background-color: #f8fbff; border: 1px solid #d8e8ff; border-radius: 10px; padding: 18px; margin: 25px 0;&quot;&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;문제의 핵심:&lt;/b&gt; 다양한 스크롤 UI는 표시 방식만 다를 뿐 같은 로그 데이터를 보여줄 수도 있습니다. 그런데 데이터가 View 안에 들어가면 같은 데이터를 여러 View에서 공유하기 어렵습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;!-- ========================================================= --&gt;&lt;!-- 2. 해결 구조 --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;!-- ========================================================= --&gt;
&lt;h1&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;2.&lt;/span&gt; &lt;span style=&quot;color: #006dd7;&quot;&gt;해결 방법&lt;/span&gt;: LogModel 하나를 세 View가 공유&lt;/h1&gt;
&lt;p style=&quot;color: #9d9d9d;&quot; data-ke-size=&quot;size14&quot;&gt;&amp;rarr; 로그 데이터를 Model 하나에 저장하고, 여러 View가 같은 데이터를 보도록 만듭니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot; data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2.1 이번 예제의 목표 구조&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 예제에서는 로그 문자열을 저장하는 LogModel을 하나만 만듭니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 QScrollArea, QListWidget, QTextEdit 세 View가 같은 LogModel을 함께 사용하도록 만듭니다.&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;460&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/czOfWk/dJMcaaS0xfC/eGsbb3wDvlUYoh3un9c46K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/czOfWk/dJMcaaS0xfC/eGsbb3wDvlUYoh3un9c46K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/czOfWk/dJMcaaS0xfC/eGsbb3wDvlUYoh3un9c46K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FczOfWk%2FdJMcaaS0xfC%2FeGsbb3wDvlUYoh3un9c46K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;460&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;460&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;pre class=&quot;ada&quot; data-ke-language=&quot;text&quot;&gt;&lt;code&gt;로그 데이터
    &amp;darr;
LogModel
    ├── ScrollView     : QScrollArea + QLabel
    ├── ListView       : QListWidget
    └── TextEditView   : QTextEdit&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터는 LogModel 안에만 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세 View는 Model의 데이터를 읽어서 각자의 방식으로 화면에 보여줍니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #f8f8f8; border: 1px solid #dddddd; border-radius: 10px; padding: 18px; margin: 25px 0;&quot;&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;참고:&lt;/b&gt; 이번 예제의 LogModel은 QAbstractItemModel을 상속한 정식 Qt Item Model은 아닙니다. 먼저 데이터를 View 밖으로 분리하고, 여러 View가 같은 데이터를 공유하는 흐름을 이해하기 위한 학습용 Model입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2.2 데이터 변경 알림 구조&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;View가 자동으로 갱신되려면 Model 데이터가 바뀌었다는 사실을 View들이 알아야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 사용하는 것이 &lt;b&gt;Signal&lt;sup&gt;시그널&lt;/sup&gt;&lt;/b&gt;입니다.&lt;/p&gt;
&lt;pre class=&quot;sas&quot; data-ke-language=&quot;text&quot;&gt;&lt;code&gt;View에서 버튼 클릭
    &amp;darr;
model.add_log()
    &amp;darr;
LogModel 내부 로그 리스트 변경
    &amp;darr;
logsChanged 시그널 발생
    &amp;darr;
모든 View의 refresh_view() 실행
    &amp;darr;
모든 화면이 같은 로그로 다시 갱신&lt;/code&gt;&lt;/pre&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; margin-top: 15px; margin-bottom: 20px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;background-color: #f5f8ff;&quot;&gt;
&lt;td style=&quot;width: 30%; padding: 10px;&quot;&gt;&lt;b&gt;구성&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 70%; padding: 10px;&quot;&gt;&lt;b&gt;역할&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;LogModel&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;로그 문자열 리스트를 저장하고 관리합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;logsChanged&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;로그 데이터가 바뀌었음을 View들에게 알려주는 시그널입니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;refresh_view()&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;각 View가 Model의 로그를 다시 읽어 화면을 갱신하는 함수입니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;add_log()&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;Model에 로그를 추가하고 logsChanged 시그널을 발생시킵니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;div style=&quot;background-color: #f8fbff; border: 1px solid #d8e8ff; border-radius: 10px; padding: 18px; margin: 25px 0;&quot;&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;해결의 핵심:&lt;/b&gt; View가 직접 데이터를 저장하지 않습니다. View는 Model에 로그 추가를 요청하고, Model이 바뀌면 다시 화면을 그립니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;!-- ========================================================= --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;!-- 3. 프로젝트 구조 만들기 --&gt;&lt;!-- ========================================================= --&gt;
&lt;h1&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;3.&lt;/span&gt; Project 구조&lt;/h1&gt;
&lt;p style=&quot;color: #9d9d9d;&quot; data-ke-size=&quot;size14&quot;&gt;&amp;rarr; Model과 세 View를 파일별로 나누어 프로젝트를 구성합니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot; data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;3.1 프로젝트 폴더 구성&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 다음과 같이 프로젝트 폴더를 만듭니다.&lt;/p&gt;
&lt;pre class=&quot;1c&quot; data-ke-language=&quot;text&quot;&gt;&lt;code&gt;ModelViewLogDemo/
 ├─ main.py            # 프로그램 시작 코드
 ├─ model_log.py       # 공통 데이터 모델
 ├─ view_scroll.py     # QScrollArea 기반 View
 ├─ view_list.py       # QListWidget 기반 View
 ├─ view_textedit.py   # QTextEdit 기반 View
 └─ widget.py          # 세 View를 QTabWidget으로 묶는 메인 화면&lt;/code&gt;&lt;/pre&gt;
&lt;figure class=&quot;fileblock&quot; data-ke-align=&quot;alignCenter&quot;&gt;&lt;a href=&quot;https://blog.kakaocdn.net/dn/bfEbTh/dJMcaiwIuv7/tVzI2pQk1PD56hzuu37OK1/main.py?attach=1&amp;amp;knm=tfile.py&quot; class=&quot;&quot;&gt;
    &lt;div class=&quot;image&quot;&gt;&lt;/div&gt;
    &lt;div class=&quot;desc&quot;&gt;&lt;div class=&quot;filename&quot;&gt;&lt;span class=&quot;name&quot;&gt;main.py&lt;/span&gt;&lt;/div&gt;
&lt;div class=&quot;size&quot;&gt;0.00MB&lt;/div&gt;
&lt;/div&gt;
  &lt;/a&gt;&lt;/figure&gt;
&lt;figure class=&quot;fileblock&quot; data-ke-align=&quot;alignCenter&quot;&gt;&lt;a href=&quot;https://blog.kakaocdn.net/dn/1Gd1B/dJMcajoL6bd/gc1stwugSqSFCeGfLhVzPk/model_log.py?attach=1&amp;amp;knm=tfile.py&quot; class=&quot;&quot;&gt;
    &lt;div class=&quot;image&quot;&gt;&lt;/div&gt;
    &lt;div class=&quot;desc&quot;&gt;&lt;div class=&quot;filename&quot;&gt;&lt;span class=&quot;name&quot;&gt;model_log.py&lt;/span&gt;&lt;/div&gt;
&lt;div class=&quot;size&quot;&gt;0.00MB&lt;/div&gt;
&lt;/div&gt;
  &lt;/a&gt;&lt;/figure&gt;
&lt;figure class=&quot;fileblock&quot; data-ke-align=&quot;alignCenter&quot;&gt;&lt;a href=&quot;https://blog.kakaocdn.net/dn/cYwdU4/dJMcajoL6be/qvwzkH3G1PMCqfSrdOjzXK/view_scroll.py?attach=1&amp;amp;knm=tfile.py&quot; class=&quot;&quot;&gt;
    &lt;div class=&quot;image&quot;&gt;&lt;/div&gt;
    &lt;div class=&quot;desc&quot;&gt;&lt;div class=&quot;filename&quot;&gt;&lt;span class=&quot;name&quot;&gt;view_scroll.py&lt;/span&gt;&lt;/div&gt;
&lt;div class=&quot;size&quot;&gt;0.00MB&lt;/div&gt;
&lt;/div&gt;
  &lt;/a&gt;&lt;/figure&gt;
&lt;figure class=&quot;fileblock&quot; data-ke-align=&quot;alignCenter&quot;&gt;&lt;a href=&quot;https://blog.kakaocdn.net/dn/bGfVOS/dJMcajoL6bg/JeUoINyZrQRPM7kn5ZQerK/view_list.py?attach=1&amp;amp;knm=tfile.py&quot; class=&quot;&quot;&gt;
    &lt;div class=&quot;image&quot;&gt;&lt;/div&gt;
    &lt;div class=&quot;desc&quot;&gt;&lt;div class=&quot;filename&quot;&gt;&lt;span class=&quot;name&quot;&gt;view_list.py&lt;/span&gt;&lt;/div&gt;
&lt;div class=&quot;size&quot;&gt;0.00MB&lt;/div&gt;
&lt;/div&gt;
  &lt;/a&gt;&lt;/figure&gt;
&lt;figure class=&quot;fileblock&quot; data-ke-align=&quot;alignCenter&quot;&gt;&lt;a href=&quot;https://blog.kakaocdn.net/dn/bwNmrK/dJMcabqQYlm/v66RhMWrdKfnl15tacgKA1/view_textedit.py?attach=1&amp;amp;knm=tfile.py&quot; class=&quot;&quot;&gt;
    &lt;div class=&quot;image&quot;&gt;&lt;/div&gt;
    &lt;div class=&quot;desc&quot;&gt;&lt;div class=&quot;filename&quot;&gt;&lt;span class=&quot;name&quot;&gt;view_textedit.py&lt;/span&gt;&lt;/div&gt;
&lt;div class=&quot;size&quot;&gt;0.00MB&lt;/div&gt;
&lt;/div&gt;
  &lt;/a&gt;&lt;/figure&gt;
&lt;figure class=&quot;fileblock&quot; data-ke-align=&quot;alignCenter&quot;&gt;&lt;a href=&quot;https://blog.kakaocdn.net/dn/ChRFQ/dJMcabqQYlr/LbIXm8K68O6k8YpJApKi1k/widget.py?attach=1&amp;amp;knm=tfile.py&quot; class=&quot;&quot;&gt;
    &lt;div class=&quot;image&quot;&gt;&lt;/div&gt;
    &lt;div class=&quot;desc&quot;&gt;&lt;div class=&quot;filename&quot;&gt;&lt;span class=&quot;name&quot;&gt;widget.py&lt;/span&gt;&lt;/div&gt;
&lt;div class=&quot;size&quot;&gt;0.00MB&lt;/div&gt;
&lt;/div&gt;
  &lt;/a&gt;&lt;/figure&gt;

&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;3.2 실행 흐름&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전체 실행 흐름은 다음과 같습니다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot; data-ke-language=&quot;text&quot;&gt;&lt;code&gt;main.py 실행
    &amp;darr;
LogModel 생성
    &amp;darr;
ViewWidget 생성
    &amp;darr;
ScrollView / ListView / TextEditView에 같은 LogModel 전달
    &amp;darr;
각 View가 logsChanged 시그널에 refresh_view() 연결
    &amp;darr;
로그 추가 시 모든 View가 자동 갱신&lt;/code&gt;&lt;/pre&gt;
&lt;div style=&quot;background-color: #f8f8f8; border: 1px solid #dddddd; border-radius: 10px; padding: 18px; margin: 25px 0;&quot;&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;참고:&lt;/b&gt; 이번 단계에서는 먼저 데이터를 Model로 분리하고 여러 View가 공유한다는 구조를 이해하는 것이 중요합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;!-- ========================================================= --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;!-- 4. model_log.py --&gt;&lt;!-- ========================================================= --&gt;
&lt;h1&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;4.&lt;/span&gt; model&lt;/h1&gt;
&lt;p style=&quot;color: #9d9d9d;&quot; data-ke-size=&quot;size14&quot;&gt;&amp;rarr; 로그 데이터를 저장하고 변경 사실을 알려주는 LogModel을 작성합니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot; data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;4.1 model_log.py 전체 코드&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 로그 데이터를 저장하는 LogModel을 작성합니다.&lt;/p&gt;
&lt;pre class=&quot;python&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;# model_log.py

from PySide6.QtCore import QObject, Signal


class LogModel(QObject):
    &quot;&quot;&quot;
    로그 문자열을 저장하는 간단한 모델 클래스입니다.

    - 내부에 로그 리스트(_logs)를 가지고 있습니다.
    - 로그가 변경될 때마다 logsChanged 시그널을 발생시킵니다.
    - View들은 이 시그널을 받아 화면을 다시 갱신합니다.
    &quot;&quot;&quot;

    logsChanged = Signal()

    def __init__(self, parent=None):
        super().__init__(parent)
        self._logs: list[str] = []

    def add_log(self, text: str):
        &quot;&quot;&quot;로그 한 줄을 추가하고 변경 사실을 알립니다.&quot;&quot;&quot;
        self._logs.append(text)
        self.logsChanged.emit()

    def clear(self):
        &quot;&quot;&quot;로그 전체를 삭제하고 변경 사실을 알립니다.&quot;&quot;&quot;
        self._logs.clear()
        self.logsChanged.emit()

    def logs(self) -&amp;gt; list[str]:
        &quot;&quot;&quot;현재 로그 리스트의 복사본을 반환합니다.&quot;&quot;&quot;
        return list(self._logs)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 파일은 이번 예제에서 가장 중요한 Model 역할을 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그 데이터는 View 안에 저장하지 않고, LogModel의 _logs 리스트에만 저장합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;4.2 코드 분석&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;1단계: QObject와 Signal 가져오기&lt;/b&gt;&lt;/h3&gt;
&lt;pre class=&quot;angelscript&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;from PySide6.QtCore import QObject, Signal&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;QObject는 Qt 객체의 기본 클래스입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Signal은 객체 간에 변경 사실을 알려주는 신호를 만들 때 사용합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2단계: logsChanged 시그널 만들기&lt;/b&gt;&lt;/h3&gt;
&lt;pre class=&quot;ini&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;logsChanged = Signal()&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;logsChanged는 로그 데이터가 바뀌었음을 View에게 알려주는 신호입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;View들은 이 시그널에 refresh_view()를 연결합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;3단계: 로그 리스트 저장하기&lt;/b&gt;&lt;/h3&gt;
&lt;pre class=&quot;sqf&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;self._logs: list[str] = []&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;_logs는 실제 로그 문자열이 저장되는 리스트입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 예제에서 데이터는 이 리스트에만 저장됩니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;4단계: 로그 추가 후 변경 알림 보내기&lt;/b&gt;&lt;/h3&gt;
&lt;pre class=&quot;ruby&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;def add_log(self, text: str):
    self._logs.append(text)
    self.logsChanged.emit()&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;add_log()는 로그를 추가하는 메서드입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그를 추가한 뒤 logsChanged.emit()을 호출하여 모든 View에게 데이터가 바뀌었다고 알려줍니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;5단계: 로그 복사본 반환하기&lt;/b&gt;&lt;/h3&gt;
&lt;pre class=&quot;ruby&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;def logs(self) -&amp;gt; list[str]:
    return list(self._logs)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;logs()는 현재 로그 리스트를 반환합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서는 원본 리스트를 그대로 반환하지 않고 복사본을 반환합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 하면 외부 View가 _logs를 직접 수정하지 못하게 막을 수 있습니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #f8fbff; border: 1px solid #d8e8ff; border-radius: 10px; padding: 18px; margin: 25px 0;&quot;&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Model/View 개념 포인트:&lt;/b&gt; LogModel은 로그 데이터를 저장하고, View들은 LogModel의 logs()를 읽어서 화면을 그립니다. 데이터 변경 알림은 logsChanged 시그널로 전달됩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;!-- ========================================================= --&gt;&lt;!-- 5. 세 개의 View 작성하기 --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;!-- ========================================================= --&gt;
&lt;h1&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;5.&lt;/span&gt; View&lt;/h1&gt;
&lt;p style=&quot;color: #9d9d9d;&quot; data-ke-size=&quot;size14&quot;&gt;&amp;rarr; 같은 LogModel을 세 가지 방식으로 보여주는 View를 작성합니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot; data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;5.1 QScrollArea 기반 View: view_scroll.py&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫 번째 View는 QScrollArea와 QLabel을 사용합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LogModel의 로그 리스트를 읽어서 QLabel 여러 개로 만들어 화면에 배치합니다.&lt;/p&gt;
&lt;pre class=&quot;ruby&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;# view_scroll.py

from PySide6.QtCore import Qt
from PySide6.QtWidgets import (
    QWidget,
    QVBoxLayout,
    QScrollArea,
    QLabel,
    QPushButton,
)

from model_log import LogModel


class ScrollView(QWidget):
    &quot;&quot;&quot;
    QScrollArea + QLabel 조합으로 로그를 보여주는 View입니다.
    같은 LogModel을 사용하면 다른 View와 항상 같은 로그를 공유합니다.
    &quot;&quot;&quot;

    def __init__(self, model: LogModel, parent=None):
        super().__init__(parent)
        self.model = model

        layout = QVBoxLayout(self)

        self.scroll_area = QScrollArea()
        self.scroll_area.setWidgetResizable(True)
        layout.addWidget(self.scroll_area)

        self.add_button = QPushButton(&quot;스크롤 뷰에 로그 추가&quot;)
        self.add_button.clicked.connect(self.add_log_clicked)
        layout.addWidget(self.add_button)

        self.content_widget = QWidget()
        self.content_layout = QVBoxLayout(self.content_widget)
        self.content_layout.setAlignment(Qt.AlignTop)

        self.scroll_area.setWidget(self.content_widget)

        self.model.logsChanged.connect(self.refresh_view)

        self.refresh_view()

    def add_log_clicked(self):
        count = len(self.model.logs()) + 1
        self.model.add_log(f&quot;[ScrollView] 추가 로그 {count}&quot;)

    def refresh_view(self):
        while self.content_layout.count():
            item = self.content_layout.takeAt(0)
            widget = item.widget()

            if widget is not None:
                widget.deleteLater()

        for text in self.model.logs():
            label = QLabel(text)
            self.content_layout.addWidget(label)

        bar = self.scroll_area.verticalScrollBar()
        bar.setValue(bar.maximum())&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ScrollView는 로그 데이터를 직접 저장하지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;버튼을 클릭하면 self.model.add_log()를 호출하여 Model에 로그를 추가합니다.&lt;/p&gt;
&lt;pre class=&quot;stylus&quot; data-ke-language=&quot;text&quot;&gt;&lt;code&gt;ScrollView 버튼 클릭
    &amp;darr;
self.model.add_log()
    &amp;darr;
LogModel에 로그 추가
    &amp;darr;
logsChanged 발생
    &amp;darr;
refresh_view()로 QLabel 다시 생성&lt;/code&gt;&lt;/pre&gt;
&lt;div style=&quot;background-color: #f8f8f8; border: 1px solid #dddddd; border-radius: 10px; padding: 18px; margin: 25px 0;&quot;&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;참고:&lt;/b&gt; refresh_view()에서는 기존 QLabel을 모두 지운 뒤, 현재 Model의 로그 개수만큼 QLabel을 새로 만듭니다. deleteLater()는 Qt에게 이 위젯을 안전한 시점에 삭제해 달라고 요청하는 함수입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;5.2 QListWidget 기반 View: view_list.py&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 번째 View는 QListWidget을 사용합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LogModel의 로그 리스트를 읽어서 QListWidget 아이템으로 표시합니다.&lt;/p&gt;
&lt;pre class=&quot;ruby&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;# view_list.py

from PySide6.QtWidgets import (
    QWidget,
    QVBoxLayout,
    QPushButton,
    QListWidget,
)

from model_log import LogModel


class ListView(QWidget):
    &quot;&quot;&quot;
    QListWidget을 이용해 로그를 보여주는 View입니다.
    모델의 로그를 QListWidget 아이템으로 변환하여 표시합니다.
    &quot;&quot;&quot;

    def __init__(self, model: LogModel, parent=None):
        super().__init__(parent)
        self.model = model

        layout = QVBoxLayout(self)

        self.list_widget = QListWidget()
        layout.addWidget(self.list_widget)

        self.add_button = QPushButton(&quot;리스트 뷰에 로그 추가&quot;)
        self.add_button.clicked.connect(self.add_log_clicked)
        layout.addWidget(self.add_button)

        self.model.logsChanged.connect(self.refresh_view)

        self.refresh_view()

    def add_log_clicked(self):
        count = len(self.model.logs()) + 1
        self.model.add_log(f&quot;[ListView] 추가 로그 {count}&quot;)

    def refresh_view(self):
        self.list_widget.clear()

        for text in self.model.logs():
            self.list_widget.addItem(text)

        self.list_widget.scrollToBottom()&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ListView는 QListWidget을 사용하지만, 데이터 원본은 QListWidget 안에 있지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;항상 LogModel의 logs()를 다시 읽어 화면을 채웁니다.&lt;/p&gt;
&lt;pre class=&quot;stylus&quot; data-ke-language=&quot;text&quot;&gt;&lt;code&gt;LogModel.logs()
    &amp;darr;
QListWidget.clear()
    &amp;darr;
addItem()으로 현재 로그 다시 표시
    &amp;darr;
scrollToBottom()&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, QListWidget은 화면 표시 역할을 맡습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그 원본 데이터는 여전히 LogModel에 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;5.3 QTextEdit 기반 View: view_textedit.py&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세 번째 View는 QTextEdit을 사용합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그 출력창처럼 한 줄씩 로그를 보여주는 화면입니다.&lt;/p&gt;
&lt;pre class=&quot;ruby&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;# view_textedit.py

from PySide6.QtGui import QTextCursor
from PySide6.QtWidgets import (
    QWidget,
    QVBoxLayout,
    QPushButton,
    QTextEdit,
)

from model_log import LogModel


class TextEditView(QWidget):
    &quot;&quot;&quot;
    QTextEdit을 사용해 로그를 보여주는 View입니다.
    전형적인 로그 출력창 UI 스타일입니다.
    &quot;&quot;&quot;

    def __init__(self, model: LogModel, parent=None):
        super().__init__(parent)
        self.model = model

        layout = QVBoxLayout(self)

        self.log_textedit = QTextEdit()
        self.log_textedit.setReadOnly(True)
        layout.addWidget(self.log_textedit)

        self.log_button = QPushButton(&quot;텍스트 뷰에 로그 추가&quot;)
        self.log_button.clicked.connect(self.add_log_clicked)
        layout.addWidget(self.log_button)

        self.model.logsChanged.connect(self.refresh_view)

        self.refresh_view()

    def add_log_clicked(self):
        count = len(self.model.logs()) + 1
        self.model.add_log(f&quot;[TextEditView] 추가 로그 {count}&quot;)

    def refresh_view(self):
        self.log_textedit.clear()

        for text in self.model.logs():
            self.log_textedit.append(text)

        cursor = self.log_textedit.textCursor()
        cursor.movePosition(QTextCursor.End)
        self.log_textedit.setTextCursor(cursor)
        self.log_textedit.ensureCursorVisible()&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TextEditView도 로그를 직접 소유하지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LogModel의 로그를 읽어 QTextEdit에 다시 표시합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;QTextEdit에는 화면 표시용으로 다시 채워 넣을 뿐입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원본 로그 데이터는 여전히 LogModel에 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;5.4 세 View의 공통 패턴&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세 View는 서로 다른 위젯을 사용하지만 공통 패턴은 같습니다.&lt;/p&gt;
&lt;pre class=&quot;css&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;self.model.logsChanged.connect(self.refresh_view)
self.refresh_view()&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫 번째 줄은 Model이 바뀌었을 때 View를 다시 그리도록 연결합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 번째 줄은 처음 화면이 만들어질 때 현재 Model 데이터를 한 번 표시합니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #f8fbff; border: 1px solid #d8e8ff; border-radius: 10px; padding: 18px; margin: 25px 0;&quot;&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;세 View의 공통 패턴:&lt;/b&gt; 각 View는 logsChanged 시그널에 refresh_view()를 연결합니다. 그래서 Model이 바뀌면 각 View가 자기 방식으로 화면을 다시 그립니다.&lt;/p&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;5.5 세 View 비교&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세 View의 차이를 정리하면 다음과 같습니다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; margin-top: 15px; margin-bottom: 20px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;background-color: #f5f8ff;&quot;&gt;
&lt;td style=&quot;width: 25%; padding: 10px;&quot;&gt;&lt;b&gt;View&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 35%; padding: 10px;&quot;&gt;&lt;b&gt;표시 방식&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 40%; padding: 10px;&quot;&gt;&lt;b&gt;Model 사용 방식&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;ScrollView&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;QScrollArea 안에 QLabel을 여러 개 생성&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;logs()를 읽어 QLabel로 다시 만듭니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;ListView&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;QListWidget 아이템으로 표시&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;logs()를 읽어 addItem()으로 다시 채웁니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;TextEditView&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;QTextEdit에 한 줄씩 append&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;logs()를 읽어 텍스트 영역을 다시 구성합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre class=&quot;ada&quot; data-ke-language=&quot;text&quot;&gt;&lt;code&gt;같은 LogModel
    &amp;darr;
ScrollView    : QLabel 여러 개
ListView      : QListWidget 아이템
TextEditView  : QTextEdit 문장들

데이터는 하나
표현 방식은 세 개&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;!-- ========================================================= --&gt;&lt;!-- 6. 메인 화면과 실행 코드 작성 --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;!-- ========================================================= --&gt;
&lt;h1&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;6.&lt;/span&gt; Widget&amp;nbsp;&lt;/h1&gt;
&lt;p style=&quot;color: #9d9d9d;&quot; data-ke-size=&quot;size14&quot;&gt;&amp;rarr; 세 View를 탭으로 묶고, main.py에서 LogModel을 전달합니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot; data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;6.1 widget.py 작성하기: 세 View를 QTabWidget으로 묶기&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 세 View를 하나의 탭 화면으로 묶습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중요한 점은 세 View에 모두 같은 model을 전달한다는 점입니다.&lt;/p&gt;
&lt;pre class=&quot;python&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;# widget.py

from PySide6.QtWidgets import QWidget, QVBoxLayout, QTabWidget

from model_log import LogModel
from view_scroll import ScrollView
from view_list import ListView
from view_textedit import TextEditView


class ViewWidget(QWidget):
    &quot;&quot;&quot;
    여러 View를 탭으로 묶은 메인 화면입니다.

    - ScrollView
    - ListView
    - TextEditView

    세 View 모두 같은 LogModel 인스턴스를 공유합니다.
    &quot;&quot;&quot;

    def __init__(self, model: LogModel, parent=None):
        super().__init__(parent)

        self.setWindowTitle(&quot;Model / View 데모 - 여러 View, 하나의 Model&quot;)
        self.resize(600, 400)

        layout = QVBoxLayout(self)

        tab_widget = QTabWidget()

        tab_widget.addTab(ScrollView(model), &quot;QScrollArea 뷰&quot;)
        tab_widget.addTab(ListView(model), &quot;QListWidget 뷰&quot;)
        tab_widget.addTab(TextEditView(model), &quot;QTextEdit 뷰&quot;)

        layout.addWidget(tab_widget)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;QTabWidget은 여러 화면을 탭으로 나누어 보여주는 위젯입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서는 ScrollView, ListView, TextEditView를 각각 탭으로 추가했습니다.&lt;/p&gt;
&lt;pre class=&quot;isbl&quot; data-ke-language=&quot;text&quot;&gt;&lt;code&gt;ViewWidget
    &amp;darr;
QTabWidget
    ├── ScrollView(model)
    ├── ListView(model)
    └── TextEditView(model)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세 View에 전달된 model은 모두 같은 객체입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 어느 탭에서 로그를 추가해도 세 View가 같은 데이터를 보게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;6.2 main.py 작성하기: 프로그램 실행 시작점 만들기&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막으로 main.py를 작성합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;main.py에서는 QApplication을 만들고, LogModel을 생성한 뒤 ViewWidget에 전달합니다.&lt;/p&gt;
&lt;pre class=&quot;python&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;# main.py

import sys
from PySide6.QtWidgets import QApplication

from model_log import LogModel
from widget import ViewWidget


def main():
    &quot;&quot;&quot;
    프로그램의 시작점입니다.

    - QApplication 생성
    - LogModel 생성
    - ViewWidget에 Model 전달
    - 이벤트 루프 실행
    &quot;&quot;&quot;
    app = QApplication(sys.argv)
    app.setApplicationName(&quot;Model / View 데모&quot;)

    model = LogModel()

    model.add_log(&quot;[Init] 프로그램 시작&quot;)
    model.add_log(&quot;[Init] Model / View 데모 준비 완료&quot;)

    window = ViewWidget(model)
    window.show()

    sys.exit(app.exec())


if __name__ == &quot;__main__&quot;:
    main()&lt;/code&gt;&lt;/pre&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; margin-top: 15px; margin-bottom: 20px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;background-color: #f5f8ff;&quot;&gt;
&lt;td style=&quot;width: 35%; padding: 10px;&quot;&gt;&lt;b&gt;코드&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 65%; padding: 10px;&quot;&gt;&lt;b&gt;역할&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;model = LogModel()&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;공통 데이터 Model을 하나만 생성합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;model.add_log(...)&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;초기 테스트 로그를 Model에 추가합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;ViewWidget(model)&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;세 View가 들어 있는 메인 화면에 Model을 전달합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;app.exec()&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;Qt 이벤트 루프를 실행합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;6.3 실행 결과 구조&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로그램을 실행하면 세 개의 탭이 있는 창이 나타납니다.&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;460&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/PGWeA/dJMcabxzI9I/tUAVpezREw5zMC67f6UMKk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/PGWeA/dJMcabxzI9I/tUAVpezREw5zMC67f6UMKk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/PGWeA/dJMcabxzI9I/tUAVpezREw5zMC67f6UMKk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FPGWeA%2FdJMcabxzI9I%2FtUAVpezREw5zMC67f6UMKk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;460&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;460&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p data-ke-size=&quot;size16&quot;&gt;어느 탭에서 로그 추가 버튼을 눌러도 LogModel에 로그가 추가됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 logsChanged 시그널을 통해 모든 View가 다시 갱신됩니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #f8fbff; border: 1px solid #d8e8ff; border-radius: 10px; padding: 18px; margin: 25px 0;&quot;&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;이번 단계의 핵심:&lt;/b&gt; 버튼은 각 View에 있지만 로그 데이터는 View에 저장되지 않습니다. 모든 로그는 LogModel 하나에 저장됩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;!-- ========================================================= --&gt;&lt;!-- 7. 기존 방식과 Model/View 방식 비교 --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;!-- ========================================================= --&gt;
&lt;h1&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;7.&lt;/span&gt; Model/View 방식 비교&lt;/h1&gt;
&lt;p style=&quot;color: #9d9d9d;&quot; data-ke-size=&quot;size14&quot;&gt;&amp;rarr; View 안에 데이터를 넣는 방식과 Model을 공유하는 방식을 비교합니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot; data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;7.1 구조 비교&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 방식과 이번 Model/View 방식을 비교해 보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 큰 차이는 로그 데이터가 어디에 저장되는가입니다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; margin-top: 15px; margin-bottom: 20px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;background-color: #f5f8ff;&quot;&gt;
&lt;td style=&quot;width: 25%; padding: 10px;&quot;&gt;&lt;b&gt;구분&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 37.5%; padding: 10px;&quot;&gt;&lt;b&gt;기존 방식&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 37.5%; padding: 10px;&quot;&gt;&lt;b&gt;Model/View 방식&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;데이터 위치&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;각 View 내부&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;LogModel 내부&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;View 역할&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;데이터 저장과 화면 표시를 함께 담당&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;Model의 데이터를 화면에 표시&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;데이터 변경&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;각 View가 개별적으로 처리&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;LogModel이 처리하고 logsChanged로 알림&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;View 동기화&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;직접 구현해야 함&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;logsChanged에 연결된 refresh_view()로 갱신&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;확장성&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;View가 늘어나면 데이터 관리가 복잡해짐&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;새 View도 같은 Model을 연결하면 됨&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre class=&quot;properties&quot; data-ke-language=&quot;text&quot;&gt;&lt;code&gt;기존 방식

QScrollArea  &amp;larr; 데이터
QListWidget  &amp;larr; 데이터
QTextEdit    &amp;larr; 데이터

각 View가 데이터를 따로 들고 있음&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;properties&quot; data-ke-language=&quot;text&quot;&gt;&lt;code&gt;Model/View 방식

ScrollView    &amp;larr;─┐
ListView      &amp;larr;── LogModel &amp;larr;── 로그 데이터
TextEditView  &amp;larr;─┘

하나의 Model을 여러 View가 함께 사용&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;7.2 의존성 관점에서 보기&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 방식에서는 View와 데이터가 강하게 묶입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 QListWidget에 로그를 직접 추가하면, 로그 데이터가 QListWidget에 종속됩니다.&lt;/p&gt;
&lt;pre class=&quot;&quot; data-ke-language=&quot;text&quot;&gt;&lt;code&gt;강한 의존 구조

QListWidget
 ├── 화면 표시
 └── 로그 데이터 저장

화면과 데이터가 한 곳에 섞여 있음&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반대로 이번 예제에서는 데이터와 화면 표시를 분리합니다.&lt;/p&gt;
&lt;pre class=&quot;cos&quot; data-ke-language=&quot;text&quot;&gt;&lt;code&gt;분리된 구조

LogModel
 └── 로그 데이터 관리

View
 └── 로그 데이터 화면 표시

역할이 나누어져 있음&lt;/code&gt;&lt;/pre&gt;
&lt;div style=&quot;background-color: #f8fbff; border: 1px solid #d8e8ff; border-radius: 10px; padding: 18px; margin: 25px 0;&quot;&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;중요한 점:&lt;/b&gt; Model/View 구조의 핵심은 View를 없애는 것이 아닙니다. View는 화면 표시를 담당하고, Model은 데이터 관리를 담당하도록 역할을 분리하는 것입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;7.3 같은 데이터, 다른 View, 자동 갱신&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 구조에서는 어느 View에서 로그를 추가해도 모든 View가 같은 데이터를 봅니다.&lt;/p&gt;
&lt;pre class=&quot;isbl&quot; data-ke-language=&quot;text&quot;&gt;&lt;code&gt;ScrollView에서 로그 추가
    &amp;darr;
LogModel.add_log()
    &amp;darr;
logsChanged 발생
    &amp;darr;
ScrollView refresh_view()
ListView refresh_view()
TextEditView refresh_view()
    &amp;darr;
세 View 모두 같은 로그 표시&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이것이 이번 예제에서 체감해야 할 핵심입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터는 하나이고, View는 여러 개입니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #f8f8f8; border: 1px solid #dddddd; border-radius: 10px; padding: 18px; margin: 25px 0;&quot;&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;기억할 문장:&lt;/b&gt; 한 곳에서 데이터를 바꾸면, 같은 Model을 보는 모든 View가 함께 갱신됩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;!-- ========================================================= --&gt;&lt;!-- 8. 추가 실습 과제 --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;!-- ========================================================= --&gt;
&lt;h1&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;8.&lt;/span&gt; 추가 실습 과제: QSplitter로 세 View를 동시에 보여주기&lt;/h1&gt;
&lt;p style=&quot;color: #9d9d9d;&quot; data-ke-size=&quot;size14&quot;&gt;&amp;rarr; 세 View를 한 화면에 동시에 보여주어 같은 Model 공유 효과를 확인합니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot; data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;8.1 QSplitter&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 예제는 QTabWidget을 사용하므로 한 번에 하나의 View만 볼 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 로그를 추가한 뒤 다른 탭으로 이동해야 각 View가 갱신되었는지 확인할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;QSplitter를 사용하면 ScrollView, ListView, TextEditView를 한 화면에 동시에 배치할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #f6f6f6; color: #222222; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a style=&quot;background-color: #e6f5ff; color: #0070d1;&quot; href=&quot;https://basiclike.tistory.com/602&quot;&gt;3.11 QScrollArea&lt;/a&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;결과&lt;/p&gt;
&lt;p style=&quot;background-color: #f6f6f6; color: #222222; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;: 구현 목표와 비교&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1242&quot; data-origin-height=&quot;474&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/GoiKe/dJMcaa6yMw8/XHtkE7WMBpcQoilLF39K20/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/GoiKe/dJMcaa6yMw8/XHtkE7WMBpcQoilLF39K20/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/GoiKe/dJMcaa6yMw8/XHtkE7WMBpcQoilLF39K20/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FGoiKe%2FdJMcaa6yMw8%2FXHtkE7WMBpcQoilLF39K20%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1242&quot; height=&quot;474&quot; data-origin-width=&quot;1242&quot; data-origin-height=&quot;474&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #f6f6f6; color: #222222; text-align: start;&quot; data-ke-size=&quot;size16&quot; data-start=&quot;3341&quot; data-end=&quot;3374&quot;&gt;&lt;b&gt;구현 목표&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #f6f6f6; color: #222222; text-align: start;&quot; data-ke-size=&quot;size16&quot; data-start=&quot;3341&quot; data-end=&quot;3374&quot;&gt;:&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a style=&quot;color: #0070d1;&quot; href=&quot;https://basiclike.tistory.com/602&quot;&gt;3.11 QScrollArea&lt;/a&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;결과와 비교&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1237&quot; data-origin-height=&quot;463&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b0Cc9i/dJMcaf02iSZ/XhItmBNjcYGDA2W90wnEkk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b0Cc9i/dJMcaf02iSZ/XhItmBNjcYGDA2W90wnEkk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b0Cc9i/dJMcaf02iSZ/XhItmBNjcYGDA2W90wnEkk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb0Cc9i%2FdJMcaf02iSZ%2FXhItmBNjcYGDA2W90wnEkk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1237&quot; height=&quot;463&quot; data-origin-width=&quot;1237&quot; data-origin-height=&quot;463&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1237&quot; data-origin-height=&quot;463&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bx9ozc/dJMcada2Df4/Zg9WLDk7UahUsjVLwzYKO0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bx9ozc/dJMcada2Df4/Zg9WLDk7UahUsjVLwzYKO0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bx9ozc/dJMcada2Df4/Zg9WLDk7UahUsjVLwzYKO0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbx9ozc%2FdJMcada2Df4%2FZg9WLDk7UahUsjVLwzYKO0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1237&quot; height=&quot;463&quot; data-origin-width=&quot;1237&quot; data-origin-height=&quot;463&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;div style=&quot;background-color: #f8fbff; border: 1px solid #d8e8ff; border-radius: 10px; padding: 18px; margin: 25px 0;&quot;&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;실습 목표:&lt;/b&gt; 어느 View에서 로그를 추가해도 세 View가 동시에 갱신되는 모습을 확인합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;8.2 QSplitter 적용 코드&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;widget.py를 다음과 같은 방식으로 바꿀 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;# widget.py

from PySide6.QtCore import Qt
from PySide6.QtWidgets import QWidget, QVBoxLayout, QSplitter

from model_log import LogModel
from view_scroll import ScrollView
from view_list import ListView
from view_textedit import TextEditView


class ViewWidget(QWidget):
    def __init__(self, model: LogModel, parent=None):
        super().__init__(parent)

        self.setWindowTitle(&quot;Model / View 데모 - QSplitter&quot;)
        self.resize(900, 400)

        layout = QVBoxLayout(self)

        splitter = QSplitter(Qt.Horizontal)

        splitter.addWidget(ScrollView(model))
        splitter.addWidget(ListView(model))
        splitter.addWidget(TextEditView(model))

        splitter.setSizes([300, 300, 300])

        layout.addWidget(splitter)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;QTabWidget 대신 QSplitter를 사용하면 탭을 전환하지 않아도 세 View를 동시에 볼 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 View는 여전히 같은 model을 전달받습니다.&lt;/p&gt;
&lt;pre class=&quot;isbl&quot; data-ke-language=&quot;text&quot;&gt;&lt;code&gt;ViewWidget
    &amp;darr;
QSplitter
    ├── ScrollView(model)
    ├── ListView(model)
    └── TextEditView(model)

화면은 3개
Model은 1개&lt;/code&gt;&lt;/pre&gt;
&lt;figure class=&quot;fileblock&quot; data-ke-align=&quot;alignCenter&quot;&gt;&lt;a href=&quot;https://blog.kakaocdn.net/dn/cXhTOg/dJMcagTdXxU/eMFvSOXROzdZvU9oa527Z1/widget%20%281%29.py?attach=1&amp;amp;knm=tfile.py&quot; class=&quot;&quot;&gt;
    &lt;div class=&quot;image&quot;&gt;&lt;/div&gt;
    &lt;div class=&quot;desc&quot;&gt;&lt;div class=&quot;filename&quot;&gt;&lt;span class=&quot;name&quot;&gt;widget (1).py&lt;/span&gt;&lt;/div&gt;
&lt;div class=&quot;size&quot;&gt;0.00MB&lt;/div&gt;
&lt;/div&gt;
  &lt;/a&gt;&lt;/figure&gt;

&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;8.3 실행 결과에서 확인할 점&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;QSplitter 버전으로 실행하면 세 View가 한 화면에 나란히 표시됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 하나의 View에서 로그 추가 버튼을 눌러 보세요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러면 나머지 두 View도 같은 로그를 함께 표시하는지 바로 확인할 수 있습니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #f8fbff; border: 1px solid #d8e8ff; border-radius: 10px; padding: 18px; margin: 25px 0;&quot;&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;추가 실습의 핵심:&lt;/b&gt; 화면은 3개여도 Model은 1개입니다. 어느 View에서 로그를 추가해도 모든 View가 같은 로그 데이터를 보여줍니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;!-- ========================================================= --&gt;&lt;!-- 9. 정리 --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;!-- ========================================================= --&gt;
&lt;h1&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;9.&lt;/span&gt; 정리&lt;/h1&gt;
&lt;p style=&quot;color: #9d9d9d;&quot; data-ke-size=&quot;size14&quot;&gt;&amp;rarr; 직접 만든 Model을 여러 View가 공유하는 핵심 흐름을 정리합니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot; data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 단계에서는 LogModel 하나를 여러 View가 함께 공유하는 구조를 만들었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심은 View가 데이터를 직접 저장하지 않고, Model의 데이터를 읽어서 화면에 보여준다는 점입니다.&lt;/p&gt;
&lt;pre class=&quot;cos&quot; data-ke-language=&quot;text&quot;&gt;&lt;code&gt;기존 방식
    &amp;darr;
View 안에 데이터 저장
    &amp;darr;
View마다 데이터가 따로 관리됨
    &amp;darr;
데이터 공유와 동기화가 어려움


Model/View 방식
    &amp;darr;
LogModel이 데이터 중앙 관리
    &amp;darr;
여러 View가 같은 Model을 바라봄
    &amp;darr;
logsChanged 시그널로 모든 View가 자동 갱신&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 단계의 핵심 개념을 다시 정리하면 다음과 같습니다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; margin-top: 15px; margin-bottom: 20px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;background-color: #f5f8ff;&quot;&gt;
&lt;td style=&quot;width: 30%; padding: 10px;&quot;&gt;&lt;b&gt;개념&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 70%; padding: 10px;&quot;&gt;&lt;b&gt;정리&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;LogModel&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;로그 문자열을 저장하고 관리하는 공통 데이터 Model입니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;logsChanged&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;로그 데이터 변경을 View들에게 알리는 시그널입니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;ScrollView&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;QScrollArea와 QLabel로 로그를 보여주는 View입니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;ListView&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;QListWidget으로 로그를 보여주는 View입니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;TextEditView&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;QTextEdit으로 로그를 보여주는 View입니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;refresh_view()&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;Model의 로그를 다시 읽어 각 View의 화면을 갱신하는 함수입니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;div style=&quot;background-color: #f8fbff; border: 1px solid #d8e8ff; border-radius: 10px; padding: 18px; margin: 25px 0;&quot;&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;이번 단계의 핵심:&lt;/b&gt; 데이터는 LogModel 하나에 저장하고, View는 그 데이터를 각자의 방식으로 보여줍니다. Model이 바뀌면 logsChanged 시그널을 통해 모든 View가 자동으로 다시 그려집니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div style=&quot;background-color: #f8f8f8; border: 1px solid #dddddd; border-radius: 10px; padding: 18px; margin: 25px 0;&quot;&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;기억할 문장:&lt;/b&gt; 데이터는 Model에 한 번만 저장하고, 여러 View는 같은 Model을 서로 다른 방식으로 보여줍니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;!-- ========================================================= --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;!-- 참고. 공식 문서로 확인하기 --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;!-- ========================================================= --&gt;
&lt;h1&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;참고.&lt;/span&gt; 공식 문서로 확인하기&lt;/h1&gt;
&lt;p style=&quot;color: #9d9d9d;&quot; data-ke-size=&quot;size14&quot;&gt;&amp;rarr; 공식 문서에서 이번 예제와 연결되는 기준을 확인합니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot; data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1. 공식 문서 참고 표&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 예제에서 사용한 QObject, Signal, QScrollArea, QListWidget, QTextEdit, QTabWidget, QSplitter는 Qt for Python 공식 문서에서 확인할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공식 문서는 실제 클래스의 역할과 메서드를 확인할 때 가장 정확한 기준이 됩니다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; margin-top: 15px; margin-bottom: 20px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;background-color: #f5f8ff;&quot;&gt;
&lt;td style=&quot;width: 25%; padding: 10px;&quot;&gt;&lt;b&gt;구분&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 35%; padding: 10px;&quot;&gt;&lt;b&gt;공식 문서&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 40%; padding: 10px;&quot;&gt;&lt;b&gt;확인할 내용&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;Model/View Programming&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;&lt;a href=&quot;https://doc.qt.io/qtforpython-6/overviews/qtwidgets-model-view-programming.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Qt for Python - Model/View Programming&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;Qt의 Model/View 구조가 데이터와 표시 방식을 분리하는 구조임을 확인할 수 있습니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;QObject&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;&lt;a href=&quot;https://doc.qt.io/qtforpython-6/PySide6/QtCore/QObject.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;PySide6.QtCore.QObject&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;Qt 객체의 기본 클래스와 시그널/슬롯 기반 통신 구조를 확인할 수 있습니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;Signal&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;&lt;a href=&quot;https://doc.qt.io/qtforpython-6/PySide6/QtCore/Signal.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;PySide6.QtCore.Signal&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;사용자 정의 시그널을 만드는 방법을 확인할 수 있습니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;QListWidget&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;&lt;a href=&quot;https://doc.qt.io/qtforpython-6/PySide6/QtWidgets/QListWidget.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;PySide6.QtWidgets.QListWidget&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;항목 기반 리스트 위젯의 사용 방식을 확인할 수 있습니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;QTextEdit&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;&lt;a href=&quot;https://doc.qt.io/qtforpython-6/PySide6/QtWidgets/QTextEdit.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;PySide6.QtWidgets.QTextEdit&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;텍스트 표시와 편집을 위한 위젯 기능을 확인할 수 있습니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;QSplitter&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;&lt;a href=&quot;https://doc.qt.io/qtforpython-6/PySide6/QtWidgets/QSplitter.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;PySide6.QtWidgets.QSplitter&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;여러 위젯을 나누어 동시에 표시하는 방법을 확인할 수 있습니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2. 이번 예제와 공식 문서 연결&lt;/b&gt;&lt;/h2&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; margin-top: 15px; margin-bottom: 20px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;background-color: #f5f8ff;&quot;&gt;
&lt;td style=&quot;width: 35%; padding: 10px;&quot;&gt;&lt;b&gt;이번 예제 코드&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 65%; padding: 10px;&quot;&gt;&lt;b&gt;공식 문서와 연결되는 의미&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;class LogModel(QObject)&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;Qt 객체로 동작하는 사용자 정의 Model을 만듭니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;logsChanged = Signal()&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;Model 데이터 변경 사실을 View에 알리는 사용자 정의 시그널입니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;self.model.logsChanged.connect(...)&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;Model 변경 신호와 View 갱신 함수를 연결합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;QTabWidget&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;세 View를 탭 구조로 나누어 보여줍니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;QSplitter&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;추가 실습에서 세 View를 동시에 보여주는 구조로 확장할 수 있습니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;3. 정리 흐름도&lt;/b&gt;&lt;/h2&gt;
&lt;div style=&quot;background-color: #f8f8f8; border: 1px solid #dddddd; border-radius: 10px; padding: 18px; margin: 25px 0;&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;공식 문서 기준으로 정리하면 다음과 같습니다.&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;stylus&quot; data-ke-language=&quot;text&quot;&gt;&lt;code&gt;데이터
    &amp;darr;
LogModel(QObject)
    &amp;darr;
logsChanged Signal
    &amp;darr;
ScrollView / ListView / TextEditView
    &amp;darr;
각 View가 같은 데이터를 서로 다른 방식으로 표시&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 이번 예제는 직접 만든 Model을 여러 View가 공유하는 기본 구조를 보여줍니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;</description>
      <category>3. PySide6/3-4. Model - View</category>
      <author>BasicLike</author>
      <guid isPermaLink="true">https://basiclike.tistory.com/613</guid>
      <comments>https://basiclike.tistory.com/613#entry613comment</comments>
      <pubDate>Fri, 29 May 2026 16:30:52 +0900</pubDate>
    </item>
    <item>
      <title>3강. Model/View 실습</title>
      <link>https://basiclike.tistory.com/775</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1627&quot; data-origin-height=&quot;554&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/SR5YJ/dJMcaiaJ4io/LPuxuCohtYObEES3rkVfe0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/SR5YJ/dJMcaiaJ4io/LPuxuCohtYObEES3rkVfe0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/SR5YJ/dJMcaiaJ4io/LPuxuCohtYObEES3rkVfe0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FSR5YJ%2FdJMcaiaJ4io%2FLPuxuCohtYObEES3rkVfe0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1627&quot; height=&quot;554&quot; data-origin-width=&quot;1627&quot; data-origin-height=&quot;554&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h1 style=&quot;color: #000000; text-align: start;&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;1.&lt;/span&gt;&lt;span&gt; &lt;/span&gt;QTableView 추가하기&lt;/h1&gt;
&lt;p style=&quot;color: #9d9d9d;&quot; data-ke-size=&quot;size14&quot;&gt;&amp;rarr; 같은 Model을 세 개의 View가 함께 사용하는 구조로 확장합니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot; data-text-less=&quot;닫기&quot; data-text-more=&quot;더보기&quot; data-ke-type=&quot;moreLess&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1.1 실습 목표&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지는&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하나의 QFileSystemModel을&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 개의 View(&lt;span style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot;&gt;QTreeView와 QListView&lt;/span&gt;)가 함께 사용했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에는 여기에 QTableView를 하나 더 추가합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;목표는 하나의 Model을 세 개의 View가 함께 사용하는 구조를 만드는 것입니다.&lt;/p&gt;
&lt;pre class=&quot;nginx&quot; style=&quot;background-color: #f7f7f7; border: 1px solid #e0e0e0; border-radius: 6px; padding: 16px; overflow-x: auto; line-height: 1.7; font-size: 14px;&quot;&gt;&lt;code&gt;QFileSystemModel
    ├── QTreeView
    ├── QListView
    └── QTableView&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;QTableView는 파일과 폴더 정보를 표 형태로 보여줄 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파일 이름, 크기, 종류, 수정 날짜 같은 정보를 열 단위로 확인할 때 적합합니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #f8fbff; border: 1px solid #d8e8ff; border-radius: 10px; padding: 18px; margin: 25px 0;&quot;&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;실습 목표&lt;/b&gt;&lt;br /&gt;QTableView를 추가해도 &lt;b&gt;새로운 Model을 만들 필요가 없다는 점&lt;/b&gt;을 확인합니다.&lt;/p&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;같은 QFileSystemModel을 QTreeView, QListView, QTableView가 함께 사용합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1.2 import에 QTableView 추가하기&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 widget.py 상단 import에 QTableView를 추가합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;QTableView는 PySide6.QtWidgets에서 가져올 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;python&quot; style=&quot;background-color: #f7f7f7; border: 1px solid #ddd; border-radius: 6px; padding: 15px; overflow-x: auto;&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;from PySide6.QtCore import QDir, Qt
from PySide6.QtWidgets import (
    QWidget,
    QSplitter,
    QFileSystemModel,
    QTreeView,
    QListView,
    QTableView, # 추가
    QVBoxLayout,
)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 widget.py 안에서 QTableView 클래스를 사용할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1.3 QTableView 생성하고 같은 Model 연결하기&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 QTreeView와 QListView과 같은 방식으로 QTableView도 추가합니다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot; style=&quot;background-color: #f7f7f7; border: 1px solid #ddd; border-radius: 6px; padding: 15px; overflow-x: auto;&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;# --- (3) QTableView 설정 ---
self.table_view = QTableView(splitter)
self.table_view.setModel(self.model)
self.table_view.setRootIndex(self.model.index(root_path))&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구조는 앞에서 작성한 QTreeView, QListView와 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중요한 점은 setModel(self.model)을 사용해 새로운 Model을 만들 필요가 없다는 점입니다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; margin-top: 15px; margin-bottom: 20px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;background-color: #f5f8ff;&quot;&gt;
&lt;td style=&quot;width: 35%; padding: 10px;&quot;&gt;&lt;b&gt;코드&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 65%; padding: 10px;&quot;&gt;&lt;b&gt;설명&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;QTableView(splitter)&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;스플리터 안에 표 형태의 View를 만듭니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;setModel(self.model)&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;QTreeView, QListView와 같은 QFileSystemModel을 연결합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;setRootIndex(...)&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;표에서 어느 폴더 내용을 보여줄지 정합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre class=&quot;reasonml&quot; style=&quot;background-color: #f7f7f7; border: 1px solid #e0e0e0; border-radius: 6px; padding: 16px; overflow-x: auto; line-height: 1.7; font-size: 14px;&quot;&gt;&lt;code&gt;QTableView
    &amp;darr;
setModel(self.model)
    &amp;darr;
같은 QFileSystemModel을 표 형태로 표시&lt;/code&gt;&lt;/pre&gt;
&lt;div style=&quot;background-color: #f8fbff; border: 1px solid #d8e8ff; border-radius: 10px; padding: 18px; margin: 25px 0;&quot;&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;중요한 점&lt;/b&gt;&lt;br /&gt;QTableView를 추가해도 Model은 새로 만들지 않습니다.&lt;/p&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;기존 self.model을 그대로 연결합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1.4 선택 변경 함수 수정하기&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전에는 트리에서 선택이 바뀌면 QListView의 Root Index만 바꿨습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 QTableView도 함께 바뀌도록 수정합니다.&lt;/p&gt;
&lt;pre class=&quot;python&quot; style=&quot;background-color: #f7f7f7; border: 1px solid #ddd; border-radius: 6px; padding: 15px; overflow-x: auto;&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;def on_tree_current_changed(self, current, previous):
    &quot;&quot;&quot;
    트리에서 선택이 바뀔 때마다 호출됩니다.

    current  : 현재 선택된 QModelIndex
    previous : 이전에 선택되어 있던 QModelIndex
    &quot;&quot;&quot;
    if self.model.isDir(current):
        self.list_view.setRootIndex(current)
        self.table_view.setRootIndex(current)  # &amp;larr; 추가
    else:
        parent_index = current.parent()
        self.list_view.setRootIndex(parent_index)
        self.table_view.setRootIndex(parent_index)  # &amp;larr; 추가&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;폴더를 선택하면 QListView와 QTableView가 모두 선택한 폴더 내용을 보여줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파일을 선택하면 해당 파일이 들어 있는 부모 폴더 내용을 보여줍니다.&lt;/p&gt;
&lt;pre class=&quot;python&quot; style=&quot;background-color: #f7f7f7; border: 1px solid #e0e0e0; border-radius: 6px; padding: 16px; overflow-x: auto; line-height: 1.7; font-size: 14px;&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;QTreeView에서 폴더 클릭
    &amp;darr;
QListView의 Root Index 변경
QTableView의 Root Index 변경


QTreeView에서 파일 클릭
    &amp;darr;
부모 폴더 인덱스 가져오기
    &amp;darr;
QListView와 QTableView가 부모 폴더 내용 표시&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1.5 전체 구조 정리&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;QTableView를 추가한 후 전체 구조는 다음과 같습니다.&lt;/p&gt;
&lt;pre class=&quot;python&quot; style=&quot;background-color: #f7f7f7; border: 1px solid #e0e0e0; border-radius: 6px; padding: 16px; overflow-x: auto; line-height: 1.7; font-size: 14px;&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;실제 파일/폴더 데이터
    &amp;darr;
QFileSystemModel
    ├──&amp;gt; QTreeView   : 트리 형태로 출력
    ├──&amp;gt; QListView   : 리스트 형태로 출력
    └──&amp;gt; QTableView  : 표 형태로 출력&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터는 하나입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 표현 방식은 세 개가 되었습니다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; margin-top: 15px; margin-bottom: 20px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;background-color: #f5f8ff;&quot;&gt;
&lt;td style=&quot;width: 30%; padding: 10px;&quot;&gt;&lt;b&gt;View&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 70%; padding: 10px;&quot;&gt;&lt;b&gt;표현 방식&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;QTreeView&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;파일과 폴더를 트리 구조로 보여줍니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;QListView&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;선택한 폴더 안의 항목을 리스트로 보여줍니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;QTableView&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;선택한 폴더 안의 항목을 표 형태로 보여줍니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;div style=&quot;background-color: #f8f8f8; border: 1px solid #dddddd; border-radius: 10px; padding: 18px; margin: 25px 0;&quot;&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;기억할 문장&lt;/b&gt;&lt;br /&gt;View가 늘어나도 같은 Model을 연결하면 됩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h1 style=&quot;color: #000000; text-align: start;&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;2.&lt;/span&gt;&lt;span&gt; &lt;/span&gt;Model 외부에서 주입하기&lt;/h1&gt;
&lt;p style=&quot;color: #9d9d9d;&quot; data-ke-size=&quot;size14&quot;&gt;&amp;rarr; Model을 Widget 내부에서 만들지 않고, 외부에서 만들어 전달하는 구조를 실습합니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot; data-text-less=&quot;닫기&quot; data-text-more=&quot;더보기&quot; data-ke-type=&quot;moreLess&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2.1 실습 목표&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;QTableView를 추가해서 하나의 Model을 세 개의 View가 함께 사용하도록 만들었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 QFileSystemModel은 여전히 Widget 안에서 만들고 있습니다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot; style=&quot;background-color: #f7f7f7; border: 1px solid #ddd; border-radius: 6px; padding: 15px; overflow-x: auto;&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;self.model = QFileSystemModel(self)

root_path = QDir.currentPath()
self.model.setRootPath(root_path)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 구조도 틀린 것은 아닙니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 프로그램이 커지면 Model을 Widget 밖에서 만들고, Widget에는 주입하는 구조가 더 유용할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;stylus&quot; style=&quot;background-color: #f7f7f7; border: 1px solid #e0e0e0; border-radius: 6px; padding: 16px; overflow-x: auto; line-height: 1.7; font-size: 14px;&quot;&gt;&lt;code&gt;main.py
    &amp;darr;
QFileSystemModel 생성
    &amp;darr;
Widget(model, root_path)로 전달
    &amp;darr;
Widget은 전달받은 Model을 View에 연결&lt;/code&gt;&lt;/pre&gt;
&lt;div style=&quot;background-color: #f8fbff; border: 1px solid #d8e8ff; border-radius: 10px; padding: 18px; margin: 25px 0;&quot;&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;실습 목표&lt;/b&gt;&lt;br /&gt;Model을 Widget 내부에서 직접 만들지 않고, 외부에서 만든 Model을 Widget에 전달하는 구조를 이해합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2.2 구조 비교&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫번째 실습 구조에서는 Model 생성, View 생성, 시그널 연결이 모두 Widget 안에 들어 있습니다.&lt;/p&gt;
&lt;pre class=&quot;python&quot; style=&quot;background-color: #f7f7f7; border: 1px solid #e0e0e0; border-radius: 6px; padding: 16px; overflow-x: auto; line-height: 1.7; font-size: 14px;&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;Widget
 ├── QFileSystemModel 생성
 ├── QTreeView 생성
 ├── QListView 생성
 ├── QTableView 생성 # 첫번째 실습에서 추가
 ├── 시그널 연결
 └── 화면 배치&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두번째 실습 단계에서는&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Model 생성을 main.py로 옮기고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Widget 에 주입하는 구조를 구현합니다.&lt;/p&gt;
&lt;pre class=&quot;python&quot; style=&quot;background-color: #f7f7f7; border: 1px solid #e0e0e0; border-radius: 6px; padding: 16px; overflow-x: auto; line-height: 1.7; font-size: 14px;&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;main.py
 └── QFileSystemModel 생성 # 추가

Widget
 ├── 전달받은 Model 사용 # 수정
 ├── QTreeView 생성
 ├── QListView 생성
 ├── QTableView 생성
 ├── 시그널 연결
 └── 화면 배치&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;QTreeView, QListView, QTableView 생성 코드는 아직 Widget 안에 그대로 둡니다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; margin-top: 15px; margin-bottom: 20px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;background-color: #f5f8ff;&quot;&gt;
&lt;td style=&quot;width: 30%; padding: 10px;&quot;&gt;&lt;b&gt;역할&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 70%; padding: 10px;&quot;&gt;&lt;b&gt;이번 단계에서의 위치&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;Model 생성&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;main.py로 이동합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;View 생성&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;아직 Widget 안에 둡니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;시그널 연결&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;아직 Widget 안에 둡니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;div style=&quot;background-color: #f8fbff; border: 1px solid #d8e8ff; border-radius: 10px; padding: 18px; margin: 25px 0;&quot;&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;중요한 점&lt;/b&gt;&lt;br /&gt;이번 단계에서는 한 번에 모든 구조를 바꾸지 않습니다.&lt;/p&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;먼저 Model 생성 위치만 Widget 밖으로 분리합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2.3 main.py에서 Model 만들기&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에는 main.py에서 QFileSystemModel을 먼저 만듭니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 만든 Model과 root_path를 Widget에 전달합니다.&lt;/p&gt;
&lt;pre class=&quot;python&quot; style=&quot;background-color: #f7f7f7; border: 1px solid #ddd; border-radius: 6px; padding: 15px; overflow-x: auto;&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;# main.py

import sys

from PySide6.QtCore import QDir
from PySide6.QtWidgets import QApplication, QFileSystemModel

from widget import Widget


def main():
    app = QApplication(sys.argv)

    # [1] Model 생성
    # app을 부모로 지정하면 프로그램이 실행되는 동안 Model이 안전하게 유지됩니다.
    model = QFileSystemModel(app)

    # [2] Model의 기준 경로 설정
    root_path = QDir.currentPath()
    model.setRootPath(root_path)

    # [3] Model과 root_path를 Widget에 전달
    window = Widget(model, root_path)
    window.show()

    sys.exit(app.exec())


if __name__ == &quot;__main__&quot;:
    main()&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 QFileSystemModel은 Widget 안이 아니라 main.py에서 만들어집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Widget은 이미 만들어진 Model을 전달받아 View에 연결하기만 합니다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; margin-top: 15px; margin-bottom: 20px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;background-color: #f5f8ff;&quot;&gt;
&lt;td style=&quot;width: 35%; padding: 10px;&quot;&gt;&lt;b&gt;코드&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 65%; padding: 10px;&quot;&gt;&lt;b&gt;의미&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;model = QFileSystemModel(app)&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;파일/폴더 정보를 제공할 Model을 main.py에서 만듭니다. &lt;br /&gt;app을 부모로 지정해 프로그램 실행 동안 유지되도록 합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;model.setRootPath(root_path)&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;Model이 읽을 기준 경로를 설정합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;Widget(model, root_path)&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;생성한 Model과 기준 경로를 Widget에 주입합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;div style=&quot;background-color: #f8f8f8; border: 1px solid #dddddd; border-radius: 10px; padding: 18px; margin: 25px 0;&quot;&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;참고&lt;/b&gt;&lt;br /&gt;이 예제에서는 Model을 Widget 밖에서 만듭니다.&lt;/p&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;그래도 app을 부모로 지정했기 때문에 프로그램이 실행되는 동안 Model 객체가 안정적으로 유지됩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2.4 Widget이 Model을 전달받도록 수정하기&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 widget.py를 수정합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Widget이 직접 Model을 만들지 않고, __init__()의 매개변수로 전달받도록 바꿉니다.&lt;/p&gt;
&lt;pre class=&quot;ruby&quot; style=&quot;background-color: #f7f7f7; border: 1px solid #ddd; border-radius: 6px; padding: 15px; overflow-x: auto;&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;# widget.py

from PySide6.QtCore import Qt
from PySide6.QtWidgets import (
    QWidget,
    QSplitter,
    QTreeView,
    QListView,
    QTableView,
    QVBoxLayout,
)


class Widget(QWidget):
    &quot;&quot;&quot;
    외부에서 전달받은 Model을 사용해
    QTreeView, QListView, QTableView를 구성하는 위젯입니다.
    &quot;&quot;&quot;

    def __init__(self, model, root_path, parent=None):
        super().__init__(parent)

        # [1] 기본 창 설정
        self.setWindowTitle(&quot;Model/View 데모 - Model 외부 주입&quot;)
        self.resize(900, 400)

        # [2] 외부에서 전달받은 Model과 경로 저장
        self.model = model
        self.root_path = root_path

        # [3] 스플리터 생성
        splitter = QSplitter(Qt.Horizontal, self)

        # [4] TreeView 설정
        self.tree_view = QTreeView(splitter)
        self.tree_view.setModel(self.model)
        self.tree_view.setRootIndex(self.model.index(self.root_path))

        # [5] ListView 설정
        self.list_view = QListView(splitter)
        self.list_view.setModel(self.model)
        self.list_view.setRootIndex(self.model.index(self.root_path))

        # [6] TableView 설정
        self.table_view = QTableView(splitter)
        self.table_view.setModel(self.model)
        self.table_view.setRootIndex(self.model.index(self.root_path))

        # [7] 트리에서 선택이 바뀌면 다른 View의 Root Index 변경
        self.tree_view.selectionModel().currentChanged.connect(
            self.on_tree_current_changed
        )

        # [8] 전체 레이아웃 설정
        layout = QVBoxLayout(self)
        layout.addWidget(splitter)

    def on_tree_current_changed(self, current, previous):
        &quot;&quot;&quot;
        트리에서 선택한 항목에 따라
        ListView와 TableView의 표시 기준을 변경합니다.
        &quot;&quot;&quot;
        if self.model.isDir(current):
            self.list_view.setRootIndex(current)
            self.table_view.setRootIndex(current)
        else:
            parent_index = current.parent()
            self.list_view.setRootIndex(parent_index)
            self.table_view.setRootIndex(parent_index)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수정된 Widget은 QFileSystemModel을 직접 생성하지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대신 main.py에서 전달받은 model을 self.model에 저장한 뒤, 여러 View에 연결합니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #f8fbff; border: 1px solid #d8e8ff; border-radius: 10px; padding: 18px; margin: 25px 0;&quot;&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;중요한 점&lt;/b&gt;&lt;br /&gt;Widget은 Model을 생성하지 않습니다.&lt;/p&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;Widget은 전달받은 Model을 각 View에 연결하고, 선택 변경 시 Root Index를 바꾸는 역할을 담당합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2.5 전체 실행 흐름&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Model을 외부에서 주입하는 구조의 전체 실행 흐름은 다음과 같습니다.&lt;/p&gt;
&lt;pre class=&quot;python&quot; style=&quot;background-color: #f7f7f7; border: 1px solid #e0e0e0; border-radius: 6px; padding: 16px; overflow-x: auto; line-height: 1.7; font-size: 14px;&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;main.py 실행
    &amp;darr;
QApplication 생성
    &amp;darr;
QFileSystemModel 생성
    &amp;darr;
root_path 설정
    &amp;darr;
Widget(model, root_path) 생성, Model 주입
    &amp;darr;
Widget이 QTreeView / QListView / QTableView 생성,  Model 주입
    &amp;darr;
세 View가 같은 Model 사용
    &amp;darr;
창 표시&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 Model 생성 위치가 Widget 내부에서 main.py로 이동했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Widget은 화면 조립과 시그널 연결에 집중할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2.6 기존 구조와 Model 주입 구조 비교&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Model을 외부에서 주입하면 역할이 더 분명해집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;main.py는 프로그램 실행과 Model 준비를 담당합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Widget은 전달받은 Model을 View에 연결하고 화면을 구성합니다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; margin-top: 15px; margin-bottom: 20px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;background-color: #f5f8ff;&quot;&gt;
&lt;td style=&quot;width: 25%; padding: 10px;&quot;&gt;&lt;b&gt;구분&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 37.5%; padding: 10px;&quot;&gt;&lt;b&gt;Widget 내부 생성&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 37.5%; padding: 10px;&quot;&gt;&lt;b&gt;Model 외부 주입&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;Model 생성 위치&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;Widget 안에서 생성&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;main.py에서 생성&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;Widget 역할&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;Model 생성과 View 조립을 모두 담당&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;전달받은 Model로 View 조립 담당&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;main.py 역할&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;Widget 실행만 담당&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;Model 생성 후 Widget에 전달&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;확장성&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;Model 생성 로직이 Widget에 묶임&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;여러 Widget에 같은 Model을 전달하기 쉬움&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;div style=&quot;background-color: #f8fbff; border: 1px solid #d8e8ff; border-radius: 10px; padding: 18px; margin: 25px 0;&quot;&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;추가 실습의 핵심&lt;/b&gt;&lt;br /&gt;Model을 외부에서 만들고 Widget에 전달하면, Widget은 화면 구성에 더 집중할 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;이 구조는 나중에 여러 화면이 같은 Model을 공유할 때도 도움이 됩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h1 style=&quot;color: #000000; text-align: start;&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;3.&lt;/span&gt;&lt;span&gt; &lt;/span&gt;View 분리하기&lt;/h1&gt;
&lt;p style=&quot;color: #9d9d9d;&quot; data-ke-size=&quot;size14&quot;&gt;&amp;rarr; QTreeView, QListView, QTableView 설정 코드를 각각의 View 클래스로 분리합니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot; data-text-less=&quot;닫기&quot; data-text-more=&quot;더보기&quot; data-ke-type=&quot;moreLess&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;3.1 실습 목표&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전 실습은 Model을 Widget 밖에서 만들고 Widget에 전달했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 QTreeView, QListView, QTableView 설정 코드는 여전히 Widget 안에 들어 있습니다.&lt;/p&gt;
&lt;pre class=&quot;python&quot; style=&quot;background-color: #f7f7f7; border: 1px solid #e0e0e0; border-radius: 6px; padding: 16px; overflow-x: auto; line-height: 1.7; font-size: 14px;&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;현재 구조

main.py
 └── QFileSystemModel 생성

Widget
 ├── 전달받은 Model 사용
 ├── QTreeView 구현
 ├── QListView 구현
 ├── QTableView 구현
 ├── 시그널 연결
 └── 화면 배치&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 단계에서는 View 설정 코드를 별도의 클래스로 나눕니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 Widget은 여러 View 모듈을 조립하는 역할에 집중하도록 바꿉니다.&lt;/p&gt;
&lt;pre class=&quot;python&quot; style=&quot;background-color: #f7f7f7; border: 1px solid #e0e0e0; border-radius: 6px; padding: 16px; overflow-x: auto; line-height: 1.7; font-size: 14px;&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;변경 구조

main.py
 └── QFileSystemModel 생성

FileTreeView
 └── QTreeView 구현 담당

FileListView
 └── QListView 구현 담당

FileTableView
 └── QTableView 구현 담당

Widget 
 ├── 전달받은 Model 사용
 ├── QTreeView 조합
 ├── QListView 조합
 ├── QTableView 조합
 ├── 시그널 연결
 └── 화면 배치&lt;/code&gt;&lt;/pre&gt;
&lt;div style=&quot;background-color: #f8fbff; border: 1px solid #d8e8ff; border-radius: 10px; padding: 18px; margin: 25px 0;&quot;&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;실습 목표&lt;/b&gt;&lt;br /&gt;각 View의 설정 코드를 별도 클래스로 분리하고, Widget은 여러 View를 하나의 화면에 조립하는 역할로 정리합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;3.2 왜 View 클래스를 분리할까?&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두번째 예제 구조에서는 Model 생성을 main.py로 분리했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 View 구현 코드는 아직 Widget 안에 모여 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드가 짧을 때는 괜찮지만, View별 설정이 늘어나면 관리가 어려워질 수 있습니다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; margin-top: 15px; margin-bottom: 20px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;background-color: #f5f8ff;&quot;&gt;
&lt;td style=&quot;width: 30%; padding: 10px;&quot;&gt;&lt;b&gt;문제&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 70%; padding: 10px;&quot;&gt;&lt;b&gt;설명&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;Widget 코드가 길어짐&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;View 생성, Model 연결, 레이아웃, 시그널 연결 코드가 한 클래스에 모입니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;View별 수정이 어려움&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;TreeView만 수정하고 싶어도 Widget 전체 코드를 같이 봐야 합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;역할이 섞임&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;Widget이 화면 조립뿐 아니라 각 View의 세부 설정까지 담당하게 됩니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 View별 설정을 각각의 클래스로 분리하면 역할이 더 명확해집니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #f8fbff; border: 1px solid #d8e8ff; border-radius: 10px; padding: 18px; margin: 25px 0;&quot;&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;문제의 핵심&lt;/b&gt;&lt;br /&gt;하나의 Widget 클래스가 너무 많은 화면 설정을 담당하면 코드가 길어지고 유지보수가 어려워질 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;3.3 프로젝트 구조 변경&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 실습에서는 파일을 다음과 같이 나눕니다.&lt;/p&gt;
&lt;pre class=&quot;python&quot; style=&quot;background-color: #f7f7f7; border: 1px solid #e0e0e0; border-radius: 6px; padding: 16px; overflow-x: auto; line-height: 1.7; font-size: 14px;&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;2View1Model_Demo/
 ├─ main.py
 ├─ widget.py
 └─ views.py # FileTreeView, FileListView, FileTableView 각각의 클래스 파일이라고 가정합니다.&lt;/code&gt;&lt;/pre&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; margin-top: 15px; margin-bottom: 20px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;background-color: #f5f8ff;&quot;&gt;
&lt;td style=&quot;width: 30%; padding: 10px;&quot;&gt;&lt;b&gt;파일&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 70%; padding: 10px;&quot;&gt;&lt;b&gt;역할&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;main.py&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;프로그램 실행 진입점이며, Model을 생성합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;widget.py&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;전달받은 Model을 사용해, 여러 View를 통합하여 화면에 배치합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;views.py&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;FileTreeView, FileListView, FileTableView 클래스를 정의합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심은 View 클래스를 views.py로 분리하는 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;3.4 views.py 작성하기&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 views.py 파일을 만듭니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 파일에는 QTreeView, QListView, QTableView를 상속한 클래스를 작성합니다.&lt;/p&gt;
&lt;pre class=&quot;python&quot; style=&quot;background-color: #f7f7f7; border: 1px solid #ddd; border-radius: 6px; padding: 15px; overflow-x: auto;&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;# views.py

from PySide6.QtWidgets import QTreeView, QListView, QTableView


class FileTreeView(QTreeView):
    &quot;&quot;&quot;
    파일/폴더 구조를 트리 형태로 보여주는 View입니다.
    &quot;&quot;&quot;

    def __init__(self, model, root_path, parent=None):
        super().__init__(parent)

        self.setModel(model)
        self.setRootIndex(model.index(root_path))


class FileListView(QListView):
    &quot;&quot;&quot;
    선택된 폴더 안의 파일/폴더를 리스트 형태로 보여주는 View입니다.
    &quot;&quot;&quot;

    def __init__(self, model, root_path, parent=None):
        super().__init__(parent)

        self.setModel(model)
        self.setRootIndex(model.index(root_path))


class FileTableView(QTableView):
    &quot;&quot;&quot;
    선택된 폴더 안의 파일/폴더를 표 형태로 보여주는 View입니다.
    &quot;&quot;&quot;

    def __init__(self, model, root_path, parent=None):
        super().__init__(parent)

        self.setModel(model)
        self.setRootIndex(model.index(root_path))&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 클래스는 Model과 root_path를 전달받습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 내부에서 setModel()과 setRootIndex()를 실행합니다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; margin-top: 15px; margin-bottom: 20px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;background-color: #f5f8ff;&quot;&gt;
&lt;td style=&quot;width: 30%; padding: 10px;&quot;&gt;&lt;b&gt;클래스&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 70%; padding: 10px;&quot;&gt;&lt;b&gt;역할&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;FileTreeView&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;QTreeView 설정을 담당합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;FileListView&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;QListView 설정을 담당합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;FileTableView&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;QTableView 설정을 담당합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;div style=&quot;background-color: #f8fbff; border: 1px solid #d8e8ff; border-radius: 10px; padding: 18px; margin: 25px 0;&quot;&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;중요한 점&lt;/b&gt;&lt;br /&gt;View 클래스는 데이터를 직접 만들지 않습니다.&lt;/p&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;외부에서 전달받은 Model을 자기 화면 방식에 맞게 보여줄 뿐입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;3.5 widget.py 수정하기&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 widget.py에서는 QTreeView, QListView, QTableView를 직접 만들지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;views.py에서 만든 FileTreeView, FileListView, FileTableView를 가져와 사용합니다.&lt;/p&gt;
&lt;pre class=&quot;ruby&quot; style=&quot;background-color: #f7f7f7; border: 1px solid #ddd; border-radius: 6px; padding: 15px; overflow-x: auto;&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;# widget.py

from PySide6.QtCore import Qt
from PySide6.QtWidgets import (
    QWidget,
    QSplitter,
    QVBoxLayout,
)

from views import FileTreeView, FileListView, FileTableView


class Widget(QWidget):
    &quot;&quot;&quot;
    외부에서 전달받은 Model을 사용해
    여러 View 클래스를 통합해서 배치하는 위젯입니다.
    &quot;&quot;&quot;

    def __init__(self, model, root_path, parent=None):
        super().__init__(parent)

        # [1] 기본 창 설정
        self.setWindowTitle(&quot;Model/View 데모 - View 클래스 분리 + Model 주입&quot;)
        self.resize(900, 400)

        # [2] 외부에서 전달받은 Model과 경로 저장
        self.model = model
        self.root_path = root_path

        # [3] 스플리터 생성
        splitter = QSplitter(Qt.Horizontal, self)

        # [4] 분리된 View 클래스 사용
        self.tree_view = FileTreeView(self.model, self.root_path, splitter)
        self.list_view = FileListView(self.model, self.root_path, splitter)
        self.table_view = FileTableView(self.model, self.root_path, splitter)

        # [5] 트리에서 선택이 바뀌면 다른 View의 Root Index 변경
        self.tree_view.selectionModel().currentChanged.connect(
            self.on_tree_current_changed
        )

        # [6] 전체 레이아웃 설정
        layout = QVBoxLayout(self)
        layout.addWidget(splitter)

    def on_tree_current_changed(self, current, previous):
        &quot;&quot;&quot;
        트리에서 선택한 항목에 따라
        ListView와 TableView의 표시 기준을 변경합니다.
        &quot;&quot;&quot;
        if self.model.isDir(current):
            self.list_view.setRootIndex(current)
            self.table_view.setRootIndex(current)
        else:
            parent_index = current.parent()
            self.list_view.setRootIndex(parent_index)
            self.table_view.setRootIndex(parent_index)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 코드에서는 Widget 안에서 QTreeView, QListView, QTableView를 직접 생성했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수정 후에는 FileTreeView, FileListView, FileTableView 클래스를 사용합니다.&lt;/p&gt;
&lt;pre class=&quot;python&quot; style=&quot;background-color: #f7f7f7; border: 1px solid #e0e0e0; border-radius: 6px; padding: 16px; overflow-x: auto; line-height: 1.7; font-size: 14px;&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;Widget
 ├── 전달받은 Model 저장
 ├── FileTreeView 생성
 ├── FileListView 생성
 ├── FileTableView 생성
 ├── 시그널 연결
 └── 화면 배치&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Widget은 이제 각각의 View 세부 설정을 직접 알 필요가 줄어듭니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대신 이미 만들어진 View 클래스를 가져와 조립하는 역할에 집중합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;3.6 main.py 작성하기&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;main.py는 9장에서 작성한 구조를 그대로 사용합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Model을 만들고, root_path를 설정한 뒤, Widget에 전달합니다.&lt;/p&gt;
&lt;pre class=&quot;python&quot; style=&quot;background-color: #f7f7f7; border: 1px solid #ddd; border-radius: 6px; padding: 15px; overflow-x: auto;&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;# main.py

import sys

from PySide6.QtCore import QDir
from PySide6.QtWidgets import QApplication, QFileSystemModel

from widget import Widget


def main():
    app = QApplication(sys.argv)

    # [1] Model 생성
    model = QFileSystemModel(app)

    # [2] Model의 기준 경로 설정
    root_path = QDir.currentPath()
    model.setRootPath(root_path)

    # [3] Model과 root_path를 Widget에 전달
    window = Widget(model, root_path)
    window.show()

    sys.exit(app.exec())


if __name__ == &quot;__main__&quot;:
    main()&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;main.py의 역할은 명확합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Model을 준비하고, Widget에 전달한 뒤, 창을 실행합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;3.7 전체 실행 흐름&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;View 클래스 분리와 Model 외부 주입을 함께 적용한 실행 흐름은 다음과 같습니다.&lt;/p&gt;
&lt;pre class=&quot;python&quot; style=&quot;background-color: #f7f7f7; border: 1px solid #e0e0e0; border-radius: 6px; padding: 16px; overflow-x: auto; line-height: 1.7; font-size: 14px;&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;main.py 실행
    &amp;darr;
QApplication 생성
    &amp;darr;
QFileSystemModel 생성
    &amp;darr;
root_path 설정
    &amp;darr;
Widget(model, root_path) 생성, Model 주입
    &amp;darr;
Widget이 FileTreeView / FileListView / FileTableView 생성, Model 주입
    &amp;darr;
세 View가 주입된 같은 Model 사용
    &amp;darr;
창 표시&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 구조에서는 Model 생성과 View 설정이 모두 분리됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;main.py는 Model을 준비합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;views.py는 View 설정을 담당합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;widget.py는 여러 View를 하나의 화면에 조립합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;3.8 기존 구조와 분리 구조 비교&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 변경은 프로그램 동작을 크게 바꾸는 것이 아닙니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 Model을 여러 View가 공유한다는 핵심은 그대로 유지됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;달라지는 것은 코드의 역할 분리입니다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; margin-top: 15px; margin-bottom: 20px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;background-color: #f5f8ff;&quot;&gt;
&lt;td style=&quot;width: 14.6215%; padding: 10px;&quot;&gt;&lt;b&gt;구분&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 38.3547%; padding: 10px;&quot;&gt;&lt;b&gt;예제2 구조&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 47.0238%; padding: 10px;&quot;&gt;&lt;b&gt;예제3 구조&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px; width: 14.6215%;&quot;&gt;Model 생성&lt;/td&gt;
&lt;td style=&quot;padding: 10px; width: 38.3547%;&quot;&gt;main.py에서 생성&lt;/td&gt;
&lt;td style=&quot;padding: 10px; width: 47.0238%;&quot;&gt;main.py에서 생성&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px; width: 14.6215%;&quot;&gt;View 생성&lt;/td&gt;
&lt;td style=&quot;padding: 10px; width: 38.3547%;&quot;&gt;Widget 안에서 직접 생성&lt;/td&gt;
&lt;td style=&quot;padding: 10px; width: 47.0238%;&quot;&gt;FileTreeView, FileListView, FileTableView에서 담당&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px; width: 14.6215%;&quot;&gt;Widget 역할&lt;/td&gt;
&lt;td style=&quot;padding: 10px; width: 38.3547%;&quot;&gt;전달받은 Model로 View를 직접 만들고 배치&lt;/td&gt;
&lt;td style=&quot;padding: 10px; width: 47.0238%;&quot;&gt;분리된 View 클래스를 조립하고 시그널 연결&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px; width: 14.6215%;&quot;&gt;View별 수정&lt;/td&gt;
&lt;td style=&quot;padding: 10px; width: 38.3547%;&quot;&gt;Widget 코드 안에서 수정&lt;/td&gt;
&lt;td style=&quot;padding: 10px; width: 47.0238%;&quot;&gt;views.py의 해당 View 클래스에서 수정&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre class=&quot;python&quot; style=&quot;background-color: #f7f7f7; border: 1px solid #e0e0e0; border-radius: 6px; padding: 16px; overflow-x: auto; line-height: 1.7; font-size: 14px;&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;# 두번째 예제 구조

main.py
 └── Model 생성

Widget
 ├── QTreeView 설정
 ├── QListView 설정
 └── QTableView 설정


# 세번째 예제 구조

main.py
 └── Model 생성

views.py
 ├── FileTreeView
 ├── FileListView
 └── FileTableView

widget.py
 └── 세 View를 조립&lt;/code&gt;&lt;/pre&gt;
&lt;div style=&quot;background-color: #f8fbff; border: 1px solid #d8e8ff; border-radius: 10px; padding: 18px; margin: 25px 0;&quot;&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;추가 실습의 핵심&lt;/b&gt;&lt;br /&gt;View 클래스를 분리하면 각 View의 설정 코드를 독립적으로 관리할 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;Widget은 여러 View를 모아 하나의 화면으로 구성하는 통합 역할에 집중합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div style=&quot;background-color: #f8f8f8; border: 1px solid #dddddd; border-radius: 10px; padding: 18px; margin: 25px 0;&quot;&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;기억할 문장&lt;/b&gt;&lt;br /&gt;Model은 main.py에서 준비하고, View 설정은 views.py에서 담당하며, Widget은 여러 View를 조립합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;</description>
      <category>3. PySide6/3-4. Model - View</category>
      <author>BasicLike</author>
      <guid isPermaLink="true">https://basiclike.tistory.com/775</guid>
      <comments>https://basiclike.tistory.com/775#entry775comment</comments>
      <pubDate>Fri, 29 May 2026 16:30:46 +0900</pubDate>
    </item>
    <item>
      <title>2강. Model/View 구조</title>
      <link>https://basiclike.tistory.com/612</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1627&quot; data-origin-height=&quot;554&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/SR5YJ/dJMcaiaJ4io/LPuxuCohtYObEES3rkVfe0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/SR5YJ/dJMcaiaJ4io/LPuxuCohtYObEES3rkVfe0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/SR5YJ/dJMcaiaJ4io/LPuxuCohtYObEES3rkVfe0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FSR5YJ%2FdJMcaiaJ4io%2FLPuxuCohtYObEES3rkVfe0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1627&quot; height=&quot;554&quot; data-origin-width=&quot;1627&quot; data-origin-height=&quot;554&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;!-- ========================================================= --&gt;&lt;!-- 0. 학습 목표 --&gt;&lt;!-- ========================================================= --&gt;
&lt;h1&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;0.&lt;/span&gt; 학습 목표&lt;/h1&gt;
&lt;p style=&quot;color: #9d9d9d;&quot; data-ke-size=&quot;size14&quot;&gt;&amp;rarr; 하나의 Model을 여러 View가 함께 사용하는 구조를 실습합니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot; data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 4.2 단계에서는 &amp;lt;&amp;lt;하나의 &lt;b&gt;Model&lt;sup&gt;모델 &lt;/sup&gt;&lt;/b&gt;&lt;span style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot;&gt;&amp;gt;&amp;gt;&lt;span style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot;&gt;을&lt;/span&gt;&lt;span style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt; &amp;lt;&amp;lt;&lt;/span&gt;여러 &lt;b&gt;View&lt;sup&gt;뷰&lt;/sup&gt;&lt;/b&gt;&amp;gt;&amp;gt;가&lt;span style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt; 함께 사용하는 구조를 실습합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1개의 &lt;b&gt;QFileSystemModel&lt;sup&gt;파일 시스템 모델&lt;/sup&gt;&lt;/b&gt; 을 만들고, 이 Model을&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2개의 &lt;b&gt;QTreeView&lt;sup&gt;트리 뷰&lt;/sup&gt;&lt;/b&gt;와 &lt;b&gt;QListView&lt;sup&gt;리스트 뷰&lt;/sup&gt;&lt;/b&gt;&amp;nbsp; View에 연결합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 통해 같은 데이터를 여러 View가 서로 다른 방식으로 보여줄 수 있다는 점을 직접 확인합니다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; margin-top: 15px; margin-bottom: 20px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;background-color: #f5f8ff;&quot;&gt;
&lt;td style=&quot;width: 30%; padding: 10px;&quot;&gt;&lt;b&gt;학습 목표&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 70%; padding: 10px;&quot;&gt;&lt;b&gt;내용&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;1개의 Model과 &lt;br /&gt;2개의 View 구조 이해&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;QTreeView와 QListView가 &lt;br /&gt;하나의 QFileSystemModel을 함께 사용하는 구조를 이해합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;QFileSystemModel 역할 이해&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;QFileSystemModel이 파일과 폴더 정보를 &lt;br /&gt;Model 형태로 제공한다는 점을 이해합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;QTreeView와 &lt;br /&gt;QListView 역할 이해&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;두 View는 데이터를 직접 저장하지 않고,&lt;br /&gt;Model의 데이터를 각자의 방식으로 보여준다는 점을 이해합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;setModel() 구조 이해&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;View에 Model을 연결할 때 &lt;b&gt;setModel()&lt;sup&gt;모델 연결 메서드&lt;/sup&gt;&lt;/b&gt;을 사용한다는 점을 이해합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;선택 변경에 따른 View 갱신 이해&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;트리에서 선택한 폴더에 따라 리스트의 기준 위치가 바뀌는 흐름을 이해합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;div style=&quot;background-color: #f8fbff; border: 1px solid #d8e8ff; border-radius: 10px; padding: 18px; margin: 25px 0;&quot;&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;이번 단계의 핵심&lt;/b&gt;&lt;br /&gt;파일/폴더 정보는 QFileSystemModel 하나를 통해 제공되고,&lt;/p&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;QTreeView와 QListView는 그 데이터를 각자의 방식으로 보여주는 View입니다.&lt;/p&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;즉, &lt;b&gt;데이터는 하나, 표현 방식은 여러 개&lt;/b&gt;입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;!-- ========================================================= --&gt;&lt;!-- 1. 이번 예제에서 만들 화면 구조 --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;!-- ========================================================= --&gt;
&lt;h1&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;1.&lt;/span&gt; 이번 예제 구조 살펴보기&lt;/h1&gt;
&lt;p style=&quot;color: #9d9d9d;&quot; data-ke-size=&quot;size14&quot;&gt;&amp;rarr; 왼쪽 TreeView와 오른쪽 ListView가 하나의 Model을 공유하는 화면을 만듭니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot; data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1.1 화면 구성&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 예제에서는 하나의 창을 좌우로 나누어 사용합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;왼쪽&lt;/b&gt;에는 폴더 구조를 트리 형태로 보여주는 QTreeView를 배치합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;오른쪽&lt;/b&gt;에는 선택된 폴더 안의 파일과 폴더를 리스트 형태로 보여주는 QListView를 배치합니다.&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1625&quot; data-origin-height=&quot;1117&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/8cZyk/dJMcafrywWG/PpLT2YDb9E7Q1gsqQpEgH0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/8cZyk/dJMcafrywWG/PpLT2YDb9E7Q1gsqQpEgH0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/8cZyk/dJMcafrywWG/PpLT2YDb9E7Q1gsqQpEgH0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F8cZyk%2FdJMcafrywWG%2FPpLT2YDb9E7Q1gsqQpEgH0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1625&quot; height=&quot;1117&quot; data-origin-width=&quot;1625&quot; data-origin-height=&quot;1117&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p data-ke-size=&quot;size16&quot;&gt;중요한 점은 왼쪽과 오른쪽 View가 서로 다른 데이터를 가지는 것이 아니라는 점입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 View는 모두 하나의 QFileSystemModel을 공유합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1.2 Model/View 관계&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 예제의 Model/View 관계는 다음과 같습니다.&lt;/p&gt;
&lt;pre class=&quot;stylus&quot; style=&quot;background-color: #f7f7f7; border: 1px solid #e0e0e0; border-radius: 6px; padding: 16px; overflow-x: auto; line-height: 1.7; font-size: 14px;&quot;&gt;&lt;code&gt;실제 파일/폴더 데이터
    &amp;darr;
QFileSystemModel
    &amp;darr;
QTreeView   QListView&lt;/code&gt;&lt;/pre&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; margin-top: 15px; margin-bottom: 20px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;background-color: #f5f8ff;&quot;&gt;
&lt;td style=&quot;width: 30%; padding: 10px;&quot;&gt;&lt;b&gt;구성 요소&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 70%; padding: 10px;&quot;&gt;&lt;b&gt;역할&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;QFileSystemModel&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;파일과 폴더 정보를 Model 형태로 제공합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;QTreeView&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;Model의 데이터를 트리 구조로 보여주는 View입니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;QListView&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;Model의 데이터를 리스트 구조로 보여주는 View입니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;QSplitter&lt;sup&gt;분할 위젯&lt;/sup&gt;&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;두 View를 좌우로 나누어 배치하는 위젯입니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;div style=&quot;background-color: #f8fbff; border: 1px solid #d8e8ff; border-radius: 10px; padding: 18px; margin: 25px 0;&quot;&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;중요한 점&lt;/b&gt;&lt;br /&gt;QTreeView와 QListView는 데이터를 직접 저장하는 것이 아닙니다.&lt;/p&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;둘 다 QFileSystemModel이 제공하는 파일/폴더 정보를 화면에 보여주는 역할만 합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;!-- ========================================================= --&gt;&lt;!-- 2. 프로젝트 구조 만들기 --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;!-- ========================================================= --&gt;
&lt;h1&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;2.&lt;/span&gt; 프로젝트 구조 만들기&lt;/h1&gt;
&lt;p style=&quot;color: #9d9d9d;&quot; data-ke-size=&quot;size14&quot;&gt;&amp;rarr; main.py와 widget.py 두 파일로 예제를 구성합니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot; data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2.1 프로젝트 폴더 구성&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 다음과 같이 프로젝트 폴더를 만듭니다.&lt;/p&gt;
&lt;pre class=&quot;stylus&quot; style=&quot;background-color: #f7f7f7; border: 1px solid #e0e0e0; border-radius: 6px; padding: 16px; overflow-x: auto; line-height: 1.7; font-size: 14px;&quot;&gt;&lt;code&gt;2View1Model_Demo/
 ├─ main.py        # 실행 진입점
 └─ widget.py      # QFileSystemModel과 두 개의 View를 만드는 위젯 코드&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파일은 총 2개입니다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; margin-top: 15px; margin-bottom: 20px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;background-color: #f5f8ff;&quot;&gt;
&lt;td style=&quot;width: 30%; padding: 10px;&quot;&gt;&lt;b&gt;파일&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 70%; padding: 10px;&quot;&gt;&lt;b&gt;역할&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;main.py&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;QApplication을 만들고 Widget을 실행하는 진입점입니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;widget.py&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;QFileSystemModel, QTreeView, QListView를 만들고 연결하는 메인 위젯 코드입니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;figure class=&quot;fileblock&quot; data-ke-align=&quot;alignCenter&quot;&gt;&lt;a href=&quot;https://blog.kakaocdn.net/dn/bVSdHc/dJMb99T4HpY/x4K4byB9aQg3j6D90s8vK0/main.py?attach=1&amp;amp;knm=tfile.py&quot; class=&quot;&quot;&gt;
    &lt;div class=&quot;image&quot;&gt;&lt;/div&gt;
    &lt;div class=&quot;desc&quot;&gt;&lt;div class=&quot;filename&quot;&gt;&lt;span class=&quot;name&quot;&gt;main.py&lt;/span&gt;&lt;/div&gt;
&lt;div class=&quot;size&quot;&gt;0.00MB&lt;/div&gt;
&lt;/div&gt;
  &lt;/a&gt;&lt;/figure&gt;
&lt;figure class=&quot;fileblock&quot; data-ke-align=&quot;alignCenter&quot;&gt;&lt;a href=&quot;https://blog.kakaocdn.net/dn/XN30D/dJMb99T4HpZ/IZ7KSFnQRyn4IOspm3N1K1/widget.py?attach=1&amp;amp;knm=tfile.py&quot; class=&quot;&quot;&gt;
    &lt;div class=&quot;image&quot;&gt;&lt;/div&gt;
    &lt;div class=&quot;desc&quot;&gt;&lt;div class=&quot;filename&quot;&gt;&lt;span class=&quot;name&quot;&gt;widget.py&lt;/span&gt;&lt;/div&gt;
&lt;div class=&quot;size&quot;&gt;0.00MB&lt;/div&gt;
&lt;/div&gt;
  &lt;/a&gt;&lt;/figure&gt;

&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2.2 실행 흐름&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로그램 실행 흐름은 다음과 같습니다.&lt;/p&gt;
&lt;pre class=&quot;python&quot; style=&quot;background-color: #f7f7f7; border: 1px solid #e0e0e0; border-radius: 6px; padding: 16px; overflow-x: auto; line-height: 1.7; font-size: 14px;&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;main.py 실행
    &amp;darr;
QApplication 생성
    &amp;darr;
Widget 생성
    &amp;darr;
QFileSystemModel 생성
    &amp;darr;
QTreeView와 QListView에 같은 Model 연결
    &amp;darr;
창 표시&lt;/code&gt;&lt;/pre&gt;
&lt;div style=&quot;background-color: #f8f8f8; border: 1px solid #dddddd; border-radius: 10px; padding: 18px; margin: 25px 0;&quot;&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;참고&lt;/b&gt;&lt;br /&gt;이번 예제의 핵심 코드는 widget.py에 있습니다.&lt;/p&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;main.py는 프로그램을 실행하기 위한 기본 진입점 역할만 합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;!-- ========================================================= --&gt;&lt;!-- 3. 메인 위젯 코드 작성 --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;!-- ========================================================= --&gt;
&lt;h1&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;3.&lt;/span&gt; &lt;span style=&quot;color: #333333;&quot;&gt;Widget&lt;/span&gt;&lt;/h1&gt;
&lt;p style=&quot;color: #9d9d9d;&quot; data-ke-size=&quot;size14&quot;&gt;&amp;rarr; 하나의 Widget 내부에, QFileSystemModel 하나와, 두 개의 View &lt;span style=&quot;color: #9d9d9d; text-align: start;&quot;&gt;를 만들고, &lt;/span&gt;연결합니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot; data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;3.1 Widget 구조&amp;nbsp;&lt;/b&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;구조는 Model 생성, View 생성, 시그널 연결이 모두 Widget 안에 들어 있습니다.&lt;/p&gt;
&lt;pre class=&quot;python&quot; style=&quot;background-color: #f7f7f7; border: 1px solid #e0e0e0; border-radius: 6px; padding: 16px; overflow-x: auto; line-height: 1.7; font-size: 14px;&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;Widget
 ├── QFileSystemModel 생성
 ├── QTreeView 생성
 ├── QListView 생성
 ├── View-Model 시그널 연결
 └── View 화면 배치&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;3.2 widget.py 전체 코드&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 widget.py 파일에 다음 코드를 작성합니다.&lt;/p&gt;
&lt;pre class=&quot;ini&quot; style=&quot;background-color: #f7f7f7; border: 1px solid #ddd; border-radius: 6px; padding: 15px; overflow-x: auto;&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;# widget.py

from PySide6.QtCore import QDir, QTimer, QModelIndex, Qt
from PySide6.QtWidgets import (
    QWidget,
    QSplitter,
    QFileSystemModel,
    QTreeView,
    QListView,
    QVBoxLayout,
)


class Widget(QWidget):
    &quot;&quot;&quot;
    하나의 QFileSystemModel을
    - QTreeView
    - QListView
    두 개의 View가 함께 공유하는 예제 위젯입니다.
    &quot;&quot;&quot;

    def __init__(self, parent=None):
        super().__init__(parent)

        # [1] 기본 창 설정
        self.setWindowTitle(&quot;Model/View 데모 - QTreeView + QListView&quot;)
        self.resize(800, 400)

        # [2] 공통 모델 생성
        self.model = QFileSystemModel(self)

        # 현재 작업 디렉토리를 루트 경로로 사용
        root_path = QDir.currentPath()
        self.model.setRootPath(root_path)

        # [3] 스플리터 생성
        splitter = QSplitter(Qt.Horizontal, self)

        # [4] 왼쪽 TreeView 설정
        self.tree_view = QTreeView(splitter)
        self.tree_view.setModel(self.model)
        self.tree_view.setRootIndex(self.model.index(root_path))

        # [5] 오른쪽 ListView 설정
        self.list_view = QListView(splitter)
        self.list_view.setModel(self.model)
        self.list_view.setRootIndex(self.model.index(root_path))

        # [6] 트리에서 선택이 바뀌면 리스트 뷰의 루트도 변경
        self.tree_view.selectionModel().currentChanged.connect(
            self.on_tree_current_changed
        )

        # [7] 전체 레이아웃 설정
        layout = QVBoxLayout(self)
        layout.addWidget(splitter)

        # [8] 프로그램 시작 직후 초기 선택 해제
        QTimer.singleShot(0, self._clear_initial_selection)

    def on_tree_current_changed(self, current, previous):
        &quot;&quot;&quot;
        트리에서 선택이 바뀔 때마다 호출됩니다.

        current  : 현재 선택된 QModelIndex
        previous : 이전에 선택되어 있던 QModelIndex
        &quot;&quot;&quot;
        if self.model.isDir(current):
            self.list_view.setRootIndex(current)
        else:
            parent_index = current.parent()
            self.list_view.setRootIndex(parent_index)

    def _clear_initial_selection(self):
        &quot;&quot;&quot;
        프로그램 시작 시 TreeView의 첫 항목이 자동 선택되어 보이는 것을 해제합니다.
        &quot;&quot;&quot;
        selection_model = self.tree_view.selectionModel()

        if selection_model is not None:
            selection_model.clearSelection()

        self.tree_view.setCurrentIndex(QModelIndex())&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 코드는 하나의 QFileSystemModel을 만들고, 그 Model을 QTreeView와 QListView에 함께 연결합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;!-- ========================================================= --&gt;&lt;!-- 4. 선택 변경으로 다른 View 갱신하기 --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h1 style=&quot;color: #000000; text-align: start;&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;4.&lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt; Model/View 구조 분석&lt;/span&gt;&lt;/h1&gt;
&lt;p style=&quot;color: #9d9d9d;&quot; data-ke-size=&quot;size14&quot;&gt;&amp;rarr; TreeView에서 선택한 항목에 따라 ListView의 표시 기준을 바꿉니다.&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;4.1 모델 생성: QFileSystemModel&lt;/b&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;먼저 파일 시스템 데이터를 표현할 Model을 만듭니다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot; style=&quot;background-color: #f7f7f7; border: 1px solid #ddd; border-radius: 6px; padding: 15px; overflow-x: auto;&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;self.model = QFileSystemModel(self)

root_path = QDir.currentPath()
self.model.setRootPath(root_path)&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;QFileSystemModel은 디스크의 파일과 폴더 구조를 Model 형태로 제공하는 Qt의 준비된 Model 클래스입니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;setRootPath()루트 경로 설정 메서드&lt;/b&gt;는 이 Model이 어느 경로부터 파일과 폴더 정보를 읽을지 지정합니다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; margin-top: 15px; margin-bottom: 20px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;background-color: #f5f8ff;&quot;&gt;
&lt;td style=&quot;width: 35%; padding: 10px;&quot;&gt;&lt;b&gt;코드&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 65%; padding: 10px;&quot;&gt;&lt;b&gt;의미&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;QFileSystemModel(self)&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;파일/폴더 정보를 제공하는 Model 객체를 만듭니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;QDir.currentPath()&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;현재 작업 디렉토리 경로를 가져옵니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;setRootPath(root_path)&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;Model이 읽을 기준 경로를 설정합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre class=&quot;gams&quot; style=&quot;background-color: #f7f7f7; border: 1px solid #e0e0e0; border-radius: 6px; padding: 16px; overflow-x: auto; line-height: 1.7; font-size: 14px;&quot;&gt;&lt;code&gt;실제 파일/폴더
    &amp;darr;
QFileSystemModel
    &amp;darr;
파일/폴더 정보를 Model 형태로 제공&lt;/code&gt;&lt;/pre&gt;
&lt;div style=&quot;background-color: #f8fbff; border: 1px solid #d8e8ff; border-radius: 10px; padding: 18px; margin: 25px 0;&quot;&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Model/View 개념 포인트&lt;/b&gt;&lt;br /&gt;QFileSystemModel은 파일/폴더 정보를 View가 사용할 수 있는 Model 형태로 제공합니다.&lt;/p&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;나중에 여러 View가 이 Model을 공유하면서 같은 데이터를 서로 다른 방식으로 보여줄 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;4.2 왼쪽 View: QTreeView 설정&lt;/b&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;왼쪽에는 QTreeView를 배치합니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;QTreeView는 Model의 데이터를 트리 구조로 보여주는 View입니다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot; style=&quot;background-color: #f7f7f7; border: 1px solid #ddd; border-radius: 6px; padding: 15px; overflow-x: auto;&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;self.tree_view = QTreeView(splitter)
self.tree_view.setModel(self.model)
self.tree_view.setRootIndex(self.model.index(root_path))&lt;/code&gt;&lt;/pre&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; margin-top: 15px; margin-bottom: 20px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;background-color: #f5f8ff;&quot;&gt;
&lt;td style=&quot;width: 35%; padding: 10px;&quot;&gt;&lt;b&gt;코드&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 65%; padding: 10px;&quot;&gt;&lt;b&gt;설명&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;QTreeView(splitter)&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;스플리터 안에 트리 형태의 View를 만듭니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;setModel(self.model)&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;앞에서 만든 QFileSystemModel을 이 View에 연결합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;setRootIndex(...)&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;트리에서 어느 폴더부터 보여줄지 정합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre class=&quot;reasonml&quot; style=&quot;background-color: #f7f7f7; border: 1px solid #e0e0e0; border-radius: 6px; padding: 16px; overflow-x: auto; line-height: 1.7; font-size: 14px;&quot;&gt;&lt;code&gt;QTreeView
    &amp;darr;
setModel(self.model)
    &amp;darr;
파일 시스템 Model의 데이터를 트리 형태로 표시&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;즉, TreeView가 데이터를 가지는 것이 아닙니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;TreeView는 QFileSystemModel이 제공하는 데이터를 트리 모양으로 보여줍니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;4.3 오른쪽 View: QListView 설정&lt;/b&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;오른쪽에는 QListView를 배치합니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;QListView는 Model의 데이터를 리스트 형태로 보여주는 View입니다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot; style=&quot;background-color: #f7f7f7; border: 1px solid #ddd; border-radius: 6px; padding: 15px; overflow-x: auto;&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;self.list_view = QListView(splitter)
self.list_view.setModel(self.model)
self.list_view.setRootIndex(self.model.index(root_path))&lt;/code&gt;&lt;/pre&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; margin-top: 15px; margin-bottom: 20px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;background-color: #f5f8ff;&quot;&gt;
&lt;td style=&quot;width: 35%; padding: 10px;&quot;&gt;&lt;b&gt;코드&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 65%; padding: 10px;&quot;&gt;&lt;b&gt;설명&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;QListView(splitter)&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;스플리터 안에 리스트 형태의 View를 만듭니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;setModel(self.model)&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;QTreeView와 같은 QFileSystemModel을 연결합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;setRootIndex(...)&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;리스트에서 어느 폴더 내용을 보여줄지 정합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre class=&quot;reasonml&quot; style=&quot;background-color: #f7f7f7; border: 1px solid #e0e0e0; border-radius: 6px; padding: 16px; overflow-x: auto; line-height: 1.7; font-size: 14px;&quot;&gt;&lt;code&gt;QListView
    &amp;darr;
setModel(self.model)
    &amp;darr;
파일 시스템 Model의 데이터를 리스트 형태로 표시&lt;/code&gt;&lt;/pre&gt;
&lt;div style=&quot;background-color: #f8fbff; border: 1px solid #d8e8ff; border-radius: 10px; padding: 18px; margin: 25px 0;&quot;&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;핵심&lt;/b&gt;&lt;br /&gt;self.model 객체는 단 하나입니다.&lt;/p&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;QTreeView와 QListView는 둘 다 setModel(self.model)로 같은 Model을 연결합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;4.4 한개의 Model을 두 View가 공유하는 구조&lt;/b&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이번 예제에서 가장 중요한 부분은 두 View가 같은 Model을 공유한다는 점입니다.&lt;/p&gt;
&lt;pre class=&quot;pf&quot; style=&quot;background-color: #f7f7f7; border: 1px solid #ddd; border-radius: 6px; padding: 15px; overflow-x: auto;&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;self.model = QFileSystemModel(self)

self.tree_view.setModel(self.model)
self.list_view.setModel(self.model)&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 구조를 그림으로 보면 다음과 같습니다.&lt;/p&gt;
&lt;pre class=&quot;ada&quot; style=&quot;background-color: #f7f7f7; border: 1px solid #e0e0e0; border-radius: 6px; padding: 16px; overflow-x: auto; line-height: 1.7; font-size: 14px;&quot;&gt;&lt;code&gt;파일/폴더 데이터
    &amp;darr;
QFileSystemModel
    ├── QTreeView  : 트리 형태로 표시
    └── QListView  : 리스트 형태로 표시&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;파일/폴더 정보는 QFileSystemModel 하나를 통해 제공됩니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;QTreeView와 QListView는 그 데이터를 서로 다른 화면 형태로 보여줄 뿐입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;!-- ========================================================= --&gt;
&lt;h1&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;5.&lt;/span&gt; &lt;span style=&quot;color: #333333;&quot;&gt;View 로직 분석&amp;nbsp;&lt;/span&gt;&lt;/h1&gt;
&lt;p style=&quot;color: #9d9d9d;&quot; data-ke-size=&quot;size14&quot;&gt;&amp;rarr; TreeView에서 선택한 항목에 따라 ListView의 표시 기준을 바꿉니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot; data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;5.1 currentChanged 시그널 연결&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 예제에서는 왼쪽 트리에서 선택한 폴더에 따라 오른쪽 리스트의 내용이 바뀌도록 만듭니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 위해 TreeView의 선택 상태를 관리하는 selectionModel()을 사용합니다.&lt;/p&gt;
&lt;pre class=&quot;ini&quot; style=&quot;background-color: #f7f7f7; border: 1px solid #ddd; border-radius: 6px; padding: 15px; overflow-x: auto;&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;self.tree_view.selectionModel().currentChanged.connect(
    self.on_tree_current_changed
)&lt;/code&gt;&lt;/pre&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; margin-top: 15px; margin-bottom: 20px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;background-color: #f5f8ff;&quot;&gt;
&lt;td style=&quot;width: 35%; padding: 10px;&quot;&gt;&lt;b&gt;코드&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 65%; padding: 10px;&quot;&gt;&lt;b&gt;설명&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;selectionModel()&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;View에서 어떤 항목이 선택되었는지 관리하는 객체를 가져옵니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;currentChanged&lt;sup&gt;현재 항목 변경 시그널&lt;/sup&gt;&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;현재 선택된 항목이 바뀔 때 발생하는 시그널입니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;connect(...)&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;시그널이 발생했을 때 실행할 함수를 연결합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre class=&quot;stylus&quot; style=&quot;background-color: #f7f7f7; border: 1px solid #e0e0e0; border-radius: 6px; padding: 16px; overflow-x: auto; line-height: 1.7; font-size: 14px;&quot;&gt;&lt;code&gt;트리에서 항목 클릭
    &amp;darr;
currentChanged 시그널 발생
    &amp;darr;
on_tree_current_changed() 실행
    &amp;darr;
리스트 뷰의 표시 기준 변경&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, TreeView에서 선택이 바뀌면 오른쪽 ListView도 그 선택에 맞춰 바뀌게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;5.2 슬롯 함수: on_tree_current_changed()&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;트리에서 선택이 바뀌면 다음 함수가 실행됩니다.&lt;/p&gt;
&lt;pre class=&quot;ini&quot; style=&quot;background-color: #f7f7f7; border: 1px solid #ddd; border-radius: 6px; padding: 15px; overflow-x: auto;&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;def on_tree_current_changed(self, current, previous):
    if self.model.isDir(current):
        self.list_view.setRootIndex(current)
    else:
        parent_index = current.parent()
        self.list_view.setRootIndex(parent_index)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;current는 새로 선택된 항목의 &lt;b&gt;QModelIndex&lt;sup&gt;모델 인덱스&lt;/sup&gt;&lt;/b&gt;입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;previous는 이전에 선택되어 있던 QModelIndex입니다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; margin-top: 15px; margin-bottom: 20px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;background-color: #f5f8ff;&quot;&gt;
&lt;td style=&quot;width: 35%; padding: 10px;&quot;&gt;&lt;b&gt;코드&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 65%; padding: 10px;&quot;&gt;&lt;b&gt;의미&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;self.model.isDir(current)&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;현재 선택된 항목이 폴더인지 확인합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;setRootIndex(current)&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;선택한 폴더를 QListView의 표시 기준으로 설정합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;current.parent()&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;파일을 선택했을 때 그 파일이 들어 있는 부모 폴더를 가져옵니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre class=&quot;stylus&quot; style=&quot;background-color: #f7f7f7; border: 1px solid #e0e0e0; border-radius: 6px; padding: 16px; overflow-x: auto; line-height: 1.7; font-size: 14px;&quot;&gt;&lt;code&gt;트리에서 폴더 클릭
    &amp;darr;
self.model.isDir(current) == True
    &amp;darr;
리스트 뷰가 그 폴더 안의 내용을 표시


트리에서 파일 클릭
    &amp;darr;
self.model.isDir(current) == False
    &amp;darr;
리스트 뷰가 그 파일의 부모 폴더 내용을 표시&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파일은 그 자체가 폴더처럼 내부 내용을 가질 수 없습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 파일을 클릭한 경우에는 해당 파일이 들어 있는 부모 폴더를 리스트의 기준 위치로 사용합니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #f8fbff; border: 1px solid #d8e8ff; border-radius: 10px; padding: 18px; margin: 25px 0;&quot;&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;중요한 점&lt;/b&gt;&lt;br /&gt;리스트 뷰의 데이터가 새로 만들어지는 것이 아닙니다.&lt;/p&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;같은 QFileSystemModel을 사용하되, QListView가 보여줄 시작 위치인 &lt;b&gt;Root Index&lt;sup&gt;루트 인덱스&lt;/sup&gt;&lt;/b&gt;만 바뀌는 것입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;5.3 초기 선택 해제 코드&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로그램을 실행하면 TreeView의 첫 항목이 자동으로 선택되어 보일 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음 실행 상태를 깔끔하게 보여주고 싶다면 다음 코드를 사용할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;ini&quot; style=&quot;background-color: #f7f7f7; border: 1px solid #ddd; border-radius: 6px; padding: 15px; overflow-x: auto;&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;QTimer.singleShot(0, self._clear_initial_selection)

def _clear_initial_selection(self):
    selection_model = self.tree_view.selectionModel()

    if selection_model is not None:
        selection_model.clearSelection()

    self.tree_view.setCurrentIndex(QModelIndex())&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;QTimer.singleShot(0, ...)은 화면 구성이 끝난 직후 한 번만 함수를 실행하도록 예약합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 안에서 clearSelection()으로 선택 상태를 지우고, setCurrentIndex(QModelIndex())로 현재 선택 인덱스도 비웁니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #f8f8f8; border: 1px solid #dddddd; border-radius: 10px; padding: 18px; margin: 25px 0;&quot;&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;참고&lt;/b&gt;&lt;br /&gt;이 코드는 Model/View 구조를 이해하는 데 필수는 아닙니다.&lt;/p&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;처음 실행했을 때 선택 표시를 깔끔하게 없애기 위한 UI 보정 코드입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;!-- ========================================================= --&gt;&lt;!-- 5. 실행 진입점 작성 --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;!-- ========================================================= --&gt;
&lt;h1&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;6.&lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt; main 구현&lt;/span&gt;&lt;/h1&gt;
&lt;p style=&quot;color: #9d9d9d;&quot; data-ke-size=&quot;size14&quot;&gt;&amp;rarr; QApplication을 만들고 Widget을 화면에 표시합니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot; data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;6.1 main.py 전체 코드&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 main.py 파일을 작성합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;main.py는 QApplication을 만들고, 앞에서 작성한 Widget을 화면에 표시하는 역할을 합니다.&lt;/p&gt;
&lt;pre class=&quot;ini&quot; style=&quot;background-color: #f7f7f7; border: 1px solid #ddd; border-radius: 6px; padding: 15px; overflow-x: auto;&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;# main.py

import sys
from PySide6.QtWidgets import QApplication

from widget import Widget


def main():
    # [1] QApplication 생성
    app = QApplication(sys.argv)

    # [2] 메인 위젯 생성
    window = Widget()
    window.show()

    # [3] 이벤트 루프 실행
    sys.exit(app.exec())


if __name__ == &quot;__main__&quot;:
    main()&lt;/code&gt;&lt;/pre&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; margin-top: 15px; margin-bottom: 20px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;background-color: #f5f8ff;&quot;&gt;
&lt;td style=&quot;width: 35%; padding: 10px;&quot;&gt;&lt;b&gt;코드&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 65%; padding: 10px;&quot;&gt;&lt;b&gt;역할&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;QApplication(sys.argv)&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;Qt GUI 프로그램 전체 실행을 관리하는 객체를 만듭니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;Widget()&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;QFileSystemModel, QTreeView, QListView가 들어 있는 창을 만듭니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;window.show()&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;창을 화면에 표시합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;app.exec()&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;Qt 이벤트 루프를 시작합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;6.2 실행 결과 구조&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로그램을 실행하면 다음과 같은 형태의 창이 나타납니다.&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1625&quot; data-origin-height=&quot;1117&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Q1FU5/dJMcag44lGB/kW1CQnNQf5pdELHGxhInZk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Q1FU5/dJMcag44lGB/kW1CQnNQf5pdELHGxhInZk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Q1FU5/dJMcag44lGB/kW1CQnNQf5pdELHGxhInZk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FQ1FU5%2FdJMcag44lGB%2FkW1CQnNQf5pdELHGxhInZk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1625&quot; height=&quot;1117&quot; data-origin-width=&quot;1625&quot; data-origin-height=&quot;1117&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p data-ke-size=&quot;size16&quot;&gt;왼쪽 트리에서 폴더를 선택하면 오른쪽 리스트는 선택된 폴더의 내용을 보여줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 데이터는 여전히 하나의 QFileSystemModel에서 가져옵니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #f8fbff; border: 1px solid #d8e8ff; border-radius: 10px; padding: 18px; margin: 25px 0;&quot;&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;이번 단계의 핵심&lt;/b&gt;&lt;br /&gt;QTreeView와 QListView는 같은 QFileSystemModel을 사용합니다.&lt;/p&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;다른 것은 데이터를 보여주는 화면 방식과 Root Index입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;!-- ========================================================= --&gt;&lt;!-- 6. 기존 방식과 Model/View 방식 비교 --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;!-- ========================================================= --&gt;
&lt;h1&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;7.&lt;/span&gt; Model/View 방식 비교&lt;/h1&gt;
&lt;p style=&quot;color: #9d9d9d;&quot; data-ke-size=&quot;size14&quot;&gt;&amp;rarr; 기존 스크롤 UI 방식과 이번 Model/View 방식을 비교합니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot; data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;7.1 이전 스크롤 UI 방식과 비교&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전 스크롤 UI 예제에서는 QLabel, QListWidget, QTextEdit 같은 View 안에 데이터를 직접 넣는 방식이 많았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 예제에서는 데이터를 QFileSystemModel을 통해 제공하고, View는 그 데이터를 보여주기만 합니다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; margin-top: 15px; margin-bottom: 20px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;background-color: #f5f8ff;&quot;&gt;
&lt;td style=&quot;width: 18.154%; padding: 10px;&quot;&gt;&lt;b&gt;구분&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 40.0673%; padding: 10px;&quot;&gt;&lt;b&gt;기존 방식&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 41.7787%; padding: 10px;&quot;&gt;&lt;b&gt;4.2 Model/View 방식&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px; width: 18.154%;&quot;&gt;데이터 위치&lt;/td&gt;
&lt;td style=&quot;padding: 10px; width: 40.0673%;&quot;&gt;View 내부 또는 위젯 내부&lt;/td&gt;
&lt;td style=&quot;padding: 10px; width: 41.7787%;&quot;&gt;QFileSystemModel을 통해 제공&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px; width: 18.154%;&quot;&gt;View 역할&lt;/td&gt;
&lt;td style=&quot;padding: 10px; width: 40.0673%;&quot;&gt;데이터 저장과 화면 표시를 함께 담당&lt;/td&gt;
&lt;td style=&quot;padding: 10px; width: 41.7787%;&quot;&gt;Model 데이터를 화면에 표시&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px; width: 18.154%;&quot;&gt;데이터 공유&lt;/td&gt;
&lt;td style=&quot;padding: 10px; width: 40.0673%;&quot;&gt;다른 View와 공유하기 어려움&lt;/td&gt;
&lt;td style=&quot;padding: 10px; width: 41.7787%;&quot;&gt;여러 View가 같은 Model을 공유 가능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px; width: 18.154%;&quot;&gt;표현 방식&lt;/td&gt;
&lt;td style=&quot;padding: 10px; width: 40.0673%;&quot;&gt;각 위젯의 내부 구조에 의존&lt;/td&gt;
&lt;td style=&quot;padding: 10px; width: 41.7787%;&quot;&gt;같은 Model을 트리, 리스트 등으로 표현 가능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px; width: 18.154%;&quot;&gt;확장성&lt;/td&gt;
&lt;td style=&quot;padding: 10px; width: 40.0673%;&quot;&gt;View가 늘어나면 데이터 처리도 중복될 수 있음&lt;/td&gt;
&lt;td style=&quot;padding: 10px; width: 41.7787%;&quot;&gt;View만 추가하고 같은 Model을 연결할 수 있음&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre class=&quot;stylus&quot; style=&quot;background-color: #f7f7f7; border: 1px solid #e0e0e0; border-radius: 6px; padding: 16px; overflow-x: auto; line-height: 1.7; font-size: 14px;&quot;&gt;&lt;code&gt;기존 방식

QScrollArea  &amp;larr; QLabel 안에 데이터
QListWidget  &amp;larr; QListWidget 안에 데이터
QTextEdit    &amp;larr; QTextEdit 안에 데이터

View마다 데이터가 따로 있음&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;stylus&quot; style=&quot;background-color: #f7f7f7; border: 1px solid #e0e0e0; border-radius: 6px; padding: 16px; overflow-x: auto; line-height: 1.7; font-size: 14px;&quot;&gt;&lt;code&gt;4.2 Model/View 방식

QTreeView &amp;larr;─┐
QListView &amp;larr;── QFileSystemModel &amp;larr;── 실제 파일/폴더 데이터

Model 하나를 여러 View가 함께 사용&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;7.2 Root Index 관점에서 보기&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 예제에서는 QTreeView와 QListView가 같은 Model을 사용하지만, 보여주는 범위는 다를 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 차이를 만드는 것이 &lt;b&gt;setRootIndex()&lt;sup&gt;루트 인덱스 설정 메서드&lt;/sup&gt;&lt;/b&gt;입니다.&lt;/p&gt;
&lt;pre class=&quot;ini&quot; style=&quot;background-color: #f7f7f7; border: 1px solid #ddd; border-radius: 6px; padding: 15px; overflow-x: auto;&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;self.tree_view.setRootIndex(self.model.index(root_path))
self.list_view.setRootIndex(self.model.index(root_path))&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 두 View 모두 같은 root_path를 기준으로 데이터를 보여줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 트리에서 다른 폴더를 선택하면 QListView의 Root Index가 바뀝니다.&lt;/p&gt;
&lt;pre class=&quot;stylus&quot; style=&quot;background-color: #f7f7f7; border: 1px solid #e0e0e0; border-radius: 6px; padding: 16px; overflow-x: auto; line-height: 1.7; font-size: 14px;&quot;&gt;&lt;code&gt;같은 Model
    &amp;darr;
QTreeView root index  : 현재 작업 폴더
QListView root index  : 트리에서 선택한 폴더&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 데이터는 같지만 View마다 보여주는 시작 위치를 다르게 설정할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;7.3 같은 Model, 다른 View, 다른 표현&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 예제의 핵심 문장은 다음과 같습니다.&lt;/p&gt;
&lt;pre class=&quot;stylus&quot; style=&quot;background-color: #f7f7f7; border: 1px solid #e0e0e0; border-radius: 6px; padding: 16px; overflow-x: auto; line-height: 1.7; font-size: 14px;&quot;&gt;&lt;code&gt;같은 Model
    &amp;darr;
QTreeView  : 트리 형태
QListView  : 리스트 형태

데이터는 하나
표현 방식은 두 개&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 구조를 이해하면 나중에 QTableView를 추가해도 어렵지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;새 View를 만들고 같은 Model을 연결하면 되기 때문입니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #f8f8f8; border: 1px solid #dddddd; border-radius: 10px; padding: 18px; margin: 25px 0;&quot;&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;기억할 문장&lt;/b&gt;&lt;br /&gt;Model은 하나이고, View는 여러 개가 될 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;!-- ========================================================= --&gt;&lt;!-- 8. 추가 실습 과제 1: QTableView 추가하기 --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h1 style=&quot;color: #000000; text-align: start;&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;8.&lt;/span&gt;&lt;span style=&quot;color: #000000; letter-spacing: -1px;&quot;&gt; 정리&lt;/span&gt;&lt;/h1&gt;
&lt;p style=&quot;color: #9d9d9d;&quot; data-ke-size=&quot;size14&quot;&gt;&amp;rarr; 하나의 Model을 여러 View가 공유하는 흐름을 마지막으로 정리합니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot; data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 단계에서는 하나의 QFileSystemModel을 QTreeView와 QListView가 함께 사용하는 구조를 살펴보았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심은 Model은 하나이고, View는 여러 개가 될 수 있다는 점입니다.&lt;/p&gt;
&lt;pre class=&quot;stylus&quot; style=&quot;background-color: #f7f7f7; border: 1px solid #e0e0e0; border-radius: 6px; padding: 16px; overflow-x: auto; line-height: 1.7; font-size: 14px;&quot;&gt;&lt;code&gt;실제 파일/폴더 데이터
    &amp;darr;
QFileSystemModel
    &amp;darr;
QTreeView   QListView
    &amp;darr;          &amp;darr;
트리 표현    리스트 표현&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 단계의 핵심 개념을 다시 정리하면 다음과 같습니다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; margin-top: 15px; margin-bottom: 20px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;background-color: #f5f8ff;&quot;&gt;
&lt;td style=&quot;width: 30%; padding: 10px;&quot;&gt;&lt;b&gt;개념&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 70%; padding: 10px;&quot;&gt;&lt;b&gt;정리&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;QFileSystemModel&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;파일과 폴더 정보를 Model 형태로 제공합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;QTreeView&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;Model의 데이터를 트리 형태로 보여주는 View입니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;QListView&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;Model의 데이터를 리스트 형태로 보여주는 View입니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;setModel()&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;View에 Model을 연결하는 메서드입니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;setRootIndex()&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;View가 Model의 어느 위치부터 보여줄지 정합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;currentChanged&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;View에서 현재 선택 항목이 바뀔 때 발생하는 시그널입니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;div style=&quot;background-color: #f8f8f8; border: 1px solid #dddddd; border-radius: 10px; padding: 18px; margin: 25px 0;&quot;&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;기억할 문장&lt;/b&gt;&lt;br /&gt;데이터는 Model에 있고, View는 그 데이터를 각자의 방식으로 보여줍니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;!-- ========================================================= --&gt;&lt;!-- 참고. 공식 문서로 확인하기 --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;!-- ========================================================= --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h1&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;참고.&lt;/span&gt; 공식 문서로 확인하기&lt;/h1&gt;
&lt;p style=&quot;color: #9d9d9d;&quot; data-ke-size=&quot;size14&quot;&gt;&amp;rarr; 공식 문서에서 이번 예제와 연결되는 기준을 확인합니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot; data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1. 공식 문서 참고 표&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 예제에서 사용한 QFileSystemModel, QTreeView, QListView, QTableView는 Qt for Python 공식 문서에서 확인할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공식 문서는 실제 클래스의 역할과 사용 가능한 메서드를 확인할 때 가장 정확한 기준이 됩니다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; margin-top: 15px; margin-bottom: 20px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;background-color: #f5f8ff;&quot;&gt;
&lt;td style=&quot;width: 21.8215%; padding: 10px;&quot;&gt;&lt;b&gt;구분&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 38.1785%; padding: 10px;&quot;&gt;&lt;b&gt;공식 문서&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 40%; padding: 10px;&quot;&gt;&lt;b&gt;확인할 내용&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px; width: 21.8215%;&quot;&gt;QFileSystemModel&lt;/td&gt;
&lt;td style=&quot;padding: 10px; width: 38.1785%;&quot;&gt;&lt;a href=&quot;https://doc.qt.io/qtforpython-6/PySide6/QtWidgets/QFileSystemModel.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;PySide6.QtWidgets.QFileSystemModel&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;padding: 10px; width: 40%;&quot;&gt;로컬 파일 시스템을 Model로 제공하는 클래스와 index(), isDir(), setRootPath() 같은 메서드를 확인할 수 있습니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px; width: 21.8215%;&quot;&gt;QTreeView&lt;/td&gt;
&lt;td style=&quot;padding: 10px; width: 38.1785%;&quot;&gt;&lt;a href=&quot;https://doc.qt.io/qtforpython-6/PySide6/QtWidgets/QTreeView.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;PySide6.QtWidgets.QTreeView&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;padding: 10px; width: 40%;&quot;&gt;Model 데이터를 트리 형태로 보여주는 View 클래스임을 확인할 수 있습니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px; width: 21.8215%;&quot;&gt;QListView&lt;/td&gt;
&lt;td style=&quot;padding: 10px; width: 38.1785%;&quot;&gt;&lt;a href=&quot;https://doc.qt.io/qtforpython-6/PySide6/QtWidgets/QListView.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;PySide6.QtWidgets.QListView&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;padding: 10px; width: 40%;&quot;&gt;Model 데이터를 리스트 형태로 보여주는 View 클래스임을 확인할 수 있습니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px; width: 21.8215%;&quot;&gt;QTableView&lt;/td&gt;
&lt;td style=&quot;padding: 10px; width: 38.1785%;&quot;&gt;&lt;a href=&quot;https://doc.qt.io/qtforpython-6/PySide6/QtWidgets/QTableView.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;PySide6.QtWidgets.QTableView&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;padding: 10px; width: 40%;&quot;&gt;Model 데이터를 표 형태로 보여주는 View 클래스임을 확인할 수 있습니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px; width: 21.8215%;&quot;&gt;Model/View &lt;br /&gt;Programming&lt;/td&gt;
&lt;td style=&quot;padding: 10px; width: 38.1785%;&quot;&gt;&lt;a href=&quot;https://doc.qt.io/qtforpython-6/overviews/qtwidgets-model-view-programming.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Qt for Python - Model/View Programming&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;padding: 10px; width: 40%;&quot;&gt;Qt의 Model/View 구조가 데이터와 표현 방식을 분리하는 구조임을 확인할 수 있습니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2. 이번 예제와 공식 문서 연결&lt;/b&gt;&lt;/h2&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; margin-top: 15px; margin-bottom: 20px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;background-color: #f5f8ff;&quot;&gt;
&lt;td style=&quot;width: 35%; padding: 10px;&quot;&gt;&lt;b&gt;이번 예제 코드 또는 개념&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 65%; padding: 10px;&quot;&gt;&lt;b&gt;공식 문서와 연결되는 의미&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;QFileSystemModel(self)&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;파일 시스템 데이터를 Model로 다루기 위한 객체를 생성합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;self.model.setRootPath(root_path)&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;Model이 사용할 파일 시스템 기준 경로를 설정합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;self.tree_view.setModel(self.model)&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;QTreeView가 QFileSystemModel의 데이터를 트리 형태로 표시하도록 연결합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;self.list_view.setModel(self.model)&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;QListView가 같은 QFileSystemModel의 데이터를 리스트 형태로 표시하도록 연결합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;setRootIndex(current)&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;View가 Model의 어느 위치부터 보여줄지 지정합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;3. 정리 흐름도&lt;/b&gt;&lt;/h2&gt;
&lt;div style=&quot;background-color: #f8f8f8; border: 1px solid #dddddd; border-radius: 10px; padding: 18px; margin: 25px 0;&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;공식 문서 기준으로 정리하면 다음과 같습니다.&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;stylus&quot; style=&quot;background-color: #f7f7f7; border: 1px solid #e0e0e0; border-radius: 6px; padding: 15px; overflow-x: auto; line-height: 1.7; font-size: 14px;&quot;&gt;&lt;code&gt;로컬 파일 시스템
    &amp;darr;
QFileSystemModel
    &amp;darr;
QTreeView / QListView / QTableView
    &amp;darr;
같은 데이터를 서로 다른 화면 형태로 표시&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 이번 예제는 Model/View 구조에서 하나의 Model을 여러 View가 공유하는 가장 기본적인 흐름을 보여줍니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>3. PySide6/3-4. Model - View</category>
      <author>BasicLike</author>
      <guid isPermaLink="true">https://basiclike.tistory.com/612</guid>
      <comments>https://basiclike.tistory.com/612#entry612comment</comments>
      <pubDate>Fri, 29 May 2026 16:30:46 +0900</pubDate>
    </item>
    <item>
      <title>1강. Model/View 개념</title>
      <link>https://basiclike.tistory.com/611</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1627&quot; data-origin-height=&quot;554&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/wQ2ME/dJMcahQraX2/RyPkKz7gkURYYClNwSs4p1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/wQ2ME/dJMcahQraX2/RyPkKz7gkURYYClNwSs4p1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/wQ2ME/dJMcahQraX2/RyPkKz7gkURYYClNwSs4p1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FwQ2ME%2FdJMcahQraX2%2FRyPkKz7gkURYYClNwSs4p1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1627&quot; height=&quot;554&quot; data-origin-width=&quot;1627&quot; data-origin-height=&quot;554&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;!-- ========================================================= --&gt;&lt;!-- ========================================================= --&gt;
&lt;h1&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;0.&lt;/span&gt; 학습 목표&lt;/h1&gt;
&lt;p style=&quot;color: #9d9d9d;&quot; data-ke-size=&quot;size14&quot;&gt;&amp;rarr; Model/View 구조의 역할, 구조 문제, 의존성 문제, 해결 흐름을 단계적으로 이해합니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot; data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 4.1 단계에서는 GUI 프로그램에서 자주 사용되는 &lt;b&gt;Model/View&lt;sup&gt;모델/뷰&lt;/sup&gt;&lt;/b&gt; 구조를 학습합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Model/View가 처음에는 어렵게 느껴질 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글에서는 복잡한 Qt Model 을 바로 다루지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 &lt;b&gt;왜 Model/View 구조가 필요한지&lt;/b&gt;를 문제 상황으로 이해하고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그다음 &lt;b&gt;Model과 View의 역할 분리&lt;/b&gt;를 차근차근 살펴봅니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; margin-top: 15px; margin-bottom: 20px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;background-color: #f5f8ff;&quot;&gt;
&lt;td style=&quot;width: 5.70905%; padding: 10px;&quot;&gt;&lt;b&gt;&amp;nbsp;&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 20.9902%; padding: 10px;&quot;&gt;&lt;b&gt;학습 목표&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 73.3007%; padding: 10px;&quot;&gt;&lt;b&gt;내용&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px; width: 5.70905%; text-align: center;&quot;&gt;1&lt;/td&gt;
&lt;td style=&quot;padding: 10px; width: 20.9902%;&quot;&gt;Model/View 구조 이해&lt;/td&gt;
&lt;td style=&quot;padding: 10px; width: 73.3007%;&quot;&gt;Model은 데이터를 관리하고, &lt;br /&gt;View는 데이터를 화면에 보여주는 역할이라는 점을 이해합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px; width: 5.70905%; text-align: center;&quot; rowspan=&quot;2&quot;&gt;2&lt;/td&gt;
&lt;td style=&quot;padding: 10px; width: 20.9902%;&quot;&gt;구조 문제 이해&lt;/td&gt;
&lt;td style=&quot;padding: 10px; width: 73.3007%;&quot;&gt;데이터가 각각의 View 안에 들어가 있으면 &lt;br /&gt;같은 데이터를 여러 화면에서 공유하기 어렵다는 점을 이해합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px; width: 20.9902%;&quot;&gt;의존성 문제 이해&lt;/td&gt;
&lt;td style=&quot;padding: 10px; width: 73.3007%;&quot;&gt;View가 데이터를 직접 들고 있으면 &lt;br /&gt;화면 코드와 데이터 코드가 강하게 묶여 유지보수가 어려워질 수 있음을 이해합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px; width: 5.70905%; text-align: center;&quot; rowspan=&quot;2&quot;&gt;3&lt;/td&gt;
&lt;td style=&quot;padding: 10px; width: 20.9902%;&quot;&gt;Model 분리 이해&lt;/td&gt;
&lt;td style=&quot;padding: 10px; width: 73.3007%;&quot;&gt;데이터를 View 밖으로 꺼내 &lt;br /&gt;Model에서 중앙 관리하는 구조를 이해합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px; width: 20.9902%;&quot;&gt;View 분리 이해&lt;/td&gt;
&lt;td style=&quot;padding: 10px; width: 73.3007%;&quot;&gt;View는 데이터를 직접 소유하지 않고, &lt;br /&gt;Model의 데이터를 화면에 표시하는 역할에 집중한다는 점을 이해합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;div style=&quot;background-color: #f8fbff; border: 1px solid #d8e8ff; border-radius: 10px; padding: 18px; margin: 25px 0;&quot;&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;이번 단계의 핵심&lt;/b&gt;&lt;br /&gt;Model/View 구조는 데이터를 저장하는 부분과 데이터를 보여주는 화면을 분리하는 구조입니다.&lt;/p&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Model은 데이터를 관리&lt;/b&gt;하고, &lt;b&gt;View는 화면 표시&lt;/b&gt;를 담당합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;!-- ========================================================= --&gt;&lt;!-- 1. Model/View 구조란? --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;!-- ========================================================= --&gt;
&lt;h1&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;1.&lt;/span&gt; Model/View 구조란?&lt;/h1&gt;
&lt;p style=&quot;color: #9d9d9d;&quot; data-ke-size=&quot;size14&quot;&gt;&amp;rarr; Model과 View가 각각 어떤 역할을 담당하는지 먼저 이해합니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot; data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1.1 Model과 View의 기본 의미&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Model/View 구조는 GUI 프로그램에서 &lt;b&gt;데이터 관리&lt;/b&gt;와 &lt;b&gt;화면 표시&lt;/b&gt;를 나누는 방식입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이름만 보면 어려워 보이지만, 역할을 나누어 보면 단순합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1779981417586&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Model
    &amp;darr;
데이터를 저장하고 관리하는 역할&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1779981426984&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;View
    &amp;darr;
데이터를 화면에 보여주는 역할&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Model은 데이터를 가지고 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;View는 Model에 있는 데이터를 가져와 사용자에게 보여줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 Model/View 구조에서는 &lt;b&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;View가 데이터를 직접 소유하지 않습니다.&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;div style=&quot;background-color: #f8fbff; border: 1px solid #d8e8ff; border-radius: 10px; padding: 18px; margin: 25px 0;&quot;&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;중요한 점&lt;/b&gt;&lt;br /&gt;View는 데이터를 보여주는 화면이고, Model은 데이터를 관리하는 저장소입니다.&lt;/p&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;이 둘을 분리하면 같은 데이터를 여러 화면에서 함께 사용할 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1.2 간단한 비유로 이해하기&lt;/b&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;GUI 프로그램에서는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;같은 데이터&lt;/b&gt;를&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;여러 화면에서 보여줘야 하는 경우&lt;/b&gt;가 많습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어 로그인한 &lt;b&gt;사용자 데이터&lt;/b&gt;는 메인 화면, 설정 화면, 프로필 화면, 알림 화면에서 모두 필요할 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이때&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;각 화면이 데이터를 따로 저장하면 문제가 생깁니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;어느 한 화면에서 데이터가 바뀌어도 다른 화면은 그 변경 사실을 모를 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;stylus&quot; style=&quot;background-color: #f7f7f7; border: 1px solid #e0e0e0; border-radius: 6px; padding: 16px; overflow-x: auto; line-height: 1.7; font-size: 14px;&quot;&gt;&lt;code&gt;사용자 데이터
    &amp;darr;
UserModel
    &amp;darr;
카드 화면
리스트 화면
텍스트 화면&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터는 하나입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 화면 표현 방식은 여러 개가 될 수 있습니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #f8f8f8; border: 1px solid #dddddd; border-radius: 10px; padding: 18px; margin: 25px 0;&quot;&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;기억할 문장&lt;/b&gt;&lt;br /&gt;Model/View 구조에서는 데이터는 하나로 관리하고, 화면 표현 방식은 여러 개로 나눌 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;!-- ========================================================= --&gt;&lt;!-- 2. 구조 문제: 데이터가 View 안에 들어가 있는 구조 --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;!-- ========================================================= --&gt;
&lt;h1&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;2.&lt;/span&gt; &lt;span style=&quot;color: #ee2323;&quot;&gt;재사용 문제&lt;/span&gt;: 데이터가 View 안에 들어가 있는 구조&lt;/h1&gt;
&lt;p style=&quot;color: #9d9d9d;&quot; data-ke-size=&quot;size14&quot;&gt;&amp;rarr; 기존 스크롤 UI 예제를 통해 데이터가 View 안에 들어간 구조를 확인합니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot; data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2.1 기존 예제에서 사용한 방식&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전 스크롤 UI 예제에서는 여러 방식으로 데이터를 화면에 표시했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;QScrollArea, QListWidget, QTextEdit은 모두 스크롤 가능한 화면을 만들 수 있는 위젯입니다.&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;367&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bRQkWx/dJMcadWm2F5/FSIiUM9lSOzC4PxsVJVoUK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bRQkWx/dJMcadWm2F5/FSIiUM9lSOzC4PxsVJVoUK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bRQkWx/dJMcadWm2F5/FSIiUM9lSOzC4PxsVJVoUK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbRQkWx%2FdJMcadWm2F5%2FFSIiUM9lSOzC4PxsVJVoUK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;367&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;367&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;table style=&quot;border-collapse: collapse; width: 100%; margin-top: 15px; margin-bottom: 20px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;background-color: #f5f8ff;&quot;&gt;
&lt;td style=&quot;width: 30%; padding: 10px;&quot;&gt;&lt;b&gt;방식&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 70%; padding: 10px;&quot;&gt;&lt;b&gt;데이터를 넣는 방법&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;QScrollArea&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;QLabel을 만들어 레이아웃에 직접 추가했습니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;QListWidget&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;addItem()으로 항목을 직접 추가했습니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;QTextEdit&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;append()로 로그 텍스트를 직접 추가했습니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방식은 간단한 화면을 만들 때는 편리합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 데이터가 View 내부에 들어간다는 공통점이 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2.2 QScrollArea: QLabel 안에 데이터가 들어감&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;QScrollArea 기반 화면에서는 QLabel을 직접 만들어 레이아웃에 추가했습니다.&lt;/p&gt;
&lt;pre class=&quot;ini&quot; style=&quot;background-color: #f7f7f7; border: 1px solid #ddd; border-radius: 6px; padding: 15px; overflow-x: auto;&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;label = QLabel(&quot;항목 1&quot;)
content_layout.addWidget(label)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 코드에서는 표시할 문자열이 QLabel 안에 직접 들어갑니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 데이터 저장소와 화면 표시가 분리되어 있지 않습니다.&lt;/p&gt;
&lt;pre class=&quot;stylus&quot; style=&quot;background-color: #f7f7f7; border: 1px solid #e0e0e0; border-radius: 6px; padding: 16px; overflow-x: auto; line-height: 1.7; font-size: 14px;&quot;&gt;&lt;code&gt;QScrollArea
    &amp;darr;
content_layout
    &amp;darr;
QLabel(&quot;항목 1&quot;)
QLabel(&quot;항목 2&quot;)
QLabel(&quot;항목 3&quot;)

데이터가 QLabel 안에 들어간 상태&lt;/code&gt;&lt;/pre&gt;
&lt;div style=&quot;background-color: #f8fbff; border: 1px solid #d8e8ff; border-radius: 10px; padding: 18px; margin: 25px 0;&quot;&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;구조 문제&lt;/b&gt;&lt;br /&gt;QLabel에 직접 들어간 데이터는 다른 View에서 쉽게 공유하기 어렵습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2.3 QListWidget: 내부 항목으로 데이터가 들어감&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;QListWidget 기반 화면에서는 addItem()으로 항목을 추가했습니다.&lt;/p&gt;
&lt;pre class=&quot;ini&quot; style=&quot;background-color: #f7f7f7; border: 1px solid #ddd; border-radius: 6px; padding: 15px; overflow-x: auto;&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;self.list_widget.addItem(&quot;항목 1&quot;)
self.list_widget.addItem(&quot;항목 2&quot;)
self.list_widget.addItem(&quot;항목 3&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;QListWidget은 항목을 쉽게 추가할 수 있는 편리한 위젯입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 addItem()으로 추가한 데이터는 QListWidget 내부 항목으로 관리됩니다.&lt;/p&gt;
&lt;pre class=&quot;stylus&quot; style=&quot;background-color: #f7f7f7; border: 1px solid #e0e0e0; border-radius: 6px; padding: 16px; overflow-x: auto; line-height: 1.7; font-size: 14px;&quot;&gt;&lt;code&gt;QListWidget
    &amp;darr;
내부 항목 저장 구조
    &amp;darr;
&quot;항목 1&quot;
&quot;항목 2&quot;
&quot;항목 3&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;div style=&quot;background-color: #f8fbff; border: 1px solid #d8e8ff; border-radius: 10px; padding: 18px; margin: 25px 0;&quot;&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;구조 문제&lt;/b&gt;&lt;br /&gt;QListWidget 내부에 들어간 항목 데이터는 다른 View와 자연스럽게 공유하기 어렵습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2.4 QTextEdit: 내부 문서에 데이터가 들어감&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;QTextEdit 기반 화면에서는 append()로 로그를 추가했습니다.&lt;/p&gt;
&lt;pre class=&quot;ini&quot; style=&quot;background-color: #f7f7f7; border: 1px solid #ddd; border-radius: 6px; padding: 15px; overflow-x: auto;&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;self.log_textedit.append(&quot;로그 1&quot;)
self.log_textedit.append(&quot;로그 2&quot;)
self.log_textedit.append(&quot;로그 3&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;append()로 추가한 텍스트는 QTextEdit 내부 문서에 저장됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;화면에 보여주기는 쉽지만, 원본 데이터를 다른 View와 공유하기는 어렵습니다.&lt;/p&gt;
&lt;pre class=&quot;stylus&quot; style=&quot;background-color: #f7f7f7; border: 1px solid #e0e0e0; border-radius: 6px; padding: 16px; overflow-x: auto; line-height: 1.7; font-size: 14px;&quot;&gt;&lt;code&gt;QTextEdit
    &amp;darr;
내부 Document
    &amp;darr;
로그 1
로그 2
로그 3&lt;/code&gt;&lt;/pre&gt;
&lt;div style=&quot;background-color: #f8fbff; border: 1px solid #d8e8ff; border-radius: 10px; padding: 18px; margin: 25px 0;&quot;&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;구조 문제&lt;/b&gt;&lt;br /&gt;로그 데이터가 QTextEdit 내부에 직접 저장되면, 다른 View에서 같은 로그 데이터를 재사용하기 어렵습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2.5 구조 문제 정리&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세 방식의 공통 문제는 데이터가 View 내부에 들어가 있다는 점입니다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; margin-top: 15px; margin-bottom: 20px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;background-color: #f5f8ff;&quot;&gt;
&lt;td style=&quot;width: 30%; padding: 10px;&quot;&gt;&lt;b&gt;View&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 35%; padding: 10px;&quot;&gt;&lt;b&gt;데이터 위치&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 35%; padding: 10px;&quot;&gt;&lt;b&gt;구조 문제&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;QScrollArea&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;QLabel 같은 자식 위젯 안&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;다른 View와 공유하기 어렵습니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;QListWidget&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;QListWidget 내부 항목&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;다른 View와 공유하기 어렵습니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;QTextEdit&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;QTextEdit 내부 문서&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;다른 View와 공유하기 어렵습니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre class=&quot;stylus&quot; style=&quot;background-color: #f7f7f7; border: 1px solid #e0e0e0; border-radius: 6px; padding: 16px; overflow-x: auto; line-height: 1.7; font-size: 14px;&quot;&gt;&lt;code&gt;QScrollArea  &amp;larr; 데이터가 QLabel 안에 있음
QListWidget  &amp;larr; 데이터가 QListWidget 안에 있음
QTextEdit    &amp;larr; 데이터가 QTextEdit 안에 있음

    &amp;darr;

View마다 데이터를 따로 들고 있는 구조&lt;/code&gt;&lt;/pre&gt;
&lt;div style=&quot;background-color: #f8fbff; border: 1px solid #d8e8ff; border-radius: 10px; padding: 18px; margin: 25px 0;&quot;&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;이번 장의 핵심&lt;/b&gt;&lt;br /&gt;View 안에 데이터가 들어가면 화면을 빠르게 만들 수는 있지만, 같은 데이터를 여러 View에서 공유하기 어렵습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;!-- ========================================================= --&gt;
&lt;h1&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;3.&lt;/span&gt; &lt;span style=&quot;color: #ee2323;&quot;&gt;의존성 문제&lt;/span&gt;: View가 데이터를 소유&lt;/h1&gt;
&lt;p style=&quot;color: #9d9d9d;&quot; data-ke-size=&quot;size14&quot;&gt;&amp;rarr; 같은 데이터를 여러 화면에서 사용할 때 View와 데이터가 강하게 묶이는 문제를 이해합니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot; data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;3.1 의존성이란?&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;의존성이란 한 코드가 다른 코드에 얼마나 강하게 묶여 있는지를 의미합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GUI 프로그램에서는 View가 데이터를 직접 들고 있을 때 의존성이 강해질 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;stylus&quot; style=&quot;background-color: #f7f7f7; border: 1px solid #e0e0e0; border-radius: 6px; padding: 16px; overflow-x: auto; line-height: 1.7; font-size: 14px;&quot;&gt;&lt;code&gt;View
 ├── 화면 표시 코드
 └── 데이터 저장 코드

화면과 데이터가 한 곳에 섞여 있음&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 간단해 보입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 화면이 늘어나고, 같은 데이터를 여러 View에서 사용하기 시작하면 문제가 커집니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #f8f8f8; border: 1px solid #dddddd; border-radius: 10px; padding: 18px; margin: 25px 0;&quot;&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;참고&lt;/b&gt;&lt;br /&gt;의존성이 강하다는 것은 한쪽 코드를 바꾸면 다른 쪽 코드도 함께 영향을 받기 쉽다는 뜻입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;3.2 사용자 정보 예시로 보기&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 로그인한 사용자 정보가 있다고 가정하겠습니다.&lt;/p&gt;
&lt;pre class=&quot;ini&quot; style=&quot;background-color: #f7f7f7; border: 1px solid #ddd; border-radius: 6px; padding: 15px; overflow-x: auto;&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;user = {
    &quot;name&quot;: &quot;홍길동&quot;,
    &quot;email&quot;: &quot;hong@example.com&quot;,
    &quot;role&quot;: &quot;관리자&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 사용자 정보는 한 화면에서만 필요한 것이 아닙니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메인 화면, 설정 화면, 프로필 화면, 알림 화면에서 모두 필요할 수 있습니다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; margin-top: 15px; margin-bottom: 20px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;background-color: #f5f8ff;&quot;&gt;
&lt;td style=&quot;width: 30%; padding: 10px;&quot;&gt;&lt;b&gt;화면&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 70%; padding: 10px;&quot;&gt;&lt;b&gt;표시해야 하는 정보&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;메인 화면&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;홍길동 님 환영합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;설정 화면&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;현재 권한: 관리자&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;프로필 화면&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;이메일: hong@example.com&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;알림 화면&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;사용자 이름 또는 권한에 따라 알림 표시&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre class=&quot;stylus&quot; style=&quot;background-color: #f7f7f7; border: 1px solid #e0e0e0; border-radius: 6px; padding: 16px; overflow-x: auto; line-height: 1.7; font-size: 14px;&quot;&gt;&lt;code&gt;user 데이터
    &amp;darr;
메인 화면
설정 화면
프로필 화면
알림 화면

같은 데이터를 여러 View에서 사용해야 함&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 같은 데이터를 여러 View에서 공유해야 하는 상황입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;3.3 각 View가 데이터를 직접 들고 있는 구조&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 코드는 각 View가 userInfo를 직접 들고 있는 예시입니다.&lt;/p&gt;
&lt;pre class=&quot;ini&quot; style=&quot;background-color: #f7f7f7; border: 1px solid #ddd; border-radius: 6px; padding: 15px; overflow-x: auto;&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;class MainView(QWidget):
    def __init__(self, userInfo):
        super().__init__()

        self.userInfo = userInfo

        label = QLabel(f&quot;{self.userInfo['name']} 님 환영합니다&quot;)


class ProfileView(QWidget):
    def __init__(self, userInfo):
        super().__init__()

        self.userInfo = userInfo

        label = QLabel(self.userInfo[&quot;email&quot;])&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 코드는 처음에는 괜찮아 보입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MainView도 userInfo를 받고, ProfileView도 userInfo를 받기 때문입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 View가 많아지면 &lt;b&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;각 View가 각 데이터를 따로 들고 있는 구조&lt;/span&gt;&lt;/b&gt;가 됩니다.&lt;/p&gt;
&lt;pre class=&quot;stylus&quot; style=&quot;background-color: #f7f7f7; border: 1px solid #e0e0e0; border-radius: 6px; padding: 16px; overflow-x: auto; line-height: 1.7; font-size: 14px;&quot;&gt;&lt;code&gt;MainView
    └── self.userInfo

ProfileView
    └── self.userInfo

SettingView
    └── self.userInfo

NotificationView
    └── self.userInfo&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;View마다 userInfo를 들고 있는 구조입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하나의 View에서 userInfo가 변경되어도 다른 View는 그 변경 사실을 자동으로 알 수 없습니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #f8fbff; border: 1px solid #d8e8ff; border-radius: 10px; padding: 18px; margin: 25px 0;&quot;&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;의존성 문제&lt;/b&gt;&lt;br /&gt;View가 데이터를 직접 들고 있으면 데이터 변경을 여러 View에 동시에 반영하기 어렵습니다.&lt;/p&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;또한 View 코드와 데이터 코드가 강하게 묶여 유지보수가 어려워질 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;3.4 데이터 변경 시 생기는 문제&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 사용자의 이메일이 바뀌었다고 가정하겠습니다.&lt;/p&gt;
&lt;pre class=&quot;ini&quot; style=&quot;background-color: #f7f7f7; border: 1px solid #ddd; border-radius: 6px; padding: 15px; overflow-x: auto;&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;user[&quot;email&quot;] = &quot;new@example.com&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 ProfileView는 새 이메일을 보여줘야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;알림 화면도 새 사용자 정보를 기준으로 동작해야 할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 각 View가 데이터를 따로 들고 있다면, 모든 View를 직접 찾아서 갱신해야 합니다.&lt;/p&gt;
&lt;pre class=&quot;stylus&quot; style=&quot;background-color: #f7f7f7; border: 1px solid #e0e0e0; border-radius: 6px; padding: 16px; overflow-x: auto; line-height: 1.7; font-size: 14px;&quot;&gt;&lt;code&gt;사용자 정보 변경
    &amp;darr;
MainView도 갱신해야 함
ProfileView도 갱신해야 함
SettingView도 갱신해야 함
NotificationView도 갱신해야 함

    &amp;darr;

화면이 많아질수록 관리가 어려워짐&lt;/code&gt;&lt;/pre&gt;
&lt;div style=&quot;background-color: #f8f8f8; border: 1px solid #dddddd; border-radius: 10px; padding: 18px; margin: 25px 0;&quot;&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;기억할 문장&lt;/b&gt;&lt;br /&gt;데이터가 View에 흩어져 있으면, 데이터 변경을 추적하고 반영하기 어렵습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;!-- ========================================================= --&gt;&lt;!-- 4. 해결 구조: Model 분리와 View 분리 --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;!-- ========================================================= --&gt;
&lt;h1&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;4.&lt;/span&gt; &lt;span style=&quot;color: #009a87;&quot;&gt;해결 방법&lt;/span&gt;: Model 분리와 View 분리&lt;/h1&gt;
&lt;p style=&quot;color: #9d9d9d;&quot; data-ke-size=&quot;size14&quot;&gt;&amp;rarr; 데이터를 Model로 분리하고, View는 화면 표시 역할에 집중하도록 구조를 바꿉니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot; data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;4.1 먼저 문제 구조 다시 보기&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 방식에서는 View가 데이터를 직접 들고 있었습니다.&lt;/p&gt;
&lt;pre class=&quot;stylus&quot; style=&quot;background-color: #f7f7f7; border: 1px solid #e0e0e0; border-radius: 6px; padding: 16px; overflow-x: auto; line-height: 1.7; font-size: 14px;&quot;&gt;&lt;code&gt;view_1  &amp;larr;── data
view_2  &amp;larr;── data
view_3  &amp;larr;── data&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 구조에서는 데이터가 View에 종속됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, View가 바뀌면 데이터 관리 방식도 함께 영향을 받을 수 있습니다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; margin-top: 15px; margin-bottom: 20px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;background-color: #f5f8ff;&quot;&gt;
&lt;td style=&quot;width: 30%; padding: 10px;&quot;&gt;&lt;b&gt;문제&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 70%; padding: 10px;&quot;&gt;&lt;b&gt;설명&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;데이터 중복&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;여러 View가 비슷한 데이터를 각각 들고 있을 수 있습니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;변경 반영 어려움&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;한 View에서 데이터가 바뀌어도 다른 View가 자동으로 알기 어렵습니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;유지보수 어려움&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;View가 많아질수록 데이터 수정 코드가 여러 곳에 흩어질 수 있습니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;의존성 증가&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;화면 코드와 데이터 코드가 강하게 묶입니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;div style=&quot;background-color: #f8fbff; border: 1px solid #d8e8ff; border-radius: 10px; padding: 18px; margin: 25px 0;&quot;&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;문제 구조&lt;/b&gt;&lt;br /&gt;View가 데이터를 직접 가지면, 데이터가 화면 코드에 묶입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;4.2 해결 1: Model을 분리한다&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫 번째 해결은 데이터를 View 밖으로 꺼내는 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 데이터를 저장하고 관리하는 별도의 객체를 만듭니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 객체를 Model이라고 생각하면 됩니다.&lt;/p&gt;
&lt;pre class=&quot;stylus&quot; style=&quot;background-color: #f7f7f7; border: 1px solid #e0e0e0; border-radius: 6px; padding: 16px; overflow-x: auto; line-height: 1.7; font-size: 14px;&quot;&gt;&lt;code&gt;view_1 &amp;larr;─┐
view_2 &amp;larr;── model &amp;larr;─ data
view_3 &amp;larr;─┘&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 구조에서는 데이터가 View 안에 들어가지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 View는 Model을 통해 같은 데이터를 봅니다.&lt;/p&gt;
&lt;pre class=&quot;ini&quot; style=&quot;background-color: #f7f7f7; border: 1px solid #ddd; border-radius: 6px; padding: 15px; overflow-x: auto;&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;class UserModel:
    def __init__(self):
        self.user = {
            &quot;name&quot;: &quot;홍길동&quot;,
            &quot;email&quot;: &quot;hong@example.com&quot;,
            &quot;role&quot;: &quot;관리자&quot;
        }

    def get_name(self):
        return self.user[&quot;name&quot;]

    def get_email(self):
        return self.user[&quot;email&quot;]

    def get_role(self):
        return self.user[&quot;role&quot;]

    def set_email(self, email):
        self.user[&quot;email&quot;] = email&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;UserModel은 사용자 정보를 한 곳에서 관리합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 사용자 정보는 여러 View에 흩어져 있지 않고, UserModel 안에 모입니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #f8fbff; border: 1px solid #d8e8ff; border-radius: 10px; padding: 18px; margin: 25px 0;&quot;&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Model 분리의 핵심&lt;/b&gt;&lt;br /&gt;데이터를 View 안에 넣지 않고, Model에서 중앙 관리합니다.&lt;/p&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;데이터가 한 곳에 모이면 여러 View가 같은 데이터를 함께 사용할 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;4.3 해결 2: View는 화면 표시 역할만 맡는다&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 번째 해결은 View의 역할을 줄이는 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;View는 데이터를 직접 저장하지 않고, Model에서 필요한 데이터를 가져와 화면에 보여주기만 합니다.&lt;/p&gt;
&lt;pre class=&quot;ini&quot; style=&quot;background-color: #f7f7f7; border: 1px solid #ddd; border-radius: 6px; padding: 15px; overflow-x: auto;&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;class MainView(QWidget):
    def __init__(self, user_model):
        super().__init__()

        self.user_model = user_model

        label = QLabel(f&quot;{self.user_model.get_name()} 님 환영합니다&quot;)


class ProfileView(QWidget):
    def __init__(self, user_model):
        super().__init__()

        self.user_model = user_model

        label = QLabel(self.user_model.get_email())&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MainView와 ProfileView는 더 이상 user 딕셔너리를 직접 관리하지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대신 같은 UserModel을 전달받고, 그 Model의 데이터를 화면에 표시합니다.&lt;/p&gt;
&lt;pre class=&quot;stylus&quot; style=&quot;background-color: #f7f7f7; border: 1px solid #e0e0e0; border-radius: 6px; padding: 16px; overflow-x: auto; line-height: 1.7; font-size: 14px;&quot;&gt;&lt;code&gt;UserModel
    ├── name: 홍길동
    ├── email: hong@example.com
    └── role: 관리자

MainView     &amp;rarr; UserModel의 name 표시
ProfileView  &amp;rarr; UserModel의 email 표시
SettingView  &amp;rarr; UserModel의 role 표시&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 Model을 바라보지만, 각 View가 보여주는 방식은 다를 수 있습니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #f8fbff; border: 1px solid #d8e8ff; border-radius: 10px; padding: 18px; margin: 25px 0;&quot;&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;View 분리의 핵심&lt;/b&gt;&lt;br /&gt;View는 데이터를 직접 소유하지 않습니다.&lt;/p&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;View는 Model의 데이터를 화면에 표시하는 역할에 집중합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;4.4 실제 Qt Model/View와의 관계&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 UserModel 예시는 Model/View 개념을 이해하기 위한 단순 예시입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 Qt에서는 더 전문적인 Model 클래스를 사용할 수 있습니다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; margin-top: 15px; margin-bottom: 20px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;background-color: #f5f8ff;&quot;&gt;
&lt;td style=&quot;width: 35%; padding: 10px;&quot;&gt;&lt;b&gt;Qt Model 클래스&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 65%; padding: 10px;&quot;&gt;&lt;b&gt;역할&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;QAbstractItemModel&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;Qt Model/View 구조의 기본이 되는 추상 Model 클래스입니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;QStringListModel&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;문자열 리스트 데이터를 Model로 다룰 때 사용할 수 있습니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;QStandardItemModel&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;일반적인 표, 트리, 리스트 데이터를 표현할 때 사용할 수 있습니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;QFileSystemModel&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;파일 시스템 데이터를 Model로 제공할 때 사용할 수 있습니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;div style=&quot;background-color: #f8f8f8; border: 1px solid #dddddd; border-radius: 10px; padding: 18px; margin: 25px 0;&quot;&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;참고&lt;/b&gt;&lt;br /&gt;이번 단계에서는 QAbstractItemModel을 직접 구현하지 않습니다.&lt;/p&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;우선 데이터를 View 밖으로 분리하고, View는 Model의 데이터를 표시한다는 핵심 개념에 집중합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;!-- ========================================================= --&gt;&lt;!-- 5. 기존 방식과 Model/View 방식 비교 --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;!-- ========================================================= --&gt;
&lt;h1&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;5.&lt;/span&gt; Model/View 방식 비교&lt;/h1&gt;
&lt;p style=&quot;color: #9d9d9d;&quot; data-ke-size=&quot;size14&quot;&gt;&amp;rarr; 데이터 위치, 역할 분리, 의존성 관점에서 두 방식을 비교합니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot; data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;5.1 구조 비교&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지 살펴본 내용을 기준으로 기존 방식과 Model/View 방식을 비교해 보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 큰 차이는 데이터가 어디에 저장되는가입니다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; margin-top: 15px; margin-bottom: 20px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;background-color: #f5f8ff;&quot;&gt;
&lt;td style=&quot;width: 25%; padding: 10px;&quot;&gt;&lt;b&gt;구분&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 37.5%; padding: 10px;&quot;&gt;&lt;b&gt;기존 방식&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 37.5%; padding: 10px;&quot;&gt;&lt;b&gt;Model/View 방식&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;데이터 위치&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;View 내부&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;Model 내부&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;View 역할&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;데이터 저장과 화면 표시를 함께 담당&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;데이터를 화면에 보여주는 역할 담당&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;Model 역할&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;없거나 약함&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;데이터를 중앙에서 관리함&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;데이터 공유&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;어려움&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;쉬움&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;화면 추가&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;화면마다 데이터 처리 코드가 중복될 수 있음&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;같은 Model을 바라보는 View만 추가하면 됨&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;유지보수&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;화면이 많아질수록 복잡해짐&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;역할이 분리되어 관리하기 쉬움&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 방식에서는 View가 데이터를 직접 가지고 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면 Model/View 방식에서는 데이터를 Model에 두고, View는 그 데이터를 화면에 보여주기만 합니다.&lt;/p&gt;
&lt;pre class=&quot;stylus&quot; style=&quot;background-color: #f7f7f7; border: 1px solid #e0e0e0; border-radius: 6px; padding: 16px; overflow-x: auto; line-height: 1.7; font-size: 14px;&quot;&gt;&lt;code&gt;기존 방식

View 1  &amp;larr;── data
View 2  &amp;larr;── data
View 3  &amp;larr;── data

각 View가 데이터를 따로 들고 있음&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;stylus&quot; style=&quot;background-color: #f7f7f7; border: 1px solid #e0e0e0; border-radius: 6px; padding: 16px; overflow-x: auto; line-height: 1.7; font-size: 14px;&quot;&gt;&lt;code&gt;Model/View 방식

View 1 &amp;larr;─┐
View 2 &amp;larr;── Model &amp;larr;── data
View 3 &amp;larr;─┘

하나의 Model을 여러 View가 함께 바라봄&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;5.2 의존성 관점에서 비교&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 방식은 View와 데이터가 강하게 묶이는 구조입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Model/View 방식은 데이터 관리와 화면 표시를 분리하는 구조입니다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; margin-top: 15px; margin-bottom: 20px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;background-color: #f5f8ff;&quot;&gt;
&lt;td style=&quot;width: 25%; padding: 10px;&quot;&gt;&lt;b&gt;관점&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 37.5%; padding: 10px;&quot;&gt;&lt;b&gt;기존 방식&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 37.5%; padding: 10px;&quot;&gt;&lt;b&gt;Model/View 방식&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;의존성&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;View와 데이터가 강하게 묶입니다.&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;Model과 View의 역할이 분리됩니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;수정 영향&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;데이터 수정이 화면 코드에 영향을 줄 수 있습니다.&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;데이터 수정은 Model 중심으로 관리할 수 있습니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;확장&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;화면이 늘어나면 데이터 처리 코드도 늘어날 수 있습니다.&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;새 View를 추가해도 같은 Model을 사용할 수 있습니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre class=&quot;python&quot; style=&quot;background-color: #f7f7f7; border: 1px solid #e0e0e0; border-radius: 6px; padding: 16px; overflow-x: auto; line-height: 1.7; font-size: 14px;&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;# 기존 방식 (화면과 데이터가 한 곳에 섞여 있음)

View
 ├── 화면 코드
 └── 데이터 코드&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1779980019308&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# Model/View 방식 (역할이 나누어져 있음)

Model
 └── 데이터 관리

View
 └── 화면 표시&lt;/code&gt;&lt;/pre&gt;
&lt;div style=&quot;background-color: #f8fbff; border: 1px solid #d8e8ff; border-radius: 10px; padding: 18px; margin: 25px 0;&quot;&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;중요한 점&lt;/b&gt;&lt;br /&gt;Model/View 구조의 핵심은 View를 없애는 것이 아닙니다.&lt;/p&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;View는 화면을 담당하고, Model은 데이터를 담당하도록 역할을 분리하는 것입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;5.3 같은 Model, 다른 View&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Model/View 구조를 사용하면 같은 데이터를 여러 View에서 다르게 보여줄 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 같은 사용자 데이터를 카드 형태, 리스트 형태, 텍스트 형태로 보여줄 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;stylus&quot; style=&quot;background-color: #f7f7f7; border: 1px solid #e0e0e0; border-radius: 6px; padding: 16px; overflow-x: auto; line-height: 1.7; font-size: 14px;&quot;&gt;&lt;code&gt;같은 Model
    &amp;darr;
CardView
ListView
TextView

데이터는 하나
표시 방식은 여러 개&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 데이터는 하나만 관리하고 화면 표현 방식만 여러 개로 나눌 수 있습니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #f8f8f8; border: 1px solid #dddddd; border-radius: 10px; padding: 18px; margin: 25px 0;&quot;&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;기억할 문장&lt;/b&gt;&lt;br /&gt;Model/View 구조에서는 데이터는 하나로 관리하더라도, 화면 표현 방식은 여러 개로 보여 줄 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;!-- ========================================================= --&gt;&lt;!-- 6. 정리 --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;!-- ========================================================= --&gt;
&lt;h1&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;6.&lt;/span&gt; 정리&lt;/h1&gt;
&lt;p style=&quot;color: #9d9d9d;&quot; data-ke-size=&quot;size14&quot;&gt;&amp;rarr; Model/View 구조의 핵심 흐름을 마지막으로 정리합니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot; data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 단계에서는 Model/View 구조의 기본 개념을 살펴보았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심은 데이터를 View 안에 저장하지 않고, Model이라는 구조로 별도 관리하는 것입니다.&lt;/p&gt;
&lt;pre class=&quot;stylus&quot; style=&quot;background-color: #f7f7f7; border: 1px solid #e0e0e0; border-radius: 6px; padding: 16px; overflow-x: auto; line-height: 1.7; font-size: 14px;&quot;&gt;&lt;code&gt;기존 방식
    &amp;darr;
View 안에 데이터 저장
    &amp;darr;
데이터 공유와 변경 반영이 어려움


Model/View 방식
    &amp;darr;
Model이 데이터 관리
    &amp;darr;
View가 화면 표시
    &amp;darr;
같은 데이터를 여러 View에서 함께 사용 가능&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 단계의 핵심 개념을 다시 정리하면 다음과 같습니다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; margin-top: 15px; margin-bottom: 20px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;background-color: #f5f8ff;&quot;&gt;
&lt;td style=&quot;width: 30%; padding: 10px;&quot;&gt;&lt;b&gt;개념&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 70%; padding: 10px;&quot;&gt;&lt;b&gt;정리&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;Model&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;데이터를 저장하고 관리하는 역할입니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;View&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;Model의 데이터를 화면에 보여주는 역할입니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;구조 문제&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;데이터가 View 안에 들어가 있으면 여러 View와 공유하기 어렵습니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;의존성 문제&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;View와 데이터가 강하게 묶이면 유지보수가 어려워질 수 있습니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;Model 분리&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;데이터를 View 밖으로 꺼내 Model에서 중앙 관리합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;View 분리&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;View는 데이터를 직접 소유하지 않고 화면 표시 역할에 집중합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;div style=&quot;background-color: #f8fbff; border: 1px solid #d8e8ff; border-radius: 10px; padding: 18px; margin: 25px 0;&quot;&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;이번 단계의 핵심&lt;/b&gt;&lt;br /&gt;Model/View 구조는 데이터를 저장하는 부분과 데이터를 보여주는 화면을 분리하는 구조입니다.&lt;/p&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;Model은 데이터를 관리하고, View는 화면 표시를 담당합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div style=&quot;background-color: #f8f8f8; border: 1px solid #dddddd; border-radius: 10px; padding: 18px; margin: 25px 0;&quot;&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;기억할 문장&lt;/b&gt;&lt;br /&gt;데이터는 Model에 두고, 화면 표시는 View가 담당합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;!-- ========================================================= --&gt;&lt;!-- 참고. 공식 문서로 확인하기 --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;!-- ========================================================= --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h1&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;참고.&lt;/span&gt; 공식 문서로 확인하기&lt;/h1&gt;
&lt;p style=&quot;color: #9d9d9d;&quot; data-ke-size=&quot;size14&quot;&gt;&amp;rarr; 공식 문서에서 Model/View 구조와 관련 클래스를 확인합니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot; data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1. 공식 문서 참고 표&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 단계에서 살펴본 Model/View 구조는 Qt for Python 공식 문서에서도 확인할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공식 문서에서는 Model/View 아키텍처가 데이터와 데이터를 표현하는 방식을 분리하여 더 유연한 구조를 만들 수 있다고 설명합니다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; margin-top: 15px; margin-bottom: 20px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;background-color: #f5f8ff;&quot;&gt;
&lt;td style=&quot;width: 22.555%; padding: 10px;&quot;&gt;&lt;b&gt;구분&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 37.445%; padding: 10px;&quot;&gt;&lt;b&gt;공식 문서&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 40%; padding: 10px;&quot;&gt;&lt;b&gt;확인할 내용&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px; width: 22.555%;&quot;&gt;Model/View &lt;br /&gt;Programming&lt;/td&gt;
&lt;td style=&quot;padding: 10px; width: 37.445%;&quot;&gt;&lt;a href=&quot;https://doc.qt.io/qtforpython-6/overviews/qtwidgets-model-view-programming.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Qt for Python - Model/View Programming&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;padding: 10px; width: 40%;&quot;&gt;Qt의 Model/View 아키텍처가 데이터와 표현 방식을 분리하는 구조임을 확인할 수 있습니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px; width: 22.555%;&quot;&gt;QAbstractItemModel&lt;/td&gt;
&lt;td style=&quot;padding: 10px; width: 37.445%;&quot;&gt;&lt;a href=&quot;https://doc.qt.io/qtforpython-6/PySide6/QtCore/QAbstractItemModel.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;PySide6.QtCore.QAbstractItemModel&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;padding: 10px; width: 40%;&quot;&gt;Qt의 Model/View 프레임워크에서 기본 Model 역할을 하는 클래스를 확인할 수 있습니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px; width: 22.555%;&quot;&gt;QListWidget&lt;/td&gt;
&lt;td style=&quot;padding: 10px; width: 37.445%;&quot;&gt;&lt;a href=&quot;https://doc.qt.io/qtforpython-6/PySide6/QtWidgets/QListWidget.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;PySide6.QtWidgets.QListWidget&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;padding: 10px; width: 40%;&quot;&gt;QListWidget이 항목 기반 리스트 위젯이라는 점을 확인할 수 있습니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px; width: 22.555%;&quot;&gt;QTextEdit&lt;/td&gt;
&lt;td style=&quot;padding: 10px; width: 37.445%;&quot;&gt;&lt;a href=&quot;https://doc.qt.io/qtforpython-6/PySide6/QtWidgets/QTextEdit.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;PySide6.QtWidgets.QTextEdit&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;padding: 10px; width: 40%;&quot;&gt;QTextEdit이 텍스트를 표시하고 편집하는 위젯이며 내부 문서와 관련된 기능을 가진다는 점을 확인할 수 있습니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2. 이번 예제와 공식 문서 연결&lt;/b&gt;&lt;/h2&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; margin-top: 15px; margin-bottom: 20px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;background-color: #f5f8ff;&quot;&gt;
&lt;td style=&quot;width: 28.643%; padding: 10px;&quot;&gt;&lt;b&gt;이번 글의 내용&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 71.357%; padding: 10px;&quot;&gt;&lt;b&gt;공식 문서와 연결되는 의미&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px; width: 28.643%;&quot;&gt;Model은 데이터 저장소&lt;/td&gt;
&lt;td style=&quot;padding: 10px; width: 71.357%;&quot;&gt;Qt의 Model/View 구조에서 &lt;br /&gt;Model은 View가 사용할 데이터를 제공하는 역할을 합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px; width: 28.643%;&quot;&gt;View는 화면 표시&lt;/td&gt;
&lt;td style=&quot;padding: 10px; width: 71.357%;&quot;&gt;View는 Model의 데이터를 &lt;br /&gt;사용자에게 보여주는 역할을 합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px; width: 28.643%;&quot;&gt;데이터와 화면 분리&lt;/td&gt;
&lt;td style=&quot;padding: 10px; width: 71.357%;&quot;&gt;데이터 관리와 표시 방식을 분리하면 &lt;br /&gt;같은 데이터를 여러 View에서 유연하게 사용할 수 있습니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px; width: 28.643%;&quot;&gt;QListWidget과 QTextEdit&lt;/td&gt;
&lt;td style=&quot;padding: 10px; width: 71.357%;&quot;&gt;편리한 위젯이지만 데이터 공유 구조를 설계하려면&lt;br /&gt;Model/View 관점을 함께 이해해야 합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;3. 정리 흐름도&lt;/b&gt;&lt;/h2&gt;
&lt;div style=&quot;background-color: #f8f8f8; border: 1px solid #dddddd; border-radius: 10px; padding: 18px; margin: 25px 0;&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;공식 문서 기준으로 정리하면 다음과 같습니다.&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;stylus&quot; style=&quot;background-color: #f7f7f7; border: 1px solid #e0e0e0; border-radius: 6px; padding: 15px; overflow-x: auto; line-height: 1.7; font-size: 14px;&quot;&gt;&lt;code&gt;데이터
    &amp;darr;
Model
    &amp;darr;
View
    &amp;darr;
사용자에게 화면으로 표시&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 이번 단계는 Model/View 구조에서 데이터와 화면 표시를 분리하는 가장 기본적인 흐름을 이해하는 단계입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>3. PySide6/3-4. Model - View</category>
      <author>BasicLike</author>
      <guid isPermaLink="true">https://basiclike.tistory.com/611</guid>
      <comments>https://basiclike.tistory.com/611#entry611comment</comments>
      <pubDate>Fri, 29 May 2026 16:30:46 +0900</pubDate>
    </item>
    <item>
      <title>6강. 로그인 정보를 파일로 저장하고 다시 불러오기</title>
      <link>https://basiclike.tistory.com/782</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1627&quot; data-origin-height=&quot;555&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/3loC9/dJMcaciWblp/FXHJfVONDNVv4yCYCkI3b1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/3loC9/dJMcaciWblp/FXHJfVONDNVv4yCYCkI3b1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/3loC9/dJMcaciWblp/FXHJfVONDNVv4yCYCkI3b1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F3loC9%2FdJMcaciWblp%2FFXHJfVONDNVv4yCYCkI3b1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1627&quot; height=&quot;555&quot; data-origin-width=&quot;1627&quot; data-origin-height=&quot;555&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h1&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;0.&lt;/span&gt; 학습 목표&lt;/h1&gt;
&lt;p style=&quot;color: #9d9d9d;&quot; data-ke-size=&quot;size14&quot;&gt;&amp;rarr; 로그인 회원 정보를 JSON 파일로 저장하고, 프로그램 시작 시 다시 불러오는 구조를 학습합니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot; data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;0.1 이번 글에서 다룰 내용&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글에서는 로그인 회원 정보를 &lt;b&gt;JSON 파일&lt;/b&gt;로 저장하고 다시 불러오는 방법을 학습합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지난 &lt;a href=&quot;https://basiclike.tistory.com/783&quot;&gt;Model - View Section의 6강. Model/View 과제 구현&lt;/a&gt; 에서는 회원 정보를 코드 안에 직접 작성했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에는 회원 정보를 &lt;b&gt;member.json&lt;/b&gt; 파일에 저장합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로필 View&lt;sup&gt;뷰&lt;/sup&gt;에서 회원 정보를 수정하면 Model&lt;sup&gt;모델&lt;/sup&gt;의 값이 바뀌고, 그 값이 파일에도 저장되도록 구현합니다.&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1448&quot; data-origin-height=&quot;1086&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nkFJ2/dJMcagTf6C4/HA3dXWut56FdmamI2CpDX0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nkFJ2/dJMcagTf6C4/HA3dXWut56FdmamI2CpDX0/img.png&quot; data-alt=&quot;Model, Storage, member.json 파일의 역할 구조&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nkFJ2/dJMcagTf6C4/HA3dXWut56FdmamI2CpDX0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnkFJ2%2FdJMcagTf6C4%2FHA3dXWut56FdmamI2CpDX0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1448&quot; height=&quot;1086&quot; data-origin-width=&quot;1448&quot; data-origin-height=&quot;1086&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Model, Storage, member.json 파일의 역할 구조&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;table style=&quot;border-collapse: collapse; width: 100%; margin-top: 15px; margin-bottom: 20px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;background-color: #f5f8ff;&quot;&gt;
&lt;td style=&quot;width: 30%; padding: 10px;&quot;&gt;&lt;b&gt;구분&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 70%; padding: 10px;&quot;&gt;&lt;b&gt;내용&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;핵심 개념&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;회원 정보를 코드 안에만 두지 않고 JSON 파일로 저장하고 다시 불러옵니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;실습 준비&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;PySide6의 QFile, QTextStream과 Python의 json 모듈을 사용합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;최종 목표&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;프로그램을 껐다 켜도 수정된 회원 정보가 member.json 파일을 통해 유지되도록 만듭니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;div style=&quot;background-color: #f8fbff; border: 1px solid #d8e8ff; border-radius: 10px; padding: 18px; margin: 25px 0;&quot;&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;이번 단계의 핵심:&lt;/b&gt; Model은 회원 데이터를 관리하고, Storage는 회원 데이터를 파일에 저장하고 불러오는 역할을 담당합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h1&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;1.&lt;/span&gt; 프로그램을 껐다 켜면 회원 정보가 사라지는 이유&lt;/h1&gt;
&lt;p style=&quot;color: #9d9d9d;&quot; data-ke-size=&quot;size14&quot;&gt;&amp;rarr; 코드 안에만 저장된 회원 정보는 프로그램이 종료되면 수정 상태가 유지되지 않습니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot; data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1.1 기존 구현의 회원 정보 저장 방식&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지난 구현에서는 회원 정보를 코드 안에 직접 작성했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들면 아래처럼 딕셔너리 형태로 회원 정보를 만들었습니다.&lt;/p&gt;
&lt;pre class=&quot;makefile&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;# 기존 방식

member = {
    &quot;id&quot;: &quot;test&quot;,
    &quot;pw&quot;: &quot;1234&quot;,
    &quot;name&quot;: &quot;홍길동&quot;,
    &quot;email&quot;: &quot;test@test.com&quot;,
    &quot;phone&quot;: &quot;010-1234-5678&quot;,
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방식은 Model/View 구조를 처음 이해하기에는 좋습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 프로그램 실행 중 프로필 정보를 수정해도, 프로그램을 종료하면 수정한 내용이 사라집니다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; margin-top: 15px; margin-bottom: 20px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;background-color: #f5f8ff;&quot;&gt;
&lt;td style=&quot;width: 35%; padding: 10px;&quot;&gt;&lt;b&gt;상황&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 65%; padding: 10px;&quot;&gt;&lt;b&gt;결과&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;프로필 View에서 이름 수정&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;실행 중에는 Model 값이 바뀔 수 있습니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;프로그램 종료&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;메모리에 있던 수정 값이 사라집니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;프로그램 다시 실행&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;코드 안에 적힌 기본 회원 정보로 다시 시작합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1.2 파일 저장이 필요한 이유&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수정한 회원 정보를 유지하려면 프로그램 밖에 데이터를 저장해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 강의에서는 데이터베이스 대신 JSON 파일을 사용합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JSON은 딕셔너리처럼 key와 value 구조를 표현하기 좋아서 회원 정보 저장에 적합합니다.&lt;/p&gt;
&lt;pre class=&quot;perl&quot; data-ke-language=&quot;text&quot;&gt;&lt;code&gt;# member.json 예시

{
    &quot;id&quot;: &quot;test&quot;,
    &quot;pw&quot;: &quot;1234&quot;,
    &quot;name&quot;: &quot;홍길동&quot;,
    &quot;email&quot;: &quot;test@test.com&quot;,
    &quot;phone&quot;: &quot;010-1234-5678&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로그램이 시작되면 member.json 파일에서 회원 정보를 읽어옵니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로필을 수정하면 Model 값을 바꾸고, 변경된 값을 다시 member.json 파일에 저장합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1.3 이번 강의에서 바꿀 구조&lt;/b&gt;&lt;/h2&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1536&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/v3vz3/dJMcageD4xx/aSI9LOsK3btPC4wQ6TcC71/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/v3vz3/dJMcageD4xx/aSI9LOsK3btPC4wQ6TcC71/img.png&quot; data-alt=&quot;코드 내부 저장 방식에서 파일 저장 방식으로 바뀌는 구조&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/v3vz3/dJMcageD4xx/aSI9LOsK3btPC4wQ6TcC71/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fv3vz3%2FdJMcageD4xx%2FaSI9LOsK3btPC4wQ6TcC71%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1536&quot; height=&quot;1024&quot; data-origin-width=&quot;1536&quot; data-origin-height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;코드 내부 저장 방식에서 파일 저장 방식으로 바뀌는 구조&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존에는 Model이 회원 정보를 코드 안에서 바로 만들었습니다.&lt;/p&gt;
&lt;pre class=&quot;nginx&quot; data-ke-language=&quot;text&quot;&gt;&lt;code&gt;# 기존 구조

MemberModel
    └── 코드 안에 회원 정보 직접 저장&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;새 구조에서는 파일 저장과 불러오기를 담당하는 MemberStorage를 추가합니다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot; data-ke-language=&quot;text&quot;&gt;&lt;code&gt;# 새 구조

MemberStorage
    ├── member.json 읽기
    └── member.json 저장

MemberModel
    ├── 회원 정보 관리
    ├── 로그인 검사
    └── 프로필 수정

TabWindow
    ├── 로그인 View
    ├── 메인화면 View
    └── 프로필 View&lt;/code&gt;&lt;/pre&gt;
&lt;div style=&quot;background-color: #f8fbff; border: 1px solid #d8e8ff; border-radius: 10px; padding: 18px; margin: 25px 0;&quot;&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;문제의 핵심:&lt;/b&gt;&lt;span style=&quot;background-color: #f8fbff; caret-color: auto; letter-spacing: 0px;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;프로그램이 끝나도 데이터를 유지하려면 Model의 값을 파일에 저장하고, 다음 실행 때 다시 불러와야 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h1&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;2.&lt;/span&gt; 회원 정보를 JSON 파일로 저장하고 불러오기&lt;/h1&gt;
&lt;p style=&quot;color: #9d9d9d;&quot; data-ke-size=&quot;size14&quot;&gt;&amp;rarr; MemberStorage를 만들어 member.json 파일을 읽고 쓰는 기능을 구현합니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot; data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2.1 프로젝트 구조 만들기&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 강의에서는 지난 Model/View 로그인 프로그램 구조를 확장합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회원 정보를 저장하고 불러오는 파일을 따로 만들기 위해 &lt;b&gt;member_storage.py&lt;/b&gt;를 추가합니다.&lt;/p&gt;
&lt;pre class=&quot;vim&quot; data-ke-language=&quot;text&quot;&gt;&lt;code&gt;# 프로젝트 구조

tab_login_file_storage/
    ├── main.py
    ├── member_model.py
    ├── member_storage.py
    └── tab_window.py&lt;/code&gt;&lt;/pre&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; margin-top: 15px; margin-bottom: 20px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;background-color: #f5f8ff;&quot;&gt;
&lt;td style=&quot;width: 30%; padding: 10px;&quot;&gt;&lt;b&gt;파일&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 70%; padding: 10px;&quot;&gt;&lt;b&gt;역할&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;main.py&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;프로그램을 실행합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;member_model.py&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;회원 정보 관리, 로그인 검사, 프로필 수정을 담당합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;member_storage.py&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;member.json 파일 저장과 불러오기를 담당합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;tab_window.py&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;QTabWidget 화면과 버튼 동작을 담당합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2.2 member_storage.py 작성하기&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 회원 정보를 파일에 저장하고 불러오는 MemberStorage를 만듭니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서는 QFile과 QTextStream으로 파일을 읽고 쓰고, Python의 json 모듈로 딕셔너리를 JSON 문자열로 바꿉니다.&lt;/p&gt;
&lt;pre class=&quot;python&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;import json

from PySide6.QtCore import QFile, QIODevice, QTextStream


class MemberStorage:
    &quot;&quot;&quot;회원 정보를 JSON 파일에 저장하고 불러오는 클래스&quot;&quot;&quot;

    def __init__(self, file_path=&quot;member.json&quot;):
        self.file_path = file_path

    def default_member(self):
        return {
            &quot;id&quot;: &quot;test&quot;,
            &quot;pw&quot;: &quot;1234&quot;,
            &quot;name&quot;: &quot;홍길동&quot;,
            &quot;email&quot;: &quot;test@test.com&quot;,
            &quot;phone&quot;: &quot;010-1234-5678&quot;,
        }

    def load_member(self):
        if not QFile.exists(self.file_path):
            member = self.default_member()
            self.save_member(member)
            return member

        file = QFile(self.file_path)

        if not file.open(QIODevice.OpenModeFlag.ReadOnly | QIODevice.OpenModeFlag.Text):
            return self.default_member()

        stream = QTextStream(file)
        json_text = stream.readAll()
        file.close()

        try:
            return json.loads(json_text)
        except json.JSONDecodeError:
            return self.default_member()

    def save_member(self, member):
        file = QFile(self.file_path)

        if not file.open(QIODevice.OpenModeFlag.WriteOnly | QIODevice.OpenModeFlag.Text):
            return False

        stream = QTextStream(file)
        stream &amp;lt;&amp;lt; json.dumps(member, ensure_ascii=False, indent=4)
        file.close()

        return True&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 클래스는 화면을 전혀 모릅니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;QLineEdit, QPushButton 같은 GUI 코드는 들어가지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오직 파일을 읽고 쓰는 역할만 담당합니다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; margin-top: 15px; margin-bottom: 20px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;background-color: #f5f8ff;&quot;&gt;
&lt;td style=&quot;width: 30%; padding: 10px;&quot;&gt;&lt;b&gt;메서드&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 70%; padding: 10px;&quot;&gt;&lt;b&gt;역할&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;default_member()&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;member.json 파일이 없을 때 사용할 기본 회원 정보를 만듭니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;load_member()&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;member.json 파일에서 회원 정보를 읽습니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;save_member()&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;회원 정보를 member.json 파일에 저장합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2.3 member.json 파일 생성 결과&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로그램을 처음 실행했을 때 member.json 파일이 없으면 기본 회원 정보를 파일로 저장합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;생성되는 member.json 파일은 아래와 비슷합니다.&lt;/p&gt;
&lt;pre class=&quot;perl&quot; data-ke-language=&quot;text&quot;&gt;&lt;code&gt;# member.json

{
    &quot;id&quot;: &quot;test&quot;,
    &quot;pw&quot;: &quot;1234&quot;,
    &quot;name&quot;: &quot;홍길동&quot;,
    &quot;email&quot;: &quot;test@test.com&quot;,
    &quot;phone&quot;: &quot;010-1234-5678&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 회원 정보는 코드 안에만 있는 것이 아니라 실제 파일로 저장됩니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #f8fbff; border: 1px solid #d8e8ff; border-radius: 10px; padding: 18px; margin: 25px 0;&quot;&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;핵심 확인:&lt;/b&gt; MemberStorage는 회원 정보를 파일에 저장하고 불러오는 역할만 담당합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h1&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;3.&lt;/span&gt; 로그인 Model에 파일 저장 기능 연결하기&lt;/h1&gt;
&lt;p style=&quot;color: #9d9d9d;&quot; data-ke-size=&quot;size14&quot;&gt;&amp;rarr; MemberModel이 MemberStorage를 사용해 회원 정보를 불러오고 저장하도록 연결합니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot; data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;3.1 member_model.py 수정하기&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 MemberModel이 회원 정보를 직접 만들지 않도록 수정합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대신 MemberStorage에서 회원 정보를 불러옵니다.&lt;/p&gt;
&lt;pre class=&quot;python&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;from member_storage import MemberStorage


class MemberModel:
    &quot;&quot;&quot;회원 정보 관리, 로그인 검사, 프로필 수정을 담당하는 클래스&quot;&quot;&quot;

    def __init__(self):
        self.storage = MemberStorage()
        self.member = self.storage.load_member()
        self.is_logged_in = False

    def check_login(self, user_id, user_pw):
        if user_id == self.member[&quot;id&quot;] and user_pw == self.member[&quot;pw&quot;]:
            self.is_logged_in = True
            return True

        return False

    def get_member_info(self):
        if not self.is_logged_in:
            return None

        return {
            &quot;id&quot;: self.member[&quot;id&quot;],
            &quot;name&quot;: self.member[&quot;name&quot;],
            &quot;email&quot;: self.member[&quot;email&quot;],
            &quot;phone&quot;: self.member[&quot;phone&quot;],
        }

    def update_profile(self, name, email, phone):
        if not self.is_logged_in:
            return False

        self.member[&quot;name&quot;] = name
        self.member[&quot;email&quot;] = email
        self.member[&quot;phone&quot;] = phone

        return self.storage.save_member(self.member)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MemberModel은 이제 시작할 때 member.json 파일에서 회원 정보를 읽습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로필을 수정하면 self.member 값이 바뀌고, save_member()를 통해 파일에도 저장됩니다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; margin-top: 15px; margin-bottom: 20px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;background-color: #f5f8ff;&quot;&gt;
&lt;td style=&quot;width: 40%; padding: 10px;&quot;&gt;&lt;b&gt;코드&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 60%; padding: 10px;&quot;&gt;&lt;b&gt;역할&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;self.storage = MemberStorage()&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;파일 저장/불러오기 도구를 준비합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;self.storage.load_member()&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;member.json 파일에서 회원 정보를 읽습니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;check_login()&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;입력한 ID/PW와 Model의 회원 정보를 비교합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;update_profile()&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;Model 값을 수정하고 파일에도 저장합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;3.2 update_profile()의 저장 흐름&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로필 수정에서 가장 중요한 코드는 update_profile()입니다.&lt;/p&gt;
&lt;pre class=&quot;ruby&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;self.member[&quot;name&quot;] = name
self.member[&quot;email&quot;] = email
self.member[&quot;phone&quot;] = phone

return self.storage.save_member(self.member)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞의 세 줄은 Model 안의 회원 정보를 수정합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막 줄은 수정된 회원 정보를 member.json 파일에 저장합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 프로그램을 종료하고 다시 실행해도 수정된 값이 유지됩니다.&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1448&quot; data-origin-height=&quot;672&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bCwT6w/dJMcahdy0Wa/BeHyappOKUyycmgG4fMXdK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bCwT6w/dJMcahdy0Wa/BeHyappOKUyycmgG4fMXdK/img.png&quot; data-alt=&quot;프로필 수정 후 member.json 파일에 저장되는 흐름&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bCwT6w/dJMcahdy0Wa/BeHyappOKUyycmgG4fMXdK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbCwT6w%2FdJMcahdy0Wa%2FBeHyappOKUyycmgG4fMXdK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1448&quot; height=&quot;672&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1448&quot; data-origin-height=&quot;672&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;프로필 수정 후 member.json 파일에 저장되는 흐름&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;div style=&quot;background-color: #f8fbff; border: 1px solid #d8e8ff; border-radius: 10px; padding: 18px; margin: 25px 0;&quot;&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;주의할 점:&lt;/b&gt; 프로필 View에서 입력값만 바꾸고 파일에 저장하지 않으면, 프로그램을 다시 실행했을 때 수정 내용이 유지되지 않습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h1&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;4.&lt;/span&gt; 화면 코드에서 Model을 함께 사용하기&lt;/h1&gt;
&lt;p style=&quot;color: #9d9d9d;&quot; data-ke-size=&quot;size14&quot;&gt;&amp;rarr; 로그인 View, 메인화면 View, 프로필 View가 하나의 MemberModel을 공유하도록 구성합니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot; data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;4.1 tab_window.py 전체 코드&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 화면을 구성하는 tab_window.py를 작성합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본 구조는 지난 Model/View 구현과 거의 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;달라진 점은 MemberModel 안에서 파일 저장 기능까지 처리된다는 것입니다.&lt;/p&gt;
&lt;pre class=&quot;python&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;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(&quot;아이디를 입력하세요&quot;)

        self.pw_input = QLineEdit()
        self.pw_input.setPlaceholderText(&quot;비밀번호를 입력하세요&quot;)
        self.pw_input.setEchoMode(QLineEdit.EchoMode.Password)

        self.message_label = QLabel(&quot;로그인 정보를 입력하세요.&quot;)

        self.login_button = QPushButton(&quot;로그인&quot;)
        self.login_button.clicked.connect(self.login)

        layout = QVBoxLayout()
        layout.addWidget(QLabel(&quot;[로그인]&quot;))
        layout.addWidget(QLabel(&quot;아이디&quot;))
        layout.addWidget(self.id_input)
        layout.addWidget(QLabel(&quot;비밀번호&quot;))
        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()
        user_pw = self.pw_input.text()

        if self.member_model.check_login(user_id, user_pw):
            self.message_label.setText(&quot;로그인 성공!&quot;)

            self.main_view.refresh()
            self.profile_view.refresh()

            self.tab_widget.setCurrentIndex(1)
        else:
            self.message_label.setText(&quot;로그인 실패! 아이디 또는 비밀번호를 확인하세요.&quot;)


class MainView(QWidget):
    def __init__(self, member_model):
        super().__init__()

        self.member_model = member_model

        self.title_label = QLabel(&quot;[메인화면] 로그인한 회원 정보&quot;)
        self.id_label = QLabel(&quot;아이디:&quot;)
        self.name_label = QLabel(&quot;이름:&quot;)
        self.email_label = QLabel(&quot;이메일:&quot;)
        self.phone_label = QLabel(&quot;전화번호:&quot;)

        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&quot;아이디: {member['id']}&quot;)
        self.name_label.setText(f&quot;이름: {member['name']}&quot;)
        self.email_label.setText(f&quot;이메일: {member['email']}&quot;)
        self.phone_label.setText(f&quot;전화번호: {member['phone']}&quot;)


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(&quot;아이디:&quot;)

        self.name_input = QLineEdit()
        self.email_input = QLineEdit()
        self.phone_input = QLineEdit()

        self.update_button = QPushButton(&quot;수정하기&quot;)
        self.update_button.clicked.connect(self.update_profile)

        self.message_label = QLabel(&quot;프로필 정보를 확인하거나 수정하세요.&quot;)

        layout = QVBoxLayout()
        layout.addWidget(QLabel(&quot;[프로필]&quot;))
        layout.addWidget(self.id_label)

        name_layout = QHBoxLayout()
        name_layout.addWidget(QLabel(&quot;이름&quot;))
        name_layout.addWidget(self.name_input)

        email_layout = QHBoxLayout()
        email_layout.addWidget(QLabel(&quot;이메일&quot;))
        email_layout.addWidget(self.email_input)

        phone_layout = QHBoxLayout()
        phone_layout.addWidget(QLabel(&quot;전화번호&quot;))
        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&quot;아이디: {member['id']}&quot;)
        self.name_input.setText(member[&quot;name&quot;])
        self.email_input.setText(member[&quot;email&quot;])
        self.phone_input.setText(member[&quot;phone&quot;])

    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(&quot;프로필 정보가 수정되고 파일에 저장되었습니다.&quot;)
        else:
            self.message_label.setText(&quot;프로필 정보를 수정할 수 없습니다.&quot;)


class TabWindow(QWidget):
    def __init__(self, member_model):
        super().__init__()

        self.setWindowTitle(&quot;로그인 정보 파일 저장&quot;)
        self.resize(420, 320)

        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.tab_widget.addTab(self.login_view, &quot;로그인&quot;)
        self.tab_widget.addTab(self.main_view, &quot;메인화면&quot;)
        self.tab_widget.addTab(self.profile_view, &quot;프로필&quot;)

        layout = QVBoxLayout()
        layout.addWidget(self.tab_widget)

        self.setLayout(layout)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;4.2 하나의 MemberModel을 공유하는 부분&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TabWindow는 MemberModel을 하나만 받습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 MainView, ProfileView, LoginView에 같은 Model을 전달합니다.&lt;/p&gt;
&lt;pre class=&quot;armasm&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;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,
)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 구조 덕분에 세 View가 같은 회원 정보를 공유합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 프로필 수정 시 MemberModel이 파일 저장까지 처리합니다.&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1672&quot; data-origin-height=&quot;941&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/uCFIe/dJMcac4mhre/92YvGtTOUWYFWGhhlb1iR0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/uCFIe/dJMcac4mhre/92YvGtTOUWYFWGhhlb1iR0/img.png&quot; data-alt=&quot;TabWindow가 하나의 MemberModel을 여러 View에 전달하는 구조&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/uCFIe/dJMcac4mhre/92YvGtTOUWYFWGhhlb1iR0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FuCFIe%2FdJMcac4mhre%2F92YvGtTOUWYFWGhhlb1iR0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1672&quot; height=&quot;941&quot; data-origin-width=&quot;1672&quot; data-origin-height=&quot;941&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;TabWindow가 하나의 MemberModel을 여러 View에 전달하는 구조&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;div style=&quot;background-color: #f8fbff; border: 1px solid #d8e8ff; border-radius: 10px; padding: 18px; margin: 25px 0;&quot;&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;중요:&lt;/b&gt; View마다 MemberModel을 새로 만들면 파일 저장 구조가 꼬일 수 있습니다. 반드시 하나의 MemberModel을 공유해야 합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h1&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;5.&lt;/span&gt; main.py에서 프로그램 실행하기&lt;/h1&gt;
&lt;p style=&quot;color: #9d9d9d;&quot; data-ke-size=&quot;size14&quot;&gt;&amp;rarr; QApplication을 만들고 MemberModel과 TabWindow를 연결해 프로그램을 실행합니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot; data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;5.1 main.py 작성하기&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막으로 main.py를 작성합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;main.py에서는 QApplication을 만들고, MemberModel과 TabWindow를 생성합니다.&lt;/p&gt;
&lt;pre class=&quot;python&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;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())&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;5.2 실행 방법&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;터미널에서 프로젝트 폴더로 이동한 뒤 아래 명령어를 실행합니다.&lt;/p&gt;
&lt;pre class=&quot;vim&quot; data-ke-language=&quot;text&quot;&gt;&lt;code&gt;python main.py&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로그램이 실행되면 로그인, 메인화면, 프로필 탭이 있는 창이 열립니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음 실행할 때 member.json 파일이 없으면 자동으로 생성됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;5.3 실행 전 파일 위치 확인&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파일 이름과 위치가 아래처럼 되어 있는지 확인합니다.&lt;/p&gt;
&lt;pre class=&quot;vim&quot; data-ke-language=&quot;text&quot;&gt;&lt;code&gt;# 실행 전 확인

tab_login_file_storage/
    ├── main.py
    ├── member_model.py
    ├── member_storage.py
    └── tab_window.py&lt;/code&gt;&lt;/pre&gt;
&lt;div style=&quot;background-color: #f8fbff; border: 1px solid #d8e8ff; border-radius: 10px; padding: 18px; margin: 25px 0;&quot;&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;주의할 점:&lt;/b&gt; 파일 이름이 다르면 import 부분에서 오류가 날 수 있습니다. 파일 이름을 main.py, member_model.py, member_storage.py, tab_window.py로 맞춰 주세요.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h1&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;6.&lt;/span&gt; 로그인 정보 저장 흐름 확인하기&lt;/h1&gt;
&lt;p style=&quot;color: #9d9d9d;&quot; data-ke-size=&quot;size14&quot;&gt;&amp;rarr; 로그인, 프로필 수정, 파일 저장, 다시 실행 후 불러오기 흐름을 확인합니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot; data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;6.1 처음 실행할 때의 흐름&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로그램을 처음 실행하면 MemberModel이 MemberStorage를 만듭니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MemberStorage는 member.json 파일이 있는지 확인합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파일이 없으면 기본 회원 정보를 만들고 member.json 파일로 저장합니다.&lt;/p&gt;
&lt;pre class=&quot;mipsasm&quot; data-ke-language=&quot;text&quot;&gt;&lt;code&gt;# 처음 실행 흐름

프로그램 실행
    &amp;darr;
MemberModel 생성
    &amp;darr;
MemberStorage 생성
    &amp;darr;
member.json 파일 존재 여부 확인
    &amp;darr;
파일이 없으면 기본 회원 정보 생성
    &amp;darr;
member.json 파일로 저장
    &amp;darr;
회원 정보를 Model에 저장&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;6.2 프로필 수정 후 저장 흐름&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로필 View에서 이름, 이메일, 전화번호를 수정합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수정하기 버튼을 누르면 MemberModel의 update_profile()이 호출됩니다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot; data-ke-language=&quot;text&quot;&gt;&lt;code&gt;# 프로필 수정 후 저장 흐름

프로필 View에서 이름, 이메일, 전화번호 입력
    &amp;darr;
수정하기 버튼 클릭
    &amp;darr;
MemberModel.update_profile() 호출
    &amp;darr;
Model의 회원 정보 변경
    &amp;darr;
MemberStorage.save_member() 호출
    &amp;darr;
member.json 파일에 저장
    &amp;darr;
메인화면 View와 프로필 View 갱신&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;6.3 다시 실행할 때의 흐름&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로그램을 종료한 뒤 다시 실행하면 member.json 파일이 이미 존재합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때는 기본값을 새로 만들지 않고, 파일에 저장된 회원 정보를 다시 읽어옵니다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot; data-ke-language=&quot;text&quot;&gt;&lt;code&gt;# 다시 실행할 때의 흐름

프로그램 다시 실행
    &amp;darr;
MemberModel 생성
    &amp;darr;
MemberStorage.load_member() 호출
    &amp;darr;
member.json 파일 읽기
    &amp;darr;
이전에 수정한 회원 정보 불러오기
    &amp;darr;
로그인 후 화면에 표시&lt;/code&gt;&lt;/pre&gt;
&lt;div style=&quot;background-color: #f8fbff; border: 1px solid #d8e8ff; border-radius: 10px; padding: 18px; margin: 25px 0;&quot;&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;핵심 확인:&lt;/b&gt; 프로필을 수정한 뒤 프로그램을 껐다 켜도 수정된 값이 유지되면 파일 저장과 불러오기가 제대로 동작한 것입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h1&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;7.&lt;/span&gt; 실행 결과 확인하기&lt;/h1&gt;
&lt;p style=&quot;color: #9d9d9d;&quot; data-ke-size=&quot;size14&quot;&gt;&amp;rarr; member.json 파일 생성, 프로필 수정, 다시 실행 후 유지 여부를 확인합니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot; data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;7.1 로그인 정보 입력&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로그램을 실행한 뒤 아래 로그인 정보를 입력합니다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; margin-top: 15px; margin-bottom: 20px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;background-color: #f5f8ff;&quot;&gt;
&lt;td style=&quot;width: 30%; padding: 10px;&quot;&gt;&lt;b&gt;항목&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 70%; padding: 10px;&quot;&gt;&lt;b&gt;입력값&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;아이디&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;test&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;비밀번호&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;1234&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그인에 성공하면 메인화면 탭으로 이동합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;7.2 프로필 수정 입력 예시&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로필 탭에서 아래처럼 정보를 수정해 봅니다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot; data-ke-language=&quot;text&quot;&gt;&lt;code&gt;# 수정 입력 예시

이름: 김철수
이메일: kim@test.com
전화번호: 010-9999-8888&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수정하기 버튼을 누르면 프로필 정보가 수정되고 파일에 저장됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트 폴더의 member.json 파일을 열어 보면 아래처럼 값이 바뀐 것을 확인할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;perl&quot; data-ke-language=&quot;text&quot;&gt;&lt;code&gt;# 수정 후 member.json 예시

{
    &quot;id&quot;: &quot;test&quot;,
    &quot;pw&quot;: &quot;1234&quot;,
    &quot;name&quot;: &quot;김철수&quot;,
    &quot;email&quot;: &quot;kim@test.com&quot;,
    &quot;phone&quot;: &quot;010-9999-8888&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;7.3 프로그램을 다시 실행해서 확인하기&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 프로그램을 종료한 뒤 다시 실행합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다시 로그인하면 코드 안의 기본값이 아니라 member.json에 저장된 값이 화면에 표시되어야 합니다.&lt;/p&gt;
&lt;pre class=&quot;makefile&quot; data-ke-language=&quot;text&quot;&gt;&lt;code&gt;# 다시 실행 후 메인화면 결과

[메인화면] 로그인한 회원 정보

아이디: test
이름: 김철수
이메일: kim@test.com
전화번호: 010-9999-8888&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 결과가 보이면 파일 저장과 불러오기가 정상적으로 동작한 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;7.4 확인 체크리스트&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 항목을 직접 확인합니다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; margin-top: 15px; margin-bottom: 20px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;background-color: #f5f8ff;&quot;&gt;
&lt;td style=&quot;width: 80%; padding: 10px;&quot;&gt;&lt;b&gt;확인 항목&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 20%; padding: 10px;&quot;&gt;&lt;b&gt;완료&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;프로그램 첫 실행 시 member.json 파일이 생성되는가?&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;□&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;로그인 성공 시 메인화면으로 이동하는가?&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;□&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;프로필 View에서 이름, 이메일, 전화번호를 수정할 수 있는가?&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;□&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;수정하기 버튼을 누르면 member.json 파일 내용이 바뀌는가?&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;□&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;프로그램을 다시 실행해도 수정된 정보가 유지되는가?&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;□&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;View가 파일을 직접 다루지 않고 Model과 Storage를 통해 처리하는가?&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;□&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h1&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;8.&lt;/span&gt; 코드 내부 저장 방식과 파일 저장 방식 비교하기&lt;/h1&gt;
&lt;p style=&quot;color: #9d9d9d;&quot; data-ke-size=&quot;size14&quot;&gt;&amp;rarr; 회원 정보를 코드 안에 두는 방식과 JSON 파일에 저장하는 방식을 비교하고 정리합니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot; data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;8.1 기존 방식&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 방식은 회원 정보를 코드 안에 직접 작성하는 구조입니다.&lt;/p&gt;
&lt;pre class=&quot;nginx&quot; data-ke-language=&quot;text&quot;&gt;&lt;code&gt;# 기존 구조

MemberModel
    └── 코드 안의 딕셔너리로 회원 정보 관리&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방식은 간단하지만 프로그램을 종료하면 수정한 내용이 유지되지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;8.2 새 방식&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;새 방식은 회원 정보를 member.json 파일에 저장하고 다시 불러오는 구조입니다.&lt;/p&gt;
&lt;pre class=&quot;gauss&quot; data-ke-language=&quot;text&quot;&gt;&lt;code&gt;# 새 구조

MemberStorage
    └── member.json 읽기/쓰기

MemberModel
    └── MemberStorage를 사용해 회원 정보 관리

View
    └── MemberModel을 통해 회원 정보 사용&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방식에서는 프로그램이 종료되어도 수정한 내용이 파일에 남아 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다시 실행하면 member.json 파일에서 이전 값을 불러올 수 있습니다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; margin-top: 15px; margin-bottom: 20px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;background-color: #f5f8ff;&quot;&gt;
&lt;td style=&quot;width: 20%; padding: 10px;&quot;&gt;&lt;b&gt;구분&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 40%; padding: 10px;&quot;&gt;&lt;b&gt;기존 방식&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 40%; padding: 10px;&quot;&gt;&lt;b&gt;새 방식&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;저장 위치&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;코드 안의 딕셔너리&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;member.json 파일&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;프로그램 종료 후 유지&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;수정 내용이 유지되지 않습니다.&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;파일에 저장된 값은 다시 불러올 수 있습니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;수정 반영&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;실행 중 Model 값만 바뀝니다.&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;Model 값과 파일 값이 함께 바뀝니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;확장 방향&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;간단한 예제에 적합합니다.&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;여러 회원, CSV, 데이터베이스 저장으로 확장하기 좋습니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;8.3 최종 구조 정리&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 강의에서 만든 최종 구조는 다음과 같습니다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot; data-ke-language=&quot;text&quot;&gt;&lt;code&gt;# 최종 구조

main.py
    &amp;darr;
TabWindow 생성
    &amp;darr;
MemberModel 생성
    &amp;darr;
MemberModel이 MemberStorage 사용
    &amp;darr;
MemberStorage가 member.json 읽기/쓰기
    &amp;darr;
View는 MemberModel을 통해 회원 정보 사용&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 구조에서 View는 파일 저장 방법을 직접 알 필요가 없습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;View는 Model에게 회원 정보 수정을 요청하고, Model이 Storage를 통해 파일 저장을 처리합니다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; margin-top: 15px; margin-bottom: 20px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;background-color: #f5f8ff;&quot;&gt;
&lt;td style=&quot;width: 30%; padding: 10px;&quot;&gt;&lt;b&gt;구성 요소&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 70%; padding: 10px;&quot;&gt;&lt;b&gt;역할&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;TabWindow&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;QTabWidget 화면과 View 연결을 담당합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;MemberModel&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;회원 정보 관리, 로그인 검사, 프로필 수정을 담당합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;MemberStorage&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;member.json 파일 저장과 불러오기를 담당합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;member.json&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;프로그램 밖에 저장되는 회원 정보 파일입니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;8.4 최종 정리&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글에서는 로그인 Model의 회원 정보를 JSON 파일에 저장하고 다시 불러오는 구조를 만들었습니다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; margin-top: 15px; margin-bottom: 20px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;background-color: #f5f8ff;&quot;&gt;
&lt;td style=&quot;width: 30%; padding: 10px;&quot;&gt;&lt;b&gt;핵심 내용&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 70%; padding: 10px;&quot;&gt;&lt;b&gt;정리&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;문제 파악&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;코드 안에만 저장된 회원 정보는 프로그램을 다시 실행하면 수정 상태가 유지되지 않습니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;문제 해결&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;MemberStorage를 만들어 member.json 파일에 회원 정보를 저장하고 다시 불러왔습니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;코드 분석&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;MemberModel이 MemberStorage를 사용해 파일 읽기/쓰기 기능을 연결하는 흐름을 살펴봤습니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;비교 정리&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;코드 내부 저장 방식은 단순하고, 파일 저장 방식은 수정 내용을 유지할 수 있습니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;div style=&quot;background-color: #f8fbff; border: 1px solid #d8e8ff; border-radius: 10px; padding: 18px; margin: 25px 0;&quot;&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;기억할 문장:&lt;/b&gt; Model 데이터가 프로그램 종료 후에도 유지되려면 파일이나 데이터베이스 같은 외부 저장소에 저장해야 합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;8.5 프로젝트 파일&lt;/b&gt;&lt;/h2&gt;
&lt;figure class=&quot;fileblock&quot; data-ke-align=&quot;alignCenter&quot;&gt;&lt;a href=&quot;https://blog.kakaocdn.net/dn/cBCaHL/dJMcajvFaHb/30efAeQUrOzchkWZvvI7o0/656.zip?attach=1&amp;amp;knm=tfile.zip&quot; class=&quot;&quot;&gt;
    &lt;div class=&quot;image&quot;&gt;&lt;/div&gt;
    &lt;div class=&quot;desc&quot;&gt;&lt;div class=&quot;filename&quot;&gt;&lt;span class=&quot;name&quot;&gt;656.zip&lt;/span&gt;&lt;/div&gt;
&lt;div class=&quot;size&quot;&gt;0.01MB&lt;/div&gt;
&lt;/div&gt;
  &lt;/a&gt;&lt;/figure&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h1&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;참고.&lt;/span&gt; 공식 문서로 확인하기&lt;/h1&gt;
&lt;p style=&quot;color: #9d9d9d;&quot; data-ke-size=&quot;size14&quot;&gt;&amp;rarr; QFile, QTextStream, JSON 저장 방식의 자세한 내용은 공식 문서에서 확인합니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot; data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;h2 style=&quot;color: #222; border-left: 5px solid #006dd7; padding-left: 15px; margin-top: 30px; margin-bottom: 15px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;참고 문서&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글에서 사용한 QFile, QTextStream, json 모듈은 아래 문서에서 자세히 확인할 수 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://doc.qt.io/qtforpython-6/PySide6/QtCore/QFile.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;PySide6 QFile 공식 문서&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://doc.qt.io/qtforpython-6/PySide6/QtCore/QTextStream.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;PySide6 QTextStream 공식 문서&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://doc.qt.io/qtforpython-6/PySide6/QtWidgets/QTabWidget.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;PySide6 QTabWidget 공식 문서&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.python.org/ko/3/library/json.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Python json 공식 문서&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div style=&quot;background-color: #f8fbff; border: 1px solid #d8e8ff; border-radius: 10px; padding: 18px; margin: 25px 0;&quot;&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;참고:&lt;/b&gt; 이번 강의에서는 회원 1명의 정보를 JSON 파일로 저장했습니다. 다음 단계에서는 비밀번호와 같은 민감한 정보를 암호화하고, 여러 회원 목록, 회원가입, CSV 저장, 데이터베이스 저장으로 확장할 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;</description>
      <category>3. PySide6/3-5. File</category>
      <author>BasicLike</author>
      <guid isPermaLink="true">https://basiclike.tistory.com/782</guid>
      <comments>https://basiclike.tistory.com/782#entry782comment</comments>
      <pubDate>Fri, 29 May 2026 16:22:46 +0900</pubDate>
    </item>
  </channel>
</rss>