31강. 텍스트와 바이너리 데이터의 차이

0. 학습 목표
→ 텍스트와 파일을 네트워크로 보낼 때 무엇이 다른지, 왜 메타데이터가 필요한지 이해합니다.
0.1 이번 글에서 다룰 내용
이번 글은 개념 중심 강의입니다.
30강까지 문자열 메시지를 encode("utf-8")로 바이트로 바꿔 보냈습니다.
파일도 결국 바이트로 보내는 것은 같습니다.
그러나 텍스트 메시지와 파일 데이터는 해석 방식과 전송 규칙이 다릅니다.
이번 강의에서는 코드를 작성하기 전에 그 차이를 이해합니다.
택배에 비유하면, 텍스트는 엽서입니다. 글자 하나하나의 의미가 그대로 보여 누구나 읽을 수 있습니다.
파일은 봉인된 택배 상자입니다. 파일의 내용물은 하나하나의 의미가 없이 전체가 하나의 의미가 됩니다.


| 구분 | 내용 |
| 이해할 것 | 텍스트와 바이너리는 모두 바이트로 전송되지만 해석 방식과 처리 규칙이 다르다는 점 |
| 정리할 것 | encode()/decode()·"rb"/"wb" 모드·메타데이터 필요성·판단 기준 |
| 확인할 것 | 파일 이름·크기(메타데이터)와 파일 내용(바이너리)을 따로 전달해야 하는 이유 |
0.2 앞으로 만들 프로젝트 구조 미리보기
이번 강의는 개념 강의라 기존 채팅 코드를 수정하지 않습니다. 파일 송수신 파트에서 사용할 구조를 미리 살펴봅니다.
chat_server/
├── protocol.py
└── server.py
chat_client/
├── protocol.py
├── client.py
├── main.py
└── downloads/
└── received_files/ ← (이후 파일 수신 강의에서 생성 예정)
파일 전송의 핵심 구조는 다음과 같습니다.
텍스트 메시지는 내용이 하나지만, 파일은 "정보"와 "내용"이 반드시 나뉩니다.
텍스트 메시지
└── {"type": "chat", "content": "안녕하세요"} ← 하나의 JSON으로 전달
파일 전송
├── 메타데이터(JSON 텍스트): 파일 이름, 파일 크기 ← 먼저 전달
└── 바이너리 데이터: 파일 내용 자체 ← 나중에 전달
1. 핵심 개념 이해하기
→ encode/decode와 바이너리 읽기 모드가 왜 다른지, 언제 어느 쪽을 써야 하는지 이해합니다.
1.1 텍스트 데이터 — encode와 decode
30강까지 채팅 메시지를 보낼 때 항상 encode("utf-8")을 거쳤습니다.
문자열은 사람이 읽는 형식이고, 네트워크는 바이트만 전달할 수 있어서입니다.
받는 쪽은 decode("utf-8")로 다시 문자열로 되돌립니다.
문자열 → encode("utf-8") → 바이트 → send() → recv() → 바이트 → decode("utf-8") → 문자열
이 흐름은 UTF-8이라는 약속된 규칙을 양쪽이 알기 때문에 가능합니다.
같은 방식으로 인코딩하면 반드시 같은 방식으로 디코딩해야 원래 문자열로 돌아옵니다.
1.2 바이너리 데이터 — encode/decode를 쓸 수 없는 이유
이미지·PDF·압축 파일은 UTF-8 문자열 규칙을 따르지 않는 바이트 덩어리입니다.
이 데이터를 억지로 decode("utf-8")하면 UnicodeDecodeError가 납니다.
택배 상자 안 물건을 "글자로 읽으려" 하는 것과 같습니다. 그래서 파일을 열 때 모드를 다르게 씁니다.
| 모드 | 의미 | 사용 예 |
"r" |
텍스트 읽기 | .txt, .csv 같은 텍스트 파일 |
"rb" |
바이너리 읽기 | .png, .pdf, .zip 같은 파일 — 파일 전송 시 송신 측 |
"w" |
텍스트 쓰기 | 문자열 저장 |
"wb" |
바이너리 쓰기 | 받은 파일 저장 — 파일 전송 시 수신 측 |
파일을 "rb"로 읽으면 bytes 객체가 반환됩니다. 이 bytes는 encode()/decode() 없이 그대로 소켓으로 보내고 그대로 파일에 저장합니다.
✔ 확인 기준: "텍스트 파일은 "r", 이미지·PDF 같은 파일은 "rb"로 읽고, 받은 파일은 "wb"로 저장한다. 이미지를 decode("utf-8")하면 UnicodeDecodeError가 난다"고 설명할 수 있으면 완료.
2. 메타데이터가 필요한 이유
→ 파일 내용만 보내면 무엇이 문제인지, 메타데이터로 어떻게 해결하는지 확인합니다.

파일 내용만 덜렁 보내면 받는 쪽은 다음 세 가지를 알 수 없습니다.
이 파일의 이름이 무엇인가?
얼마나 받아야 파일 하나가 끝나는가?
받은 데이터가 다 온 것인가, 아직 더 오는 것인가?
그래서 파일 내용보다 먼저 파일 정보(송장)를 JSON으로 보냅니다. 이것을 메타데이터라고 합니다.
{
"type": "file_info",
"filename": "photo.png",
"size": 204800
}
받는 쪽은 이 메시지를 보고 "photo.png라는 파일이 204,800바이트 온다"고 준비합니다. 그 다음 바이너리 데이터 조각이 도착하고, size만큼 받으면 파일 하나가 완성됩니다.
1) 메타데이터(JSON 텍스트) 전송 → "photo.png, 204800바이트가 갈게"
↓
2) 바이너리 데이터 조각 전송 → 실제 파일 내용
↓
3) size만큼 다 받으면 완료 ← 받는 쪽이 끝을 판단하는 기준
메타데이터와 바이너리를 따로 보내는 이유는 이렇습니다.
메타데이터는 기존 protocol.py의 send_message()로 JSON 텍스트 메시지처럼 보낼 수 있습니다.
파일 내용은 send_message()를 쓸 수 없고(바이너리이므로) 소켓으로 직접 send()해야 합니다.
두 가지가 전송 방식 자체가 다르기 때문에 반드시 나뉩니다.
✔ 확인 기준: "파일 이름·크기는 JSON 텍스트 메시지(메타데이터)로, 파일 내용은 바이너리로 따로 전달하며, 받는 쪽은 size만큼 받으면 끝났다고 판단한다"고 설명할 수 있으면 완료.
3. 판단 기준 정리하기
→ 언제 텍스트로 다루고 언제 바이너리로 다룰지 기준과, 자주 하는 오해를 정리합니다.
3.1 상황별 처리 방식
| 상황 | 처리 방식 |
| 사용자가 입력한 채팅 문장 | 텍스트 — encode()/decode() |
JSON 메시지 구조 (chat, whisper 등) |
텍스트 — send_message() 그대로 |
| 파일 이름, 파일 크기 | 텍스트 — JSON 안에 담아 send_message()로 전달 |
| 이미지·PDF·zip 파일 내용 | 바이너리 — "rb"로 읽고 소켓으로 직접 전송 |
| 받은 파일 저장 | 바이너리 — "wb"로 저장 |
3.2 자주 하는 오해
| 오해 | 올바른 이해 |
| 파일 이름만 보내면 파일 전송이 된다 | 파일 이름은 정보(메타데이터)이고 내용이 아닙니다. 이름과 내용을 반드시 따로 보내야 합니다 |
| 파일 전체를 한 번에 보내면 항상 된다 | 큰 파일은 메모리·네트워크에 부담이 됩니다. 조각으로 나누어 보내는 방식이 필요합니다 |
| 바이너리 데이터가 깨져 보이면 오류다 | 바이너리 데이터는 사람이 글자로 읽기 어려울 뿐입니다. 이미지 뷰어·PDF 뷰어가 올바르게 해석합니다. decode()를 시도하지 말고 그대로 저장하세요 |
4. 예시로 검토하기
→ 문자열의 encode/decode와 파일 바이너리 읽기를 직접 확인하고, 메타데이터 추출까지 연결합니다.
4.1 문자열과 바이트 왕복하기
텍스트가 바이트로 바뀌고 다시 문자열로 돌아오는 과정입니다.
text = "안녕하세요"
data = text.encode("utf-8") # 문자열 → 바이트
print(text) # 안녕하세요
print(data) # b'\xec\x95\x88\xeb\x85\x95\xed\x95\x98\xec\x84\xb8\xec\x9a\x94'
첫 줄은 사람이 읽는 문자열이고, 두 번째 줄은 네트워크로 보낼 수 있는 바이트 표현입니다. 같은 인코딩 방식으로 디코딩하면 문자열로 돌아옵니다.
data = b'\xec\x95\x88\xeb\x85\x95\xed\x95\x98\xec\x84\xb8\xec\x9a\x94'
text = data.decode("utf-8") # 바이트 → 문자열
print(text) # 안녕하세요
4.2 파일을 바이너리로 읽고 메타데이터 준비하기

파일을 바이너리 모드로 읽으면 사람이 읽기 어려운 바이트가 나옵니다. 이것은 오류가 아니라 파일의 실제 바이트를 본 것입니다. 앞의 \x89PNG는 "이 파일은 PNG 이미지"라는 표시입니다.
# sample.png 파일이 있다고 가정합니다
with open("sample.png", "rb") as file: # 바이너리 읽기 모드
data = file.read(20) # 앞부분 20바이트만 읽어 보기
print(data) # b'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR'
이 데이터를 억지로 decode("utf-8")하면 UnicodeDecodeError가 납니다.
이미지 바이트는 UTF-8 규칙을 따르지 않기 때문입니다.
파일 전송에서는 이 바이트를 그대로 보내고 그대로 저장해야 합니다.
실제 전송에서 필요한 메타데이터는 os 모듈로 간단히 만들 수 있습니다.
import os
filepath = "sample.png"
filename = os.path.basename(filepath) # "sample.png"
filesize = os.path.getsize(filepath) # 파일 크기(바이트 수)
# 메타데이터 → JSON 텍스트 메시지로 전달
metadata = {
"type": "file_info",
"filename": filename,
"size": filesize
}
print(metadata)
# {'type': 'file_info', 'filename': 'sample.png', 'size': 204800}
이 metadata는 기존 send_message()로 그대로 보낼 수 있습니다. 받는 쪽은 이 메시지를 먼저 받고 준비한 뒤, 뒤따라 오는 바이너리 데이터를 filesize만큼 모아 파일로 저장합니다.
- 텍스트는 encode()와 decode()로 문자열과 바이트를 오간다
- 파일 내용은 바이너리 데이터로 다룬다 ("rb"로 읽고 "wb"로 저장)
- 바이너리를 decode()하면 UnicodeDecodeError가 난다
- 파일 이름·크기(메타데이터)는 JSON 텍스트 메시지로 먼저 전달한다
- 받는 쪽은 size만큼 받으면 파일 하나가 완성됐다고 판단한다
→ 다음 강의 (32강): 이번에 이해한 "메타데이터 먼저, 바이너리 나중" 개념을 실제 메시지 규칙으로 설계합니다. file_info 메시지에 어떤 필드가 필요한지 정하고, 받는 쪽이 size만큼 받아 파일을 완성하는 흐름을 protocol.py 약속으로 정리합니다.