학습 로그/Python3

D+53[Python] PySide6 GUI 연동하기(22~26강)

goodjob^^ 2026. 6. 23. 21:06

ㅇ 일 자 : 2026년 6월 23일(화)
ㅇ 주 제 : GUI 연동하기 

 

22강 채팅 화면 레이아웃 만들기
23강 GUI에서 서버 접속하기
24강 GUI에서 메세지 보내기
25강 GUI에서 실시간 메세지 받기
26강 GUI 상태 관리와 연결 종료 처리
27강 사용자 목록 동기화 
28강 상대 선택 UI 만들기

 

※ 아래 주소는 가상의 실습형 주소임


22강. 채팅 화면 레이아웃 만들기
self.send_button.clicked.connect (self.on_send_clicked) 버튼 클릭
self.message_input.retrunPressed.connect(sefl.on_send_clicked) 엔터 입력
def....... (버튼과 앤터를 입력 받은 것을 실행한다.)


23강. GUI에서 서버 접속하기
서버와 연결하기, 크라이언트가 서버에 연결을 요청한다. 
사용자가 잘 사용할 수 있도록 만들어 주는 것, 사용자 입장에서 봐야 한다.
프로젝트 단계별 나누어서 구현하기 (각 담당자별) : 예) 테스트-설계-구조
cs팀, ai팀, QT팀 : 분석과 설계가 되어서 팀별 개인임무 분담하기

23강 코드

# GUI 연결하기 실습 (23강)

import socket  # 소켓 모듈 불러오기
import sys

import self
from PySide6.QtWidgets import (
    QApplication,
    QWidget,
    QTextEdit,  # 채팅 표시 영역
    QLineEdit,  # 입력창
    QPushButton,  # 보내기 버튼
    QLabel,  # 서버연결 상태 표시
    QVBoxLayout,  # 가로 레이아웃
    QHBoxLayout,  # 세로 레이아웃
)

# from protocol import send_message    ### 17강에서 만들었던 전송 함수 가져와서 24강에서 사용한다

HOST = "127.0.0.1"  ### 서버 IP (server.py와 주소가 같아야 한다.)
PORT = 5000  #### 서버 포트 (server.py와 포트가 같아야 한다.)


class ChatWindow(QWidget):
    def __init__(self):
        super().__init__()

        self.client_socket = None  ### 연결하기 전이므로 None를 작성하여 시작하기로 한다.

        self.setWindowTitle("GUI채팅창")
        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)  ### status 라벨을 top 라벨로 변경해 준다.
        main_layout.addWidget(self.chat_display)
        main_layout.addLayout(input_layout)

        self.setLayout(main_layout)

        self.connect_button.clicked.connect(self.connect_to_server)  ### connect 서버 추가해 준다
        self.send_button.clicked.connect(self.on_send_clicked)  # 버튼 클릭 "이벤트" 연결 on_send_clicked
        self.message_input.returnPressed.connect(self.on_send_clicked)  # 앤터 입력 "이벤트" 연결 on_send_clicked

    ## === 클라이언트 - 서버 접속 기능을 연결하기 위해 connect_to_server() 메서드를 추가해 준다.
    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

        self.chat_display.append(f"나: {text}") # 임시네트워크 전송으로 교체하여 보내는 이가 "나"를 지칭해 출력함
        self.message_input.clear()


app = QApplication(sys.argv)

window = ChatWindow()
window.show()

sys.exit(app.exec())  # 윈도우 창이 바로 꺼지지 않게 계속 유지되도록 하는 역할을 한다.


24강. GUI에서 메세지 보내기
메세지 전송할 때 프로토콜에서 전송한다. from protocol import send_message
threads : 메세지가 전송텀이 있어서 입력 될 때까지 멈추기 때문에 스레드를 사용한다.

※ 주요 내용 실습 

채팅화면 레이아웃 만들기
GUI에서 서버 접속하기


25강. GUI에서 실시간 메세지 받기
시그널 만들기 UI에 스레드를 사용하기 
수신 스레드가 필요한 이유? 
recv()가 동작할 때 별도의 수신 스레드를 작성해서 실행을 한다. 즉 2개의 실행을 하므로 별도의 동작을 실행한다. 
시그널과 슬롯을 연결하는 것? 
메세지를 받으면 받은 메세지를 UI에 보여지게 한다. 

 

 

26강. GUI 상태 관리와 연결 종료 처리

 


 

<<  26강 실습과제 풀이>> 

 

과제A,B. 서버에 회원 처리 추가하기 / 로그인 창 만들기

로그인 화면 추가하기 실행 화면

로그인 소스코드

# 3. 로그인 페이지
class LoginPage(QWidget):
    # 로그인 성공 시 실행할 시그널 (아이디 전달)
    login_success = Signal(str)

    def __init__(self):
        super().__init__()
        self.setWindowTitle("로그인")
        self.resize(300, 200)

        layout = QVBoxLayout()

        self.id_input = QLineEdit()
        self.id_input.setPlaceholderText("아이디")
        self.pw_input = QLineEdit()
        self.pw_input.setPlaceholderText("비밀번호")
        self.pw_input.setEchoMode(QLineEdit.Password)

        self.login_btn = QPushButton("로그인")
        self.register_btn = QPushButton("회원가입")  # 회원가입 이동 버튼 추가

        layout.addWidget(QLabel("채팅 프로그램 로그인"))
        layout.addWidget(self.id_input)
        layout.addWidget(self.pw_input)
        layout.addWidget(self.login_btn)
        layout.addWidget(self.register_btn)
        self.setLayout(layout)

        self.login_btn.clicked.connect(self.check_login)
        self.register_btn.clicked.connect(self.open_register_page)

        self.reg_page = None  # 회원가입 창 객체 보유용 변수

    def check_login(self):
        user_id = self.id_input.text().strip()
        password = self.pw_input.text().strip()

        # 서버 인증 시뮬레이션
        if user_id in USER_DB and USER_DB[user_id] == password:
            self.login_success.emit(user_id)
            self.close()  # 로그인 창 닫기
        else:
            QMessageBox.warning(
                self, "인증 실패", "아이디 또는 비밀번호가 틀렸습니다."
            )

    def open_register_page(self):
        self.reg_page = RegisterPage()
        # 회원가입 성공 시 로그인 입력창에 가입한 아이디를 세팅해주면 편리합니다.
        self.reg_page.register_success.connect(
            lambda: self.id_input.setText(self.reg_page.id_input.text())
        )
        self.reg_page.show()

 

과제C. 회원가입 처리하기

가상의 회원가입 화면
가상의 회원가입이 완료된 화면

 회원가입 소스코드

# 2. 회원가입 페이지 (추가됨)
class RegisterPage(QWidget):
    register_success = Signal()  # 회원가입 성공 시 발생할 시그널

    def __init__(self):
        super().__init__()
        self.setWindowTitle("회원가입")
        self.resize(300, 200)

        layout = QVBoxLayout()

        self.id_input = QLineEdit()
        self.id_input.setPlaceholderText("새로운 아이디")

        self.pw_input = QLineEdit()
        self.pw_input.setPlaceholderText("새로운 비밀번호")
        self.pw_input.setEchoMode(QLineEdit.Password)

        self.register_btn = QPushButton("가입하기")
        self.cancel_btn = QPushButton("취소")

        layout.addWidget(QLabel("회원가입 정보를 입력하세요"))
        layout.addWidget(self.id_input)
        layout.addWidget(self.pw_input)
        layout.addWidget(self.register_btn)
        layout.addWidget(self.cancel_btn)
        self.setLayout(layout)

        self.register_btn.clicked.connect(self.check_register)
        self.cancel_btn.clicked.connect(self.close)

    def check_register(self):
        user_id = self.id_input.text().strip()
        password = self.pw_input.text().strip()

        if not user_id or not password:
            QMessageBox.warning(
                self, "오류", "아이디와 비밀번호를 모두 입력해주세요."
            )
            return

        if user_id in USER_DB:
            QMessageBox.warning(self, "오류", "이미 존재하는 아이디입니다.")
        else:
            USER_DB[user_id] = password
            QMessageBox.information(
                self, "성공", "회원가입이 완료되었습니다!"
            )
            self.register_success.emit()
            self.close()

 

 

과제D. 진입점 바꾸기

의미 : 
기능 호출 방식의 변화 : 예를 들어 '플러스 버튼(+)'을 눌러야만 쓸 수 있던 기능을 이제 채팅 입력창에서 @나 특정 명령어를 통해 바로 호출할 수 있게 되는 등, 더 빠르고 편리하게 접근하도록 UI/UX가 개선됨을 뜻이다. 

※ 실행해서 확인 하기 화면 ☞ 실행해서 서버에서 메세지를 받아주는 기능 추가 수정 필요

로그인 화면 추가하기 실행 화면
채팅방 채팅 처리 화면 (서버에 넘기는 부분이 수정 필요함)

☞ 실행해서 서버에서 메세지를 받아주는 기능 추가 수정후 화면

채팅을 사용자 1이 받아서 출력하는 화면
사용자가 추가된 채팅 서버 화면
다수의 사용자들과 채팅을 시행한 화면

※ 파일 구조

채팅창 구현하기 파일 구조

chat_client / main.py (총괄 코드)

# main.py

import sys  # 시스템 관련 파라미터 및 프로그램 종료를 위한 라이브러리
import socket  # 네트워크 통신을 위한 소켓 라이브러리
import threading  # 백그라운드에서 메시지를 수신하기 위한 멀티쓰레딩 라이브러리

# PySide6에서 GUI 이벤트 처리 및 시그널-슬롯 구조를 사용하기 위한 모듈 임포트
from PySide6.QtCore import Signal, Slot, Qt
from PySide6.QtWidgets import (
    QApplication,  # QT 어플리케이션을 제어하는 메인 클래스
    QWidget,  # 기본 윈도우 창 레이아웃의 베이스 클래스
    QTextEdit,  # 멀티라인 텍스트 표시 및 편집 창 (채팅 기록 창)
    QLineEdit,  # 단일라인 텍스트 입력 창 (메시지 입력 창)
    QPushButton,  # 클릭 가능한 버튼 (접속, 전송 버튼)
    QLabel,  # 텍스트를 화면에 표시하는 라벨 (상태 표시용)
    QVBoxLayout,  # 위젯을 수직(세로)으로 배열하는 레이아웃
    QHBoxLayout,  # 위젯을 수평(가로)으로 배열하는 레이아웃
    QMessageBox,  # 경고 및 알림 메시지 창을 띄우기 위한 클래스 (추가됨)
)

# 사용자 정의 프로토콜 모듈에서 메시지 송수신 함수를 임포트
from protocol import receive_messages, send_message

# 접속할 서버의 IP 주소와 포트 번호 설정 (로컬 호스트)
HOST = "127.0.0.1"
PORT = 5000

# 1. 메모리 기반 사용자 데이터 (서버 인증용 딕셔너리)
USER_DB = {"user1": "1234", "user2": "5678"}


# 2. 회원가입 페이지 (추가됨)
class RegisterPage(QWidget):
    register_success = Signal()  # 회원가입 성공 시 발생할 시그널

    def __init__(self):
        super().__init__()
        self.setWindowTitle("회원가입")
        self.resize(300, 200)

        layout = QVBoxLayout()

        self.id_input = QLineEdit()
        self.id_input.setPlaceholderText("새로운 아이디")

        self.pw_input = QLineEdit()
        self.pw_input.setPlaceholderText("새로운 비밀번호")
        self.pw_input.setEchoMode(QLineEdit.Password)

        self.register_btn = QPushButton("가입하기")
        self.cancel_btn = QPushButton("취소")

        layout.addWidget(QLabel("회원가입 정보를 입력하세요"))
        layout.addWidget(self.id_input)
        layout.addWidget(self.pw_input)
        layout.addWidget(self.register_btn)
        layout.addWidget(self.cancel_btn)
        self.setLayout(layout)

        self.register_btn.clicked.connect(self.check_register)
        self.cancel_btn.clicked.connect(self.close)

    def check_register(self):
        user_id = self.id_input.text().strip()
        password = self.pw_input.text().strip()

        if not user_id or not password:
            QMessageBox.warning(
                self, "오류", "아이디와 비밀번호를 모두 입력해주세요."
            )
            return

        if user_id in USER_DB:
            QMessageBox.warning(self, "오류", "이미 존재하는 아이디입니다.")
        else:
            USER_DB[user_id] = password
            QMessageBox.information(
                self, "성공", "회원가입이 완료되었습니다!"
            )
            self.register_success.emit()
            self.close()


# 3. 로그인 페이지
class LoginPage(QWidget):
    # 로그인 성공 시 실행할 시그널 (아이디 전달)
    login_success = Signal(str)

    def __init__(self):
        super().__init__()
        self.setWindowTitle("로그인")
        self.resize(300, 200)

        layout = QVBoxLayout()

        self.id_input = QLineEdit()
        self.id_input.setPlaceholderText("아이디")
        self.pw_input = QLineEdit()
        self.pw_input.setPlaceholderText("비밀번호")
        self.pw_input.setEchoMode(QLineEdit.Password)

        self.login_btn = QPushButton("로그인")
        self.register_btn = QPushButton("회원가입")  # 회원가입 이동 버튼 추가

        layout.addWidget(QLabel("채팅 프로그램 로그인"))
        layout.addWidget(self.id_input)
        layout.addWidget(self.pw_input)
        layout.addWidget(self.login_btn)
        layout.addWidget(self.register_btn)
        self.setLayout(layout)

        self.login_btn.clicked.connect(self.check_login)
        self.register_btn.clicked.connect(self.open_register_page)

        self.reg_page = None  # 회원가입 창 객체 보유용 변수

    def check_login(self):
        user_id = self.id_input.text().strip()
        password = self.pw_input.text().strip()

        # 서버 인증 시뮬레이션
        if user_id in USER_DB and USER_DB[user_id] == password:
            self.login_success.emit(user_id)
            self.close()  # 로그인 창 닫기
        else:
            QMessageBox.warning(
                self, "인증 실패", "아이디 또는 비밀번호가 틀렸습니다."
            )

    def open_register_page(self):
        self.reg_page = RegisterPage()
        # 회원가입 성공 시 로그인 입력창에 가입한 아이디를 세팅해주면 편리합니다.
        self.reg_page.register_success.connect(
            lambda: self.id_input.setText(self.reg_page.id_input.text())
        )
        self.reg_page.show()


# 4. 채팅창 페이지
class ChatWindow(QWidget):
    # 백그라운드 쓰레드에서 수신한 메시지 데이터를 메인 GUI 쓰레드로 안전하게 전달하기 위한 커스텀 시그널 정의
    message_received = Signal(dict)

    def __init__(self):
        super().__init__()  # 부모 클래스(QWidget)의 생성자 호출

        # 초기 변수 및 상태 설정
        self.user_id = ""  # 현재 로그인한 사용자의 아이디 저장 변수
        self.client_socket = None  # 서버와 연결할 소켓 객체 저장 변수
        self.receive_thread = None  # 메시지 수신을 담당할 백그라운드 쓰레드 변수
        self.running = False  # 수신 루프 실행 여부를 제어하는 플래그

        # GUI 창의 기본 속성 설정
        self.setWindowTitle("채팅방")  # 창 제목 설정
        self.resize(560, 480)  # 창 크기 설정 (가로 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
        )  # 구성된 메인 레이아웃을 현재 위젯 창에 적용

        # 이벤트 시그널과 핸들러(슬롯) 함수들을 연결 (Connect)
        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
        )  # 입력창에서 엔터키 입력 시
        self.message_received.connect(
            self.display_message
        )  # 새 메시지 시그널 수신 시 화면 표시 함수 호출

    def set_user(self, user_id):
        """로그인 성공 후 유저 아이디를 세팅하고 타이틀을 변경하는 함수"""
        self.user_id = user_id
        self.setWindowTitle(f"채팅방 - [{self.user_id}]님")

    def connect_to_server(self):
        """서버에 TCP 소켓 연결을 시도하고 수신 쓰레드를 시작하는 함수"""
        try:
            # IPv4, TCP 프로토콜을 사용하는 소켓 생성 및 서버 연결 시도
            self.client_socket = socket.socket(
                socket.AF_INET, socket.SOCK_STREAM
            )
            self.client_socket.connect((HOST, PORT))

            # 연결 성공 시 UI 상태 업데이트
            self.status_label.setText("상태: 서버에 연결됨")
            self.chat_display.append("[시스템] 서버에 연결되었습니다.")

            # 버튼 및 입력창 활성화/비활성화 제어
            self.connect_button.setEnabled(
                False
            )  # 재접속 방지를 위해 비활성화
            self.message_input.setEnabled(True)  # 입력 허용
            self.send_button.setEnabled(True)  # 전송 허용

            # 백그라운드 수신 쓰레드 생성 및 시작
            self.running = True
            self.receive_thread = threading.Thread(target=self.receive_loop)
            self.receive_thread.daemon = (
                True  # 메인 프로그램 종료 시 쓰레드도 자동 종료 설정
            )
            self.receive_thread.start()

        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 receive_loop(self):
        """백그라운드에서 실행되며 서버로부터 데이터를 지속적으로 수신하는 루프 함수"""
        buffer = ""  # 데이터가 쪼개져 올 때를 대비한 임시 버퍼 공간

        while self.running:
            try:
                buffer, messages, connected = receive_messages(
                    self.client_socket, buffer
                )

                if not connected:
                    self.message_received.emit(
                        {
                            "type": "system",
                            "content": "서버와의 연결이 끊어졌습니다.",
                        }
                    )
                    break

                for message in messages:
                    self.message_received.emit(message)

            except Exception as error:
                self.message_received.emit(
                    {
                        "type": "error",
                        "content": f"메시지 수신 중 오류가 발생했습니다: {error}",
                    }
                )
                break

        self.running = False

    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:
            # 보낼 때 발신자 정보(sender)도 프로토콜에 실어 보냅니다.
            send_message(
                self.client_socket,
                {"type": "chat", "sender": self.user_id, "content": text},
            )

            self.chat_display.append(f"나: {text}")
            self.message_input.clear()

        except Exception as error:
            self.chat_display.append(f"[오류] 메시지 전송 실패: {error}")

    @Slot(dict)
    def display_message(self, message):
        """수신 쓰레드가 보낸 시그널을 받아 화면에 분류 처리하는 슬롯 함수"""
        message_type = message.get("type")

        if message_type == "chat":
            sender = message.get("sender", "unknown")
            content = message.get("content", "")
            # 내가 보낸 메시지가 상대방에게서 다시 브로드캐스트되어 오는 중복 출력을 방지
            if sender != self.user_id:
                self.chat_display.append(f"{sender}: {content}")

        elif message_type == "system":
            content = message.get("content", "")
            self.chat_display.append(f"[시스템] {content}")

        elif message_type == "error":
            content = message.get("content", "")
            self.chat_display.append(f"[오류] {content}")

        else:
            self.chat_display.append(f"[알 수 없는 메시지] {message}")


# 5. 흐름 제어 및 프로그램 실행 진입점
if __name__ == "__main__":
    app = QApplication(sys.argv)

    login_page = LoginPage()
    chat_window = ChatWindow()

    # 로그인 성공 시그널 연결 로직
    def on_login_success(user_id):
        chat_window.set_user(user_id)  # 채팅창에 로그인된 유저 세팅
        chat_window.show()  # 채팅창 켜기
        chat_window.connect_to_server()  # 로그인 완료와 동시에 서버 접속 자동 실행

    login_page.login_success.connect(on_login_success)
    login_page.show()  # 처음에 로그인 화면 띄우기

    sys.exit(app.exec())

 

chat_server / server.py

# server.py

import socket
import threading
from protocol import receive_messages, send_message

HOST = "127.0.0.1"
PORT = 5000

# 현재 연결된 클라이언트 소켓 목록
clients = []


def print_clients():
    print(f"현재 접속자 수: {len(clients)}")


def broadcast(message, sender_socket):
    """메시지를 보낸 클라이언트를 제외한 모든 접속자에게 메시지 전송"""
    for client in clients[:]:
        if client != sender_socket:
            try:
                send_message(client, message)
            except Exception:
                if client in clients:
                    clients.remove(client)


def handle_client(client_socket, client_address):
    print(f"클라이언트 처리 시작: {client_address}")
    buffer = ""

    with client_socket:
        while True:
            try:
                buffer, messages, connected = receive_messages(
                    client_socket, buffer
                )

                if not connected:
                    break

                for message in messages:
                    msg_type = message.get("type")

                    # 1. 채팅 처리
                    if msg_type == "chat":
                        # main.py에서 이미 'sender'와 'content'를 실어서 보냅니다.
                        # 서버는 받은 형태 그대로 다른 클라이언트들에게 브로드캐스트합니다.
                        broadcast(message, client_socket)

                    # 2. 종료 처리
                    elif msg_type == "exit":
                        return

            except Exception as error:
                print(f"클라이언트 처리 오류: {client_address}, {error}")
                break

    # 소켓 연결 종료 시 목록에서 정리
    if client_socket in clients:
        clients.remove(client_socket)

    print(f"접속자 목록에서 제거: {client_address}")
    print_clients()


# 서버 실행
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as server_socket:
    # 소켓 재사용 설정 (서버 재시작 시 포트 바인딩 에러 방지)
    server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    server_socket.bind((HOST, PORT))
    server_socket.listen()
    print("채팅 서버가 시작되었습니다. 클라이언트 연결을 대기합니다.")

    while True:
        client_socket, client_address = server_socket.accept()
        clients.append(client_socket)
        print(f"새로운 클라이언트 접속: {client_address}")
        print_clients()

        threading.Thread(
            target=handle_client,
            args=(client_socket, client_address),
            daemon=True,
        ).start()

 

chat_client / client.py

import socket
import threading

from protocol import receive_messages, send_message

HOST = "127.0.0.1"
PORT = 5000


def receive_loop(client_socket):
    buffer = ""

    while True:
        try:
            buffer, messages, connected = receive_messages(client_socket, buffer)

            if not connected:
                print("서버와의 연결이 끊어졌습니다.")
                break

            for message in messages:
                if message.get("type") == "chat":
                    sender = message.get("sender", "unknown")
                    content = message.get("content", "")
                    print(f"\n{sender}: {content}")
                else:
                    print(f"\n수신 메시지: {message}")

        except Exception:
            break


with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as client_socket:
    client_socket.connect((HOST, PORT))

    print("서버에 연결되었습니다.")
    print("전체 채팅을 시작합니다. 종료하려면 exit을 입력하세요.")

    receive_thread = threading.Thread(
        target=receive_loop,
        args=(client_socket,)
    )
    receive_thread.daemon = True
    receive_thread.start()

    while True:
        text = input("")

        if text == "exit":
            send_message(client_socket, {"type": "exit"})
            print("서버와의 연결을 종료합니다.")
            break

        send_message(client_socket, {
            "type": "chat",
            "content": text
        })

print("클라이언트 소켓을 닫았습니다.")

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))  # 바이트로 바꿔 전부 전송


def receive_messages(sock, buffer):  # 25강에서 사용 (24강에서는 호출 안 함)
    data = sock.recv(1024)

    if not data:
        return buffer, [], False

    buffer += data.decode(ENCODING)
    messages = []

    while DELIMITER in buffer:
        raw_message, buffer = buffer.split(DELIMITER, 1)

        if raw_message.strip():
            message = json.loads(raw_message)
            messages.append(message)

    return buffer, messages, True

= 끝 =