22강. 채팅 화면 레이아웃 만들기

0. 학습 목표
→ 이번 글에서 무엇을 이해하고, 무엇을 만들고, 무엇을 확인할지 먼저 정리합니다.
0.1 이번 글에서 다룰 내용
이번 글은 UI 중심 강의입니다.
21강에서 PySide6 기본 창과 위젯을 만들었습니다.
창은 뜨고 위젯도 화면에 표시되지만, 아직 버튼을 눌러도 아무 반응이 없습니다.
이번 강의에서는 21강 chat_client/main.py를 수정해 두 가지를 합니다.
첫째, 상태 라벨을 위로 올리고 채팅 표시 영역이 화면 대부분을 차지하도록 레이아웃을 정리합니다.
둘째, 버튼 클릭과 Enter 입력을 같은 함수에 연결해 입력한 메시지가 채팅 표시 영역에 추가되는지 확인합니다.
아직 서버에 접속하거나 실제로 메시지를 전송하지 않습니다. 이번 강의의 목표는 화면이 제대로 반응하는지 확인하는 것입니다. 아래 코드에서 ➕는 21강 코드에 새로 더해진 줄, ✏️는 위치나 형태가 바뀐 줄을 뜻합니다. 최종 정리 코드에는 마커 없이 깨끗한 코드만 싣습니다.

| 구분 | 내용 |
| 이해할 것 | 채팅 화면을 상태·메시지·입력 세 영역으로 나누는 이유와 이벤트 연결 방식 |
| 만들 것 | chat_client/main.py 수정 — 레이아웃 재배치 + 임시 이벤트 연결 |
| 확인할 것 | 버튼 클릭·Enter 입력으로 메시지가 채팅 표시 영역에 추가되는지 |
0.2 이번 강의에서 직접 다루는 구조
이번 강의에서는 21강에서 만든 chat_client/main.py를 수정합니다.
chat_server/
├── protocol.py ← 유지
└── server.py ← 유지
chat_client/
├── protocol.py ← 유지
├── client.py ← 유지
└── main.py ← ✏️ 이번 강의에서 수정
(✏️ 수정 · 표시 없음은 변경 없음)
1. UI 구조 확인하기
→ 왜 3영역으로 나누는지, 상태 라벨이 위로 가는 이유를 확인합니다.

21강에서는 상태 라벨이 맨 아래에 있었습니다. 이번 강의에서는 맨 위로 올립니다.
사용자가 채팅 창을 열었을 때 "지금 서버에 연결되어 있는가"를 가장 먼저 확인할 수 있어야 하기 때문입니다.
채팅 내용을 보기 전에 연결 여부부터 파악하는 것이 자연스러운 순서입니다.
이 배치 기준이 채팅 화면의 3영역 구조입니다.
| 영역 | 위젯 | 역할 |
| 상단 — 상태 영역 | status_label |
연결 상태, 사용자 안내 문구 표시 |
| 중앙 — 메시지 영역 | chat_display |
채팅 내용, 시스템 메시지, 오류 메시지 표시 |
| 하단 — 입력 영역 | message_input + send_button |
메시지 입력과 보내기 동작 처리 |
이 3영역 구조는 23강(서버 접속)부터 26강(연결 종료 처리)까지 그대로 유지됩니다. 각 강의에서는 영역 안의 내용만 달라집니다.
✔ 확인 기준: "상태 라벨은 위, 채팅 표시 영역은 중앙, 입력창과 버튼은 아래"라고 말할 수 있으면 완료. main_layout에 위젯을 추가하는 순서가 화면 배치 순서와 일치한다는 점도 함께 기억하세요.
2. 화면 요소 배치하기
→ 21강 코드에서 status_label 위치를 바꾸고 chat_display에 안내 문구를 추가합니다.
21강 완성 코드에서 main_layout에 위젯을 추가하는 순서를 바꾸고 안내 문구를 더합니다. main_layout에 추가하는 순서가 화면 위→아래 배치 순서와 정확히 일치합니다. 첫 번째로 추가한 위젯이 화면 맨 위에, 마지막으로 추가한 레이아웃이 맨 아래에 표시됩니다.
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(520, 480) # ✏️ 채팅 화면답게 조금 더 크게 (500x400 → 520x480)
self.chat_display = QTextEdit()
self.chat_display.setReadOnly(True)
self.chat_display.setPlaceholderText("채팅 메시지가 여기에 표시됩니다.") # ➕ 빈 상태 안내 문구
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.status_label) # ✏️ 맨 위로 이동 (21강에서는 맨 아래)
main_layout.addWidget(self.chat_display)
main_layout.addLayout(input_layout)
self.setLayout(main_layout)
app = QApplication(sys.argv)
window = ChatWindow()
window.show()
sys.exit(app.exec())

python chat_client/main.py
상태 라벨이 맨 위, 채팅 표시 영역이 중앙, 입력창과 버튼이 아래에 배치되면 1차 수정 완료입니다. 창 크기를 조금 키운 이유는 QTextEdit이 세로 레이아웃에서 남는 공간을 채우며 늘어나기 때문입니다. 상태 라벨과 입력 줄은 작게 유지되고 채팅 표시 영역이 화면 대부분을 차지하게 됩니다.
✔ 확인 기준: 상태 라벨이 맨 위에 보이면 완료. main_layout.addWidget(self.status_label)이 가장 먼저 호출되어 있는지 확인하세요.
3. 이벤트 연결하기
→ 버튼 클릭과 Enter 입력을 on_send_clicked()에 연결해 임시 UI 테스트 동작을 구현합니다.
self.setLayout(main_layout) 바로 아래에 이벤트 연결 코드 2줄을 추가하고, on_send_clicked() 메서드를 ChatWindow 클래스 안에 새로 만듭니다.
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(520, 480)
self.chat_display = QTextEdit()
self.chat_display.setReadOnly(True)
self.chat_display.setPlaceholderText("채팅 메시지가 여기에 표시됩니다.")
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.status_label)
main_layout.addWidget(self.chat_display)
main_layout.addLayout(input_layout)
self.setLayout(main_layout)
self.send_button.clicked.connect(self.on_send_clicked) # ➕ 버튼 클릭 → on_send_clicked
self.message_input.returnPressed.connect(self.on_send_clicked) # ➕ Enter 입력 → on_send_clicked
def on_send_clicked(self): # ➕ 보내기 처리 함수
text = self.message_input.text().strip() # 앞뒤 공백 제거 후 입력값을 가져온다
if not text: # 빈 메시지는 보내지 않는다
return
self.chat_display.append(f"나: {text}") # 임시 — 23강에서 네트워크 전송으로 교체
self.message_input.clear() # 전송 후 입력창을 비운다
app = QApplication(sys.argv)
window = ChatWindow()
window.show()
sys.exit(app.exec())
이벤트 연결에서 가장 흔한 실수는 함수 이름 뒤에 괄호를 붙이는 것입니다.
self.send_button.clicked.connect(self.on_send_clicked) # 올바른 형태 — 함수 이름만 전달
# self.send_button.clicked.connect(self.on_send_clicked()) # 잘못된 형태 — ()를 붙이면 즉시 실행돼 연결 안 됨
괄호가 있으면 "지금 당장 함수를 실행해서 그 결과를 연결해라"가 됩니다. 결과값이 None이라 연결이 되지 않고, 클릭해도 아무 반응이 없습니다. 이벤트 연결 코드는 위젯 생성과 레이아웃 배치를 마친 뒤 마지막에 모아 쓰는 것이 좋습니다. 23강 이후 이벤트 연결이 늘어날 때 한눈에 파악하기 쉬워집니다.

| 이벤트 | 신호 | 연결 함수 |
| 보내기 버튼 클릭 | send_button.clicked |
on_send_clicked() |
| 입력창에서 Enter 입력 | message_input.returnPressed |
on_send_clicked() |
✔ 확인 기준: 입력창에 "안녕하세요" 입력 후 보내기 클릭 → 채팅 표시 영역에 "나: 안녕하세요" 추가, 입력창 비워짐. 버튼 클릭과 Enter 입력 모두 동작하면 완료. connect()에 괄호 없이 함수명만 전달했는지 확인하세요.
4. 화면에서 결과 확인하기
→ 전체 동작을 실행해서 확인하고, 자주 만나는 오류를 점검합니다.
4.1 실행 결과 확인하기

python chat_client/main.py
입력창에 안녕하세요를 입력하고 보내기 버튼을 누르면 채팅 표시 영역에 다음처럼 추가됩니다.
나: 안녕하세요
Enter를 눌러도 같은 결과가 나와야 합니다. 메시지가 추가된 뒤 입력창은 비워져 있어야 합니다. 이번 강의의 메시지 추가는 서버 전송이 아닌 UI 테스트용 임시 동작입니다. 다른 클라이언트에게 전달되지 않습니다.
✔ 확인 기준: 3영역 배치 확인, 메시지 입력 후 채팅 표시 영역에 추가, 입력창 자동 비워짐이 모두 확인되면 완료. self.message_input.clear()가 on_send_clicked() 안에 있는지 확인하세요.
4.2 실행이 안 될 때 확인할 것
| 오류 상황 | 원인 및 해결 방법 |
| 창이 바로 닫힌다 | sys.exit(app.exec())가 없습니다. 마지막 줄에 작성합니다 |
| 상태 라벨이 여전히 맨 아래에 있다 | main_layout.addWidget(self.status_label) 호출 순서를 확인합니다. 첫 번째로 addWidget()을 호출해야 맨 위에 표시됩니다 |
| 버튼을 눌러도 반응이 없다 | self.send_button.clicked.connect(self.on_send_clicked) 누락이거나, connect(self.on_send_clicked())처럼 괄호를 붙였습니다. 함수 이름만 전달합니다 |
| Enter를 눌러도 반응이 없다 | self.message_input.returnPressed.connect(self.on_send_clicked) 누락입니다. 이벤트 연결 2줄이 모두 있는지 확인합니다 |
| 입력창이 비워지지 않는다 | on_send_clicked() 안에 self.message_input.clear()가 없습니다 |
| 빈 메시지가 채팅창에 추가된다 | if not text: return 조건이 없거나, .strip() 없이 .text()만 쓰고 있습니다 |
| 위젯이 화면에 보이지 않는다 | self.setLayout(main_layout) 누락이거나, 위젯을 만들고 addWidget()을 호출하지 않았습니다 |
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(520, 480)
self.chat_display = QTextEdit()
self.chat_display.setReadOnly(True)
self.chat_display.setPlaceholderText("채팅 메시지가 여기에 표시됩니다.")
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.status_label)
main_layout.addWidget(self.chat_display)
main_layout.addLayout(input_layout)
self.setLayout(main_layout)
self.send_button.clicked.connect(self.on_send_clicked)
self.message_input.returnPressed.connect(self.on_send_clicked)
def on_send_clicked(self):
text = self.message_input.text().strip()
if not text:
return
self.chat_display.append(f"나: {text}")
self.message_input.clear()
app = QApplication(sys.argv)
window = ChatWindow()
window.show()
sys.exit(app.exec())
5.2 최종 확인 표
| 확인할 코드 | 의미 |
main_layout.addWidget(self.status_label) (첫 번째) |
상태 라벨을 맨 위에 배치 |
setPlaceholderText("채팅 메시지가 여기에 표시됩니다.") |
채팅 표시 영역 빈 상태 안내 문구 |
send_button.clicked.connect(self.on_send_clicked) |
버튼 클릭 이벤트 연결 |
message_input.returnPressed.connect(self.on_send_clicked) |
Enter 입력 이벤트 연결 |
text = self.message_input.text().strip() |
입력창 값을 앞뒤 공백 제거 후 가져옴 |
if not text: return |
빈 메시지 전송 방지 |
self.chat_display.append(f"나: {text}") |
채팅 표시 영역에 메시지 추가 (임시 — 23강에서 교체) |
self.message_input.clear() |
전송 후 입력창 비우기 |
chat_display.append()로 직접 화면에 추가하는 부분은 임시입니다. 23강에서 서버 접속 기능을 연결하면 이 줄이 네트워크 전송 코드로 교체됩니다. 나머지(빈 메시지 확인, clear())는 그대로 유지됩니다.
→ 다음 강의 (23강): 이번에 만든 화면에 서버 접속 기능을 연결합니다. 접속 버튼과 connect_to_server()를 추가하고, 서버 접속 결과에 따라 status_label 텍스트가 바뀌도록 만듭니다. 이번 강의에서 임시로 만든 on_send_clicked() 안의 chat_display.append() 줄이 실제 네트워크 전송 코드로 교체됩니다.