29강. 귓속말 메시지 전송 구현

0. 학습 목표
→ whisper 메시지 타입을 추가해 선택한 상대 한 명에게만 메시지를 전달합니다.
0.1 이번 글에서 다룰 내용
이번 글은 구현 중심 강의입니다.
28강까지 사용자 목록에서 상대를 선택하고 selected_user에 저장할 수 있었습니다.
하지만 그 선택은 화면에만 머물러 있었습니다.
이번 강의에서는 새 메시지 타입 whisper를 추가해,
selected_user를 target으로 삼아 선택한 상대에게만 메시지를 보냅니다.
서버는 target과 주소가 일치하는 클라이언트를 찾아 그 한 명에게만 전달합니다.

# 클라이언트
GUI에서 사용자 선택 (28강 selected_user)
↓
입력창에 메시지 작성 → 귓속말 버튼 클릭
↓
{"type": "whisper", "target": ..., "content": ...} 전송
↓
# 서버
서버가 target 주소로 클라이언트 찾기 → 그 소켓에만 전달
↓
받는 사람 화면에 [귓속말 받음] 표시 ← 이번 강의의 성공 지점
핵심 성공 기준은 선택한 상대에게만 메시지가 표시되고, 관계없는 다른 사용자에게는 보이지 않는 것입니다.
| 구분 | 내용 |
| 이해할 것 | 전체 채팅과 귓속말은 메시지 타입과 전달 대상이 다르다는 점 |
| 만들 것 | 서버 find_client_by_identifier() · send_whisper() + GUI 귓속말 버튼 · on_whisper_clicked() |
| 확인할 것 | 선택한 상대에게만 메시지가 표시되고 다른 사용자에게는 보이지 않는지 |
0.2 이번 강의에서 직접 다루는 구조
이번 강의에서는 서버와 GUI 클라이언트를 함께 수정합니다. protocol.py는 그대로 사용합니다.
chat_server/
├── protocol.py ← 유지
└── server.py ← ✏️ 이번 강의에서 수정 (귓속말 전달)
chat_client/
├── protocol.py ← 유지
├── client.py ← 유지
└── main.py ← ✏️ 이번 강의에서 수정 (귓속말 버튼·전송)
(➕ 새로 생성 · ✏️ 수정 · 표시 없음은 변경 없음)
이번 강의에서 다루는 메시지 타입입니다. whisper가 새로 추가됩니다.
| 메시지 타입 | 역할 |
chat |
전체 채팅 메시지 — 모두에게 전달 |
whisper |
귓속말 — 특정 사용자 한 명에게만 전달 (이번 강의 추가) |
error |
귓속말 실패 등 오류 안내 |
1. 실습 준비하기
→ 전체 채팅과 귓속말의 차이를 코드 수준으로 확인하고, 이번 강의에서 정할 규칙을 정리합니다.
1.1 전체 채팅과 귓속말의 차이
두 메시지의 차이는 형태와 전달 범위입니다. 귓속말에는 받을 대상을 지정하는 target 키가 있습니다.
# 전체 채팅 — 모두에게
{"type": "chat", "content": "전체 채팅입니다."}
# 귓속말 — target 한 명에게만
{"type": "whisper", "target": "('127.0.0.1', 52345)", "content": "귓속말입니다."}
전체 채팅: A → 서버 → B, C 모두 전달
귓속말: A → 서버 → B에게만 전달, C에게는 전달하지 않음
1.2 이번 강의의 귓속말 규칙
| 규칙 | 내용 |
| 대상 선택 | 28강에서 만든 selected_user를 target으로 사용 |
| 서버 전달 | target과 client["address"]가 일치하는 클라이언트에게만 전송 |
| 보낸 사람 표시 | [귓속말 보냄] 상대: 내용 — GUI에서 직접 표시 |
| 받는 사람 표시 | [귓속말 받음] 보낸사람: 내용 — 서버가 전달한 whisper 메시지를 GUI가 표시 |
보낸 사람 화면에는 GUI가 직접 [귓속말 보냄]을 표시하고, 서버에서 별도로 "보냈습니다" 시스템 메시지를 보내지 않습니다. 한 동작에 두 줄이 겹치는 중복을 피하기 위해서입니다.
✔ 확인 기준: 귓속말은 broadcast()가 아니라 target 한 명의 소켓에만 보내며, selected_user가 target이 된다는 연결을 말할 수 있으면 완료.
2. 서버 - 귓속말 전송 함수 만들기
→ target으로 클라이언트를 찾는 함수와 그 한 명에게만 보내는 함수를 추가합니다.
2.1 대상 찾기 함수
27강에서 clients를 {"socket", "address"} 딕셔너리 리스트로 바꿔 둔 덕분에, 식별자 문자열로 대상 클라이언트를 찾을 수 있습니다.

def find_client_by_identifier(identifier): # ➕ 주소 또는 닉네임으로 클라이언트 찾기
for client in clients:
if client["address"] == identifier: # 주소 문자열과 비교
return client
if client.get("nickname") == identifier: # 닉네임이 있으면 닉네임과도 비교
return client
return None
27강 기본 코드 기준(주소 문자열)으로 구현하되, 27강 과제로 닉네임을 도입했다면 client["nickname"]과 비교하도록 함께 확인하는 방식을 사용합니다.

27강 과제로 닉네임을 추가하지 않았다면 client.get("nickname")은 항상 None을 반환하므로 동작에 문제가 없습니다. 닉네임을 추가한 경우에는 목록에 닉네임이 표시되어 selected_user가 닉네임 문자열이 되므로, 이 두 번째 비교 줄이 필요합니다.
2.2 귓속말 전송 함수
대상을 찾았다면 그 소켓으로만 메시지를 보냅니다. 성공 여부를 True/False로 돌려주어, 호출한 쪽이 보낸 사람에게 실패 결과를 알려 줄 수 있게 합니다.
def send_whisper(sender_identifier, target_identifier, content): # ➕ 대상 한 명에게만 전송
target_client = find_client_by_identifier(target_identifier)
if target_client is None: # 대상이 없으면 실패
return False
send_message(target_client["socket"], { # broadcast가 아니라 그 소켓에만
"type": "whisper",
"sender": sender_identifier,
"content": content
})
return True
✔ 확인 기준: send_whisper()가 broadcast()를 쓰지 않고 target_client["socket"] 하나에만 보내며, 대상이 없을 때 False를 반환하면 완료.
💡 강사 팁 — "왜 서버가 보낸 사람에게 확인 메시지를 안 보내나요?"
귓속말을 보낸 뒤 보낸 사람 화면에 [귓속말 보냄]을 표시하는 방법은 두 가지입니다.
| 방식 | 흐름 | 문제점 |
| 서버가 보내는 방식 | 전송 성공 → 서버가 system 메시지를 보낸 사람에게 발송 |
화면에 두 줄이 겹침 — GUI의 append와 서버 메시지가 동시에 표시됨 |
| GUI가 직접 표시 (현재 방식) | 전송 직후 on_whisper_clicked()에서 바로 append |
없음 — 보낸 사람만 볼 내용이므로 GUI가 처리하는 것이 자연스럽습니다 |
"보낸 사람만 볼 메시지는 GUI가 직접 처리하고, 다른 사람에게 전달할 메시지만 서버를 거친다"는 원칙을 기억하면 됩니다. 30강에서 귓속말 오류 처리를 설계할 때도 같은 원칙이 적용됩니다.
3. 서버 - whisper 메시지 처리 연결하기
→ handle_client()의 메시지 분기에 whisper 처리를 추가합니다.
handle_client()의 메시지 타입 분기에서 chat 처리 옆에 whisper 분기를 추가합니다. 대상과 내용을 검사한 뒤 send_whisper()로 전달하고, 실패 시에만 보낸 사람에게 오류를 알립니다.
if message_type == "whisper": # ➕ 귓속말 처리
target = message.get("target", "")
content = message.get("content", "").strip()
if not target: # 대상 미선택
send_error(client_socket, "귓속말 대상을 선택하세요.")
continue
if not content: # 빈 내용
send_error(client_socket, "귓속말 내용을 입력하세요.")
continue
success = send_whisper(address_text, target, content)
if not success: # 실패 시에만 오류 알림
send_error(client_socket, "귓속말 대상을 찾을 수 없습니다.")
continue
성공 시에는 서버에서 별도 메시지를 보내지 않습니다. 보낸 사람 GUI가 직접 [귓속말 보냄]을 표시하기 때문입니다. 서버가 성공 메시지까지 보내면 보낸 사람 화면에 두 줄이 중복됩니다. 서버 귓속말 처리 순서를 정리하면 다음과 같습니다.
whisper 메시지 수신
↓
target 확인 → content 확인
↓
find_client_by_identifier(target)
↓
대상 있음 → 그 소켓에만 whisper 전송 (보낸 사람에겐 별도 메시지 없음)
대상 없음 → 보낸 사람에게 error 전송 ← 이번 강의의 성공 지점
✔ 확인 기준: handle_client() 분기에 message_type == "whisper"가 있고, 성공 시 서버가 추가 메시지를 보내지 않으며, 마지막에 continue가 있으면 완료.
4. 클라이언트 - 귓속말 버튼과 전송 연결하기
→ 귓속말 버튼을 배치하고, on_whisper_clicked()와 수신 표시를 연결합니다.
4.1 귓속말 버튼 추가하기
입력 영역에 "귓속말" 버튼을 더합니다. 연결 상태에 따라 버튼을 켜고 꺼야 하므로 set_connected_state()에도 한 줄 추가합니다.
self.send_button = QPushButton("보내기")
self.whisper_button = QPushButton("귓속말") # ➕ 귓속말 버튼
input_layout = QHBoxLayout()
input_layout.addWidget(self.message_input)
input_layout.addWidget(self.send_button)
input_layout.addWidget(self.whisper_button) # ➕ 입력 영역에 배치
self.send_button.clicked.connect(self.on_send_clicked)
self.message_input.returnPressed.connect(self.on_send_clicked)
self.whisper_button.clicked.connect(self.on_whisper_clicked) # ➕ 클릭 → 귓속말
def set_connected_state(self, connected):
self.connect_button.setEnabled(not connected)
self.message_input.setEnabled(connected)
self.send_button.setEnabled(connected)
self.whisper_button.setEnabled(connected) # ➕ 귓속말 버튼도 함께 관리
if connected:
self.status_label.setText("상태: 서버에 연결됨")
else:
self.status_label.setText("상태: 서버에 연결되지 않음")
self.user_list.clear()
self.selected_user = None
self.selected_user_label.setText("선택한 상대: 없음")
4.2 귓속말 전송 함수 만들기
on_whisper_clicked()는 on_send_clicked()와 비슷하지만, 28강에서 만든 selected_user가 있는지 먼저 확인하고 target에 넣습니다. 전송에 성공하면 GUI에서 직접 [귓속말 보냄]을 표시합니다. 서버가 별도로 확인 메시지를 보내지 않으므로 이 한 줄이 유일한 전송 확인 표시입니다.
def on_whisper_clicked(self): # ➕ 귓속말 보내기
text = self.message_input.text().strip()
if not text:
return
if self.client_socket is None:
self.chat_display.append("[오류] 서버에 먼저 접속하세요.")
return
if self.selected_user is None: # ➕ 대상 선택 확인
self.chat_display.append("[오류] 귓속말 상대를 선택하세요.")
return
try:
send_message(self.client_socket, {
"type": "whisper",
"target": self.selected_user, # 선택한 상대가 target
"content": text
})
self.chat_display.append(f"[귓속말 보냄] {self.selected_user}: {text}")
self.message_input.clear()
except Exception as error:
self.chat_display.append(f"[오류] 귓속말 전송 실패: {error}")
self.disconnect_from_server()
4.3 귓속말 수신 표시 추가하기
display_message()에 whisper 분기를 추가해, 받은 귓속말을 전체 채팅과 구분되게 표시합니다.
elif message_type == "whisper": # ➕ 귓속말 수신 표시
sender = message.get("sender", "unknown")
content = message.get("content", "")
self.chat_display.append(f"[귓속말 받음] {sender}: {content}")
| 메시지 타입 | GUI 표시 |
chat |
보낸사람: 내용 |
whisper (받음) |
[귓속말 받음] 보낸사람: 내용 |
whisper (보냄) |
[귓속말 보냄] 상대: 내용 — GUI에서 직접 표시 |
system |
[시스템] 내용 |
error |
[오류] 내용 |
✔ 확인 기준: 사용자를 선택한 뒤 귓속말 버튼을 누르면 내 화면에 [귓속말 보냄]이 표시되고, 선택한 상대 GUI에만 [귓속말 받음]이 표시되면 완료.
5. 실행 결과 확인하기
→ GUI 클라이언트 3개를 실행해 귓속말이 대상에게만 전달되는지 확인합니다.

귓속말이 대상에게만 가는지 확인하려면 클라이언트를 3개 띄우는 것이 좋습니다. 서버 1개, GUI 클라이언트 3개를 각 터미널에서 실행합니다.
python chat_server/server.py
python chat_client/main.py # 터미널 2 — 보내는 사람
python chat_client/main.py # 터미널 3 — 받는 사람
python chat_client/main.py # 터미널 4 — 무관한 사람
세 GUI 모두 "서버 접속"을 누른 뒤, 첫 번째 GUI에서 두 번째 사용자를 목록에서 선택하고 메시지를 입력한 다음 "귓속말" 버튼을 누릅니다.
보낸 사람(첫 번째 GUI) 화면:
[귓속말 보냄] ('127.0.0.1', 52345): 이건 귓속말입니다
받는 사람(두 번째 GUI) 화면:
[귓속말 받음] ('127.0.0.1', 52344): 이건 귓속말입니다
세 번째 GUI에는 아무것도 표시되지 않아야 합니다. 이것이 귓속말이 전체 채팅과 다른 점입니다.
✔ 확인 기준: 선택한 대상에게만 귓속말이 표시되고 세 번째 클라이언트에는 보이지 않으면 완료. 모두에게 보이면 서버가 broadcast()가 아닌 send_whisper()로 보내는지 확인하세요.
5.1 실행이 안 될 때 확인할 것
| 오류 상황 | 원인 및 해결 방법 |
| 귓속말이 모든 클라이언트에게 보인다 | broadcast()로 보냈습니다. send_whisper()로 대상 소켓에만 보냅니다 |
| 귓속말 버튼이 비활성화돼 있다 | set_connected_state()에 self.whisper_button.setEnabled(connected)를 추가합니다 |
| 상대를 선택했는데 "대상 없음" 오류 | target 문자열과 client["address"] 또는 client["nickname"] 형식이 다릅니다. 목록에 표시된 문자열과 서버 저장 값이 일치하는지 확인합니다 |
| 내 화면엔 보이는데 상대에게 안 간다 | handle_client()에 message_type == "whisper" 분기가 없습니다. 서버 분기를 확인합니다 |
| 선택 없이 귓속말 버튼을 눌러도 반응이 없다 | if self.selected_user is None: 조건으로 안내 메시지를 표시합니다 |
6. 최종 코드 정리하기
→ 이번 강의에서 완성한 server.py와 chat_client/main.py 전체 코드를 정리합니다.
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 get_user_list():
return [client["address"] for client in clients]
def broadcast_user_list():
user_list_message = {
"type": "user_list",
"users": get_user_list()
}
for client in clients[:]:
try:
send_message(client["socket"], user_list_message)
except Exception:
pass
def broadcast(message, sender_socket=None):
for client in clients[:]:
client_socket = client["socket"]
if client_socket != sender_socket:
try:
send_message(client_socket, message)
except Exception:
pass
def send_error(client_socket, content):
send_message(client_socket, {
"type": "error",
"content": content
})
def remove_client(client_socket):
for client in clients[:]:
if client["socket"] == client_socket:
clients.remove(client)
return client["address"]
return None
def find_client_by_identifier(identifier): # ➕ 주소 또는 닉네임으로 찾기
for client in clients:
if client["address"] == identifier:
return client
if client.get("nickname") == identifier: # 닉네임 도입 시 함께 비교
return client
return None
def send_whisper(sender_identifier, target_identifier, content): # ➕
target_client = find_client_by_identifier(target_identifier)
if target_client is None:
return False
send_message(target_client["socket"], {
"type": "whisper",
"sender": sender_identifier,
"content": content
})
return True
def handle_client(client_socket, client_address):
print(f"클라이언트 처리 시작: {client_address}")
buffer = ""
address_text = str(client_address)
try:
broadcast({
"type": "system",
"content": f"{address_text} 님이 입장했습니다."
}, client_socket)
broadcast_user_list()
while True:
try:
buffer, messages, connected = receive_messages(client_socket, buffer)
if not connected:
print(f"클라이언트 연결 끊김: {client_address}")
break
exit_requested = False
for message in messages:
print(f"{client_address} 메시지: {message}")
message_type = message.get("type")
if message_type == "exit":
print(f"클라이언트 종료 요청: {client_address}")
exit_requested = True
break
if message_type == "chat":
content = message.get("content", "").strip()
if not content:
send_error(client_socket, "빈 메시지는 보낼 수 없습니다.")
continue
broadcast({
"type": "chat",
"sender": address_text,
"content": content
}, client_socket)
continue
if message_type == "whisper":
target = message.get("target", "")
content = message.get("content", "").strip()
if not target:
send_error(client_socket, "귓속말 대상을 선택하세요.")
continue
if not content:
send_error(client_socket, "귓속말 내용을 입력하세요.")
continue
success = send_whisper(address_text, target, content)
if not success:
send_error(client_socket, "귓속말 대상을 찾을 수 없습니다.")
continue
send_error(client_socket, f"지원하지 않는 메시지 타입입니다: {message_type}")
if exit_requested:
break
except Exception as error:
print(f"클라이언트 처리 오류: {client_address}, {error}")
break
finally:
try:
client_socket.close()
except Exception:
pass
removed_address = remove_client(client_socket)
broadcast({
"type": "system",
"content": f"{address_text} 님이 퇴장했습니다."
}, client_socket)
broadcast_user_list()
print(f"접속자 목록에서 제거: {removed_address}")
print_clients()
print(f"클라이언트 처리 종료: {client_address}")
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("서버를 시작합니다.")
print("클라이언트 접속 대기 중...")
while True:
client_socket, client_address = server_socket.accept()
address_text = str(client_address)
print(f"클라이언트 접속: {client_address}")
clients.append({
"socket": client_socket,
"address": address_text
})
print(f"접속자 목록에 추가: {client_address}")
print_clients()
client_thread = threading.Thread(
target=handle_client,
args=(client_socket, client_address)
)
client_thread.daemon = True
client_thread.start()
6.2 chat_client/main.py
import socket
import sys
import threading
from PySide6.QtCore import Signal, Slot
from PySide6.QtWidgets import (
QApplication,
QWidget,
QTextEdit,
QLineEdit,
QPushButton,
QLabel,
QListWidget,
QVBoxLayout,
QHBoxLayout,
)
from protocol import receive_messages, send_message
HOST = "127.0.0.1"
PORT = 5000
class ChatWindow(QWidget):
message_received = Signal(dict)
def __init__(self):
super().__init__()
self.client_socket = None
self.receive_thread = None
self.running = False
self.selected_user = None
self.setWindowTitle("채팅")
self.resize(700, 480)
self.status_label = QLabel("상태: 서버에 연결되지 않음")
self.connect_button = QPushButton("서버 접속")
self.chat_display = QTextEdit()
self.chat_display.setReadOnly(True)
self.chat_display.setPlaceholderText("채팅 메시지가 여기에 표시됩니다.")
self.user_list = QListWidget()
self.user_list.setMaximumWidth(150)
self.selected_user_label = QLabel("선택한 상대: 없음")
self.message_input = QLineEdit()
self.message_input.setPlaceholderText("메시지를 입력하세요")
self.send_button = QPushButton("보내기")
self.whisper_button = QPushButton("귓속말")
top_layout = QHBoxLayout()
top_layout.addWidget(self.status_label)
top_layout.addWidget(self.connect_button)
user_layout = QVBoxLayout()
user_layout.addWidget(self.selected_user_label)
user_layout.addWidget(self.user_list)
content_layout = QHBoxLayout()
content_layout.addWidget(self.chat_display)
content_layout.addLayout(user_layout)
input_layout = QHBoxLayout()
input_layout.addWidget(self.message_input)
input_layout.addWidget(self.send_button)
input_layout.addWidget(self.whisper_button)
main_layout = QVBoxLayout()
main_layout.addLayout(top_layout)
main_layout.addLayout(content_layout)
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)
self.whisper_button.clicked.connect(self.on_whisper_clicked)
self.message_received.connect(self.display_message)
self.user_list.itemClicked.connect(self.on_user_selected)
self.set_connected_state(False)
def set_connected_state(self, connected):
self.connect_button.setEnabled(not connected)
self.message_input.setEnabled(connected)
self.send_button.setEnabled(connected)
self.whisper_button.setEnabled(connected)
if connected:
self.status_label.setText("상태: 서버에 연결됨")
else:
self.status_label.setText("상태: 서버에 연결되지 않음")
self.user_list.clear()
self.selected_user = None
self.selected_user_label.setText("선택한 상대: 없음")
def connect_to_server(self):
try:
self.client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.client_socket.connect((HOST, PORT))
self.running = True
self.receive_thread = threading.Thread(target=self.receive_loop)
self.receive_thread.daemon = True
self.receive_thread.start()
self.set_connected_state(True)
self.chat_display.append("[시스템] 서버에 연결되었습니다.")
except ConnectionRefusedError:
self.client_socket = None
self.connect_button.setEnabled(True)
self.status_label.setText("상태: 서버 연결 실패")
self.chat_display.append("[오류] 서버가 실행 중인지 확인하세요.")
except Exception as error:
self.client_socket = None
self.connect_button.setEnabled(True)
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 OSError:
break
except Exception as error:
self.message_received.emit({
"type": "error",
"content": f"메시지 수신 중 오류가 발생했습니다: {error}"
})
break
self.running = False
self.message_received.emit({
"type": "state",
"connected": 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
if text == "exit":
self.disconnect_from_server()
self.message_input.clear()
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}")
self.disconnect_from_server()
def on_whisper_clicked(self):
text = self.message_input.text().strip()
if not text:
return
if self.client_socket is None:
self.chat_display.append("[오류] 서버에 먼저 접속하세요.")
return
if self.selected_user is None:
self.chat_display.append("[오류] 귓속말 상대를 선택하세요.")
return
try:
send_message(self.client_socket, {
"type": "whisper",
"target": self.selected_user,
"content": text
})
self.chat_display.append(f"[귓속말 보냄] {self.selected_user}: {text}")
self.message_input.clear()
except Exception as error:
self.chat_display.append(f"[오류] 귓속말 전송 실패: {error}")
self.disconnect_from_server()
def disconnect_from_server(self):
if self.client_socket is None:
self.set_connected_state(False)
return
try:
send_message(self.client_socket, {"type": "exit"})
except Exception:
pass
self.running = False
try:
self.client_socket.close()
except Exception:
pass
self.client_socket = None
self.set_connected_state(False)
self.chat_display.append("[시스템] 서버와의 연결을 종료했습니다.")
@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", "")
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}")
elif message_type == "user_list":
users = message.get("users", [])
self.update_user_list(users)
elif message_type == "whisper":
sender = message.get("sender", "unknown")
content = message.get("content", "")
self.chat_display.append(f"[귓속말 받음] {sender}: {content}")
elif message_type == "state":
connected = message.get("connected", False)
if not connected:
self.client_socket = None
self.set_connected_state(False)
else:
self.chat_display.append(f"[알 수 없는 메시지] {message}")
def on_user_selected(self, item):
self.selected_user = item.text()
self.selected_user_label.setText(f"선택한 상대: {self.selected_user}")
def update_user_list(self, users):
self.user_list.clear()
for user in users:
self.user_list.addItem(user)
if self.selected_user not in users:
self.selected_user = None
self.selected_user_label.setText("선택한 상대: 없음")
def closeEvent(self, event):
self.disconnect_from_server()
event.accept()
app = QApplication(sys.argv)
window = ChatWindow()
window.show()
sys.exit(app.exec())
6.3 최종 확인 표
| 확인할 코드 | 의미 |
| server.py | |
find_client_by_identifier(identifier) |
주소 또는 닉네임으로 대상 클라이언트 찾기 |
send_whisper(...) |
대상 소켓 하나에만 전송, 성공 여부 반환 |
message_type == "whisper" 분기 |
target · content 검사 후 전달, 실패 시에만 오류 안내 |
| main.py | |
self.whisper_button = QPushButton("귓속말") |
귓속말 버튼 생성·배치 |
whisper_button.setEnabled(connected) |
연결 상태에 따라 버튼 관리 |
on_whisper_clicked() |
selected_user를 target으로 귓속말 전송, GUI에서 직접 [귓속말 보냄] 표시 |
elif message_type == "whisper": |
받은 귓속말을 [귓속말 받음]으로 표시 |
메시지 타입은 whisper, 대상 키는 target으로 서버와 클라이언트가 동일하게 맞춰야 합니다. 전체 채팅과 귓속말이 한 화면에서 구분되어 보이면 이번 강의의 목표를 달성한 것입니다.
→ 다음 강의 (30강): 귓속말이 동작하니, 이제 오류 상황을 더 친절하게 다듬습니다. 상대를 고르지 않은 경우, 상대가 방금 퇴장한 경우, 자기 자신을 선택한 경우를 구분해 GUI에 이해하기 쉬운 안내를 표시합니다.