20강 개념 중심 ⏱ 약 20분

 

0. 학습 목표

→ 터미널 채팅 코드 중 무엇이 GUI에서 그대로 쓰이고, 무엇이 어디로 바뀌는지 정리합니다.

더보기

0.1 이번 글에서 다룰 내용

이번 글은 개념 중심 강의입니다.

 

19강에서 코드를 ChatServer / ChatClient / ChatWindow로 나누는 역할 분리 기준을 잡았습니다.

이번 강의에서는 그 분리를 전제로 터미널 채팅과 GUI 채팅 사이를 연결합니다.

직접 코드를 작성하거나 파일을 만들지는 않습니다. 대신 한 가지 핵심 질문에 답합니다.

지금까지 만든 코드 중 무엇이 GUI에서 그대로 쓰이고, 무엇이 바뀌는가?

GUI로 넘어간다고 해서 네트워크 원리가 바뀌지 않습니다. 서버 접속, 메시지 전송·수신, JSON 프로토콜은 GUI에서도 그대로 사용합니다. 달라지는 것은 사용자가 입력하고 결과를 보는 방식뿐입니다.

구분 내용
이해할 것 터미널 입출력 코드(input(), print())가 GUI 위젯·이벤트로 바뀌는 방식
정리할 것 기존 코드와 GUI 구조의 대응 관계 — 무엇이 유지되고 무엇이 바뀌는가
확인할 것 GUI 메인 흐름에서 recv()를 직접 실행하면 화면이 멈추는 이유

 

0.2 앞으로 만들 프로젝트 구조 미리보기

이번 강의에서는 파일을 생성하거나 수정하지 않습니다. 21강부터 만들 GUI 구조를 미리 확인합니다.

chat_server/
├── protocol.py          ← 메시지 규칙 (그대로 유지)
└── server.py            ← 이번 강의에서 수정하지 않음

chat_client/
├── protocol.py          ← 메시지 규칙 (그대로 유지)
├── client.py            ← 터미널 채팅 버전 (유지)
└── main.py              ← 21강에서 생성 예정 (GUI 진입점)

 

1. 핵심 개념 이해하기

→ 터미널 클라이언트의 두 흐름을 확인하고, GUI로 넘어가도 유지되는 코드를 정리합니다.

더보기

1.1 터미널 클라이언트의 두 가지 흐름

터미널 채팅 클라이언트는 크게 두 가지 흐름으로 동작했습니다.

 

보내는 흐름은 사용자가 키보드로 입력하고 Enter를 누르면 input()이 값을 돌려주고 send_message()로 서버에 전달하는 것입니다.

# 터미널: 보내는 흐름 (client.py 메인 루프)
while True:
    text = input("")                                          # 입력이 올 때까지 멈춰서 기다린다

    if text == "exit":
        send_message(client_socket, {"type": "exit"})         # 종료 메시지 전송
        break

    send_message(client_socket, {                             # 일반 채팅 전송
        "type": "chat",
        "content": text
    })


받는 흐름
은 별도 스레드에서 서버 메시지가 오면 타입에 따라 분기해 print()로 출력하는 것입니다.

# 터미널: 받는 흐름 (receive_loop 안)
message_type = message.get("type")

if message_type == "chat":
    sender = message.get("sender", "unknown")
    content = message.get("content", "")
    print(f"\n{sender}: {content}")
elif message_type == "system":
    print(f"\n[시스템] {content}")
elif message_type == "error":
    print(f"\n[오류] {content}")

이 두 흐름은 GUI에서도 그대로 존재합니다. 다만 input()print() 자리에 화면 위젯이 들어옵니다.

 

1.2 GUI에서도 계속 사용하는 코드

GUI로 넘어간다고 해서 지금까지 만든 코드를 버리는 것이 아닙니다. GUI 파트의 핵심은 네트워크를 다시 배우는 것이 아니라, 이미 만든 네트워크 기능을 화면 조작과 연결하는 것입니다.

기존 코드 GUI에서도 유지되는 이유
protocol.py JSON 메시지 송수신 규칙은 서버와 클라이언트 모두에서 그대로 필요하다
send_message() GUI에서도 서버로 메시지를 보낼 때 동일하게 사용한다
receive_loop() 구조 서버 메시지를 계속 기다리는 루프 자체는 유지된다
메시지 타입 분기 chat / system / error 타입을 나누어 표시하는 것은 그대로다

✔ 확인 기준: "protocol.py와 메시지 타입 분기는 GUI에서도 그대로 유지되고, 달라지는 것은 print() 대신 화면 위젯에 표시하는 방식이다"라고 설명할 수 있으면 완료입니다.

 

2. 구조나 흐름으로 확인하기

→ 터미널과 GUI의 입력 방식 차이, 그리고 GUI에서 화면이 멈추면 안 되는 이유를 확인합니다.

더보기

2.1 터미널과 GUI의 입력 방식 차이

터미널은 전화 안내 서비스와 비슷합니다.

"번호를 말씀해 주세요"라고 한 뒤 당신이 말할 때까지 시스템 전체가 멈춰서 기다립니다.

 

GUI는 은행 창구와 비슷합니다.

창구 직원은 한 고객을 응대하면서도 다른 고객이 번호표를 뽑거나 자리에 앉는 행동에 계속 반응합니다.

"누가 무언가를 했을 때 반응한다"가 중심입니다.

비유 코드에서의 대응
전화 안내 "번호를 말씀해 주세요" input() — 입력이 올 때까지 멈춰서 기다림
은행 창구 직원 GUI 메인 흐름 — 이벤트에 반응
고객이 번호표를 뽑는 행동 버튼 클릭 이벤트
창구 직원이 번호표 확인 클릭 이벤트와 연결된 함수 실행
뒷줄에서 문서 처리하는 직원 수신 스레드 — 서버 메시지를 별도로 처리

 

2.2 GUI에서 화면이 멈추면 안 되는 이유

터미널에서는 recv()가 기다리는 동안 입력이 막혀도 큰 문제가 없었습니다.

하지만 GUI에서는 상황이 다릅니다.

 

GUI 메인 흐름에서 recv()를 직접 실행하면 서버 메시지가 올 때까지 화면 전체가 응답을 멈춥니다.

GUI 메인 흐름에서 recv() 직접 실행 → 문제 발생
├── 창이 응답하지 않는 것처럼 보인다
├── 버튼이 눌리지 않는다
└── 입력창에 글자를 쓸 수 없다

 

그래서 GUI에서는 네트워크 수신을 반드시 화면 흐름과 분리해야 합니다.

GUI 메인 흐름  → 창 표시 / 버튼 클릭 / 입력창 관리
수신 스레드     → recv() → 받은 메시지를 GUI에 전달   ← 화면을 멈추지 않는 핵심
역할 담당
서버 접속 / 메시지 보내기 / 메시지 받기 ChatClient
입력창 / 버튼 / 채팅 표시 영역 관리 ChatWindow
JSON 메시지 인코딩·디코딩·구분 처리 protocol.py

✔ 확인 기준: "GUI 메인 흐름에서 recv()를 직접 실행하면 화면 전체가 멈추기 때문에, 수신은 반드시 별도 스레드에서 처리해야 한다"고 설명할 수 있으면 완료입니다.

 

3. 판단 기준 정리하기

→ 터미널 코드의 각 요소가 GUI에서 어디로 이동하는지, 그리고 주의해야 할 판단 기준을 정리합니다.

더보기

3.1 터미널 코드와 GUI 구조 비교

터미널 채팅 코드 GUI 채팅 구조 설명
input("") 메시지 입력창 사용자가 텍스트를 입력하는 위치
Enter 입력 보내기 버튼 클릭 또는 Enter 이벤트 메시지 전송을 시작하는 행동
send_message() ChatClient.send_chat() 내부 서버로 메시지를 보내는 기능
receive_loop() ChatClient의 수신 스레드 서버 메시지를 계속 받는 흐름
print() 채팅 표시 영역 받은 메시지를 보여 주는 위치
"exit" 입력 종료 버튼 또는 창 닫기 서버에 종료 메시지 전송

 

3.2 함수 대응표

GUI는 완전히 새로운 프로그램이 아니라, 기존 터미널 코드의 입출력 부분을 화면 위젯으로 교체한 것입니다. 네트워크 로직은 그대로 살아남습니다.

기존 코드 새 구조에서의 위치
send_message(..., {"type": "chat", ...}) ChatClient.send_chat(content)
send_message(..., {"type": "exit"}) ChatClient.send_exit()
receive_loop(client_socket) ChatClient.receive_loop()
print(f"\n{sender}: {content}") ChatWindow.add_chat_message(sender, content)
print(f"\n[시스템] {content}") ChatWindow.add_system_message(content)
print(f"\n[오류] {content}") ChatWindow.add_error_message(content)

 

3.3 자주 하는 오해

오해 올바른 이해
GUI로 바꾸면 네트워크 코드를 새로 짜야 한다 protocol.py, send_message(), receive_loop() 구조는 그대로 유지됩니다. 화면 입출력 부분만 교체합니다
GUI 메인 흐름에서 recv()를 직접 실행해도 된다 화면 전체가 멈춥니다. 수신은 반드시 별도 스레드에서 처리합니다
버튼 함수 안에서 JSON을 직접 만들면 편하다 역할 분리가 무너집니다. ChatWindow는 화면만, ChatClient는 네트워크만 담당합니다
수신 스레드에서 채팅 표시 위젯을 직접 수정한다 무작위 충돌이 발생할 수 있습니다. PySide6 신호를 사용해 GUI 메인 흐름으로 메시지를 전달합니다. 25강에서 구현합니다
창 닫을 때 서버에 종료 메시지를 보내지 않아도 된다 서버에서 ConnectionResetError가 발생합니다. 창 닫기 이벤트에서 ChatClient.send_exit()close()로 정리합니다

✔ 확인 기준: 대응표를 보지 않고 "input()은 입력창으로, print()는 채팅 표시 영역으로 바뀐다"고 말할 수 있으면 완료. send_message()receive_loop()ChatClient 안으로 들어가는 이유를 설명할 수 있으면 충분합니다.

 

4. 작은 예시로 검토하기

→ 보내는 흐름과 받는 흐름이 GUI 코드에서 어떤 모양이 되는지 미리 확인합니다.

더보기

이번 강의에서는 이 코드를 직접 실행하지 않습니다. 21강 이후 단계적으로 완성합니다. 지금은 "터미널 코드가 이런 모양으로 바뀐다"는 흐름을 파악하는 것이 목표입니다.

 

4.1 버튼 클릭으로 메시지를 보내는 GUI 방식

터미널에서는 input()으로 입력을 받아 바로 전송했습니다. GUI에서는 버튼 클릭 함수 안에서 입력창 값을 읽어 전송합니다.

# ChatWindow: 보내기 버튼 클릭 시 실행되는 함수
def on_send_clicked(self):
    text = self.message_input.text()    # 입력창에서 글자를 가져온다

    if not text:                         # 빈 메시지는 보내지 않는다
        return

    self.client.send_chat(text)          # 네트워크 처리는 ChatClient에 맡긴다
    self.message_input.clear()           # 전송 후 입력창을 비운다

ChatWindow가 직접 JSON을 만들지 않습니다. JSON 메시지 구성은 ChatClient가 담당합니다.

# ChatClient: 실제 네트워크 전송
def send_chat(self, content):
    send_message(self.socket, {           # 17강 send_message()를 그대로 재사용
        "type": "chat",
        "content": content
    })
코드 의미
self.message_input.text() 입력창의 현재 글자를 가져온다
if not text: 빈 문자열이면 전송하지 않는다
self.client.send_chat(text) JSON 구성과 전송을 ChatClient에 위임한다
self.message_input.clear() 보낸 뒤 입력창을 비운다

 

4.2 받은 메시지를 화면에 표시하는 GUI 방식

터미널에서는 print()로 출력했습니다. GUI에서는 채팅 표시 위젯에 텍스트를 추가합니다. 메시지 타입별 분기 자체는 그대로 유지됩니다.

# ChatWindow: 받은 메시지를 화면에 표시하는 함수들
def add_chat_message(self, sender, content):
    self.chat_display.append(f"{sender}: {content}")    # print() 대신 채팅 표시 영역에 추가

def add_system_message(self, content):
    self.chat_display.append(f"[시스템] {content}")

def add_error_message(self, content):
    self.chat_display.append(f"[오류] {content}")

달라진 것은 print() 한 줄이 self.chat_display.append() 한 줄로 바뀐 것뿐입니다. 타입별로 나누는 로직은 18강과 동일합니다.

단, 수신 스레드에서 self.chat_display.append()를 직접 호출하면 당장은 동작하는 것처럼 보여도 실행 중 무작위로 프로그램이 종료되거나 화면이 깨질 수 있습니다. PySide6를 포함한 대부분의 GUI 프레임워크는 화면 업데이트를 메인 스레드에서만 해야 한다는 규칙이 있기 때문입니다. 수신 스레드에서 GUI로 전달하는 신호(Signal) 연결은 25강에서 구현합니다.

✔ 확인 기준: "input()은 입력창과 버튼 클릭 이벤트로 바뀌고, print()는 채팅 표시 영역에 메시지를 추가하는 함수로 바뀐다"고 설명할 수 있으면 완료. ChatWindow가 직접 JSON을 만들지 않고 ChatClient에 위임하는 이유를 말할 수 있으면 충분합니다.

→ 다음 강의 (21강): 채팅 표시 영역·메시지 입력창·보내기 버튼으로 구성된 기본 창을 PySide6로 만들고, 창이 화면에 뜨는 것부터 확인합니다. 이번 강의에서 잡은 ChatWindow 구조가 실제 위젯 배치 코드로 구현됩니다.