23강. GUI에서 서버 접속하기

0. 학습 목표
→ 이번 글에서 무엇을 이해하고, 무엇을 만들고, 무엇을 확인할지 먼저 정리합니다.
0.1 이번 글에서 다룰 내용
이번 글은 구현 중심 강의입니다.
22강까지는 GUI 화면이 버튼 클릭과 Enter 입력에 반응했지만, 서버와는 완전히 분리된 상태였습니다. 입력한 메시지도 화면에만 추가될 뿐 어디로도 전송되지 않았습니다. 이번 강의에서 그 분리된 GUI와 서버 사이에 실제 소켓 연결을 만듭니다.
아직 메시지를 전송하지는 않습니다. 이번 강의의 목표는 더 작고 분명합니다.
서버 실행
↓
GUI 창에서 "서버 접속" 버튼 클릭
↓
127.0.0.1:5000 서버에 소켓 연결
↓
상태 라벨이 "상태: 서버에 연결됨"으로 바뀜
↓
입력창과 보내기 버튼이 활성화됨 ← 이번 강의의 성공 지점
핵심 성공 기준은 상태 라벨 텍스트가 바뀌는 것입니다. 이것이 확인되면 GUI와 서버 사이에 소켓 연결이 실제로 성립한 것입니다. 아래 코드에서 ➕는 22강 코드까지는 없던 새로 더해진 줄, ✏️는 위치나 형태가 바뀐 줄을 뜻합니다. 최종 정리 코드에는 마커 없이 깨끗한 코드만 싣습니다.

| 구분 | 내용 |
| 이해할 것 | GUI 버튼 클릭이 소켓 연결 함수와 이어지는 방식, 연결 성공·실패를 화면에 반영하는 구조 |
| 만들 것 | chat_client/main.py 수정 — 접속 버튼(connect_button) + connect_to_server() 메서드 |
| 확인할 것 | 서버 실행 후 GUI 접속 버튼을 눌렀을 때 상태 라벨이 바뀌고 입력창이 활성화되는지 |
0.2 이번 강의에서 직접 다루는 구조
이번 강의에서는 22강에서 만든 chat_client/main.py를 수정합니다. 서버는 이전 파트에서 만든 chat_server/server.py를 그대로 실행해 사용합니다.
chat_server/
├── protocol.py ← 유지
└── server.py ← 유지 (실행용으로만 사용)
chat_client/
├── protocol.py ← 유지
├── client.py ← 유지
└── main.py ← ✏️ 이번 강의에서 수정
(✏️ 수정 · 표시 없음은 변경 없음)
1. 실습 준비하기
→ 서버를 먼저 실행하고, 이번 강의에서 사용할 연결 정보를 확인합니다.
GUI 클라이언트가 접속하려면 서버가 먼저 실행 중이어야 합니다. 서버는 "기다리는 쪽", 클라이언트는 "요청하는 쪽"이기 때문입니다. 터미널을 두 개 열어 하나는 서버용, 하나는 GUI 클라이언트용으로 사용합니다.
터미널 1에서 서버를 실행합니다.
python chat_server/server.py
서버를 시작합니다.
클라이언트 접속 대기 중...
서버 터미널이 종료되지 않고 대기 상태를 유지하면 준비 완료입니다. accept()에서 멈춘 것처럼 보이는 상태가 정상입니다. 이번 강의에서 사용하는 연결 정보는 다음과 같습니다.
| 항목 | 값 |
| 서버 IP (HOST) | "127.0.0.1" |
| 서버 포트 (PORT) | 5000 |
| 수정 파일 | chat_client/main.py |
| 실행 명령어 | python chat_client/main.py |
✔ 확인 기준: python chat_server/server.py 실행 후 "클라이언트 접속 대기 중..." 출력과 함께 서버가 대기 상태면 완료. Address already in use가 나오면 이전 서버가 포트 5000을 쓰고 있는 것이니 기존 서버 터미널을 종료하고 재실행하세요.
2. 클라이언트 - 접속 버튼과 상수 추가하기
→ 22강 코드에 import socket, HOST/PORT 상수, 접속 버튼을 더하고 입력 위젯을 처음에 비활성화합니다.
22강 완성 코드에 네트워크 연결을 위한 준비물을 추가합니다. 파일 맨 위에 import socket을, ChatWindow 클래스 바깥에 HOST와 PORT 상수를 정의합니다. 그리고 상태 라벨 옆에 "서버 접속" 버튼을 두고, 입력창과 보내기 버튼은 처음부터 비활성화합니다.
import socket # ➕ 소켓 모듈 추가
import sys
from PySide6.QtWidgets import (
QApplication,
QWidget,
QTextEdit,
QLineEdit,
QPushButton,
QLabel,
QVBoxLayout,
QHBoxLayout,
)
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("채팅")
self.resize(560, 480) # ✏️ 접속 버튼 자리를 위해 가로를 조금 늘림 (520 → 560)
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) # ✏️ addWidget(status_label) → 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) # ➕ 접속 버튼 → connect_to_server
self.send_button.clicked.connect(self.on_send_clicked)
self.message_input.returnPressed.connect(self.on_send_clicked)
def on_send_clicked(self):
text = self.message_input.text().strip()
if not text:
return
self.chat_display.append(f"나: {text}") # 임시 — 24강에서 네트워크 전송으로 교체
self.message_input.clear()
app = QApplication(sys.argv)
window = ChatWindow()
window.show()
sys.exit(app.exec())
상태 라벨과 접속 버튼을 top_layout(가로 배치)으로 묶은 뒤 그 레이아웃을 main_layout에 넣습니다. 그래서 addWidget이 addLayout으로 바뀌었습니다.
입력창과 보내기 버튼을 처음부터 setEnabled(False)로 막아두는 이유가 있습니다. 서버에 연결되지 않은 상태에서 메시지를 보내면 보낼 소켓 자체가 없어 오류가 납니다. 연결이 성공한 뒤에만 입력이 가능하도록 처음부터 잠가둡니다.
| 상태 | 입력창 | 보내기 버튼 |
| 서버 연결 전 | 비활성화 | 비활성화 |
| 서버 연결 성공 | 활성화 | 활성화 |
| 서버 연결 실패 | 비활성화 유지 | 비활성화 유지 |
✔ 확인 기준: python chat_client/main.py 실행 후 상태 라벨 오른쪽에 "서버 접속" 버튼이 나타나고, 입력창과 보내기 버튼이 회색으로 비활성화되면 완료. 아직 접속 버튼을 눌러도 아무 동작이 없는 것이 정상입니다. connect_to_server()는 다음 단계에서 추가합니다.
3. 클라이언트 - 서버 접속 기능 연결하기
→ connect_to_server() 메서드를 추가해 버튼 클릭이 실제 소켓 연결로 이어지게 합니다.
on_send_clicked() 바로 위에 connect_to_server() 메서드를 추가합니다. 이 메서드가 접속 버튼 클릭과 실제 소켓 연결을 잇는 핵심입니다.
def connect_to_server(self): # ➕ 접속 버튼이 호출하는 메서드
try:
self.client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # ➕ 소켓 생성 후 self에 저장
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}")
소켓을 만들 때 반드시 self.client_socket = socket.socket(...)으로 저장해야 합니다. 24강에서 메시지를 보낼 때 self.client_socket으로 이 소켓에 접근하기 때문입니다.
self.client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # 올바른 형태 — self에 저장해 계속 사용
# client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # 잘못된 형태 — 함수가 끝나면 소켓이 사라짐
client_socket처럼 로컬 변수로 만들면 connect_to_server()가 끝나는 순간 소켓이 사라져, 나중에 메시지를 보내려 할 때 소켓을 찾을 수 없습니다. connect_to_server() 안에서 일어나는 흐름은 다음과 같습니다.
소켓 생성 (socket.socket())
↓
서버에 연결 시도 (client_socket.connect((HOST, PORT)))
↓
┌────────────────────────┬───────────────────────────┐
성공 실패(ConnectionRefusedError) 그 외 오류(Exception)
↓ ↓ ↓
상태 라벨 변경 상태 라벨 "연결 실패" 상태 라벨 "연결 실패"
시스템 메시지 오류 안내 메시지 오류 내용 표시
입력창·버튼 활성화 ← 이번 강의의 성공 지점
ConnectionRefusedError는 "서버가 꺼져 있다"는 가장 흔한 원인이라 따로 잡아 더 명확한 안내를 줍니다. except Exception은 네트워크 차단이나 잘못된 IP 같은 예상치 못한 나머지 상황을 대비한 안전망입니다. 하나의 except Exception만 써도 동작하지만, 가장 흔한 오류에 구체적인 안내를 주는 편이 학습자에게 도움이 됩니다.
✔ 확인 기준: 서버 실행 후 접속 버튼 클릭 → "상태: 서버에 연결됨"과 "[시스템] 서버에 연결되었습니다." 표시, 입력창·보내기 버튼이 활성화되면 완료. ConnectionRefusedError 메시지가 나오면 서버 터미널이 살아 있는지 먼저 확인하세요.
4. 실행 결과 확인하기
→ 서버를 실행한 뒤 GUI에서 접속하고, 연결 실패 상황도 확인합니다.
4.1 서버 실행 후 GUI 접속 확인하기

터미널 1(서버)이 대기 중인 상태에서, 터미널 2에서 GUI 클라이언트를 실행합니다.
python chat_client/main.py
GUI 창에서 "서버 접속" 버튼을 누르면 채팅 표시 영역에 다음처럼 나타납니다.
[시스템] 서버에 연결되었습니다.
동시에 상태 라벨이 "상태: 서버에 연결됨"으로 바뀌고, 입력창과 보내기 버튼이 활성화됩니다. 서버 터미널(터미널 1)에는 다음처럼 출력됩니다.
클라이언트 접속: ('127.0.0.1', 52344)
포트 번호(52344)는 매번 달라집니다. 서버가 클라이언트를 받아들일 때 자동으로 배정하는 번호이므로 값이 달라도 정상입니다.
4.2 서버 없이 접속해 오류 처리 확인하기
이번에는 서버를 끈 상태에서 "서버 접속" 버튼을 눌러봅니다. 채팅 표시 영역에 다음처럼 표시됩니다.
[오류] 서버가 실행 중인지 확인하세요.
상태 라벨은 "상태: 서버 연결 실패"로 바뀝니다. 이것은 잘못된 동작이 아니라 정상적인 오류 처리입니다. ConnectionRefusedError가 발생했고, GUI가 그 상황을 사용자에게 알려준 것입니다. 프로그램이 종료되거나 멈추지 않고 그대로 살아 있다는 점이 중요합니다.
✔ 확인 기준: 연결 성공 시 "상태: 서버에 연결됨"과 입력창·버튼 활성화, 연결 실패 시 "상태: 서버 연결 실패"와 오류 메시지 표시 — 두 경우 모두 프로그램이 종료되지 않고 GUI가 살아 있으면 완료. HOST와 PORT가 서버와 일치하는지 확인하세요.
4.3 실행이 안 될 때 확인할 것
| 오류 상황 | 원인 및 해결 방법 |
ConnectionRefusedError |
서버가 실행되지 않았습니다. python chat_server/server.py를 먼저 실행합니다 |
Address already in use |
이전 서버가 포트 5000을 사용 중입니다. 기존 서버 터미널을 종료하고 재실행합니다 |
| 접속 버튼을 눌러도 반응이 없다 | self.connect_button.clicked.connect(self.connect_to_server) 누락이거나, 괄호를 붙여 connect_to_server()로 전달했습니다. 함수 이름만 전달합니다 |
| 상태 라벨이 바뀌지 않는다 | connect_to_server() 안의 self.status_label.setText(...) 호출을 확인합니다 |
| 입력창이 계속 비활성화되어 있다 | 연결 성공 블록에 self.message_input.setEnabled(True)가 없거나, 오류가 나서 except 블록으로 빠졌습니다. 채팅창의 오류 메시지를 먼저 확인합니다 |
| 24강에서 소켓을 못 찾는다 | connect_to_server() 안에서 로컬 변수로 만들었습니다. 반드시 self.client_socket = socket.socket(...)으로 저장합니다 |
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,
)
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
self.chat_display.append(f"나: {text}")
self.message_input.clear()
app = QApplication(sys.argv)
window = ChatWindow()
window.show()
sys.exit(app.exec())
5.2 최종 확인 표
| 확인할 코드 | 의미 |
import socket |
소켓 모듈 불러오기 |
HOST = "127.0.0.1" / PORT = 5000 |
서버 연결 주소·포트 (서버와 일치해야 함) |
self.client_socket = None |
연결 전 소켓 초기값 |
self.connect_button = QPushButton("서버 접속") |
서버 접속 버튼 생성 |
setEnabled(False) (입력창·보내기) |
연결 전 입력 위젯 잠금 |
main_layout.addLayout(top_layout) |
상태 라벨 + 접속 버튼 가로 묶음 배치 |
connect_button.clicked.connect(self.connect_to_server) |
접속 버튼 클릭 이벤트 연결 |
self.client_socket = socket.socket(...) |
소켓 생성 후 self에 저장 (24강에서 재사용) |
self.client_socket.connect((HOST, PORT)) |
서버에 연결 시도 |
except ConnectionRefusedError |
서버 미실행 시 안내 처리 |
setEnabled(True) (연결 성공 블록) |
연결 후 입력창·보내기 활성화 |
on_send_clicked() 안의 chat_display.append(f"나: {text}")는 여전히 임시 동작입니다. 24강에서 이 줄이 self.client_socket으로 실제 메시지를 전송하는 코드로 교체됩니다. 이번 강의에서 self.client_socket에 소켓을 저장해 둔 것이 바로 그 전송의 출발점입니다.
→ 다음 강의 (24강): 이번에 저장한 self.client_socket을 사용해 실제로 메시지를 전송합니다. on_send_clicked()의 임시 chat_display.append() 줄이 self.client_socket.sendall(...) 전송 코드로 교체되어, 입력한 메시지가 서버로 실제 전달됩니다.