0. 학습 목표

→ 직접 만든 LogModel 하나를 여러 View가 함께 사용하는 구조를 학습합니다.

더보기

이번 단계에서는 직접 만든 LogModel로그 모델 하나를 여러 View가 함께 공유하는 구조를 학습합니다.

지난 단계에서는 QFileSystemModel처럼 Qt가 제공하는 Model과 Signal을 사용했습니다.

 

이번에는 로그 문자열을 저장하는 Model 과 Signal 을 직접 만들고, 이 Model을 세 개의 View에서 함께 사용합니다.

학습 목표 내용
이전 예제의 문제 확인 QScrollArea, QListWidget, QTextEdit 내부에
데이터가 직접 저장되는 구조의 한계를 이해합니다.
LogModel 역할 이해 로그 문자열을 한 곳에서 저장하고 관리하는
공통 데이터 Model을 이해합니다.
logsChanged 시그널 이해 Model 데이터가 변경되면
여러 View에 변경 사실을 알리는 구조를 이해합니다.
3개의 View 공유 구조 이해 QScrollArea, QListWidget, QTextEdit이
하나의 LogModel을 공유하는 흐름을 이해합니다.
Model/View 구조 체감 어느 View에서 로그를 추가해도
모든 View가 같은 데이터를 보여주는 구조를 체감합니다.

이번 단계의 핵심: 로그 데이터는 LogModel 하나에만 저장되고, QScrollArea, QListWidget, QTextEdit은 같은 LogModel의 데이터를 각자의 방식으로 보여줍니다.

 

1. 문제 파악: 이전 스크롤 UI 예제 다시 보기

→ View 안에 데이터가 직접 들어가면 어떤 문제가 생기는지 확인합니다.

더보기

1.1 지난 예제에서 사용한 세 가지 View

이전 스크롤 UI 예제에서 스크롤 가능한 UI를 만들기 위해 세 가지 방식을 사용했습니다.

QScrollArea, QListWidget, QTextEdit은 모두 데이터를 화면에 보여줄 수 있는 View 역할을 합니다.

참고: 여기서 말하는 View는 Qt의 엄격한 Model/View 클래스만 의미하지 않습니다.

이번 글에서는 데이터를 화면에 보여주는 UI 역할이라는 넓은 의미로 View라고 부릅니다.

View 사용 방식
QScrollArea 기반 탭 QLabel을 만들어 content_layout.addWidget(label)로 직접 추가했습니다.
QListWidget 기반 탭 self.list_widget.addItem("항목")으로 항목을 직접 추가했습니다.
QTextEdit 기반 탭 self.log_textedit.append("로그")로 텍스트를 직접 추가했습니다.

처음에는 이 방식이 간단하고 편해 보입니다.

하지만 View 안에 데이터가 직접 들어간다는 문제가 있습니다.

 

1.2 QScrollArea 기반 탭의 문제

QScrollArea 기반 탭에서는 QLabel을 직접 만들어 레이아웃에 추가했습니다.

label = QLabel("로그 1")
content_layout.addWidget(label)

이 방식에서는 로그 문자열이 QLabel 안에 들어갑니다.

즉, 데이터가 별도의 Model에 있는 것이 아니라 GUI 위젯 안에 직접 저장됩니다.

QScrollArea
    ↓
content_layout
    ↓
QLabel("로그 1")
QLabel("로그 2")
QLabel("로그 3")

데이터가 QLabel 안에 들어간 상태

 

1.3 QListWidget 기반 탭의 문제

QListWidget 기반 탭에서는 addItem()으로 데이터를 추가했습니다.

self.list_widget.addItem("로그 1")
self.list_widget.addItem("로그 2")
self.list_widget.addItem("로그 3")

이 방식에서는 로그 데이터가 QListWidget 내부 항목으로 저장됩니다.

다른 View에서 같은 로그 데이터를 사용하려면 별도로 데이터를 다시 넘겨야 합니다.

QListWidget
    ↓
내부 아이템
    ↓
"로그 1"
"로그 2"
"로그 3"

데이터가 QListWidget 안에 들어간 상태

 

1.4 QTextEdit 기반 탭의 문제

QTextEdit 기반 탭에서는 append()로 로그를 추가했습니다.

self.log_textedit.append("로그 1")
self.log_textedit.append("로그 2")
self.log_textedit.append("로그 3")

이 방식에서는 텍스트가 QTextEdit 내부 문서에 바로 저장됩니다.

역시 다른 View와 데이터를 공유하기 어렵습니다.

QTextEdit
    ↓
내부 Document
    ↓
로그 1
로그 2
로그 3

데이터가 QTextEdit 안에 들어간 상태

 

1.5 공통된 근본 문제

세 방식의 공통 문제는 데이터가 View 안에 저장된다는 점입니다.

즉, View가 데이터를 보여주는 역할뿐 아니라 데이터를 직접 들고 있는 구조입니다.

View 데이터 저장 위치 문제
QScrollArea QLabel 안 다른 View와 공유하기 어렵습니다.
QListWidget QListWidget 내부 아이템 다른 View와 공유하기 어렵습니다.
QTextEdit QTextEdit 내부 문서 다른 View와 공유하기 어렵습니다.

문제의 핵심: 다양한 스크롤 UI는 표시 방식만 다를 뿐 같은 로그 데이터를 보여줄 수도 있습니다. 그런데 데이터가 View 안에 들어가면 같은 데이터를 여러 View에서 공유하기 어렵습니다.

 

2. 해결 방법: LogModel 하나를 세 View가 공유

→ 로그 데이터를 Model 하나에 저장하고, 여러 View가 같은 데이터를 보도록 만듭니다.

더보기

2.1 이번 예제의 목표 구조

이번 예제에서는 로그 문자열을 저장하는 LogModel을 하나만 만듭니다.

그리고 QScrollArea, QListWidget, QTextEdit 세 View가 같은 LogModel을 함께 사용하도록 만듭니다.

로그 데이터
    ↓
LogModel
    ├── ScrollView     : QScrollArea + QLabel
    ├── ListView       : QListWidget
    └── TextEditView   : QTextEdit

데이터는 LogModel 안에만 있습니다.

세 View는 Model의 데이터를 읽어서 각자의 방식으로 화면에 보여줍니다.

참고: 이번 예제의 LogModel은 QAbstractItemModel을 상속한 정식 Qt Item Model은 아닙니다. 먼저 데이터를 View 밖으로 분리하고, 여러 View가 같은 데이터를 공유하는 흐름을 이해하기 위한 학습용 Model입니다.

 

2.2 데이터 변경 알림 구조

View가 자동으로 갱신되려면 Model 데이터가 바뀌었다는 사실을 View들이 알아야 합니다.

이때 사용하는 것이 Signal시그널입니다.

View에서 버튼 클릭
    ↓
model.add_log()
    ↓
LogModel 내부 로그 리스트 변경
    ↓
logsChanged 시그널 발생
    ↓
모든 View의 refresh_view() 실행
    ↓
모든 화면이 같은 로그로 다시 갱신
구성 역할
LogModel 로그 문자열 리스트를 저장하고 관리합니다.
logsChanged 로그 데이터가 바뀌었음을 View들에게 알려주는 시그널입니다.
refresh_view() 각 View가 Model의 로그를 다시 읽어 화면을 갱신하는 함수입니다.
add_log() Model에 로그를 추가하고 logsChanged 시그널을 발생시킵니다.

해결의 핵심: View가 직접 데이터를 저장하지 않습니다. View는 Model에 로그 추가를 요청하고, Model이 바뀌면 다시 화면을 그립니다.

 

3. Project 구조

→ Model과 세 View를 파일별로 나누어 프로젝트를 구성합니다.

더보기

3.1 프로젝트 폴더 구성

먼저 다음과 같이 프로젝트 폴더를 만듭니다.

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으로 묶는 메인 화면
main.py
0.00MB
model_log.py
0.00MB
view_scroll.py
0.00MB
view_list.py
0.00MB
view_textedit.py
0.00MB
widget.py
0.00MB

 

3.2 실행 흐름

전체 실행 흐름은 다음과 같습니다.

main.py 실행
    ↓
LogModel 생성
    ↓
ViewWidget 생성
    ↓
ScrollView / ListView / TextEditView에 같은 LogModel 전달
    ↓
각 View가 logsChanged 시그널에 refresh_view() 연결
    ↓
로그 추가 시 모든 View가 자동 갱신

참고: 이번 단계에서는 먼저 데이터를 Model로 분리하고 여러 View가 공유한다는 구조를 이해하는 것이 중요합니다.

 

4. model

→ 로그 데이터를 저장하고 변경 사실을 알려주는 LogModel을 작성합니다.

더보기

4.1 model_log.py 전체 코드

먼저 로그 데이터를 저장하는 LogModel을 작성합니다.

# model_log.py

from PySide6.QtCore import QObject, Signal


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

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

    logsChanged = Signal()

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

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

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

    def logs(self) -> list[str]:
        """현재 로그 리스트의 복사본을 반환합니다."""
        return list(self._logs)

이 파일은 이번 예제에서 가장 중요한 Model 역할을 합니다.

로그 데이터는 View 안에 저장하지 않고, LogModel의 _logs 리스트에만 저장합니다.

 

4.2 코드 분석

1단계: QObject와 Signal 가져오기

from PySide6.QtCore import QObject, Signal

QObject는 Qt 객체의 기본 클래스입니다.

Signal은 객체 간에 변경 사실을 알려주는 신호를 만들 때 사용합니다.

2단계: logsChanged 시그널 만들기

logsChanged = Signal()

logsChanged는 로그 데이터가 바뀌었음을 View에게 알려주는 신호입니다.

View들은 이 시그널에 refresh_view()를 연결합니다.

3단계: 로그 리스트 저장하기

self._logs: list[str] = []

_logs는 실제 로그 문자열이 저장되는 리스트입니다.

이번 예제에서 데이터는 이 리스트에만 저장됩니다.

4단계: 로그 추가 후 변경 알림 보내기

def add_log(self, text: str):
    self._logs.append(text)
    self.logsChanged.emit()

add_log()는 로그를 추가하는 메서드입니다.

로그를 추가한 뒤 logsChanged.emit()을 호출하여 모든 View에게 데이터가 바뀌었다고 알려줍니다.

5단계: 로그 복사본 반환하기

def logs(self) -> list[str]:
    return list(self._logs)

logs()는 현재 로그 리스트를 반환합니다.

여기서는 원본 리스트를 그대로 반환하지 않고 복사본을 반환합니다.

이렇게 하면 외부 View가 _logs를 직접 수정하지 못하게 막을 수 있습니다.

Model/View 개념 포인트: LogModel은 로그 데이터를 저장하고, View들은 LogModel의 logs()를 읽어서 화면을 그립니다. 데이터 변경 알림은 logsChanged 시그널로 전달됩니다.

 

5. View

→ 같은 LogModel을 세 가지 방식으로 보여주는 View를 작성합니다.

더보기

5.1 QScrollArea 기반 View: view_scroll.py

첫 번째 View는 QScrollArea와 QLabel을 사용합니다.

LogModel의 로그 리스트를 읽어서 QLabel 여러 개로 만들어 화면에 배치합니다.

# 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):
    """
    QScrollArea + QLabel 조합으로 로그를 보여주는 View입니다.
    같은 LogModel을 사용하면 다른 View와 항상 같은 로그를 공유합니다.
    """

    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("스크롤 뷰에 로그 추가")
        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"[ScrollView] 추가 로그 {count}")

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

ScrollView는 로그 데이터를 직접 저장하지 않습니다.

버튼을 클릭하면 self.model.add_log()를 호출하여 Model에 로그를 추가합니다.

ScrollView 버튼 클릭
    ↓
self.model.add_log()
    ↓
LogModel에 로그 추가
    ↓
logsChanged 발생
    ↓
refresh_view()로 QLabel 다시 생성

참고: refresh_view()에서는 기존 QLabel을 모두 지운 뒤, 현재 Model의 로그 개수만큼 QLabel을 새로 만듭니다. deleteLater()는 Qt에게 이 위젯을 안전한 시점에 삭제해 달라고 요청하는 함수입니다.

 

5.2 QListWidget 기반 View: view_list.py

두 번째 View는 QListWidget을 사용합니다.

LogModel의 로그 리스트를 읽어서 QListWidget 아이템으로 표시합니다.

# view_list.py

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

from model_log import LogModel


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

    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("리스트 뷰에 로그 추가")
        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"[ListView] 추가 로그 {count}")

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

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

        self.list_widget.scrollToBottom()

ListView는 QListWidget을 사용하지만, 데이터 원본은 QListWidget 안에 있지 않습니다.

항상 LogModel의 logs()를 다시 읽어 화면을 채웁니다.

LogModel.logs()
    ↓
QListWidget.clear()
    ↓
addItem()으로 현재 로그 다시 표시
    ↓
scrollToBottom()

즉, QListWidget은 화면 표시 역할을 맡습니다.

로그 원본 데이터는 여전히 LogModel에 있습니다.

 

5.3 QTextEdit 기반 View: view_textedit.py

세 번째 View는 QTextEdit을 사용합니다.

로그 출력창처럼 한 줄씩 로그를 보여주는 화면입니다.

# view_textedit.py

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

from model_log import LogModel


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

    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("텍스트 뷰에 로그 추가")
        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"[TextEditView] 추가 로그 {count}")

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

TextEditView도 로그를 직접 소유하지 않습니다.

LogModel의 로그를 읽어 QTextEdit에 다시 표시합니다.

QTextEdit에는 화면 표시용으로 다시 채워 넣을 뿐입니다.

원본 로그 데이터는 여전히 LogModel에 있습니다.

 

5.4 세 View의 공통 패턴

세 View는 서로 다른 위젯을 사용하지만 공통 패턴은 같습니다.

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

첫 번째 줄은 Model이 바뀌었을 때 View를 다시 그리도록 연결합니다.

두 번째 줄은 처음 화면이 만들어질 때 현재 Model 데이터를 한 번 표시합니다.

세 View의 공통 패턴: 각 View는 logsChanged 시그널에 refresh_view()를 연결합니다. 그래서 Model이 바뀌면 각 View가 자기 방식으로 화면을 다시 그립니다.

 

5.5 세 View 비교

세 View의 차이를 정리하면 다음과 같습니다.

View 표시 방식 Model 사용 방식
ScrollView QScrollArea 안에 QLabel을 여러 개 생성 logs()를 읽어 QLabel로 다시 만듭니다.
ListView QListWidget 아이템으로 표시 logs()를 읽어 addItem()으로 다시 채웁니다.
TextEditView QTextEdit에 한 줄씩 append logs()를 읽어 텍스트 영역을 다시 구성합니다.
같은 LogModel
    ↓
ScrollView    : QLabel 여러 개
ListView      : QListWidget 아이템
TextEditView  : QTextEdit 문장들

데이터는 하나
표현 방식은 세 개

 

6. Widget 

→ 세 View를 탭으로 묶고, main.py에서 LogModel을 전달합니다.

더보기

6.1 widget.py 작성하기: 세 View를 QTabWidget으로 묶기

이제 세 View를 하나의 탭 화면으로 묶습니다.

중요한 점은 세 View에 모두 같은 model을 전달한다는 점입니다.

# 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):
    """
    여러 View를 탭으로 묶은 메인 화면입니다.

    - ScrollView
    - ListView
    - TextEditView

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

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

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

        layout = QVBoxLayout(self)

        tab_widget = QTabWidget()

        tab_widget.addTab(ScrollView(model), "QScrollArea 뷰")
        tab_widget.addTab(ListView(model), "QListWidget 뷰")
        tab_widget.addTab(TextEditView(model), "QTextEdit 뷰")

        layout.addWidget(tab_widget)

QTabWidget은 여러 화면을 탭으로 나누어 보여주는 위젯입니다.

여기서는 ScrollView, ListView, TextEditView를 각각 탭으로 추가했습니다.

ViewWidget
    ↓
QTabWidget
    ├── ScrollView(model)
    ├── ListView(model)
    └── TextEditView(model)

세 View에 전달된 model은 모두 같은 객체입니다.

따라서 어느 탭에서 로그를 추가해도 세 View가 같은 데이터를 보게 됩니다.

 

6.2 main.py 작성하기: 프로그램 실행 시작점 만들기

마지막으로 main.py를 작성합니다.

main.py에서는 QApplication을 만들고, LogModel을 생성한 뒤 ViewWidget에 전달합니다.

# main.py

import sys
from PySide6.QtWidgets import QApplication

from model_log import LogModel
from widget import ViewWidget


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

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

    model = LogModel()

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

    window = ViewWidget(model)
    window.show()

    sys.exit(app.exec())


if __name__ == "__main__":
    main()
코드 역할
model = LogModel() 공통 데이터 Model을 하나만 생성합니다.
model.add_log(...) 초기 테스트 로그를 Model에 추가합니다.
ViewWidget(model) 세 View가 들어 있는 메인 화면에 Model을 전달합니다.
app.exec() Qt 이벤트 루프를 실행합니다.

 

6.3 실행 결과 구조

프로그램을 실행하면 세 개의 탭이 있는 창이 나타납니다.

어느 탭에서 로그 추가 버튼을 눌러도 LogModel에 로그가 추가됩니다.

그리고 logsChanged 시그널을 통해 모든 View가 다시 갱신됩니다.

이번 단계의 핵심: 버튼은 각 View에 있지만 로그 데이터는 View에 저장되지 않습니다. 모든 로그는 LogModel 하나에 저장됩니다.

 

7. Model/View 방식 비교

→ View 안에 데이터를 넣는 방식과 Model을 공유하는 방식을 비교합니다.

더보기

7.1 구조 비교

기존 방식과 이번 Model/View 방식을 비교해 보겠습니다.

가장 큰 차이는 로그 데이터가 어디에 저장되는가입니다.

구분 기존 방식 Model/View 방식
데이터 위치 각 View 내부 LogModel 내부
View 역할 데이터 저장과 화면 표시를 함께 담당 Model의 데이터를 화면에 표시
데이터 변경 각 View가 개별적으로 처리 LogModel이 처리하고 logsChanged로 알림
View 동기화 직접 구현해야 함 logsChanged에 연결된 refresh_view()로 갱신
확장성 View가 늘어나면 데이터 관리가 복잡해짐 새 View도 같은 Model을 연결하면 됨
기존 방식

QScrollArea  ← 데이터
QListWidget  ← 데이터
QTextEdit    ← 데이터

각 View가 데이터를 따로 들고 있음
Model/View 방식

ScrollView    ←─┐
ListView      ←── LogModel ←── 로그 데이터
TextEditView  ←─┘

하나의 Model을 여러 View가 함께 사용

 

7.2 의존성 관점에서 보기

기존 방식에서는 View와 데이터가 강하게 묶입니다.

예를 들어 QListWidget에 로그를 직접 추가하면, 로그 데이터가 QListWidget에 종속됩니다.

강한 의존 구조

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

화면과 데이터가 한 곳에 섞여 있음

반대로 이번 예제에서는 데이터와 화면 표시를 분리합니다.

분리된 구조

LogModel
 └── 로그 데이터 관리

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

역할이 나누어져 있음

중요한 점: Model/View 구조의 핵심은 View를 없애는 것이 아닙니다. View는 화면 표시를 담당하고, Model은 데이터 관리를 담당하도록 역할을 분리하는 것입니다.

 

7.3 같은 데이터, 다른 View, 자동 갱신

이번 구조에서는 어느 View에서 로그를 추가해도 모든 View가 같은 데이터를 봅니다.

ScrollView에서 로그 추가
    ↓
LogModel.add_log()
    ↓
logsChanged 발생
    ↓
ScrollView refresh_view()
ListView refresh_view()
TextEditView refresh_view()
    ↓
세 View 모두 같은 로그 표시

이것이 이번 예제에서 체감해야 할 핵심입니다.

데이터는 하나이고, View는 여러 개입니다.

기억할 문장: 한 곳에서 데이터를 바꾸면, 같은 Model을 보는 모든 View가 함께 갱신됩니다.

 

8. 추가 실습 과제: QSplitter로 세 View를 동시에 보여주기

→ 세 View를 한 화면에 동시에 보여주어 같은 Model 공유 효과를 확인합니다.

더보기

8.1 QSplitter

현재 예제는 QTabWidget을 사용하므로 한 번에 하나의 View만 볼 수 있습니다.

그래서 로그를 추가한 뒤 다른 탭으로 이동해야 각 View가 갱신되었는지 확인할 수 있습니다.

QSplitter를 사용하면 ScrollView, ListView, TextEditView를 한 화면에 동시에 배치할 수 있습니다.

 

3.11 QScrollArea 결과

: 구현 목표와 비교

 

 

구현 목표 

: 3.11 QScrollArea 결과와 비교

실습 목표: 어느 View에서 로그를 추가해도 세 View가 동시에 갱신되는 모습을 확인합니다.

 

8.2 QSplitter 적용 코드

widget.py를 다음과 같은 방식으로 바꿀 수 있습니다.

# 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("Model / View 데모 - QSplitter")
        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)

QTabWidget 대신 QSplitter를 사용하면 탭을 전환하지 않아도 세 View를 동시에 볼 수 있습니다.

각 View는 여전히 같은 model을 전달받습니다.

ViewWidget
    ↓
QSplitter
    ├── ScrollView(model)
    ├── ListView(model)
    └── TextEditView(model)

화면은 3개
Model은 1개
widget (1).py
0.00MB

 

8.3 실행 결과에서 확인할 점

QSplitter 버전으로 실행하면 세 View가 한 화면에 나란히 표시됩니다.

이제 하나의 View에서 로그 추가 버튼을 눌러 보세요.

그러면 나머지 두 View도 같은 로그를 함께 표시하는지 바로 확인할 수 있습니다.

추가 실습의 핵심: 화면은 3개여도 Model은 1개입니다. 어느 View에서 로그를 추가해도 모든 View가 같은 로그 데이터를 보여줍니다.

 

9. 정리

→ 직접 만든 Model을 여러 View가 공유하는 핵심 흐름을 정리합니다.

더보기

이번 단계에서는 LogModel 하나를 여러 View가 함께 공유하는 구조를 만들었습니다.

핵심은 View가 데이터를 직접 저장하지 않고, Model의 데이터를 읽어서 화면에 보여준다는 점입니다.

기존 방식
    ↓
View 안에 데이터 저장
    ↓
View마다 데이터가 따로 관리됨
    ↓
데이터 공유와 동기화가 어려움


Model/View 방식
    ↓
LogModel이 데이터 중앙 관리
    ↓
여러 View가 같은 Model을 바라봄
    ↓
logsChanged 시그널로 모든 View가 자동 갱신

이번 단계의 핵심 개념을 다시 정리하면 다음과 같습니다.

개념 정리
LogModel 로그 문자열을 저장하고 관리하는 공통 데이터 Model입니다.
logsChanged 로그 데이터 변경을 View들에게 알리는 시그널입니다.
ScrollView QScrollArea와 QLabel로 로그를 보여주는 View입니다.
ListView QListWidget으로 로그를 보여주는 View입니다.
TextEditView QTextEdit으로 로그를 보여주는 View입니다.
refresh_view() Model의 로그를 다시 읽어 각 View의 화면을 갱신하는 함수입니다.

이번 단계의 핵심: 데이터는 LogModel 하나에 저장하고, View는 그 데이터를 각자의 방식으로 보여줍니다. Model이 바뀌면 logsChanged 시그널을 통해 모든 View가 자동으로 다시 그려집니다.

기억할 문장: 데이터는 Model에 한 번만 저장하고, 여러 View는 같은 Model을 서로 다른 방식으로 보여줍니다.

 

 

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

→ 공식 문서에서 이번 예제와 연결되는 기준을 확인합니다.

더보기

1. 공식 문서 참고 표

이번 예제에서 사용한 QObject, Signal, QScrollArea, QListWidget, QTextEdit, QTabWidget, QSplitter는 Qt for Python 공식 문서에서 확인할 수 있습니다.

공식 문서는 실제 클래스의 역할과 메서드를 확인할 때 가장 정확한 기준이 됩니다.

구분 공식 문서 확인할 내용
Model/View Programming Qt for Python - Model/View Programming Qt의 Model/View 구조가 데이터와 표시 방식을 분리하는 구조임을 확인할 수 있습니다.
QObject PySide6.QtCore.QObject Qt 객체의 기본 클래스와 시그널/슬롯 기반 통신 구조를 확인할 수 있습니다.
Signal PySide6.QtCore.Signal 사용자 정의 시그널을 만드는 방법을 확인할 수 있습니다.
QListWidget PySide6.QtWidgets.QListWidget 항목 기반 리스트 위젯의 사용 방식을 확인할 수 있습니다.
QTextEdit PySide6.QtWidgets.QTextEdit 텍스트 표시와 편집을 위한 위젯 기능을 확인할 수 있습니다.
QSplitter PySide6.QtWidgets.QSplitter 여러 위젯을 나누어 동시에 표시하는 방법을 확인할 수 있습니다.

 

2. 이번 예제와 공식 문서 연결

이번 예제 코드 공식 문서와 연결되는 의미
class LogModel(QObject) Qt 객체로 동작하는 사용자 정의 Model을 만듭니다.
logsChanged = Signal() Model 데이터 변경 사실을 View에 알리는 사용자 정의 시그널입니다.
self.model.logsChanged.connect(...) Model 변경 신호와 View 갱신 함수를 연결합니다.
QTabWidget 세 View를 탭 구조로 나누어 보여줍니다.
QSplitter 추가 실습에서 세 View를 동시에 보여주는 구조로 확장할 수 있습니다.

 

3. 정리 흐름도

공식 문서 기준으로 정리하면 다음과 같습니다.

데이터
    ↓
LogModel(QObject)
    ↓
logsChanged Signal
    ↓
ScrollView / ListView / TextEditView
    ↓
각 View가 같은 데이터를 서로 다른 방식으로 표시

즉, 이번 예제는 직접 만든 Model을 여러 View가 공유하는 기본 구조를 보여줍니다.