~ / posts / developer

CSV 엑셀 한글 깨짐 해결, UTF-8 BOM·CP949·따옴표 규칙 정리

CSV를 엑셀로 열면 한글이 깨지는 건 UTF-8 파일을 엑셀이 CP949로 읽기 때문이고, 맨 앞에 BOM 3바이트(EF BB BF)를 붙이면 해결돼요. RFC 4180 따옴표 규칙, 쉼표·줄바꿈 값, 앞자리 0과 지수 표기, JSON 평탄화까지 정리했어요.

서버에서 내려받은 CSV를 엑셀로 열었더니 홍길동이 �솉湲몃룞 같은 외계어로 바뀌어 있던 경험, 개발자라면 한 번쯤 있을 거예요. 결론부터 말하면 한국어 윈도우 엑셀은 BOM이 없는 CSV를 CP949(EUC-KR 확장)로 읽기 때문에 UTF-8 파일이 깨지고, 파일 맨 앞에 UTF-8 BOM 3바이트(EF BB BF)를 붙여 저장하면 엑셀이 UTF-8로 알아보고 한글이 제대로 나와요. 메모장이나 VS Code에서는 멀쩡한데 엑셀에서만 깨지는 이유가 바로 이거예요.

그런데 한글 깨짐만 고치면 끝이 아니더라고요. 주소에 쉼표가 들어가면 칸이 밀리고, 전화번호 010의 0이 사라지고, 16자리 카드번호 끝자리가 0으로 바뀌고, JSON을 CSV로 바꾸면 [object Object]가 찍혀요. 이 글에서는 실제 바이트와 실행 결과로 원인을 하나씩 짚고, CSV를 "엑셀에서도 안 깨지게" 만드는 체크리스트까지 정리했어요.

핵심 요약

엑셀 한글 깨짐은 인코딩 불일치예요. UTF-8로 저장하되 맨 앞에 BOM(EF BB BF)을 붙이거나, 엑셀의 "텍스트/CSV에서 가져오기"로 인코딩을 65001(UTF-8)로 지정하세요.

RFC 4180 규칙상 쉼표·큰따옴표·줄바꿈이 들어간 값은 전체를 큰따옴표로 감싸고, 값 안의 큰따옴표는 두 번("")으로 씁니다.

앞자리 0, 12자리 이상 숫자의 지수 표기, 15자리 넘는 숫자의 끝자리 손실은 CSV가 아니라 엑셀의 자동 형식 변환 탓이라 가져오기 단계에서 열 형식을 "텍스트"로 지정해야 막을 수 있어요.

JSON을 CSV로 바꿀 땐 중첩 객체는 address.city처럼 점 경로 열로 펼치고, 배열은 JSON 문자열로 넣는 게 정보 손실이 가장 적어요.

CSV 엑셀 한글 깨짐, 왜 생길까

CSV 파일에는 "나는 어떤 인코딩이다"라는 정보가 없어요. 그냥 바이트의 나열이라, 여는 프로그램이 인코딩을 추측해야 합니다. 그리고 한국어 윈도우의 엑셀은 별다른 표시가 없으면 시스템 기본 코드 페이지인 CP949로 읽어요.

문제는 요즘 서버·파이썬·자바스크립트가 만드는 CSV가 거의 전부 UTF-8이라는 점이에요. "홍길동"을 바이트로 보면 차이가 분명합니다(파이썬으로 직접 확인한 값).

인코딩"홍길동" 바이트바이트 수
UTF-8ED 99 8D EA B8 B8 EB 8F 999
CP949C8 AB B1 E6 B5 BF6

UTF-8 바이트 9개를 CP949 규칙(대부분 2바이트가 한 글자)으로 읽으면, 첫 바이트 ED는 짝이 맞지 않아 �가 되고 이어서 99 8D → 솉, EA B8 → 湲, B8 EB → 몃, 8F 99 → 룞처럼 엉뚱하게 짝이 지어져 �솉湲몃룞이 됩니다. 헤더 이름,전화번호도 같은 방식으로 �씠由�,�쟾�솕踰덊샇가 돼요. 반대로 CP949로 저장한 파일을 UTF-8로 열면 ȫ�浿처럼 깨지고요. 깨진 모양이 한자·특수문자 섞인 한글이면 UTF-8을 CP949로 읽은 것, 물음표와 라틴 문자가 섞이면 CP949를 UTF-8로 읽은 것이라고 기억해 두면 디버깅이 빨라져요. 한글 바이트 구조는 한글 한 글자 몇 바이트? UTF-8과 EUC-KR 차이에 자세히 정리했어요.

UTF-8 CSV 파일을 BOM 없이 열면 엑셀이 CP949로 해석해 한글이 깨지고, 맨 앞에 EF BB BF 3바이트 BOM을 붙이면 UTF-8로 인식해 정상 표시되는 흐름도
BOM 3바이트가 엑셀의 인코딩 판단을 바꿔요

해결 1: UTF-8 BOM 붙여서 저장하기

BOM(Byte Order Mark)은 파일 맨 앞에 붙이는 U+FEFF 문자예요. UTF-8로 쓰면 EF BB BF 3바이트이고, 엑셀은 이 3바이트를 보면 "UTF-8 파일이구나" 하고 제대로 읽어요. 내용은 그대로 두고 앞에 3바이트만 붙이는 거라 용량 부담도 사실상 없습니다.

같은 CSV 1,000행의 인코딩별 파일 크기
010,00020,00030,00040,00050,000UTF-8 · 파일 크기 46,041바이트46,041UTF-8UTF-8 + BOM · 파일 크기 46,044바이트46,044UTF-8 + BOMCP949 · 파일 크기 38,029바이트38,029CP949

BOM은 딱 3바이트. CP949가 작은 건 한글 1자가 2바이트라서예요

단위: 바이트 · 자료: "홍길동,서울시 중구,01012345678,04524" 1,000행 + 헤더를 파이썬으로 인코딩해 직접 계산
표로 보기
구분파일 크기
UTF-846,041
UTF-8 + BOM46,044
CP94938,029

언어별로 BOM을 붙이는 방법은 이렇습니다.

# 파이썬: encoding을 utf-8-sig로 주면 BOM이 붙어요
import csv
with open('users.csv', 'w', encoding='utf-8-sig', newline='') as f:
    csv.writer(f).writerows(rows)
# pandas
df.to_csv('users.csv', index=False, encoding='utf-8-sig')
// 브라우저에서 다운로드: 문자열 앞에 \uFEFF를 붙여요
const blob = new Blob(['\uFEFF' + csvText], { type: 'text/csv;charset=utf-8' });
// Node.js
fs.writeFileSync('users.csv', '\uFEFF' + csvText, 'utf8');

자바는 OutputStreamWriter로 UTF-8 스트림을 연 뒤 첫 글자로 '\uFEFF'를 쓰면 되고, 스프링에서 응답으로 내려줄 때도 본문 맨 앞에 같은 문자를 붙이면 돼요. 파이썬에서 newline=''를 꼭 주는 이유는, 안 주면 윈도우에서 줄바꿈이 \r\r\n으로 두 번 들어가 빈 줄이 생기기 때문이에요.

BOM의 부작용도 알아 두기

BOM은 엑셀에겐 고마운 표시지만, 다른 프로그램에겐 그냥 "보이지 않는 문자 하나"예요. 그래서 이런 일이 생깁니다.

그래서 실무에서는 "사람이 엑셀로 열 파일"에만 BOM을 붙이고, 시스템끼리 주고받는 CSV는 BOM 없는 UTF-8로 맞추는 걸 추천해요.

해결 2: BOM 없이 엑셀에서 제대로 여는 법

받은 파일을 고칠 수 없을 때는 엑셀에서 여는 방법을 바꾸면 돼요. 파일을 더블클릭하지 말고 데이터 → 텍스트/CSV에서 메뉴로 가져오면, 미리보기 창 위쪽 "파일 원본"에서 65001: 유니코드(UTF-8)를 고를 수 있어요. 이 화면에서 열 형식을 미리 지정할 수 있어서 뒤에 나올 앞자리 0 문제도 같이 해결됩니다.

다른 방법들도 정리해 볼게요.

상황방법비고
내가 만드는 파일UTF-8 + BOM으로 저장가장 확실
받은 파일, 한 번만 열기데이터 → 텍스트/CSV에서, 65001 선택열 형식도 지정 가능
받은 파일, 고쳐서 공유메모장/VS Code에서 "UTF-8 (BOM)"으로 다시 저장VS Code는 오른쪽 아래 인코딩 클릭
옛 시스템이 CP949만 받음CP949로 인코딩해 저장이모지·일부 한글은 표현 불가
구글 시트그냥 가져오기UTF-8을 기본으로 잘 읽음

CP949로 저장하는 건 최후의 수단이에요. CP949는 이모지를 아예 담을 수 없고, 이름에 들어가는 일부 옛 한글이나 특수 기호도 빠지거나 ?로 바뀌어요. 파이썬에서 encode('cp949')를 하면 UnicodeEncodeError가 나는 순간이 그때입니다.

RFC 4180 CSV 따옴표 규칙, 쉼표와 줄바꿈이 들어간 값

인코딩 다음으로 많이 깨지는 게 "칸 밀림"이에요. CSV는 이름 그대로 쉼표로 칸을 나누는데, 값 안에 쉼표가 있으면 어디까지가 한 칸인지 알 수 없잖아요. 이걸 정한 게 RFC 4180이고, 핵심 규칙은 네 가지예요.

  1. 줄 끝은 CRLF(\r\n)를 쓴다. (대부분의 파서는 LF만 있어도 읽어요.)
  2. 값에 쉼표, 큰따옴표, 줄바꿈이 있으면 값 전체를 큰따옴표로 감싼다.
  3. 감싼 값 안의 큰따옴표는 두 번 써서 이스케이프한다. (" → "") 백슬래시 \"는 표준이 아니에요.
  4. 첫 줄은 헤더일 수 있고, 모든 행의 칸 수는 같아야 한다.

예를 들어 메모가 배송 전 연락, "부재 시" 문 앞인 행을 파이썬 csv 모듈로 쓰면 이렇게 나와요.

이름,메모,전화번호,우편번호
홍길동,"배송 전 연락, ""부재 시"" 문 앞",01012345678,04524

쉼표가 있으니 전체를 감쌌고, 안의 큰따옴표는 ""로 두 번 들어갔어요. 줄바꿈이 들어간 주소도 마찬가지로 따옴표 안이면 한 칸으로 읽혀요.

직접 split(',') 하면 안 되는 이유

CSV를 읽을 때 line.split(',')로 자르는 코드가 정말 많은데, 위 행 하나만 넣어도 칸이 5개로 늘어나요. 따옴표 안의 줄바꿈까지 생각하면 readline()으로 한 줄씩 읽는 것도 틀린 방법이고요. CSV는 반드시 파서로 읽고 쓰세요. 파이썬 csv, 자바스크립트 Papa Parse, 자바 OpenCSV·Apache Commons CSV 같은 라이브러리가 이 규칙을 다 처리해 줍니다.

자주 보는 깨짐 패턴을 정리하면 이래요.

증상원인해결
특정 행만 칸이 오른쪽으로 밀림값 안의 쉼표를 따옴표로 안 감쌈CSV 라이브러리로 쓰기
한 행이 두 행으로 쪼개짐값 안의 줄바꿈 + 줄 단위 읽기파서로 읽기
따옴표가 \"로 남음백슬래시 이스케이프(비표준)""로 이스케이프
모든 데이터가 A열 한 칸에구분자가 세미콜론·탭가져오기에서 구분 기호 지정

마지막 줄은 유럽 지역 설정의 엑셀이 쉼표 대신 세미콜론(;)을 구분자로 쓰기 때문에 생겨요.

엑셀 앞자리 0 사라짐과 지수 표기 문제

인코딩도 맞고 따옴표도 맞는데 데이터가 바뀌는 경우가 남아 있어요. 이건 CSV 파일 문제가 아니라 엑셀이 열면서 "숫자처럼 보이는 값"을 숫자로 바꾸기 때문이에요. 파일을 메모장으로 열어 보면 01012345678이 멀쩡히 있는데, 엑셀에서는 1012345678로 보이는 거죠. 그리고 그 상태로 저장하면 그때부터는 파일에서도 0이 사라집니다.

원래 자릿수와 엑셀이 숫자로 바꾼 뒤 남는 유효 자릿수
원래 자릿수엑셀에 남는 자릿수
05101520휴대폰번호 010 · 원래 자릿수 11자리휴대폰번호 010 · 엑셀에 남는 자릿수 10자리휴대폰번호 010우편번호 0으로 시작 · 원래 자릿수 5자리우편번호 0으로 시작 · 엑셀에 남는 자릿수 4자리우편번호 0으로 시작바코드 13자리 · 원래 자릿수 13자리바코드 13자리 · 엑셀에 남는 자릿수 13자리바코드 13자리카드번호 16자리 · 원래 자릿수 16자리카드번호 16자리 · 엑셀에 남는 자릿수 15자리카드번호 16자리주문번호 18자리 · 원래 자릿수 18자리주문번호 18자리 · 엑셀에 남는 자릿수 15자리주문번호 18자리

13자리 바코드는 숫자는 남지만 일반 서식에서 8.80123E+12처럼 지수로 보여요

단위: 자리 · 자료: 엑셀 숫자 정밀도(유효숫자 15자리) 규칙으로 계산한 예시
표로 보기
구분원래 자릿수엑셀에 남는 자릿수
휴대폰번호 0101110
우편번호 0으로 시작54
바코드 13자리1313
카드번호 16자리1615
주문번호 18자리1815

엑셀이 바꾸는 규칙은 크게 세 가지예요.

그 밖에 1-2, 3/4 같은 값이 날짜로 바뀌는 것도 같은 원리예요. 최신 Microsoft 365 엑셀에는 "파일 → 옵션 → 데이터 → 자동 데이터 변환"에서 앞자리 0 제거, 긴 숫자 지수 변환 등을 끌 수 있는 설정이 생겼지만, 받는 사람 PC 설정에 기대는 방법이라 파일 쪽에서 막는 게 더 확실해요.

앞자리 0을 지키는 방법

  1. 받는 쪽: 데이터 → 텍스트/CSV에서 가져오기 → "데이터 변환"에서 해당 열 형식을 "텍스트"로 지정. 가장 정석이에요.
  2. 만드는 쪽, 엑셀 전용 파일: 값을 ="01012345678" 형태로 쓰면 엑셀이 수식 결과를 문자열로 보여 줘요. 단 다른 프로그램에서는 이 값이 그대로 ="..." 문자열로 읽히니 엑셀 전용일 때만 쓰세요.
  3. 아예 xlsx로 내보내기: 셀 형식을 텍스트로 지정해서 저장할 수 있어 가장 깔끔해요.
  4. ID를 숫자로 다루지 않기: 전화번호, 우편번호, 카드번호는 계산할 일이 없는 "문자열 ID"예요. DB와 JSON에서도 숫자 타입 대신 문자열로 두는 게 맞아요. 자바스크립트 숫자는 2^53 - 1(9007199254740991, 16자리)을 넘으면 정밀도가 깨지니, 긴 ID는 JSON에서도 문자열이어야 합니다.

CSV 인젝션도 함께 막기

값이 =, +, -, @로 시작하면 엑셀이 수식으로 해석할 수 있어요. 사용자가 입력한 이름에 =HYPERLINK(...) 같은 걸 넣어 두면, 관리자가 내려받은 CSV를 열 때 실행되는 "CSV 인젝션"이 됩니다. 사용자 입력을 CSV로 내보낼 땐 이 문자로 시작하는 값 앞에 작은따옴표(')를 붙여 문자열로 취급되게 하는 방어가 흔히 쓰여요.

JSON ↔ CSV 변환: 중첩 객체 평탄화

API 응답 JSON을 엑셀로 넘겨 달라는 요청은 정말 흔하죠. 그런데 JSON은 나무 구조이고 CSV는 표라서, 그대로는 안 들어가요. 아무 처리 없이 문자열로 바꾸면 중첩 객체 칸에 [object Object]가 찍히는 게 그래서예요.

[
  {"id": 1, "name": "홍길동", "address": {"city": "서울", "zip": "04524"}, "tags": ["vip", "new"]},
  {"id": 2, "name": "김철수", "address": {"city": "부산"}, "tags": []}
]

이걸 표로 만드는 일반적인 방법이 평탄화(flatten)예요. 중첩 객체의 키를 점으로 이어 address.city, address.zip 같은 열 이름을 만들고, 배열은 JSON 문자열로 한 칸에 넣습니다. 파이썬으로 변환한 실제 결과예요.

id,name,address.city,address.zip,tags
1,홍길동,서울,04524,"[""vip"", ""new""]"
2,김철수,부산,,[]

여기서 눈여겨볼 점이 세 가지 있어요.

반대로 CSV → JSON은 헤더를 키로 쓰고 행마다 객체를 만들면 되는데, address.city 같은 점 경로 헤더를 다시 중첩 객체로 복원할지 정해야 해요. 복원하면 원래 구조로 돌아오고, 안 하면 "address.city": "서울"이라는 평평한 키가 됩니다. JSON 문법 자체가 자꾸 깨진다면 JSON 문법 오류 패턴을, 데이터 형식 선택이 고민이면 JSON·YAML·XML 비교를 참고하세요.

엑셀용 CSV 내보내기 체크리스트

지금까지 내용을 "다운로드 버튼 만들 때 확인할 것"으로 묶어 볼게요.

마지막 항목의 파일명은 퍼센트 인코딩 규칙을 따라요. 원리는 URL 인코딩 원리와 한글 URL에서 다뤘어요.

자주 묻는 질문

CSV를 메모장에서 열면 멀쩡한데 엑셀에서만 깨지는 이유는?

메모장과 VS Code는 내용을 보고 UTF-8을 잘 추측하지만, 한국어 윈도우 엑셀은 BOM이 없으면 CP949로 읽기 때문이에요. 메모장에서 "다른 이름으로 저장 → 인코딩: UTF-8(BOM)"으로 다시 저장하면 엑셀에서도 제대로 보여요.

맥 엑셀에서도 BOM이 필요한가요?

네, 맥용 엑셀도 BOM이 없는 UTF-8 CSV를 다른 인코딩으로 읽어 한글이 깨지는 경우가 많아요. BOM을 붙인 파일은 윈도우와 맥 양쪽에서 제대로 열리니 엑셀용 파일에는 붙여 두는 게 안전합니다.

CSV 값 안에 큰따옴표가 있으면 어떻게 쓰나요?

값 전체를 큰따옴표로 감싸고, 안에 있는 큰따옴표는 두 번 써요. 그는 "안녕"이라고 했다는 "그는 ""안녕""이라고 했다"가 됩니다. 백슬래시로 이스케이프하는 건 RFC 4180 표준이 아니라 엑셀에서 깨져요.

엑셀에서 전화번호 앞 0이 사라지는 걸 막으려면?

더블클릭으로 열지 말고 데이터 → 텍스트/CSV에서 가져온 뒤 그 열을 "텍스트" 형식으로 지정하세요. 이미 열어서 0이 사라진 상태로 저장했다면 파일에서도 0이 지워진 것이니, 원본 파일에서 다시 가져와야 해요.

JSON을 CSV로 바꾸면 [object Object]가 나와요.

중첩 객체를 문자열로 그냥 바꿔서 생기는 현상이에요. address.city처럼 점 경로로 평탄화하거나, 객체를 JSON.stringify한 문자열로 넣어야 합니다. JSON ↔ CSV 변환기를 쓰면 평탄화와 따옴표 처리를 한 번에 해 줘요.

정리

CSV가 엑셀에서 깨지는 문제는 세 층으로 나눠 보면 쉬워요. 인코딩(UTF-8 + BOM 또는 가져오기에서 65001), 구조(RFC 4180 따옴표 규칙을 라이브러리로 지키기), 엑셀의 자동 변환(앞자리 0, 지수 표기, 15자리 정밀도 — 텍스트 형식 지정이나 xlsx). 여기에 JSON에서 넘어오는 데이터라면 평탄화 규칙을 하나 정해 두면 끝이에요.

API 응답을 바로 표로 바꿔 보려면 JSON ↔ CSV 변환기를, 변환 전에 JSON 구조를 확인하거나 문법 오류를 찾으려면 JSON 포매터를 써 보세요.

#CSV 한글 깨짐#엑셀 CSV 깨짐#UTF-8 BOM#CP949#RFC 4180#CSV 따옴표#엑셀 앞자리 0#JSON CSV 변환
← 이전 글크론 표현식 읽는 법, 5필드 문법과 예제 20개·타임존 함정 정리다음 글 →MD5 SHA-1 SHA-256 차이, 해시와 암호화·체크섬 확인법 정리

더 둘러보기

💻 개발자 다른 도구

🔥 많이 찾는 도구

🔮 재미로 한 번 더

📝 읽어 볼 만한 글

글 전체 보기 →