
파일 저장은 됐는데 실패로 기록된다면—cp949 로그 인코딩부터 확인하세요.
본문 저장과 로그 출력이 한 try 블록에 묶이면 저장 성공 뒤 로그의 한 글자 때문에 전체 작업이 실패로 기록될 수 있습니다. 저장과 로깅의 예외 범위를 먼저 분리하세요. 2026년 7월 26일 Python 3.12.10 재현과 파이썬 공식 reconfigure 문서를 대조했습니다.
저장은 끝났고, 틀린 건 결과 기록이었습니다
실패로 적힌 2편은 원고 파일 크기도 정상이고 내용도 끝까지 들어가 있었습니다. 실패 사유 칸에 남은 문장은 이거였어요.
UnicodeEncodeError: 'cp949' codec can't encode character '\u2014'
in position 11: illegal multibyte sequence
내용을 못 만들었다는 말이 아닙니다. 글자 하나를 화면에 찍지 못했다는 말이에요. 파일을 쓰고 닫는 작업은 이미 끝난 뒤였고, 그 다음 줄에서 터졌습니다. 결과표의 2/13은 생성 실패율이 아니라 기록 실패율이었던 거죠.
이런 오진이 위험한 이유는 숫자가 조용히 틀리기 때문입니다. 결과표만 보고 다시 돌리면 이미 있는 글을 또 만들거나, 반대로 진짜 실패 2건을 놓치게 됩니다.
🔎 긴 대시 한 글자가 cp949에서 걸리는 이유
윈도우 한국어 환경의 기본 문자표는 cp949입니다. 여기에는 한글과 한자가 다 들어 있지만, 서양식 문장부호는 일부만 들어 있어요. 하필 제목에 쓴 긴 대시(—, 유니코드 U+2014)가 그 표에 없는 글자였습니다.
같은 자리에서 네 글자를 직접 시험해봤습니다. 결과가 이렇게 갈렸어요.
- — (U+2014, EM DASH) : cp949로 변환 실패
- – (U+2013, EN DASH) : 변환 실패
- ― (U+2015, HORIZONTAL BAR) : 성공, 바이트값 A1AA
- - (보통 하이픈) : 성공, 바이트값 2D
눈으로는 거의 구분이 안 되는데, 셋째 줄은 통과하고 첫째 줄은 막힙니다. 우편 신청서의 6칸짜리 번호란에 7자리를 적으면 내용이 맞고 틀리고를 따지기 전에 접수 자체가 안 되는 것과 같아요. 글자가 나쁜 게 아니라, 통로가 그 글자를 담을 칸이 없는 상태입니다.
💡 왜 콘솔에서는 안 나고 로그 파일로 넘길 때 났나
출력을 화면이 아니라 파일로 넘기면 콘솔 규칙 대신 시스템 기본 문자표를 씁니다. 제 환경에서 그 값은 cp949였고, 같은 코드를 콘솔에서만 돌렸을 때는 문제가 드러나지 않았습니다. 무인 실행으로 돌리는 순간 조건이 바뀐 셈이에요.
진짜 원인은 문자표가 아니라 try 하나였습니다
결론부터 말하면 문자표는 방아쇠일 뿐입니다. 성공을 실패로 바꿔 적은 건 저장과 출력이 같은 예외 처리 블록에 묶여 있던 구조였어요. 사고 당시 코드는 이렇게 생겼습니다.
for job in jobs:
try:
save_article(job.id, job.html) # 여기까지 정상 완료
print(f'[저장 완료] {job.title}') # 여기서 UnicodeEncodeError
except Exception as e:
mark_failed(job.id, e) # 성공한 글이 실패로 기록됨
continue
except가 잡는 범위 안에 두 종류의 사고가 섞여 있습니다. 파일을 못 쓴 사고와, 다 쓰고 나서 화면에 못 찍은 사고요. 이 둘을 한 바구니에 담으면 결과표가 무슨 뜻인지 아무도 확신할 수 없게 됩니다.
제일 많이 놓치는 지점이 여기예요. 급하면 제목에서 긴 대시를 지우거나 금지 문자 목록을 만들어 걸러내고 싶어집니다. 그건 이번 한 글자만 막는 임시 대응입니다. 따옴표, 화살표, 특수 기호는 앞으로도 계속 들어오니까요. 고쳐야 하는 건 출력 실패가 생성 실패로 승격되는 경계입니다.
고친 코드와, 재실행이 중복을 만들지 않게 한 장치
수리는 두 갈래로 했습니다. 출력 통로 자체를 UTF-8로 다시 열고, 저장과 출력을 서로 다른 블록으로 떼어냈어요. 프로그램 맨 앞에 두 줄을 넣습니다.
import sys
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
sys.stderr.reconfigure(encoding='utf-8', errors='replace')
파이썬 공식 문서에는 reconfigure(*, encoding=None, errors=None, newline=None, line_buffering=None, write_through=None)로 적혀 있고 3.7 버전에서 추가됐습니다. 새 설정을 적용하기 전에 밀린 출력을 먼저 내보낸다는 설명도 함께 있어요. 그리고 반복문은 이렇게 나눴습니다.
for job in jobs:
if already_saved(job.id): # 재실행 안전장치
print(f'[건너뜀] {job.id}')
continue
try:
save_article(job.id, job.html)
except Exception as e:
mark_failed(job.id, e) # 진짜 저장 실패만 여기로
continue
mark_saved(job.id)
print(f'[저장 완료] {job.title}') # 출력 실패는 생성 실패가 아님
확인 결과는 이렇게 나왔습니다. 고치기 전 같은 문장을 로그 파일로 내보내면 종료 코드 1로 죽고 로그 파일은 빈 상태였습니다. 고친 뒤에는 종료 코드 0으로 끝났고, 로그 파일 안에 긴 대시가 UTF-8 바이트 E2 80 94로 온전히 남았어요. 글자를 물음표로 바꿔치기해서 넘긴 게 아니라 원문 그대로 기록됐다는 뜻입니다.
⚠️ 여기서 한 번 더 걸리는 함정 세 가지
같은 수리를 따라 하다가 다시 넘어질 수 있는 지점이 세 군데 있습니다. 셋 다 공식 문서에 적혀 있는데 잘 안 읽히는 부분이에요.
1. encoding만 넘기면 errors가 다시 엄격해집니다
공식 문서는 "지정하지 않은 항목은 현재 설정을 유지하지만, encoding을 지정하고 errors를 지정하지 않으면 errors='strict'가 사용된다"고 못 박고 있습니다. 실제로 reconfigure(encoding='utf-8')만 실행하니 오류 처리 방식이 strict로 바뀌었어요. 두 값을 반드시 함께 넘기세요.
2. replace는 안전장치일 뿐, 글자를 살려주지 않습니다
공식 문서 기준으로 replace는 변환할 때 물음표(?)를 대신 넣습니다. 즉 통로가 여전히 cp949라면 프로그램은 안 죽지만 로그에는 제목 ? 부제처럼 남아요. 죽지 않게 하는 것과 원문을 보존하는 것은 다른 문제라서, 통로를 UTF-8로 바꾸는 쪽이 먼저입니다.
3. 오류 메시지 쪽은 원래 안 죽습니다
환경변수로 통로를 바꿀 때 표기법은 encodingname:errorhandler인데, 공식 문서는 "stderr의 경우 :errorhandler 부분은 무시되며 처리 방식은 항상 backslashreplace"라고 설명합니다. 그래서 예외 메시지는 깨진 채로라도 나오고, 정작 사람이 보라고 찍은 진행 로그만 프로그램을 멈춰 세웁니다. 오류창이 조용하다고 안전하다는 뜻이 아니에요.
통로를 코드로 못 건드리는 상황이라면 실행 전에 환경변수로 같은 효과를 낼 수 있습니다. PYTHONIOENCODING=utf-8:replace를 넣거나, 전체를 UTF-8로 돌리는 PYTHONUTF8=1(또는 실행 옵션 -X utf8)을 쓰는 방법이 공식 문서에 나와 있어요. 다만 어느 쪽이든 저장과 출력을 같은 try에서 떼어내는 작업은 따로 해야 합니다. 통로를 넓혀도 예외 하나가 성공을 실패로 적는 구조는 그대로 남으니까요.
자주 나오는 질문
로그가 문제라면 print를 지우면 되지 않나요?
그러면 프로그램은 안 죽지만 다음에 진짜 실패가 났을 때 원인을 볼 수 없습니다. 출력은 남기고 예외 처리 범위만 나누는 쪽이 안전해요. 무인으로 돌리는 작업일수록 남는 기록이 유일한 단서입니다.
제목에서 긴 대시만 빼도 해결되는 거 아닌가요?
이번 한 글자는 막힙니다. 그런데 cp949에 없는 글자는 계속 새로 들어옵니다. 위에서 확인했듯 짧은 대시도 막히고, 겉모습이 거의 같은 U+2015는 통과해요. 글자를 하나씩 금지 목록에 넣는 방식으로는 끝이 안 납니다.
이미 실패로 기록된 글은 어떻게 되돌리나요?
먼저 저장 폴더에서 파일이 실제로 있는지, 내용이 끝까지 들어갔는지 확인합니다. 있으면 결과 기록만 성공으로 바꾸면 됩니다. 다시 돌릴 때는 이미 저장된 것을 건너뛰는 검사를 켜두어야 같은 글이 두 번 만들어지지 않아요.
확인한 기준과 공식 출처
재현된 오류와 수정 결과는 해당 Windows 환경에서 관측한 값이고, reconfigure의 인자와 오류 처리 방식은 파이썬 공식 문서 기준입니다. 실행 환경에 따라 기본 문자 인코딩이 다를 수 있습니다.
- 확인일: 2026년 7월 26일
- 확인 환경: 윈도우 한국어 설정, Python 3.12.10, 시스템 기본 문자표 cp949
- 대조한 항목: reconfigure의 인자 구성과 errors 기본값, replace가 변환 때 넣는 글자, PYTHONIOENCODING 표기법과 stderr 예외 규칙
- 재현 방법: 긴 대시가 들어간 문장을 출력하는 코드를 로그 파일로 내보내 종료 코드와 로그 내용을 비교
- 파이썬 공식 문서 - PYTHONIOENCODING 환경변수 (2026-07-26 확인)
- 파이썬 공식 문서 - UTF-8 모드 (2026-07-26 확인)
다음 편에서는 오늘 나온 다른 문제를 이어서 다룹니다. 결과 기록이 한 번 틀린 뒤에 재실행을 걸었을 때, 어디까지가 안전하고 어디서부터 같은 작업이 두 번 돌아가는지 실제 순서대로 확인해보려고 합니다.