UnicodeDecodeError: 'utf-8' codec can't decode byte 0xc7 in position 0 같은 줄을 만나면 대부분 encoding='utf-8'을 붙였다가 또 실패하고, 그다음엔 errors='ignore'로 넘겨버린다. 그러면 에러는 사라지지만 글자는 영원히 깨진 채로 남는다.
이 글은 한글 파일을 읽을 때 나는 UnicodeDecodeError를 기준으로, 에러 메시지에서 무엇을 읽어야 하는지와 읽는 쪽·쓰는 쪽을 어떤 순서로 고치는지를 정리했다. 예제는 내 맥의 python3 3.9.6과 최신 문서(Python 3.15 계열)를 함께 확인해서 썼다.
30초 요약
| 궁금한 것 | 답 |
|---|---|
| 에러의 정체 | 파일은 바이트 덩어리이고, 파이썬이 그 바이트를 지정한 코덱으로 글자로 바꾸지 못했다는 뜻이다 (UnicodeDecodeError) |
| 가장 흔한 원인 | cp949(한국어 윈도우 기본)로 저장된 파일을 utf-8로 읽는 경우. 엑셀 CSV에서 자주 나온다 (표준 인코딩 목록) |
| 1차 처방 | 읽는 코드에 encoding="cp949" 또는 encoding="utf-8-sig"를 명시한다 |
| 기본값이 바뀐다 | Python 3.15부터 UTF-8 모드가 기본이라 open()의 기본 인코딩이 UTF-8이다 (PEP 686) |
| 진단 도구 | python3 -X warn_default_encoding으로 인코딩을 안 적은 open()을 경고로 잡는다 (PEP 597) |
| 인코딩을 모를 때 | file -I, charset-normalizer, iconv로 확인·변환한다 |
| 절대 하지 말 것 | errors="ignore"로 덮고 넘어가기. 글자가 조용히 사라진다 |
| 반대쪽 에러 | 쓰기·출력에서 막히면 UnicodeEncodeError다. 처방이 다르다 |

한글 깨짐은 파일 손상이 아니라 코덱 선택 문제라는 것을 정리한 히어로 이미지
목차
- UnicodeDecodeError는 왜 나는가
- 에러 메시지에서 읽어야 하는 네 가지
- 해결 1 — 읽는 쪽에 encoding을 지정한다
- 해결 2 — cp949와 BOM(utf-8-sig) 구분
- 해결 3 — 인코딩을 모를 때 확인하고 변환하기
- 그래도 안 될 때 — 쓰는 쪽과 실행 환경
- 증상별 처방 표
- 내가 막혔던 곳
- 참고 자료
UnicodeDecodeError는 왜 나는가
파이썬 3에서 str은 글자, bytes는 바이트다. 파일을 텍스트 모드로 열면 파이썬이 바이트를 읽어서 코덱으로 글자로 바꾸는데, 이때 바이트가 그 코덱의 규칙에 맞지 않으면 UnicodeDecodeError가 난다. 같은 파일이라도 어떤 코덱을 고르느냐에 따라 성공과 실패가 갈린다.
내 맥에서 직접 확인한 결과다. 같은 "한글"이라는 글자를 utf-8로 저장하면 b'\xed\x95\x9c\xea\xb8\x80', cp949로 저장하면 b'\xc7\xd1\xb1\xdb'가 된다. 이 cp949 바이트를 utf-8로 읽으면 바로 에러가 난다.
>>> "한글".encode("cp949")
b'\xc7\xd1\xb1\xdb'
>>> "한글".encode("cp949").decode("utf-8")
UnicodeDecodeError: 'utf-8' codec can't decode byte 0xc7 in position 0: invalid continuation byte
cp949의 첫 바이트 0xC7은 utf-8에서 "앞 글자의 뒤를 잇는 바이트"로만 쓰이는 값이라, 파이썬 입장에서는 문장이 시작하자마자 규칙이 깨진 것이다. 반대 방향도 똑같이 실패한다. utf-8 바이트를 cp949로 읽으면 'cp949' codec can't decode byte 0xed in position 0: illegal multibyte sequence가 나온다.
정리하면 이 에러는 파일이 깨진 게 아니다. 내가 고른 코덱이 파일과 안 맞는 것이다. 파일은 그대로 두고 읽는 쪽을 고치면 된다.
에러 메시지에서 읽어야 하는 네 가지
트레이스백은 생각보다 많은 정보를 준다. 아래 한 줄만 제대로 읽어도 절반은 해결된다.
UnicodeDecodeError: 'utf-8' codec can't decode byte 0xc7 in position 12: invalid continuation byte
| 보이는 것 | 의미 | 판단 |
|---|---|---|
'utf-8' codec | 내가 지정했거나 기본값으로 잡힌 코덱 | 이 코덱이 파일과 다르다 |
byte 0xc7 | 문제가 된 바이트 값 | 0x80~0xFF 구간이면 cp949 의심 |
position 12 | 파일 앞부분 12바이트 지점 | 앞쪽이면 BOM이나 헤더 문제 |
invalid continuation byte | 실패 이유 | utf-8 규칙 위반이면 cp949 파일일 가능성이 크다 |
position이 0이고 byte가 0xef라면 BOM(Byte Order Mark)이 원인일 수 있다. utf-8 BOM은 EF BB BF 세 바이트다. 이때는 utf-8-sig로 읽으면 BOM을 건너뛴다. 처리 방식은 아래 해결 2에서 따로 다룬다.
어느 파일에서 났는지 안 보이면 트레이스백 위쪽 프레임을 봐야 한다. open()을 여러 번 부르는 코드라면 파일 경로를 로그로 찍는 편이 빠르다. 나는 예외를 잡아서 경로를 붙여 다시 올리는 방식을 쓴다.
try:
text = open(path, encoding="utf-8").read()
except UnicodeDecodeError as exc:
raise RuntimeError(f"{path} 디코딩 실패: 코덱={exc.encoding}, 위치={exc.start}") from exc
UnicodeDecodeError에는 encoding, object, start, end, reason 속성이 있어서 어느 지점에서 막혔는지 코드로 꺼낼 수 있다. (내장 예외)
여러 파일을 한꺼번에 읽는 배치라면, 원인을 찾기 전에 -X warn_default_encoding을 붙여 실행해 보는 것도 방법이다. 인코딩을 적지 않은 open()마다 EncodingWarning이 찍힌다. 이 경고는 Python 3.10부터 있고 기본은 꺼져 있다. (PEP 597)
해결 1 — 읽는 쪽에 encoding을 지정한다
결론은 간단하다. 텍스트를 읽는 모든 지점에 인코딩을 적는다. 파이썬 3.15 문서 기준으로, 텍스트 모드에서 encoding을 생략하면 UTF-8이 기본이고, UTF-8 모드가 꺼진 환경에서는 locale.getencoding()이 대신 쓰인다. 즉 같은 코드가 OS와 버전에 따라 다르게 동작할 수 있다. (open())
이 불확실성을 없애려고 파이썬은 3.15부터 UTF-8 모드를 기본으로 켠다. (PEP 686) 그래도 3.14 이하를 쓰는 사람이 많으니, 코드에는 인코딩을 직접 적는 습관이 안전하다.
# 가장 기본
from pathlib import Path
text = Path("메모.txt").read_text(encoding="utf-8")
# 한 줄씩 읽기
with open("로그.txt", encoding="utf-8") as f:
for line in f:
print(line.rstrip())
csv 모듈은 newline=""을 같이 넣어야 줄바꿈 처리가 꼬이지 않는다. 엑셀이 만든 파일이면 cp949부터 시도한다.
import csv
with open("주소록.csv", newline="", encoding="cp949") as f:
rows = list(csv.DictReader(f))
pandas를 쓴다면 read_csv의 encoding 인자로 같은 값을 넘긴다. encoding_errors의 기본값은 'strict'라서, 인코딩이 틀리면 조용히 넘어가지 않고 에러를 낸다. (pandas.read_csv)
import pandas as pd
df = pd.read_csv("매출.csv", encoding="cp949")
df = pd.read_csv("매출.csv", encoding="utf-8-sig") # BOM 있는 utf-8
JSON은 예외가 적다. 교환용 JSON 텍스트는 UTF-8로 쓰는 것이 표준이고, 파이썬 json 모듈도 그 전제로 동작한다. (RFC 8259 §8.1) 그래도 파일을 직접 열어 읽는다면 인코딩을 적어두는 편이 좋다.

증상별 처방을 한 장에 정리한 카드 — 코덱·BOM·errors 처리 순서
위 코드처럼 읽는 지점마다 encoding을 붙여두면, 나중에 다른 사람이 같은 파일을 열어도 에러 없이 같은 결과를 본다. 인코딩은 "환경이 정해주는 값"이 아니라 "내가 정하는 값"으로 다루는 게 핵심이다.
해결 2 — cp949와 BOM(utf-8-sig) 구분
한국어 환경에서 실제로 마주치는 인코딩은 크게 셋이다. 이름이 비슷해서 헷갈리는데, 파이썬 표준 인코딩 목록에는 cp949가 949, ms949, uhc와 같은 코덱으로 등록돼 있다. (표준 인코딩)
| 인코딩 | 어디서 나오나 | 읽는 법 |
|---|---|---|
utf-8 | 요즘 에디터, 웹, 리눅스·맥 기본 | encoding="utf-8" |
utf-8-sig | 엑셀의 "CSV UTF-8", 윈도우 메모장 일부 저장 | encoding="utf-8-sig" |
cp949 | 한국어 윈도우의 ANSI 저장, 관공서·공공데이터 CSV | encoding="cp949" |
euc-kr | 오래된 웹 페이지, 옛 시스템 | encoding="euc-kr" (실패하면 cp949) |
utf-8-sig는 읽을 때 앞의 BOM을 건너뛰고, 쓸 때는 BOM을 붙인다. (codecs — utf_8_sig) 이걸 안 쓰면 첫 컬럼 이름이 '\ufeff이름'처럼 보이면서, 값은 멀쩡한데 컬럼 매칭만 실패하는 이상한 상황이 된다. KeyError: '이름'이 나는데 눈으로는 똑같아 보이는 증상이 그거다.
cp949와 euc-kr은 겹치는 부분이 많지만 같은 코덱은 아니다. cp949가 euc-kr보다 넓은 문자 집합을 담는다. 그래서 euc-kr로 저장된 파일은 cp949로도 대부분 읽힌다. 반대는 안 된다. 실무 순서는 utf-8 → cp949 → euc-kr로 두는 편이 무난하다.
def read_korean_text(path):
for enc in ("utf-8-sig", "cp949"):
try:
return path.read_text(encoding=enc), enc
except UnicodeDecodeError:
continue
raise UnicodeDecodeError("unknown", b"", 0, 1, f"{path} 인코딩 판별 실패")
이 함수는 BOM 있는 utf-8과 cp949를 모두 잡는다. 다만 "성공했다"가 "제대로 읽혔다"를 보장하지는 않는다. 아래 섹션에서 이 함정을 다룬다.
해결 3 — 인코딩을 모를 때 확인하고 변환하기
파일이 어떤 인코딩인지 모르면 추측보다 확인이 먼저다. 맥·리눅스에서는 file 명령으로 charset을 볼 수 있다.
file -I 주소록.csv # macOS
file -i 주소록.csv # Linux
charset=unknown-8bit처럼 나오면 판별에 실패한 것이다. 이럴 때 파이썬에서는 charset-normalizer를 쓴다. requests가 의존성으로 함께 설치하는 라이브러리라 이미 깔려 있는 경우가 많다.
from charset_normalizer import from_bytes
raw = open("주소록.csv", "rb").read()
best = from_bytes(raw).best()
print(best.encoding) # 예: cp949
text = str(best)
바이너리로 읽어서("rb") 디코딩을 직접 하는 게 요령이다. 텍스트 모드로 열면 그 순간 이미 에러가 나기 때문이다.
이미 인코딩을 알았다면 파일 자체를 utf-8로 바꿔두는 편이 오래 편하다. iconv는 맥과 리눅스에 기본으로 있다.
iconv -f CP949 -t UTF-8 주소록.csv > 주소록-utf8.csv
주의할 점은 짧은 문자열의 인코딩 판별이 원리적으로 불가능하다는 것이다. 바이트 몇 개는 여러 코덱에서 동시에 유효하다. 자동 판별은 "후보를 좁히는 도구"로만 쓰고, 사람이 눈으로 결과를 확인하는 절차를 남겨두는 게 안전하다.
그래도 안 될 때 — 쓰는 쪽과 실행 환경
읽기에서 고쳤는데도 에러가 나면 이번엔 반대쪽이다. UnicodeEncodeError는 글자를 바이트로 바꾸는 과정에서 났다는 뜻이라 처방이 다르다.
첫째, 터미널 출력에서 막히는 경우다. print()가 UnicodeEncodeError: 'ascii' codec can't encode characters로 죽으면 표준출력의 인코딩이 ascii로 잡힌 것이다. 환경 변수로 고정한다.
PYTHONIOENCODING=utf-8 python3 app.py
python3 -X utf8 app.py # UTF-8 모드 강제
UTF-8 모드는 PYTHONUTF8=1로도 켤 수 있고, 3.15부터는 기본값이다. (PEP 540, PEP 686)
둘째, 파일을 쓸 때 인코딩을 안 적은 경우다. 읽기만 고치고 쓰기는 그대로 두면, 저장된 파일이 실행 환경에 따라 달라진다.
with open("결과.csv", "w", encoding="utf-8-sig", newline="") as f:
f.write("이름,점수\n")
엑셀에서 다시 열어야 하는 파일이라면 utf-8-sig가 편하다. BOM이 있어야 엑셀이 utf-8로 인식한다.
셋째, 외부 명령의 출력을 읽을 때다. subprocess의 text=True는 로캘 인코딩을 쓰기 때문에, 실행 환경이 바뀌면 깨진다.
import subprocess
out = subprocess.run(["ls"], capture_output=True, text=True, encoding="utf-8").stdout
넷째, 파일 이름 자체가 문제인 경우다. 리눅스에서 잘못된 바이트가 섞인 파일명은 surrogateescape 오류 처리기로 문자열에 담긴다. 이걸 다시 인코딩해야 원래 파일명으로 돌아간다. (에러 처리기)
import os
for name in os.listdir("."):
raw = name.encode("utf-8", "surrogateescape")
errors 기본값은 strict이고, replace는 깨진 글자를 �로 바꾸며 ignore는 아예 지운다. 데이터를 살려야 하는 자리에서 ignore를 쓰면 행 길이가 달라져서 나중에 원인 찾기 어려운 버그가 된다. (에러 처리기)
증상별 처방 표
지금까지 내용을 증상 기준으로 다시 묶었다. 에러 문구를 그대로 검색해 들어온 사람을 위한 표다.
| 증상 | 원인 | 처방 |
|---|---|---|
'utf-8' codec can't decode byte 0xc7 | cp949 파일을 utf-8로 읽음 | encoding="cp949" |
'cp949' codec can't decode byte 0xed | utf-8 파일을 cp949로 읽음 | encoding="utf-8" |
첫 컬럼명이 \ufeff...로 보임 | utf-8 BOM | encoding="utf-8-sig" |
'ascii' codec can't encode (출력) | 표준출력이 ascii | PYTHONIOENCODING=utf-8 |
pandas UnicodeDecodeError | read_csv 기본 인코딩 | encoding= 인자 지정 |
| 파일명 처리에서 surrogate 에러 | 잘못된 바이트 파일명 | surrogateescape |
| 인코딩을 모르겠음 | 판별 필요 | file -I, charset-normalizer, iconv |
내가 막혔던 곳
가장 오래 헤맨 건 공공데이터 CSV였다. pd.read_csv(path)가 UnicodeDecodeError를 내서 encoding="cp949"를 붙였더니 이번엔 읽히는데, 컬럼 이름이 지역이 아니라 \ufeff지역처럼 잡혀서 df["지역"]이 KeyError를 냈다. 화면에 출력된 컬럼명은 멀쩡해 보여서 한참을 의심하지 못했다. BOM은 눈에 안 보이니까. 결국 repr(df.columns[0])로 찍어보고 나서야 원인을 알았다. 지금은 한국어 CSV를 받으면 utf-8-sig를 먼저 시도하고, 실패하면 cp949로 내려간다.
두 번째는 errors="ignore"의 유혹이었다. 급할 때 이걸 붙이면 에러가 사라지고 코드는 끝까지 돌아서 성공한 것처럼 보인다. 그런데 그 뒤에 만든 집계표에서 특정 지역 이름만 비어 있었다. 복구가 안 되는 손실이었다. 그 뒤로는 ignore 대신 replace를 쓰고, 깨진 개수를 세서 로그로 남긴다. 몇 건이 깨졌는지 알아야 데이터를 믿을지 결정할 수 있다.
세 번째는 환경 차이였다. 내 맥에서는 잘 돌던 스크립트가 윈도우 노트북에서 같은 파일을 읽다가 죽었다. 맥은 기본이 utf-8이고 한국어 윈도우는 ANSI 코드 페이지가 cp949라서, 인코딩을 안 적은 open()이 서로 다르게 동작한 것이다. 그때부터 텍스트를 여는 코드에는 예외 없이 encoding을 적는다. 파일을 옮겨 다니는 프로젝트라면 Python 가상환경 venv 만들기처럼 실행 환경을 문서에 남겨두는 습관도 같이 들였다.
참고 자료
- UnicodeDecodeError — Built-in Exceptions —
encoding·start·end·reason속성 - open() — Built-in Functions — 텍스트 모드 기본 인코딩 규칙
- codecs — 표준 인코딩 —
cp949/949/ms949/uhc별칭 - codecs — 에러 처리기 —
strict·replace·surrogateescape - codecs — utf_8_sig — BOM 처리
- PEP 686 — Make UTF-8 mode default — Python 3.15 기본값 변경
- PEP 597 — Add optional EncodingWarning —
-X warn_default_encoding - PEP 540 — UTF-8 mode —
PYTHONUTF8 - pandas.read_csv —
encoding,encoding_errors - RFC 8259 §8.1 — JSON은 UTF-8
- 관련 글: ModuleNotFoundError 해결 · pip install 오류 해결 · Python 가상환경 venv 만들기
'IT' 카테고리의 다른 글
| OpenAI dots란? — 컴퓨터 여러 대에 붙는 AI 에이전트 총정리 (0) | 2026.11.10 |
|---|---|
| 부이(vooy) vs 피클 비교 — 말로 시키는 한국 AI 에이전트, 뭘 골라야 하나 (2026) (0) | 2026.11.09 |
| Gemini 사용법 — 무료 계정으로 Gems·Deep Research까지 쓰는 순서 (0) | 2026.11.09 |
| GPT-6 Cyber란? — 취약점 찾고 익스플로잇까지 쓰는 보안 특화 모델 총정리 (0) | 2026.11.09 |
| Cursor 사용법 — 설치 후 바로 쓰는 단축키·Rules 정리 (0) | 2026.11.08 |


















