18강 구현 중심 ⏱ 약 35분

 

0. 학습 목표

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

더보기

0.1 이번 글에서 다룰 내용

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

 

17강까지 JSON 메시지를 안정적으로 분리해 Python 딕셔너리로 꺼낼 수 있게 됐습니다. 이번 강의에서는 꺼낸 메시지의 type 값을 보고 처리 방식을 나눕니다.

type이 "chat"   → 일반 채팅으로 브로드캐스팅
type이 "system" → 입장·퇴장 같은 서버 알림
type이 "error"  → 잘못된 요청에 대한 오류 응답
type이 "exit"   → 클라이언트 연결 종료 처리

입장 알림·퇴장 알림·일반 채팅·오류 메시지는 서로 다른 의미를 가집니다. 이 메시지들을 모두 같은 방식으로 출력하면 클라이언트가 어떻게 보여 줘야 할지 알 수 없습니다. 이번 강의에서 메시지 종류별 처리 흐름을 완성합니다.

 

이번 강의 코드에는 변경 마커를 사용합니다. 는 17강까지 없던 새 코드, ✏️는 17강에서 형태가 바뀐 부분을 뜻합니다. 6. 최종 정리 코드에는 마커 없이 깨끗한 코드만 싣습니다.

구분 내용
이해할 것 메시지 type에 따라 서버의 처리 방식과 클라이언트의 표시 방식이 달라진다는 것
만들 것 시스템·오류·일반 채팅 메시지를 구분하는 server.pyclient.py
확인할 것 입장·퇴장 알림과 오류 메시지가 일반 채팅과 다르게 표시되는지

 

0.2 이번 강의에서 직접 다루는 구조

이번 강의에서는 17강에서 만든 protocol.py는 그대로 사용하고 서버·클라이언트 두 파일을 수정합니다.

chat_server/
├── protocol.py    ← 17강에서 만든 파일 그대로 사용
└── server.py      ← 이번 강의에서 수정

chat_client/
├── protocol.py    ← 17강에서 복사한 파일 그대로 사용
└── client.py      ← 이번 강의에서 수정

server.py에 이번 강의에서 추가·수정되는 구조입니다.

chat_server/server.py
├── 기존: broadcast(), handle_client(), 메인 루프
├── 추가: send_error(client_socket, content)   → 오류 메시지 전용 함수
└── 수정: handle_client()
    ├── 추가: 접속 시 join 시스템 메시지 broadcast
    ├── 수정: type별 분기 (chat / exit / 빈 메시지 / 알 수 없는 type)
    └── 추가: 종료 후 leave 시스템 메시지 broadcast

 

1. 실습 준비하기

→ 17강 코드가 정상 실행되는지 확인하고, 이번 강의에서 추가할 처리 규칙을 정리합니다.

더보기

먼저 17강 서버가 정상 실행되는지 확인합니다.

python chat_server/server.py
메시지 구분 처리 서버를 시작합니다.
클라이언트 접속 대기 중...

확인 후 Ctrl + C로 서버를 종료합니다. 17강 코드에서는 아래 세 메시지가 모두 같은 방식으로 출력됐습니다. 의미가 전혀 다른데도 화면에서 구분되지 않았던 것입니다.

{"type": "chat",   "content": "안녕하세요"}
{"type": "system", "content": "새 사용자가 입장했습니다."}
{"type": "error",  "content": "지원하지 않는 메시지입니다."}

이번 강의에서 각 상황을 다르게 처리합니다. 서버가 상황별로 무엇을 하는지 먼저 정리합니다.

상황 서버 처리
클라이언트가 접속함 다른 클라이언트에게 system 입장 메시지 전송
chat 메시지 전송 다른 클라이언트에게 일반 채팅으로 전달
빈 채팅 전송 보낸 클라이언트에게만 error 메시지 전송
알 수 없는 type 전송 보낸 클라이언트에게만 error 메시지 전송
exit 전송 목록에서 제거하고 system 퇴장 메시지 전송

클라이언트는 같은 메시지라도 type에 따라 화면 표시 형식을 다르게 합니다.

type 출력 예시
chat ('127.0.0.1', 52344): 안녕하세요
system [시스템] ('127.0.0.1', 52344) 님이 입장했습니다.
error [오류] 빈 메시지는 보낼 수 없습니다.
알 수 없는 타입 [알 수 없는 메시지] {...}

✔ 확인 기준: 17강 서버가 정상 실행되면 이번 강의 수정을 시작할 준비가 된 것입니다. chat_server/protocol.pyserver.py와 같은 폴더에 있는지 확인하세요.

 

2. 서버 - send_error() 함수 추가하기

→ 시스템·오류 메시지 구조를 정의하고, 오류를 요청자에게만 보내는 전용 함수를 추가합니다.

더보기

2.1 시스템 메시지와 오류 메시지 구조

시스템 메시지는 사용자가 직접 입력한 채팅이 아니라, 서버가 상황을 알리기 위해 보내는 메시지입니다.

# 입장 알림
{"type": "system", "content": "('127.0.0.1', 52344) 님이 입장했습니다."}

# 퇴장 알림
{"type": "system", "content": "('127.0.0.1', 52344) 님이 퇴장했습니다."}

오류 메시지는 클라이언트가 잘못된 요청을 했을 때, 서버가 요청한 클라이언트에게만 보내는 메시지입니다.

{"type": "error", "content": "빈 메시지는 보낼 수 없습니다."}

오류를 broadcast()로 보내면 모든 클라이언트에게 전달됩니다. 잘못한 사람에게만 알려야 하므로 별도 함수가 필요합니다.

 

2.2 broadcast() 수정과 send_error() 추가하기

chat_server/server.py를 열고 broadcast() 시그니처를 바꾼 뒤, 아래에 send_error()를 추가합니다.

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=None):     # ✏️ sender_socket → sender_socket=None (기본값 추가)
    for client in clients[:]:                   # 순회 중 목록이 바뀔 수 있어 복사본 사용
        if client != sender_socket:
            try:
                send_message(client, message)
            except Exception:                   # 17강과 동일 — except Exception 유지
                if client in clients:
                    clients.remove(client)

def send_error(client_socket, content):         # ➕ 오류 메시지 전용 함수
    error_message = {
        "type": "error",
        "content": content
    }
    send_message(client_socket, error_message)  # 요청한 클라이언트에게만 전송

broadcast()의 두 번째 인자를 sender_socket=None으로 바꾼 이유가 있습니다. 입장·퇴장 시스템 메시지는 보낸 사람을 제외할 필요 없이 모두에게 보내야 합니다. 기본값이 None이면 broadcast(message)처럼 두 번째 인자 없이도 호출할 수 있습니다.

예외 처리는 17강과 똑같이 except Exception을 유지합니다. 모든 예외를 잡는 except: (bare except)로 되돌리면 Ctrl + C 같은 종료 신호까지 함께 삼켜 버리기 때문입니다.

✔ 확인 기준: send_error()broadcast()의 차이를 "send_error는 요청자에게만, broadcast는 나머지 전체에게"로 설명할 수 있으면 완료. send_error() 안에서 broadcast()가 아니라 send_message()를 직접 호출하는지 확인하세요.

 

3. 서버 - handle_client()에 type 분기 완성하기

→ 접속 시 입장 메시지, type별 분기, 종료 후 퇴장 메시지를 단계적으로 추가합니다.

더보기

3.1 접속 시 입장 메시지 추가하기

handle_client()에서 with client_socket: 블록이 시작되기 전에 입장 메시지를 보냅니다. 접속 직후, 아직 메시지를 받기 전에 다른 클라이언트들에게 알리는 시점입니다.

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

    buffer = ""
    address_text = str(client_address)           # 이후 메시지에서 반복 사용할 주소 문자열

    join_message = {                             # ➕ 입장 알림 시스템 메시지
        "type": "system",
        "content": f"{address_text} 님이 입장했습니다."
    }
    broadcast(join_message, client_socket)       # ➕ 본인 제외 전체에게 입장 알림

    with client_socket:
        while True:
            ...

broadcast(join_message, client_socket)에서 두 번째 인자로 본인 소켓을 넘기므로, 방금 접속한 사람에게는 입장 알림이 가지 않고 기존 접속자들에게만 전달됩니다.

 

3.2 type별 분기 추가하기

while True 안에서 type을 기준으로 처리를 나눕니다. 17강에서는 exit을 받으면 곧바로 return했지만, 이번에는 break로 바꿉니다. for문 안에서 break를 만나면 for문만 빠져나오고 while True는 계속 돌기 때문에, while 밖에서 any()로 한 번 더 확인해 while까지 탈출합니다.

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

    buffer = ""
    address_text = str(client_address)

    join_message = {
        "type": "system",
        "content": f"{address_text} 님이 입장했습니다."
    }
    broadcast(join_message, client_socket)

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

                if not connected:
                    print(f"클라이언트 연결 끊김: {client_address}")
                    break

                for message in messages:
                    print(f"{client_address} 메시지: {message}")

                    message_type = message.get("type")       # type 값을 먼저 꺼내 분기에 사용

                    if message_type == "exit":
                        print(f"클라이언트 종료 요청: {client_address}")
                        break                                # ✏️ 17강 return → break (정리 코드 실행을 위해)

                    if message_type == "chat":
                        content = message.get("content", "").strip()

                        if not content:                      # ➕ 빈 채팅이면 요청자에게만 오류 응답
                            send_error(client_socket, "빈 메시지는 보낼 수 없습니다.")
                            continue

                        broadcast_message = {
                            "type": "chat",
                            "sender": address_text,
                            "content": content
                        }
                        broadcast(broadcast_message, client_socket)
                        continue

                    # 알 수 없는 type → 요청자에게만 오류 응답
                    send_error(client_socket, f"지원하지 않는 메시지 타입입니다: {message_type}")

                if any(message.get("type") == "exit" for message in messages):   # ➕ for의 break는 for만 탈출 → while도 탈출
                    break

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

exit을 받았을 때 return으로 바로 함수를 끝내고 싶을 수 있습니다. 하지만 return을 쓰면 with 블록 아래에 있는 퇴장 메시지 전송과 clients.remove() 정리 코드가 실행되지 않습니다. 정리 코드가 반드시 실행되도록 break로 반복문을 빠져나와 with 블록 아래까지 흐름이 내려오게 합니다. 이것이 return 대신 break + any() 구조를 쓰는 이유입니다.

 

3.3 종료 후 퇴장 메시지 추가하기

with 블록이 끝난 뒤, clients.remove() 전에 퇴장 메시지를 브로드캐스팅합니다. 이미 연결이 끊긴 본인은 목록에서 빠졌으므로 남은 사람들에게만 전달됩니다. 메인 루프는 17강과 동일하게 유지됩니다.

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=None):
    for client in clients[:]:
        if client != sender_socket:
            try:
                send_message(client, message)
            except Exception:
                if client in clients:
                    clients.remove(client)

def send_error(client_socket, content):
    error_message = {
        "type": "error",
        "content": content
    }
    send_message(client_socket, error_message)

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

    buffer = ""
    address_text = str(client_address)

    join_message = {
        "type": "system",
        "content": f"{address_text} 님이 입장했습니다."
    }
    broadcast(join_message, client_socket)

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

                if not connected:
                    print(f"클라이언트 연결 끊김: {client_address}")
                    break

                for message in messages:
                    print(f"{client_address} 메시지: {message}")

                    message_type = message.get("type")

                    if message_type == "exit":
                        print(f"클라이언트 종료 요청: {client_address}")
                        break

                    if message_type == "chat":
                        content = message.get("content", "").strip()

                        if not content:
                            send_error(client_socket, "빈 메시지는 보낼 수 없습니다.")
                            continue

                        broadcast_message = {
                            "type": "chat",
                            "sender": address_text,
                            "content": content
                        }
                        broadcast(broadcast_message, client_socket)
                        continue

                    send_error(client_socket, f"지원하지 않는 메시지 타입입니다: {message_type}")

                if any(message.get("type") == "exit" for message in messages):
                    break

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

    # with 블록 종료 후 — 소켓은 이미 닫혔고, 정리 시작
    if client_socket in clients:
        clients.remove(client_socket)

    leave_message = {                            # ➕ 퇴장 알림 시스템 메시지
        "type": "system",
        "content": f"{address_text} 님이 퇴장했습니다."
    }
    broadcast(leave_message)                     # ➕ 남은 모든 클라이언트에게 퇴장 알림

    print(f"접속자 목록에서 제거: {client_address}")
    print_clients()
    print(f"클라이언트 처리 종료: {client_address}")


with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as server_socket:
    server_socket.bind((HOST, PORT))
    server_socket.listen()

    print("시스템/오류 메시지 처리 서버를 시작합니다.")
    print("클라이언트 접속 대기 중...")

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

        clients.append(client_socket)
        print(f"접속자 목록에 추가: {client_address}")
        print_clients()

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

✔ 확인 기준: python chat_server/server.py 실행 후 "시스템/오류 메시지 처리 서버를 시작합니다." 출력과 함께 대기 상태가 되면 완료. join_message broadcast()with 블록 시작 전에, leave_message broadcast()with 블록 끝난 뒤에 있는지 확인하세요.

 

4. 클라이언트 - type별 출력 분기 추가하기

→ receive_loop() 안에서 수신한 메시지 type에 따라 출력 형식을 나눕니다.

더보기

17강에서는 chat이 아니면 모두 "수신 메시지"로 똑같이 출력했습니다. 이번에는 if/elifsystem·error를 따로 표시합니다. 메인 흐름(연결·스레드·입력 루프)은 17강과 동일합니다.

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:
                message_type = message.get("type")          # type별로 출력 형식을 나눈다

                if message_type == "chat":
                    sender = message.get("sender", "unknown")
                    content = message.get("content", "")
                    print(f"\n{sender}: {content}")

                elif message_type == "system":              # ➕ 시스템 메시지 별도 출력
                    content = message.get("content", "")
                    print(f"\n[시스템] {content}")

                elif message_type == "error":               # ➕ 오류 메시지 별도 출력
                    content = message.get("content", "")
                    print(f"\n[오류] {content}")

                else:
                    print(f"\n[알 수 없는 메시지] {message}")

        except Exception:                                   # 17강과 동일 — 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("클라이언트 소켓을 닫았습니다.")

달라진 부분을 17강과 나란히 정리하면 다음과 같습니다.

메시지 type 이전(17강) 이번(18강)
chat sender: content sender: content (동일)
system 수신 메시지: {...} [시스템] content
error 수신 메시지: {...} [오류] content

수신 루프의 except도 17강과 동일하게 except Exception을 유지합니다. except: (bare except)로 되돌리면 Ctrl + C 종료 신호까지 잡아먹어 클라이언트가 깔끔하게 종료되지 않습니다.

✔ 확인 기준: 서버가 실행 중일 때 python chat_client/client.py를 실행하면 "서버에 연결되었습니다." 출력과 함께 입력 대기 상태가 되면 완료. receive_loop() 안에서 if/elifmessage_type을 분기하는지 확인하세요.

 

5. 실행 결과 확인하기

→ 입장·일반 채팅·오류·퇴장이 각각 다른 형식으로 표시되는지 순서대로 확인합니다.

더보기

5.1 입장 메시지 확인하기

터미널 1에서 서버를 실행합니다.

python chat_server/server.py
시스템/오류 메시지 처리 서버를 시작합니다.
클라이언트 접속 대기 중...

터미널 2와 3에서 각각 클라이언트를 실행합니다. 두 번째 클라이언트가 접속하면 먼저 접속해 있던 클라이언트 A 화면에 시스템 메시지가 표시됩니다.

[시스템] ('127.0.0.1', 52345) 님이 입장했습니다.

 

5.2 일반 채팅과 오류 메시지 확인하기

클라이언트 A에서 메시지를 입력하면 클라이언트 B 화면에 일반 채팅으로 표시됩니다.

('127.0.0.1', 52344): 안녕하세요

이번에는 클라이언트 A에서 아무 내용 없이 Enter만 입력합니다. A 화면에만 오류 메시지가 표시되고, 빈 메시지는 클라이언트 B에게 전달되지 않습니다.

[오류] 빈 메시지는 보낼 수 없습니다.

 

5.3 퇴장 메시지와 흔한 오류 확인하기

클라이언트 A에서 exit을 입력하면 A는 종료되고, 클라이언트 B 화면에 퇴장 시스템 메시지가 표시됩니다.

[시스템] ('127.0.0.1', 52344) 님이 퇴장했습니다.

입장·채팅·오류·퇴장이 서로 다른 형식으로 출력되고, 오류 메시지가 A에게만 표시되고 B에게는 전달되지 않으면 이번 강의는 성공입니다. 오류가 나오면 어느 단계에서 어긋났는지 아래 표로 점검합니다.

오류 상황 원인 및 해결 방법
시스템 메시지가 보이지 않음 접속·퇴장 시 broadcast()를 호출하지 않았음. join_message·leave_message를 만들어 broadcast()로 전달한다
오류 메시지가 모든 클라이언트에게 보임 오류를 broadcast()로 보냈음. send_error()로 요청한 클라이언트에게만 보낸다
빈 메시지가 전체 채팅으로 전달됨 content.strip() 검사가 없음. chat 분기 안에 if not content: 조건을 추가한다
exit 후 접속자 수가 줄지 않음 return으로 함수가 바로 끝나 clients.remove()가 실행되지 않았음. break + any() 구조로 with 블록 아래까지 내려오게 한다
입장 메시지가 접속한 본인에게도 보임 broadcast(join_message)sender_socket 인자를 빠뜨렸음. broadcast(join_message, client_socket)으로 본인을 제외한다

✔ 확인 기준: 네 가지 메시지 타입이 모두 다른 형식으로 표시되면 완료. 서버에서 send_error()broadcast()가 각각 적절한 상황에 호출되는지, 클라이언트 receive_loop()에서 elifsystemerror를 구분하는지 확인하세요.

 

6. 최종 코드 정리하기

→ 이번 강의에서 완성한 두 파일의 전체 코드를 한곳에 정리합니다. protocol.py는 17강 코드 그대로입니다.

더보기

6.1 chat_server/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=None):
    for client in clients[:]:
        if client != sender_socket:
            try:
                send_message(client, message)
            except Exception:
                if client in clients:
                    clients.remove(client)

def send_error(client_socket, content):
    error_message = {
        "type": "error",
        "content": content
    }
    send_message(client_socket, error_message)

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

    buffer = ""
    address_text = str(client_address)

    join_message = {
        "type": "system",
        "content": f"{address_text} 님이 입장했습니다."
    }
    broadcast(join_message, client_socket)

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

                if not connected:
                    print(f"클라이언트 연결 끊김: {client_address}")
                    break

                for message in messages:
                    print(f"{client_address} 메시지: {message}")

                    message_type = message.get("type")

                    if message_type == "exit":
                        print(f"클라이언트 종료 요청: {client_address}")
                        break

                    if message_type == "chat":
                        content = message.get("content", "").strip()

                        if not content:
                            send_error(client_socket, "빈 메시지는 보낼 수 없습니다.")
                            continue

                        broadcast_message = {
                            "type": "chat",
                            "sender": address_text,
                            "content": content
                        }
                        broadcast(broadcast_message, client_socket)
                        continue

                    send_error(client_socket, f"지원하지 않는 메시지 타입입니다: {message_type}")

                if any(message.get("type") == "exit" for message in messages):
                    break

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

    if client_socket in clients:
        clients.remove(client_socket)

    leave_message = {
        "type": "system",
        "content": f"{address_text} 님이 퇴장했습니다."
    }
    broadcast(leave_message)

    print(f"접속자 목록에서 제거: {client_address}")
    print_clients()
    print(f"클라이언트 처리 종료: {client_address}")


with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as server_socket:
    server_socket.bind((HOST, PORT))
    server_socket.listen()

    print("시스템/오류 메시지 처리 서버를 시작합니다.")
    print("클라이언트 접속 대기 중...")

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

        clients.append(client_socket)
        print(f"접속자 목록에 추가: {client_address}")
        print_clients()

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

 

6.2 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:
                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":
                    content = message.get("content", "")
                    print(f"\n[시스템] {content}")

                elif message_type == "error":
                    content = message.get("content", "")
                    print(f"\n[오류] {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("클라이언트 소켓을 닫았습니다.")

 

6.3 최종 확인 표

확인할 코드 위치 의미
def send_error(client_socket, content) server.py 오류 메시지를 요청자에게만 전송
broadcast(message, sender_socket=None) server.py 기본값 None — 인자 생략 시 전체 전송
broadcast(join_message, client_socket) handle_client() 입장 알림 — 본인 제외 전달
content.strip() + if not content: handle_client() 빈 메시지 검사
break + any(... "exit" ...) handle_client() for + while 이중 탈출(정리 코드 실행)
broadcast(leave_message) handle_client() 퇴장 알림 — with 블록 밖에서 실행
elif message_type == "system" receive_loop() 시스템 메시지 별도 출력
elif message_type == "error" receive_loop() 오류 메시지 별도 출력

→ 다음 강의 (18.5강 체크포인트): 지금까지 만든 코드 전체를 점검합니다. 서버 실행 순서·스레드 처리·접속자 목록·브로드캐스팅·JSON 메시지 구분·시스템/오류 메시지 처리까지 전체 흐름을 다시 확인하고, 이후 GUI 단계로 넘어갈 준비를 합니다.