7.3 Model/View 구조

1. 학습 목표
1. 지난 포스트(6.11 QScrollArea)에서 문제 확인
6.11 QScrollArea
다양한 스크롤 UI 위젯(QScrollArea, QListWidget, QTextEdit)을 사용해 보고,이를 하나의 창에서 QTabWidget 으로 탭 구조로 통합해 보기 1. QScrollArea더보기# tab_scroll_area.pyfrom PySide6.QtWidgets import ( QWidget, QVBoxLayo
basiclike.tistory.com
1.1 QScrollArea 기반 탭
라벨을 직접 content_layout.addWidget(label) 형태로 추가
→ 데이터가 GUI 위젯 안에 저장됨
→ 다른 View와 데이터 공유 불가
1.2 QListWidget 기반 탭
self.list_widget.addItem 데이터 ("항목")
→ 아이템 데이터는 QListWidget 내부에 별도의 ListModel로 저장됨
→ 다른 View와 데이터 공유 불가
1.3 QTextEdit 기반 탭
self.log_textedit.append("로그")
→ 텍스트는 QTextEdit 내부 문서(Document)에 바로 저장됨
→ 역시 다른 View와 데이터 공유 불가능
1.4 공통된 근본적 문제
다양한 스크롤 UI(View)는 결국 “표시 방식만 다를 뿐, 데이터는 동일할 수 있다”
QScrollArea, QListWidget, QTextEdit은 각각 자체적으로 데이터를 내부에 저장한다
→ 즉 “뷰(View)에 데이터가 저장되는 구조”
2. Model/View 구조 구현

로그 문자열을 저장하는 LogModel 1개
이를 서로 다른 3개의 View(QScrollArea / QListWidget / QTextEdit)가 같이 공유하는 구조
2. 프로젝트 구조 만들기
ModelViewLogDemo/
├─ main.py # 프로그램 진입점
├─ model_log.py # ★ 공통 데이터 모델 (LogModel)
├─ view_scroll.py # QScrollArea 기반 뷰
├─ view_list.py # QListWidget 기반 뷰
├─ view_textedit.py # QTextEdit 기반 뷰
└─ widget.py # 탭(QTabWidget)으로 3개 뷰를 묶은 메인 화면
3. log_model.py
: 공통 데이터 모델 (LogModel)
# log_model.py
from PySide6.QtCore import QObject, Signal
class LogModel(QObject):
"""
로그 문자열을 저장하는 간단한 모델 클래스.
- 내부에 로그 리스트(_logs)를 가지고 있음
- 로그가 변경될 때마다 logsChanged 시그널을 emit해서
View들이 자동으로 화면을 갱신할 수 있게 해준다.
"""
logsChanged = Signal() # 데이터 변경 알림 시그널
def __init__(self, parent=None):
super().__init__(parent)
self._logs: list[str] = []
def add_log(self, text: str):
"""로그 한 줄 추가 후, 모든 View에 변경 사실 알림"""
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) # 복사본 반환 (외부에서 직접 수정 못 하도록)
- 로그 문자열들을 리스트로 관리
- 로그가 추가되면 logsChanged 시그널 발행 → 모든 View에게 “다시 그려라” 알림
4. view_scroll.py
: QScrollArea 기반 뷰
# 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):
"""모델의 로그 리스트를 QListWidget에 반영"""
self.list_widget.clear()
for text in self.model.logs():
self.list_widget.addItem(text)
# 맨 아래 항목으로 스크롤
self.list_widget.scrollToBottom()
- LogModel의 로그 리스트를 QLabel 여러 개로 생성하여 스크롤 영역에 표시
- “로그 추가” 버튼을 누르면 모델에 로그를 추가
- 모델이 바뀌면(logsChanged) 전체 라벨을 다시 그림
5. view_list.py
: QListWidget 기반 뷰
# view_scroll.py
from PySide6.QtWidgets import (
QWidget, QVBoxLayout, QScrollArea,
QLabel, QPushButton
)
from PySide6.QtCore import Qt
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)
# [1] 스크롤 영역
self.scroll_area = QScrollArea()
self.scroll_area.setWidgetResizable(True)
layout.addWidget(self.scroll_area)
# [2] 로그 추가 버튼
self.add_button = QPushButton("스크롤 뷰에 로그 추가")
self.add_button.clicked.connect(self.add_log_clicked)
layout.addWidget(self.add_button)
# [3] 스크롤 안 실제 내용 위젯
self.content_widget = QWidget()
self.content_layout = QVBoxLayout(self.content_widget)
self.content_layout.setAlignment(Qt.AlignTop)
self.scroll_area.setWidget(self.content_widget)
# [4] 모델 변경 시, 화면 갱신
self.model.logsChanged.connect(self.refresh_view)
# 초기 표시
self.refresh_view()
def add_log_clicked(self):
"""버튼 클릭 시 모델에 로그 추가 (Model만 수정, View는 자동 반영)"""
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)
w = item.widget()
if w is not None:
w.deleteLater()
# 모델에서 로그 읽어와 새 라벨 생성
for text in self.model.logs():
label = QLabel(text)
self.content_layout.addWidget(label)
# 스크롤을 맨 아래로 내리기
bar = self.scroll_area.verticalScrollBar()
bar.setValue(bar.maximum())
- LogModel의 로그들을 QListWidget 아이템으로 보여줌
- 버튼 클릭 시 모델에 로그 추가
- logsChanged 시그널을 받아 리스트를 다시 채움
6. view_textedit.py
: QTextEdit 기반 뷰
# view_textedit.py
from PySide6.QtWidgets import (
QWidget, QVBoxLayout,
QPushButton, QTextEdit
)
from PySide6.QtGui import QTextCursor
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):
"""모델 로그를 QTextEdit 전체 내용으로 반영"""
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()
- LogModel의 로그들을 한 줄씩 QTextEdit에 보여주는 로그창
- “로그 추가” 버튼으로 모델에 로그 추가
- 모델 변경 시 텍스트 영역 전체를 다시 그림 (append 기반 또는 전체 재구성 가능)
7. widget.py
: 탭(QTabWidget)으로 3개 뷰를 묶은 메인 화면
# 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(스크롤 / 리스트 / 텍스트)를 탭으로 묶은 메인 화면.
- 세 개의 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()
# 같은 model을 세 개의 View에 전달
tab_widget.addTab(ScrollView(model), "QScrollArea 뷰")
tab_widget.addTab(ListView(model), "QListWidget 뷰")
tab_widget.addTab(TextEditView(model), "QTextEdit 뷰")
layout.addWidget(tab_widget)
LogModel 1개를 받아서, 세 개를 QTabWidget으로 탭 구조로 묶어 보여줌
- ScrollView
- ListView
- TextEditView
8. main.py
: 실행 진입점 작성
# main.py
import sys
from PySide6.QtWidgets import QApplication
from model_log import LogModel
from widget import ViewWidget
def main():
"""
프로그램의 진입점(Entry Point).
- QApplication 생성
- LogModel(공통 데이터 모델) 생성
- ViewWidget(여러 View를 가진 화면)에 모델을 주입
- 이벤트 루프 실행
"""
# [1] QApplication 생성
app = QApplication(sys.argv)
app.setApplicationName("Model / View 데모")
# [2] 공통 모델 생성 (★ 모든 View가 이 모델을 공유)
model = LogModel()
# 초기 테스트 로그 몇 개 추가
model.add_log("[Init] 프로그램 시작")
model.add_log("[Init] Model / View 데모 준비 완료")
# [3] 메인 위젯(여러 View를 가진 화면) 생성
window = ViewWidget(model)
window.show()
# [4] 이벤트 루프 실행 (GUI 프로그램 유지)
sys.exit(app.exec())
if __name__ == "__main__":
main()
9. 추가 실습 과제
: (1) QSplitter 를 사용하여 3개의 뷰를 동시에 보이도록 구현하기
: 구현 목표와 비교

구현 목표
: 6.11 QScrollArea 결과와 비교


QTabWidget → QSplitter 변경으로
- 탭을 전환할 필요 없이
- 모든 View가 동시에 보이므로 Model–View 구조 이해가 더 쉬워짐
화면(View)이 3개여도 Model은 1개
- 모든 View가 같은 데이터(로그)를 공유
- 한 곳에서 데이터 변경하면 모든 화면이 자동 업데이트
# widget.py 힌트 코드
from PySide6.QtCore import Qt
from PySide6.QtWidgets import QSplitter # 상단 import에 추가
# __init__ 안에서
# --- Splitter 설정 ---
layout = QVBoxLayout(self)
# [1] 수평 Splitter 생성
splitter = QSplitter(Qt.Horizontal)
# [2] 같은 Model을 공유하는 3개의 View 추가
splitter.addWidget(ScrollView(model)) # 왼쪽
splitter.addWidget(ListView(model)) # 가운데
splitter.addWidget(TextEditView(model)) # 오른쪽
# [3] 초기 splitter 비율 설정 (선택)
splitter.setSizes([300, 300, 300])
layout.addWidget(splitter)