21강. PySide6 기본 창과 위젯 구성

0. 학습 목표
→ 이번 글에서 무엇을 이해하고, 무엇을 만들고, 무엇을 확인할지 먼저 정리합니다.
0.1 이번 글에서 다룰 내용
이번 글은 UI 중심 강의입니다.
19강에서 화면은 ChatWindow가 담당하기로 정했고,
20강에서는 터미널의 input()이 입력창으로, print()가 채팅 표시 영역으로 바뀐다는 대응 관계를 정리했습니다.
이번 강의에서는 그 위젯들을 실제로 화면에 배치합니다.
아직 서버에 접속하거나 메시지를 보내지 않습니다. 이번 강의의 목표는 단 하나입니다.
채팅 화면 뼈대가 화면에 뜨는 것
화면에 무언가가 떠야 이후 강의에서 버튼 이벤트를 연결하고 서버 접속을 붙이는 작업이 가능해집니다. 아래 코드에서 ➕는 이번 단계에서 새로 추가된 줄, ✏️는 형태가 바뀐 줄을 뜻합니다. 최종 정리 코드에는 마커 없이 깨끗한 코드만 싣습니다.

| 구분 | 내용 |
| 이해할 것 | PySide6 앱의 기본 실행 구조(QApplication → QWidget → app.exec())와 레이아웃 배치 방식 |
| 만들 것 | chat_client/main.py — 채팅 창 뼈대 (서버 연결 없음) |
| 확인할 것 | GUI 창이 열리고 채팅 표시 영역·입력창·버튼·상태 라벨이 화면에 표시되는지 |
0.2 이번 강의에서 직접 다루는 구조
이번 강의에서는 chat_client/ 폴더에 main.py를 새로 만듭니다.
chat_server/
├── protocol.py ← 유지
└── server.py ← 유지
chat_client/
├── protocol.py ← 유지
├── client.py ← 유지
└── main.py ← ➕ 이번 강의에서 생성
(➕ 새로 생성 · 표시 없음은 변경 없음)
1. UI 구조 확인하기
→ 이번 강의에서 만들 화면이 어떤 위젯과 레이아웃으로 구성되는지 먼저 파악합니다.

이번에 만들 채팅 창은 네 가지 위젯과 두 가지 레이아웃으로 구성됩니다.
| 위젯 | 클래스 | 역할 |
| 채팅 표시 영역 | QTextEdit |
받은 메시지가 쌓임. 읽기 전용 |
| 메시지 입력창 | QLineEdit |
사용자가 보낼 메시지를 입력 |
| 보내기 버튼 | QPushButton |
클릭 시 메시지 전송 (이벤트 연결은 24강) |
| 상태 라벨 | QLabel |
현재 연결 상태 표시 |
| 세로 레이아웃 | QVBoxLayout |
위젯을 위→아래로 배치 |
| 가로 레이아웃 | QHBoxLayout |
입력창과 버튼을 한 줄에 배치 |
레이아웃은 두 층으로 구성됩니다. 바깥은 QVBoxLayout(세로)으로 채팅 표시 영역 / 입력 줄 / 상태 라벨을 쌓고, 입력 줄 안쪽은 QHBoxLayout(가로)으로 입력창과 버튼을 나란히 배치합니다.
✔ 확인 기준: "채팅 표시 영역은 QTextEdit, 입력창은 QLineEdit, 버튼은 QPushButton"이라고 말할 수 있으면 완료. 레이아웃이 두 층(QVBoxLayout 안에 QHBoxLayout)으로 구성된다는 점도 함께 확인하세요.
2. 화면 요소 배치하기
→ PySide6를 설치하고, 빈 창부터 시작해 클래스 구조로 전환합니다.
2.1 PySide6 설치 확인 및 파일 만들기
지금까지 쓰던 socket, threading, json과 달리 PySide6는 기본 라이브러리가 아니므로 설치가 필요합니다.
pip install PySide6
설치 후 다음 명령어로 확인합니다.
python -c "import PySide6; print('PySide6 설치 확인')"
PySide6 설치 확인
가상환경을 여러 개 쓰는 경우 pip install을 실행한 터미널과 python을 실행하는 터미널이 같은 환경을 가리키는지 먼저 확인하세요. which python(macOS/Linux) 또는 where python(Windows)으로 현재 어느 Python을 쓰는지 확인할 수 있습니다.
설치가 확인되면 chat_client/ 폴더 안에 main.py를 만듭니다.
touch chat_client/main.py
파일 이름은 반드시 main.py로 맞춥니다. 이번 강의의 실행 명령어가 python chat_client/main.py이기 때문입니다.
✔ 확인 기준: import 오류 없이 "PySide6 설치 확인"이 출력되고, chat_client/main.py 파일이 생성되면 완료입니다.
2.2 빈 창 띄우기 — 1차 코드
chat_client/main.py에 다음 코드를 작성합니다. PySide6 창이 뜨는지 먼저 확인합니다.
import sys
from PySide6.QtWidgets import QApplication, QWidget
app = QApplication(sys.argv) # GUI 프로그램 실행 관리자
window = QWidget()
window.setWindowTitle("채팅")
window.resize(500, 400)
window.show()
sys.exit(app.exec()) # 이벤트 루프 시작 — 창이 닫힐 때까지 대기
python chat_client/main.py
빈 창이 열리면 성공입니다. app.exec()에서 프로그램이 대기 상태로 들어가고, 창을 닫으면 종료됩니다. 터미널 채팅에서 while True로 프로그램이 계속 실행되게 했던 것처럼, GUI에서는 app.exec()가 그 역할을 합니다. sys.exit(app.exec())가 없으면 창이 뜨는 순간 바로 꺼집니다.
✔ 확인 기준: 제목이 "채팅"인 빈 창이 열리고 닫을 때까지 유지되면 완료. 창이 바로 닫히면 sys.exit(app.exec()) 누락 여부를 먼저 확인하세요.
2.3 클래스 구조로 전환하기 — 2차 코드
앞으로 버튼 이벤트, 서버 접속, 수신 스레드가 추가됩니다. 19강에서 정한 대로 화면은 ChatWindow가 담당하므로, 처음부터 클래스로 구성해야 이후 기능을 깔끔하게 추가할 수 있습니다.
import sys
from PySide6.QtWidgets import QApplication, QWidget
class ChatWindow(QWidget): # ➕ 화면을 담당할 ChatWindow 클래스 (19강에서 정한 역할)
def __init__(self):
super().__init__() # ➕ QWidget 초기화를 먼저 실행 — 반드시 필요
self.setWindowTitle("채팅")
self.resize(500, 400)
app = QApplication(sys.argv)
window = ChatWindow() # ✏️ QWidget() → ChatWindow()로 교체
window.show()
sys.exit(app.exec())
결과는 2.2와 동일하게 빈 창이 뜹니다. 달라진 것은 창 설정이 ChatWindow.__init__ 안으로 들어온 것입니다. 앞으로 위젯과 이벤트 연결 코드가 모두 이 클래스 안에 추가됩니다. super().__init__()를 빠뜨리면 창이 아예 뜨지 않거나 TypeError가 납니다. QWidget을 상속받을 때는 반드시 부모 클래스의 초기화를 먼저 실행해야 합니다.
✔ 확인 기준: 빈 창이 열리면 완료. 오류가 나면 super().__init__() 누락 여부를 먼저 확인하세요.
3. 위젯 생성하고 레이아웃에 배치하기
→ 위젯을 생성하고 레이아웃으로 배치합니다. 이번 강의에서 이벤트 연결(버튼 클릭 → 함수)은 하지 않습니다.
ChatWindow.__init__ 안에 위젯 생성과 레이아웃 배치 코드를 추가합니다. 2차 코드에 이어서 작성합니다.
import sys
from PySide6.QtWidgets import (
QApplication,
QWidget,
QTextEdit,
QLineEdit,
QPushButton,
QLabel,
QVBoxLayout,
QHBoxLayout,
)
class ChatWindow(QWidget):
def __init__(self):
super().__init__()
self.setWindowTitle("채팅")
self.resize(500, 400)
# 위젯 생성
self.chat_display = QTextEdit() # ➕ 채팅 표시 영역 (20강 print() 자리)
self.chat_display.setReadOnly(True) # 사용자가 직접 입력하지 못하게
self.message_input = QLineEdit() # ➕ 입력창 (20강 input() 자리)
self.message_input.setPlaceholderText("메시지를 입력하세요")
self.send_button = QPushButton("보내기") # ➕ 보내기 버튼 (이벤트 연결은 24강)
self.status_label = QLabel("상태: 서버에 연결되지 않음") # ➕ 상태 표시 라벨
# 레이아웃 배치
input_layout = QHBoxLayout() # ➕ 입력창과 버튼을 가로로
input_layout.addWidget(self.message_input)
input_layout.addWidget(self.send_button)
main_layout = QVBoxLayout() # ➕ 전체를 세로로
main_layout.addWidget(self.chat_display)
main_layout.addLayout(input_layout)
main_layout.addWidget(self.status_label)
self.setLayout(main_layout) # ➕ 창에 레이아웃 적용 (빠뜨리면 위젯 미표시)
app = QApplication(sys.argv)
window = ChatWindow()
window.show()
sys.exit(app.exec())
레이아웃 구조를 정리하면 다음과 같습니다.

main_layout (QVBoxLayout — 세로)
├── chat_display (QTextEdit — 채팅 표시 영역)
├── input_layout (QHBoxLayout — 가로)
│ ├── message_input (QLineEdit — 입력창)
│ └── send_button (QPushButton — 보내기 버튼)
└── status_label (QLabel — 상태 표시)
self.setLayout(main_layout)을 빠뜨리면 레이아웃을 만들어도 창에 적용되지 않습니다. 위젯이 화면에 나타나지 않는 가장 흔한 원인입니다.
✔ 확인 기준: python chat_client/main.py 실행 후 채팅 표시 영역, 입력창과 보내기 버튼(같은 줄), 상태 라벨이 화면에 표시되면 완료. 오류가 나면 self.setLayout(main_layout) 호출 여부와 addWidget() / addLayout() 순서를 확인하세요.
4. 화면에서 결과 확인하기
→ 실행해서 위젯이 모두 제자리에 표시되는지 확인하고, 자주 만나는 오류를 점검합니다.
4.1 실행 결과 확인하기

python chat_client/main.py
화면에 채팅 표시 영역, 입력창과 보내기 버튼(같은 줄), 상태 라벨이 모두 보여야 합니다. 이번 강의에서는 보내기 버튼을 눌러도 아무 동작을 하지 않습니다. 버튼 클릭 이벤트 연결은 24강에서 진행합니다. 이번 강의의 성공 기준은 위젯이 화면에 표시되고 입력창에 글자를 입력할 수 있는 것입니다.
✔ 확인 기준: 채팅 표시 영역, 입력창, 보내기 버튼, 상태 라벨이 모두 보이고, 입력창에 글자를 입력할 수 있으며, 창이 멈추지 않으면 완료입니다.
4.2 실행이 안 될 때 확인할 것
| 오류 상황 | 원인 및 해결 방법 |
ModuleNotFoundError: No module named 'PySide6' |
PySide6가 설치되지 않았습니다. pip install PySide6를 실행합니다. 가상환경이 여러 개인 경우 실행 중인 Python 환경과 설치 환경이 같은지 확인합니다 |
| 창이 바로 닫힌다 | sys.exit(app.exec())가 없거나 이벤트 루프에 진입하지 않았습니다. 마지막 줄을 확인합니다 |
TypeError 또는 창이 뜨지 않는다 |
super().__init__()가 없습니다. ChatWindow.__init__ 첫 줄에 추가합니다 |
| 위젯이 화면에 보이지 않는다 | self.setLayout(main_layout) 누락입니다. addWidget()으로 위젯을 추가한 뒤 반드시 setLayout()을 호출합니다 |
| 입력창과 버튼이 세로로 배치된다 | input_layout에 QHBoxLayout 대신 QVBoxLayout을 사용했습니다. 입력창과 버튼은 QHBoxLayout에 넣습니다 |
| 채팅 표시 영역에 직접 입력할 수 있다 | setReadOnly(True) 누락입니다. self.chat_display.setReadOnly(True)를 추가합니다 |
| 한글이 깨져 보인다 | 파일을 UTF-8로 저장합니다. 에디터에서 인코딩 설정을 확인합니다 |
5. 최종 코드 정리하기
→ 이번 강의에서 완성한 chat_client/main.py 전체 코드를 한곳에 정리합니다.
5.1 chat_client/main.py
import sys
from PySide6.QtWidgets import (
QApplication,
QWidget,
QTextEdit,
QLineEdit,
QPushButton,
QLabel,
QVBoxLayout,
QHBoxLayout,
)
class ChatWindow(QWidget):
def __init__(self):
super().__init__()
self.setWindowTitle("채팅")
self.resize(500, 400)
self.chat_display = QTextEdit()
self.chat_display.setReadOnly(True)
self.message_input = QLineEdit()
self.message_input.setPlaceholderText("메시지를 입력하세요")
self.send_button = QPushButton("보내기")
self.status_label = QLabel("상태: 서버에 연결되지 않음")
input_layout = QHBoxLayout()
input_layout.addWidget(self.message_input)
input_layout.addWidget(self.send_button)
main_layout = QVBoxLayout()
main_layout.addWidget(self.chat_display)
main_layout.addLayout(input_layout)
main_layout.addWidget(self.status_label)
self.setLayout(main_layout)
app = QApplication(sys.argv)
window = ChatWindow()
window.show()
sys.exit(app.exec())
5.2 최종 확인 표
| 확인할 코드 | 의미 |
QApplication(sys.argv) |
GUI 프로그램 실행 관리자 생성 |
class ChatWindow(QWidget) |
QWidget을 상속받은 채팅 창 클래스 |
super().__init__() |
부모 클래스 QWidget 초기화 (필수) |
self.chat_display = QTextEdit() |
채팅 메시지가 쌓이는 영역 |
setReadOnly(True) |
채팅 표시 영역에 직접 입력 불가 설정 |
self.message_input = QLineEdit() |
메시지 입력 한 줄 입력창 |
self.send_button = QPushButton("보내기") |
전송 버튼 (이벤트 연결은 24강에서) |
self.status_label = QLabel(...) |
연결 상태 표시 라벨 |
QHBoxLayout() |
입력창과 버튼을 가로로 배치 |
QVBoxLayout() |
전체 위젯을 세로로 배치 |
self.setLayout(main_layout) |
창에 레이아웃 적용 (빠뜨리면 위젯 미표시) |
window.show() |
창을 화면에 표시 |
sys.exit(app.exec()) |
이벤트 루프 시작 — 창이 닫힐 때까지 대기 |
→ 다음 강의 (22강): 이번에 만든 기본 창의 레이아웃을 채팅 화면답게 다듬습니다. 채팅 표시 영역이 입력창보다 훨씬 크게 보이도록 비율을 조정하고, 창 최소 크기와 여백을 정리합니다. 23강(서버 접속), 24강(메시지 보내기), 25강(실시간 수신)이 모두 이 main.py 위에 기능을 하나씩 추가하는 방식으로 이어집니다.