35강. GUI 파일 선택과 전송 상태 표시

0. 학습 목표
→ 파일 전송 상태를 GUI에 연결해, 사용자가 선택·전송 중·완료 상태를 화면에서 확인할 수 있도록 합니다.
0.1 이번 글에서 다룰 내용
이번 글은 UI 중심 강의입니다.
33강에서 파일 보내기, 34강에서 파일 저장하기를 완성했습니다.
파일 전송 기능 자체는 동작하지만,
GUI 화면에서 사용자가 선택한 파일이 무엇인지, 지금 전송 중인지, 완료되었는지 알기 어렵습니다.
이번 강의에서는 파일 전송 상태를 표시하는 위젯을 main.py에 추가합니다.

파일 선택 → 선택한 파일명 표시
→ 전송 시작 메시지 표시
→ 진행률 증가
→ 전송 완료 또는 오류 표시
| 구분 | 내용 |
| 이해할 것 | 메인 스레드에서 진행률 바를 실시간으로 갱신하려면 QApplication.processEvents()가 필요하다는 것 |
| 만들 것 | 파일명 라벨, 전송 상태 라벨, 진행률 바, 파일 선택·전송 함수 |
| 확인할 것 | 파일 전송 중 진행률이 단계별로 올라가고 완료 메시지가 표시되는지 |
0.2 이번 강의에서 직접 다루는 구조
이번 강의에서는 main.py를 수정합니다. protocol.py와 server.py는 34강 기준 코드를 그대로 사용합니다.
chat_server/
├── protocol.py
└── server.py
chat_client/
├── protocol.py
├── client.py
├── main.py ← ✏️ 이번 강의에서 수정
└── downloads/
└── received_files/
(➕ 새로 생성 · ✏️ 수정 · 표시 없음은 변경 없음)
main.py의 ChatWindow에 추가되는 구성입니다.
ChatWindow
├── file_button ← ➕ 파일 선택 버튼
├── selected_file_label ← ➕ 선택한 파일명 표시
├── file_status_label ← ➕ 전송 상태 텍스트 표시
├── file_progress_bar ← ➕ 전송 진행률 바
├── on_file_clicked() ← ➕ 파일 선택 이벤트 처리
└── send_file() ← ➕ 진행률 포함 파일 전송
1. UI 구조 확인하기
→ 이번 강의에서 추가할 화면 요소와 전송 상태 흐름을 정리합니다.
1.1 지금까지 UI에서 확인할 수 없었던 것
33~34강을 거치며 파일 전송·수신 기능은 완성됐습니다. 그러나 GUI 화면에서는 다음 네 가지를 알 방법이 없었습니다.
어떤 파일을 선택했는가?
지금 전송 중인가, 완료된 것인가?
전체의 몇 퍼센트가 전송되었는가?
오류가 났는가?
파일 전송처럼 시간이 걸리는 작업에서 상태 표시는 필수입니다. 상태 표시가 없으면 사용자는 버튼을 다시 누르거나 프로그램이 멈춘 줄 알고 강제 종료합니다.
1.2 추가할 화면 요소와 전송 상태 흐름

| 화면 요소 | 역할 |
file_button |
보낼 파일을 선택하는 버튼 |
selected_file_label |
선택한 파일 이름을 표시 |
file_status_label |
대기 · 전송 준비 · 전송 중 · 전송 완료 · 전송 오류 상태를 텍스트로 표시 |
file_progress_bar |
파일 전송 진행률(0~100%)을 막대로 표시 |
파일 전송 상태는 다음 순서로 바뀝니다.
파일 상태: 대기
↓ (파일 선택)
선택한 파일: sample.png / 파일 상태: 전송 준비
↓ (전송 시작)
파일 상태: 전송 중 / 진행률: 0 → 100
↓ (완료)
파일 상태: 전송 완료
↓ (오류 발생 시)
파일 상태: 전송 오류
전체 창 배치는 다음 순서를 권장합니다.
상단 상태 영역
채팅 / 사용자 목록 영역
파일 전송 상태 영역 ← 이번 강의에서 추가
메시지 입력 영역
✔ 확인 기준: 네 가지 화면 요소와 다섯 가지 상태 문구(대기 · 전송 준비 · 전송 중 · 전송 완료 · 전송 오류)를 설명할 수 있으면 완료.
2. 화면 요소 배치하기
→ QProgressBar를 추가하고 파일 전송 UI 위젯을 만들어 레이아웃에 배치합니다.
2.1 import 추가하기
이번 강의에서 새로 필요한 모듈을 추가합니다. QApplication.processEvents()는 3.3에서 설명할 진행률 바 실시간 갱신에 필요합니다.
import base64 # ➕ Base64 인코딩
import os # ➕ 파일 이름·크기 확인
import uuid # ➕ 파일 전송마다 고유 ID 생성
from PySide6.QtWidgets import (
...,
QApplication, # ➕ 이벤트 루프 처리 (진행률 바 실시간 갱신)
QFileDialog, # ➕ 파일 선택 창
QProgressBar, # ➕ 진행률 바 위젯
)
CHUNK_SIZE = 4096 # ➕ 파일 조각 크기 (4KB)
CHUNK_SIZE를 protocol.py(65536)보다 작은 4096으로 설정하는 이유가 있습니다. 64KB 조각으로 보내면 작은 파일(64KB 미만)은 조각이 하나뿐이라 진행률이 0%에서 100%로 바로 뛰어버립니다. 4KB로 나누면 조각 수가 많아져 진행률 바가 단계별로 올라가는 것을 볼 수 있습니다.
2.2 파일 전송 위젯과 레이아웃 만들기
__init__() 안에 위젯 네 개를 추가합니다. 초기값으로 라벨은 "없음"·"대기" 상태, 진행률 바는 0으로 설정합니다.
self.file_button = QPushButton("파일 선택") # ➕
self.selected_file_label = QLabel("선택한 파일: 없음") # ➕
self.file_status_label = QLabel("파일 상태: 대기") # ➕
self.file_progress_bar = QProgressBar() # ➕
self.file_progress_bar.setValue(0) # 초기 진행률 0
파일 관련 위젯을 하나의 레이아웃으로 묶어 기존 메인 레이아웃에 연결합니다.
file_layout = QVBoxLayout() # ➕ 파일 전송 영역 레이아웃
file_layout.addWidget(self.selected_file_label)
file_layout.addWidget(self.file_status_label)
file_layout.addWidget(self.file_progress_bar)
file_layout.addWidget(self.file_button)
main_layout.addLayout(file_layout) # 기존 메인 레이아웃에 추가
✔ 확인 기준: 파일 선택 버튼, 파일명 라벨, 상태 라벨, 진행률 바가 화면에 보이고 초기 진행률이 0으로 표시되면 완료. QProgressBar와 QApplication import와 main_layout.addLayout(file_layout) 호출이 있는지 확인하세요.
3. 상태와 이벤트 연결하기
→ 파일 선택 이벤트를 연결하고, 진행률을 실시간으로 갱신하는 파일 전송 함수를 단계별로 작성합니다.
3.1 파일 버튼 활성화와 이벤트 연결

파일 버튼은 서버에 접속된 상태에서만 활성화해야 합니다. 기존 set_connected_state()에 한 줄을 추가합니다.
# set_connected_state() 안에 추가
self.file_button.setEnabled(connected) # ➕ 접속 상태에 따라 활성화/비활성화
버튼 클릭 시그널을 on_file_clicked()에 연결합니다.
self.file_button.clicked.connect(self.on_file_clicked) # ➕ 클릭 이벤트 연결
3.2 on_file_clicked() 작성하기
버튼을 누르면 파일 선택 창이 열립니다. 파일을 고르면 라벨을 업데이트한 뒤 전송 함수를 호출합니다. 파일 선택 창에서 취소하면 빈 문자열이 반환되므로 이때는 바로 반환합니다.
def on_file_clicked(self): # ➕
if self.client_socket is None: # 접속 전 방어
self.show_error("서버에 먼저 접속하세요.")
return
file_path, _ = QFileDialog.getOpenFileName( # 파일 선택 창 열기
self,
"보낼 파일 선택"
)
if not file_path: # 취소 시 빈 문자열 반환
return
filename = os.path.basename(file_path)
self.selected_file_label.setText(f"선택한 파일: {filename}")
self.file_status_label.setText("파일 상태: 전송 준비")
self.file_progress_bar.setValue(0)
self.send_file(file_path)
3.3 send_file() 작성하기
먼저 file_info 메시지를 보내고 상태를 "전송 중"으로 바꿉니다. uuid.uuid4()로 매번 새로운 파일 ID를 만들어, 같은 이름의 파일을 두 번 보낼 때도 조각이 섞이지 않도록 합니다.
def send_file(self, file_path): # ➕
try:
file_id = str(uuid.uuid4()) # 고유 파일 전송 ID 생성
filename = os.path.basename(file_path)
file_size = os.path.getsize(file_path)
self.file_status_label.setText("파일 상태: 전송 중")
self.file_progress_bar.setValue(0)
send_message(self.client_socket, {
"type": "file_info",
"file_id": file_id,
"filename": filename,
"size": file_size,
"sender": str(self.client_socket.getsockname()) # 발신자 정보 포함
})
파일을 조각으로 읽으며 진행률을 계산합니다. 조각을 보낼 때마다 sent_size를 늘리고 진행률 바를 갱신합니다.
QApplication.processEvents()가 없으면 반복문이 끝나기 전까지 화면이 갱신되지 않아 진행률이 0에서 100%로 순간 이동합니다. 이 한 줄이 매 조각마다 화면을 실시간으로 갱신합니다.
sent_size = 0
with open(file_path, "rb") as file:
while True:
chunk = file.read(CHUNK_SIZE)
if not chunk:
break
encoded = base64.b64encode(chunk).decode("utf-8")
send_message(self.client_socket, {
"type": "file_chunk",
"file_id": file_id,
"data": encoded
})
sent_size += len(chunk)
if file_size > 0:
progress = int(sent_size / file_size * 100)
else:
progress = 100
self.file_progress_bar.setValue(progress)
QApplication.processEvents() # 진행률 바를 즉시 화면에 반영
모든 조각을 보낸 뒤 file_end를 전송하고 완료 상태를 표시합니다.
send_message(self.client_socket, {
"type": "file_end",
"file_id": file_id
})
self.file_progress_bar.setValue(100)
self.file_status_label.setText("파일 상태: 전송 완료")
self.show_system(f"파일 전송 완료: {filename}")
except FileNotFoundError:
self.file_status_label.setText("파일 상태: 전송 오류")
self.show_error("선택한 파일을 찾을 수 없습니다.")
except Exception as error:
self.file_status_label.setText("파일 상태: 전송 오류")
self.show_error(f"파일 전송 실패: {error}")
💡 강사 팁 — processEvents()의 한계와 다음 단계send_file()은 현재 GUI 메인 스레드에서 실행됩니다. QApplication.processEvents()를 청크마다 호출해 창이 반응하도록 만들었지만, 파일이 매우 크거나 네트워크가 느리면 UI가 여전히 버벅이는 느낌이 날 수 있습니다.
| 방식 | 동작 | 적합한 상황 |
processEvents() (현재) |
메인 스레드에서 전송하되 틈틈이 이벤트 처리 | 수십 MB 이하 파일, 학습용 프로토타입 |
| 별도 스레드 + Signal | 전송을 스레드에 맡기고 Signal로 진행률 전달 | 대용량 파일, 실무 수준 안정성 |
지금은 processEvents() 방식으로 충분합니다. 파일 전송을 별도 스레드로 분리하고 진행률을 Signal로 넘기는 패턴은 25강의 수신 스레드와 구조가 같습니다. 이 과정을 끝낸 뒤 스스로 개선해 보기 좋은 다음 단계 과제입니다.
✔ 확인 기준: QApplication.processEvents()가 반복문 안의 setValue(progress) 직후에 있는지, sent_size += len(chunk)도 반복문 안에 있는지 확인하세요. show_system()과 show_error()가 기존 메서드 이름과 일치하는지도 확인합니다.
4. 화면에서 결과 확인하기
→ 서버를 실행하고 파일을 선택해 전송 상태가 화면에 올바르게 표시되는지 확인합니다.
4.1 정상 실행 확인

서버를 먼저 실행하고, GUI 클라이언트를 실행해 접속한 뒤 파일 선택 버튼을 눌러 작은 이미지나 텍스트 파일을 선택합니다.
# 터미널 1 — 서버 실행
python chat_server/server.py
# 터미널 2 — GUI 클라이언트 실행
python chat_client/main.py
GUI에서 다음 순서로 상태가 바뀌면 성공입니다.
선택한 파일: sample.png
파일 상태: 전송 준비
파일 상태: 전송 중 ← 진행률 바 0 → 100
파일 상태: 전송 완료
[시스템] 파일 전송 완료: sample.png
서버 터미널에도 34강에서 만든 수신 로그가 출력됩니다.
파일 수신 시작: sample.png, 크기: N bytes
파일 조각 저장: sample.png (4096/N bytes)
...
파일 저장 완료: .../chat_server/received_files/sample.png
✔ 확인 기준: 진행률 바가 단계별로 올라가며 최종적으로 100%에 도달하고, 상태 라벨이 "전송 완료"로 바뀌면 완료. 진행률이 0%에서 100%로 바로 뛰면 QApplication.processEvents()가 반복문 안에 있는지 확인하세요.
4.2 흔한 오류와 해결 방법
| 오류 상황 | 원인 및 해결 방법 |
NameError: QProgressBar |
from PySide6.QtWidgets import QProgressBar가 없습니다. import에 추가하세요 |
| 파일 버튼이 항상 비활성화됨 | set_connected_state()에 self.file_button.setEnabled(connected)가 없습니다 |
| 파일 선택 창이 열리지 않음 | self.file_button.clicked.connect(self.on_file_clicked) 연결이 누락됐습니다 |
| 진행률이 0%에서 100%로 바로 뜀 | QApplication.processEvents()가 반복문 밖에 있거나 없습니다. 반복문 안 setValue() 직후로 이동하세요 |
ZeroDivisionError |
파일 크기가 0인데 진행률 계산을 시도했습니다. if file_size > 0: 조건을 확인하세요 |
AttributeError: show_system |
show_system() 대신 다른 이름을 썼습니다. 30강 기준으로 추가된 메서드 이름을 확인하세요 |
| 전송 중 GUI가 잠시 멈춤 | send_file()이 메인 스레드에서 실행됩니다. 이번 강의에서는 작은 파일(수백 KB 이하)로 테스트하고, 이후 별도 스레드로 개선할 수 있습니다 |
5. 최종 코드 정리하기
→ 이번 강의에서 main.py에 추가한 전체 코드를 확인합니다.
5.1 main.py — 추가된 부분 전체
import base64
import os
import uuid
from PySide6.QtWidgets import (
...,
QApplication,
QFileDialog,
QProgressBar,
)
CHUNK_SIZE = 4096 # 4KB — protocol.py의 65536보다 작아 진행률이 세밀하게 표시됨
# __init__() 안에 추가
self.file_button = QPushButton("파일 선택")
self.selected_file_label = QLabel("선택한 파일: 없음")
self.file_status_label = QLabel("파일 상태: 대기")
self.file_progress_bar = QProgressBar()
self.file_progress_bar.setValue(0)
file_layout = QVBoxLayout()
file_layout.addWidget(self.selected_file_label)
file_layout.addWidget(self.file_status_label)
file_layout.addWidget(self.file_progress_bar)
file_layout.addWidget(self.file_button)
main_layout.addLayout(file_layout)
self.file_button.clicked.connect(self.on_file_clicked)
# set_connected_state() 안에 추가
self.file_button.setEnabled(connected)
# 새로 추가하는 메서드
def on_file_clicked(self):
if self.client_socket is None:
self.show_error("서버에 먼저 접속하세요.")
return
file_path, _ = QFileDialog.getOpenFileName(self, "보낼 파일 선택")
if not file_path:
return
filename = os.path.basename(file_path)
self.selected_file_label.setText(f"선택한 파일: {filename}")
self.file_status_label.setText("파일 상태: 전송 준비")
self.file_progress_bar.setValue(0)
self.send_file(file_path)
def send_file(self, file_path):
try:
file_id = str(uuid.uuid4())
filename = os.path.basename(file_path)
file_size = os.path.getsize(file_path)
self.file_status_label.setText("파일 상태: 전송 중")
self.file_progress_bar.setValue(0)
send_message(self.client_socket, {
"type": "file_info",
"file_id": file_id,
"filename": filename,
"size": file_size,
"sender": str(self.client_socket.getsockname())
})
sent_size = 0
with open(file_path, "rb") as file:
while True:
chunk = file.read(CHUNK_SIZE)
if not chunk:
break
encoded = base64.b64encode(chunk).decode("utf-8")
send_message(self.client_socket, {
"type": "file_chunk",
"file_id": file_id,
"data": encoded
})
sent_size += len(chunk)
if file_size > 0:
progress = int(sent_size / file_size * 100)
else:
progress = 100
self.file_progress_bar.setValue(progress)
QApplication.processEvents()
send_message(self.client_socket, {
"type": "file_end",
"file_id": file_id
})
self.file_progress_bar.setValue(100)
self.file_status_label.setText("파일 상태: 전송 완료")
self.show_system(f"파일 전송 완료: {filename}")
except FileNotFoundError:
self.file_status_label.setText("파일 상태: 전송 오류")
self.show_error("선택한 파일을 찾을 수 없습니다.")
except Exception as error:
self.file_status_label.setText("파일 상태: 전송 오류")
self.show_error(f"파일 전송 실패: {error}")
5.2 최종 확인 표
| 추가된 코드 | 역할 |
QProgressBar, QFileDialog |
진행률 바 위젯, 파일 선택 창 |
QApplication.processEvents() |
메인 스레드 블로킹 상태에서 진행률 바를 매 조각마다 즉시 화면에 반영 |
import uuid |
파일 전송마다 고유 ID 생성 (조각 혼선 방지) |
CHUNK_SIZE = 4096 |
4KB 조각으로 나눠 진행률이 단계별로 표시되도록 함 |
file_button.setEnabled(connected) |
접속 상태에 따라 파일 버튼 활성화/비활성화 |
on_file_clicked() |
파일 선택 창 열기, 라벨·진행률 초기화, send_file() 호출 |
send_file() |
file_info → file_chunk 반복(진행률 실시간 갱신) → file_end 전송 |
sender: str(self.client_socket.getsockname()) |
file_info에 발신자 소켓 주소 포함 (32강 설계 기준) |
→ 다음 강의 (35.5강 체크포인트): 33~35강에서 만든 파일 전송·저장·상태 표시를 포함해, 지금까지 구현한 모든 기능(GUI 채팅 송수신, 사용자 목록 동기화, 귓속말, 파일 전송)을 함께 실행해 봅니다. 기능 간 충돌이나 자주 발생하는 오류를 점검하고 정리합니다.