24강 구현 중심 ⏱ 약 25분

 

0. 학습 목표

→ 이번 글에서 무엇을 이해하고, 무엇을 만들고, 무엇을 확인할지 먼저 정리합니다.

더보기

0.1 이번 글에서 다룰 내용

이번 글은 구현 중심 강의입니다.

 

23강에서 GUI가 서버에 접속하는 기능을 만들었습니다.

하지만 보내기 버튼을 누르면 채팅창에 나: 메시지가 화면에만 임시로 추가될 뿐, 어디로도 전송되지 않았습니다.

이번 강의에서는 그 임시 동작을 실제 네트워크 전송으로 바꿉니다.

 

이번 강의에서도 다른 클라이언트가 보낸 메시지를 GUI에서 받지는 않습니다. 목표는 한 방향, "GUI에서 서버로 보내기"입니다.

메시지 입력
        ↓
보내기 버튼 클릭 또는 Enter 입력
        ↓
{"type": "chat", "content": "..."} 메시지 생성
        ↓
send_message()로 서버에 전송
        ↓
입력창 비우기                          ← 이번 강의의 성공 지점

핵심 성공 기준은 GUI에서 보낸 메시지가 서버 터미널에 JSON으로 출력되는 것입니다. 그동안 17강에서 만들어 둔 protocol.pysend_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.pymain.py가 같은 chat_client/ 폴더에 있는지 확인하세요.

 

2. 클라이언트 - 전송 함수 가져오기

→ send_message() import를 추가하고 GUI가 서버로 보낼 메시지 형식을 확인합니다.

더보기

main.py 위쪽 import 영역에 protocolsend_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 오류가 없으면 완료. 보낼 메시지가 typecontent 두 키를 가진 딕셔너리라는 점을 기억하세요.

 

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.pysend_message()가 메시지 끝에 \n을 붙이는지 확인하세요.

 

4.2 실행이 안 될 때 확인할 것

오류 상황 원인 및 해결 방법
ModuleNotFoundError: protocol protocol.pymain.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.pyreceive_messages()를 별도 수신 스레드에서 호출해 다른 클라이언트의 메시지를 GUI 채팅창에 표시합니다. PySide6에서는 수신 스레드가 화면을 직접 수정하지 않도록 주의해야 하며, 그 방법을 함께 다룹니다.