24강. GUI에서 메시지 보내기

0. 학습 목표
→ 이번 글에서 무엇을 이해하고, 무엇을 만들고, 무엇을 확인할지 먼저 정리합니다.
0.1 이번 글에서 다룰 내용
이번 글은 구현 중심 강의입니다.
23강에서 GUI가 서버에 접속하는 기능을 만들었습니다.
하지만 보내기 버튼을 누르면 채팅창에 나: 메시지가 화면에만 임시로 추가될 뿐, 어디로도 전송되지 않았습니다.
이번 강의에서는 그 임시 동작을 실제 네트워크 전송으로 바꿉니다.
이번 강의에서도 다른 클라이언트가 보낸 메시지를 GUI에서 받지는 않습니다. 목표는 한 방향, "GUI에서 서버로 보내기"입니다.
메시지 입력
↓
보내기 버튼 클릭 또는 Enter 입력
↓
{"type": "chat", "content": "..."} 메시지 생성
↓
send_message()로 서버에 전송
↓
입력창 비우기 ← 이번 강의의 성공 지점
핵심 성공 기준은 GUI에서 보낸 메시지가 서버 터미널에 JSON으로 출력되는 것입니다. 그동안 17강에서 만들어 둔 protocol.py의 send_message()를 GUI에서 처음으로 사용하게 됩니다. 아래 코드에서 ➕는 23강 코드까지는 없던 새로 더해진 줄, ✏️는 위치나 형태가 바뀐 줄을 뜻합니다. 최종 정리 코드에는 마커 없이 깨끗한 코드만 싣습니다.

| 구분 | 내용 |
| 이해할 것 | GUI 이벤트(on_send_clicked)가 네트워크 전송 함수(send_message)와 연결되는 방식 |
| 만들 것 | chat_client/main.py 수정 — 입력창 메시지를 서버로 보내는 on_send_clicked() |
| 확인할 것 | GUI에서 보낸 메시지가 서버 터미널에 JSON 형태로 출력되는지 |
0.2 이번 강의에서 직접 다루는 구조
이번 강의에서는 23강에서 만든 chat_client/main.py를 수정합니다. protocol.py는 17강에서 이미 만들어 둔 파일이며, 이번 강의에서 그 안의 send_message()를 GUI에서 처음으로 가져다 씁니다. 서버는 이전 파트의 chat_server/server.py를 그대로 실행해 사용합니다.
chat_server/
├── protocol.py ← 유지
└── server.py ← 유지 (실행용으로만 사용)
chat_client/
├── protocol.py ← 유지 (send_message()를 이번 강의에서 처음 사용)
├── client.py ← 유지
└── main.py ← ✏️ 이번 강의에서 수정
(✏️ 수정 · 표시 없음은 변경 없음)
1. 실습 준비하기
→ 서버를 먼저 실행하고, protocol.py가 클라이언트 폴더에 있는지 확인합니다.
1.1 서버 먼저 실행하기
GUI가 메시지를 보내려면 받아줄 서버가 먼저 실행 중이어야 합니다. 터미널 1에서 이전 파트에서 만든 서버를 실행합니다.
python chat_server/server.py
서버를 시작합니다.
클라이언트 접속 대기 중...
서버 터미널이 종료되지 않고 대기 상태를 유지하면 준비 완료입니다.
1.2 protocol.py 확인하기
GUI 클라이언트도 서버와 똑같은 메시지 규칙을 써야 합니다. chat_client/protocol.py는 17강에서 이미 만들어 둔 파일이므로 새로 만들 필요는 없습니다. main.py와 같은 폴더에 있는지만 확인합니다. 이번 강의에서는 그중 send_message()만 사용하고, receive_messages()는 25강에서 사용합니다.
# chat_client/protocol.py (이번 강의에서 사용하는 부분)
import json
ENCODING = "utf-8"
DELIMITER = "\n"
def send_message(sock, message):
json_text = json.dumps(message, ensure_ascii=False) # 메시지를 JSON 문자열로 변환
data = json_text + DELIMITER # 끝에 줄바꿈을 붙여 메시지 경계 표시
sock.sendall(data.encode(ENCODING)) # 바이트로 바꿔 전부 전송
# receive_messages()는 25강에서 사용합니다
✔ 확인 기준: python chat_client/main.py 실행 시 ModuleNotFoundError가 발생하지 않으면 완료. 오류가 나면 protocol.py와 main.py가 같은 chat_client/ 폴더에 있는지 확인하세요.
2. 클라이언트 - 전송 함수 가져오기
→ send_message() import를 추가하고 GUI가 서버로 보낼 메시지 형식을 확인합니다.
main.py 위쪽 import 영역에 protocol의 send_message를 가져오는 한 줄을 추가합니다.
import socket
import sys
from PySide6.QtWidgets import (
QApplication,
QWidget,
QTextEdit,
QLineEdit,
QPushButton,
QLabel,
QVBoxLayout,
QHBoxLayout,
)
from protocol import send_message # ➕ 17강에서 만든 전송 함수 가져오기
HOST = "127.0.0.1"
PORT = 5000
이 한 줄을 추가하면 GUI 코드에서 send_message()를 직접 호출할 수 있습니다. 이번 강의에서 GUI가 서버로 보내는 일반 채팅 메시지 형식은 다음과 같습니다. 서버와 약속한 규칙(type으로 메시지 종류를 구분)을 그대로 따릅니다.
send_message(self.client_socket, { # 첫 번째 인자: 연결된 소켓
"type": "chat", # 두 번째 인자: 보낼 메시지(딕셔너리)
"content": text
})
| 파일 | 이번 강의에서의 역할 |
protocol.py |
JSON 메시지 전송 함수(send_message()) 제공 (이미 존재) |
main.py |
send_message()를 가져와 GUI 입력값을 서버로 전송 |
✔ 확인 기준: from protocol import send_message 한 줄이 import 영역에 추가되어 있고, 실행 시 import 오류가 없으면 완료. 보낼 메시지가 type과 content 두 키를 가진 딕셔너리라는 점을 기억하세요.
3. 클라이언트 - 메시지 전송 연결하기
→ on_send_clicked()의 임시 동작을 실제 send_message() 전송으로 교체합니다.
23강의 on_send_clicked()는 메시지를 화면에만 추가했습니다. 이번에는 실제로 서버에 전송합니다. 전송 전에 서버 연결 여부를 먼저 확인해, 접속하지 않은 상태에서 보내려는 조작을 막습니다.
def on_send_clicked(self):
text = self.message_input.text().strip()
if not text: # 빈 메시지는 보내지 않는다
return
if self.client_socket is None: # ➕ 서버 연결 전이면 전송 막기 (방어적 검사)
self.chat_display.append("[오류] 서버에 먼저 접속하세요.")
return
try:
send_message(self.client_socket, { # ✏️ 화면 출력 대신 실제 전송으로 교체
"type": "chat",
"content": text
})
self.chat_display.append(f"나: {text}") # 내 화면에도 보낸 메시지 표시
self.message_input.clear() # 전송 후 입력창 비우기
except Exception as error: # ➕ 전송 중 오류를 화면에 안내
self.chat_display.append(f"[오류] 메시지 전송 실패: {error}")
23강에서는 chat_display.append(f"나: {text}") 한 줄이 전부였습니다. 이번에는 그 위에 send_message() 호출이 들어가, 화면 표시 전에 먼저 서버로 메시지를 보냅니다.
self.client_socket is None 검사는 방어적 코딩입니다.
23강에서 서버 연결 전에 입력창과 보내기 버튼을 setEnabled(False)로 잠가두었기 때문에 정상 사용 중에는 이 분기에 도달하기 어렵습니다. 다만 향후 코드를 수정하다가 setEnabled 처리가 바뀌더라도 프로그램이 오류 없이 안내 메시지를 보여줄 수 있도록 넣어 둡니다.
on_send_clicked() 안에서 일어나는 흐름입니다.
입력값 읽기 (strip)
↓
빈 메시지인가? ──예──→ 그냥 종료
↓ 아니오
연결돼 있나? ──아니오──→ "[오류] 서버에 먼저 접속하세요." 표시 후 종료
↓ 예
send_message()로 서버에 전송
↓
내 화면에 "나: ..." 표시 + 입력창 비우기 ← 이번 강의의 성공 지점
✔ 확인 기준: 메시지를 입력하고 보내기를 누르면 서버 터미널에 JSON 메시지가 출력되면 완료. 출력이 없으면 send_message()를 import했는지, 서버에 먼저 접속했는지 확인하세요.
4. 실행 결과 확인하기
→ 서버를 실행하고 GUI에서 메시지를 보내 서버 터미널에서 수신을 확인합니다.
4.1 실행 결과 확인하기

터미널 1(서버)이 대기 중인 상태에서, 터미널 2에서 GUI 클라이언트를 실행합니다.
python chat_client/main.py
"서버 접속" 버튼을 누르면 GUI 채팅창에 다음이 표시됩니다.
[시스템] 서버에 연결되었습니다.
입력창에 안녕하세요 GUI입니다를 입력하고 보내기 버튼을 누릅니다. GUI 채팅 표시 영역에는 다음처럼 추가됩니다.
나: 안녕하세요 GUI입니다
동시에 서버 터미널(터미널 1)에는 다음처럼 출력됩니다.
('127.0.0.1', 52344) 메시지: {'type': 'chat', 'content': '안녕하세요 GUI입니다'}
포트 번호(52344)는 매번 달라지므로 값이 달라도 정상입니다. 터미널 클라이언트를 하나 더 실행해 두면 GUI가 보낸 메시지가 그 터미널에도 표시됩니다. 다만 아직 GUI는 다른 클라이언트의 메시지를 받지 못합니다. 수신 스레드를 아직 만들지 않았기 때문이며, 이는 25강에서 구현합니다. 지금은 정상 동작입니다.
✔ 확인 기준: 서버 터미널에 {'type': 'chat', 'content': '안녕하세요 GUI입니다'} 형태의 메시지가 출력되면 완료. 출력이 없으면 protocol.py의 send_message()가 메시지 끝에 \n을 붙이는지 확인하세요.
4.2 실행이 안 될 때 확인할 것
| 오류 상황 | 원인 및 해결 방법 |
ModuleNotFoundError: protocol |
protocol.py가 main.py와 다른 폴더에 있습니다. chat_client/ 폴더에 함께 두고 실행합니다 |
ConnectionRefusedError |
서버를 실행하지 않고 접속했습니다. python chat_server/server.py를 먼저 실행합니다 |
| 보내도 서버에 출력되지 않는다 | send_message()를 호출하지 않았거나 이벤트 연결이 빠졌습니다. on_send_clicked() 안에 send_message(...)가 있는지 확인합니다 |
AttributeError: 'NoneType' |
서버 연결 전에 메시지를 보내려 했습니다. if self.client_socket is None: 검사가 있는지 확인합니다 |
| GUI에는 나오는데 서버엔 안 보인다 | 화면 출력만 하고 실제 전송을 빠뜨렸습니다. chat_display.append() 위에 send_message(self.client_socket, ...)가 있는지 확인합니다 |
| 서버에서 JSON 오류가 난다 | 기존 send() 방식과 send_message() 방식이 섞였습니다. GUI에서는 반드시 send_message()로만 보냅니다 |
| 다른 클라이언트 메시지가 GUI에 안 보인다 | 정상입니다. 수신 스레드를 아직 만들지 않았으며 25강에서 구현합니다 |
5. 최종 코드 정리하기
→ 이번 강의에서 완성한 chat_client/main.py 전체 코드를 한곳에 정리합니다.
5.1 chat_client/main.py
import socket
import sys
from PySide6.QtWidgets import (
QApplication,
QWidget,
QTextEdit,
QLineEdit,
QPushButton,
QLabel,
QVBoxLayout,
QHBoxLayout,
)
from protocol import send_message
HOST = "127.0.0.1"
PORT = 5000
class ChatWindow(QWidget):
def __init__(self):
super().__init__()
self.client_socket = None
self.setWindowTitle("채팅")
self.resize(560, 480)
self.chat_display = QTextEdit()
self.chat_display.setReadOnly(True)
self.chat_display.setPlaceholderText("채팅 메시지가 여기에 표시됩니다.")
self.message_input = QLineEdit()
self.message_input.setPlaceholderText("메시지를 입력하세요")
self.message_input.setEnabled(False)
self.send_button = QPushButton("보내기")
self.send_button.setEnabled(False)
self.status_label = QLabel("상태: 서버에 연결되지 않음")
self.connect_button = QPushButton("서버 접속")
top_layout = QHBoxLayout()
top_layout.addWidget(self.status_label)
top_layout.addWidget(self.connect_button)
input_layout = QHBoxLayout()
input_layout.addWidget(self.message_input)
input_layout.addWidget(self.send_button)
main_layout = QVBoxLayout()
main_layout.addLayout(top_layout)
main_layout.addWidget(self.chat_display)
main_layout.addLayout(input_layout)
self.setLayout(main_layout)
self.connect_button.clicked.connect(self.connect_to_server)
self.send_button.clicked.connect(self.on_send_clicked)
self.message_input.returnPressed.connect(self.on_send_clicked)
def connect_to_server(self):
try:
self.client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.client_socket.connect((HOST, PORT))
self.status_label.setText("상태: 서버에 연결됨")
self.chat_display.append("[시스템] 서버에 연결되었습니다.")
self.connect_button.setEnabled(False)
self.message_input.setEnabled(True)
self.send_button.setEnabled(True)
except ConnectionRefusedError:
self.status_label.setText("상태: 서버 연결 실패")
self.chat_display.append("[오류] 서버가 실행 중인지 확인하세요.")
except Exception as error:
self.status_label.setText("상태: 서버 연결 실패")
self.chat_display.append(f"[오류] 연결 중 문제가 발생했습니다: {error}")
def on_send_clicked(self):
text = self.message_input.text().strip()
if not text:
return
if self.client_socket is None:
self.chat_display.append("[오류] 서버에 먼저 접속하세요.")
return
try:
send_message(self.client_socket, {
"type": "chat",
"content": text
})
self.chat_display.append(f"나: {text}")
self.message_input.clear()
except Exception as error:
self.chat_display.append(f"[오류] 메시지 전송 실패: {error}")
app = QApplication(sys.argv)
window = ChatWindow()
window.show()
sys.exit(app.exec())
5.2 최종 확인 표
| 확인할 코드 | 의미 |
from protocol import send_message |
전송 함수 가져오기 |
if not text: return |
빈 메시지 전송 방지 |
if self.client_socket is None: |
서버 연결 전 전송 차단 (방어적 검사) |
send_message(self.client_socket, {"type": "chat", ...}) |
chat 메시지를 서버로 실제 전송 |
self.chat_display.append(f"나: {text}") |
보낸 메시지를 내 화면에 표시 |
self.message_input.clear() |
전송 후 입력창 비우기 |
except Exception as error |
전송 실패 시 화면에 오류 안내 |
이번 강의로 GUI는 서버에 메시지를 "보내는" 일을 완성했습니다. 메시지 전송은 send_message()로 통일하고 메시지 타입은 chat을 사용합니다. 아직 "받는" 쪽은 비어 있는데, 이 부분은 다음 강의에서 채웁니다.
→ 다음 강의 (25강): 이번 강의에서 만든 "보내기"에 "받기"를 더합니다. protocol.py의 receive_messages()를 별도 수신 스레드에서 호출해 다른 클라이언트의 메시지를 GUI 채팅창에 표시합니다. PySide6에서는 수신 스레드가 화면을 직접 수정하지 않도록 주의해야 하며, 그 방법을 함께 다룹니다.