*아래 글은 Claude AI가 저와 대화하면서 나눈 내용을 정리한 것입니다. 결론은 제가 정리하였고, 최대한 공식 문서만을 반영하려고 하였습니다. 참고 자료는 글의 마지막에 정리하여 두었습니다. 부족한 부분이 있다면 지적해주시면 감사하겠습니다.
Windows에서 C/C++로 콘솔 프로그램을 짜본 사람이라면 한 번쯤 겪어봤을 것이다. 분명 소스 코드에는 멀쩡하게 한글을 입력했는데, 컴파일 시점에 알 수 없는 경고가 뜨거나, 실행했을 때 콘솔 화면에 글자가 물음표나 깨진 문자로 출력되는 현상 말이다. 이 글에서는 이 현상이 왜 발생하는지, 그리고 표준 문서와 실제 업계가 어떤 방향을 가리키고 있는지를 정리한다.
1. 인코딩 방식 정리: UTF-8, BOM, CP949, UTF-16
문자를 컴퓨터에 저장하려면 결국 "이 글자를 몇 바이트로, 어떤 규칙으로 표현할 것인가"를 정해야 한다. 이 규칙을 인코딩(encoding)이라고 부른다.
UTF-8
유니코드 문자를 1바이트에서 4바이트 사이의 가변 길이로 표현하는 인코딩이다. 영문/숫자(ASCII 범위)는 1바이트로 그대로 표현되고, 한글 같은 non-ASCII 문자는 3바이트로 표현된다. 예를 들어 "안"이라는 글자는 UTF-8에서 EC 95 88 세 바이트로 저장된다. 현재 웹, 대부분의 운영체제, 대부분의 프로그래밍 언어에서 사실상 표준으로 자리 잡은 인코딩이다.
BOM (Byte Order Mark)
BOM은 파일 맨 앞에 붙는 특수한 바이트 시퀀스로, UTF-8의 경우 EF BB BF 세 바이트가 여기에 해당한다. 이 바이트를 유니코드로 디코딩하면 U+FEFF(ZERO WIDTH NO-BREAK SPACE)라는 문자 코드가 되는데, 이 문자를 파일 맨 앞에 실제 텍스트의 일부로 남겨두지 않고 "이 파일이 무슨 인코딩인지 알려주는 신호"로만 쓰겠다는 것이 BOM의 개념이다.
BOM의 원래 목적은 UTF-16처럼 한 글자를 여러 바이트로 표현하면서 바이트 순서(리틀엔디안/빅엔디안)가 모호해질 수 있는 인코딩에서, 그 순서를 알려주는 것이다. UTF-8은 한 바이트씩 순서대로 쌓아나가는 방식이라 바이트 순서 문제 자체가 없다. 그래서 유니코드 표준 원문(Unicode Standard, 2.6절 "Encoding Schemes")은 다음과 같이 명시한다.
"Use of a BOM is neither required nor recommended for UTF-8" (BOM 사용은 UTF-8에서 필수도 아니고 권장되지도 않는다)
다만 이 문장의 해석을 두고는 유니코드 컨소시엄 내부에서도 이견이 있었다. 2021년 유니코드 기술위원회에 제출된 문서(L2/21-038)는, 이 문장이 "BOM 사용에 반대하는 권고"로 읽히기도 하고 "찬성도 반대도 아닌 무입장"으로 읽히기도 한다는 점을 스스로 지적하며 문구의 모호함을 인정한 바 있다. 즉 "BOM을 쓰지 마라"는 강한 금지라기보다는, "굳이 필요하지 않으니 기본값으로 삼지는 말라"는 정도의 방향성으로 이해하는 편이 정확하다.
한편, 이 문제를 실무적으로 다뤄온 한 개발자의 글(Manski's Dev Log, "UTF-8 and BOM")은 다음과 같은 절충안을 제시한다. 평상시 파일은 BOM 없는 UTF-8을 기본으로 하되, 최종 사용자에게 노출되는 텍스트가 포함된 코드 파일이나 리소스 파일에 한해서는 인코딩 오류(이상한 문자 출력)를 막기 위해 BOM이 있는 UTF-8로 저장하라는 것이다. 이 절충안은 뒤에서 다룰 우리의 실제 상황(콘솔에 한글을 직접 출력하는 소스 파일)에 정확히 들어맞는 조언이기도 하다.
CP949 (한국어 Windows 기본 코드페이지)
CP949는 한글을 2바이트로 표현하는 완성형 인코딩으로, EUC-KR을 확장한 한국어 Windows의 기본 코드페이지다. 예를 들어 "안"이라는 글자는 CP949에서 B0 C8 두 바이트로 저장된다. UTF-8과는 완전히 다른 바이트 체계이며, 한국어 Windows에서만 통용되는 지역화된 인코딩이다.
UTF-16
한 글자를 기본적으로 2바이트로 표현하는 인코딩으로, Windows 내부(NT 커널)와 C#의 string, C++의 wchar_t 등에서 사용된다. UTF-8과는 바이트 구조 자체가 다르기 때문에, 두 인코딩 사이를 오갈 때는 반드시 변환 과정을 거쳐야 한다. BOM이 있다고 해서 UTF-8과 UTF-16이 서로 호환되는 것은 아니다. BOM은 그저 "이 파일이 셋 중 무엇인지"를 알려주는 라벨일 뿐, 인코딩 간 변환을 대신해주지 않는다.
2. 어디서, 왜 글자가 깨지는가
한글이 깨지는 현상은 하나의 원인이 아니라, 아래와 같은 파이프라인의 여러 단계에서 발생할 수 있다.
[.cpp 소스 파일] → [컴파일러가 소스를 읽음] → [실행 파일(.exe) 생성] → [콘솔이 출력을 해석]
1단계: 소스 파일을 컴파일러가 잘못 해석하는 경우
MSVC 컴파일러는 소스 파일 맨 앞에 BOM이 있는지를 확인해, BOM이 있으면 해당 인코딩으로 정확히 읽고, 없으면 시스템 코드페이지(한국어 Windows에서는 CP949)로 간주해버린다. 이는 Microsoft 공식 문서에도 명시되어 있다.
"By default, Visual Studio detects a byte-order mark... If no byte-order mark is found, it assumes that the source file is encoded in the current user code page, unless you've specified a code page by using /utf-8 or the /source-charset option." (출처: Microsoft Learn, /utf-8 컴파일러 옵션 문서)
만약 실제 파일이 BOM 없는 UTF-8로 저장되어 있다면, 컴파일러는 이를 CP949로 잘못 해석한다. 이때 한글을 표현하는 UTF-8 멀티바이트 시퀀스가 우연히 개행 문자와 비슷한 바이트 조합으로 해석되면서, 문자열 리터럴이 예상치 못한 곳에서 끊기는 등 실제 컴파일 오류(C2001, C2143 등)로 이어지기도 한다. 이 경고는 C4819("현재 코드 페이지에서 표시할 수 없는 문자")로 나타나는데, 이는 경고일 뿐이라 빌드 자체는 통과하는 경우가 많다. 다만 빌드가 통과했다는 것이 인코딩이 올바르다는 뜻은 아니며, 실제로는 손상된 문자열이 실행 파일에 그대로 저장된다.
2단계: 실행 파일에 문자열이 저장되는 방식
컴파일러가 소스를 정확히 읽었다 하더라도, 그 문자열을 실행 파일 안에 저장할 때 어떤 인코딩으로 저장할지(실행 문자집합, execution charset)는 별개의 문제다. MSVC는 별도 옵션이 없으면 기본적으로 narrow 문자열(char*)을 시스템 코드페이지로 변환해 저장한다. 이는 1990년대 Windows API가 설계될 때부터 이어진 하위 호환성 정책이다.
3단계: 콘솔이 출력을 해석하는 방식
콘솔(cmd.exe)은 자신만의 기본 코드페이지를 갖고 있으며, 한국어 Windows에서는 이 값이 기본적으로 CP949다. 실행 파일 안에 저장된 문자열의 인코딩과 콘솔이 그것을 해석하는 방식이 일치해야만 화면에 정상적으로 출력된다. 즉 다음 네 가지 조합 중 인코딩이 서로 일치하는 경우에만 정상 출력이 가능하다.
실행 파일 안 문자열 콘솔의 해석 방식 결과
| CP949 | CP949 | 정상 |
| UTF-8 | UTF-8 | 정상 |
| CP949 | UTF-8 | 깨짐 |
| UTF-8 | CP949 | 깨짐 |
지금까지 실무에서 쓰여 온 BOM, /utf-8 컴파일러 옵션, SetConsoleOutputCP, 시스템 로캘의 UTF-8 설정은 모두 이 "인코딩"과 "디코딩" 단계를 서로 일치시키기 위한 각기 다른 수단일 뿐이다.
3. 표준 문서와 업계의 실제 관행
유니코드 표준과 C++ 표준위원회
유니코드 표준은 UTF-8을 텍스트 교환의 사실상 표준으로 삼으면서도, BOM에 대해서는 앞서 살펴본 것처럼 다소 모호한 태도("필수도 권장도 아니다")를 취하고 있다.
C++ 표준위원회(WG21)가 제출한 UTF-8 소스 인코딩 지원 제안서(P2295R3)는 이 지점을 조금 더 명확히 한다. 이 문서는 BOM이 파일에 존재하더라도 그것을 실제 텍스트 콘텐츠로 취급해서는 안 되며(무시되어야 하며), 다만 MSVC처럼 BOM을 UTF-8 감지 수단으로 활용하는 구현 방식도 유효하다고 인정한다. 여기서 "무시되어야 한다"는 것은 "BOM을 쓰지 말라"는 뜻이 아니라, "파일을 읽는 도구가 BOM을 감지에는 활용하되, 그 바이트 자체를 실제 콘텐츠의 일부(눈에 보이지 않는 유령 문자)로 남겨서는 안 된다"는, 도구 구현자를 향한 요구사항에 가깝다. 표준위원회는 BOM 사용 자체를 강제하지도 금지하지도 않는 입장을 취하되, 모든 C++ 컴파일러가 UTF-8 소스 파일을 최소한 지원해야 한다는 방향으로 논의를 진행하고 있다.
대형 오픈소스 프로젝트의 실제 관행
- 구글 C++ 스타일 가이드(공식 문서, google.github.io/styleguide): 소스 코드에 non-ASCII 문자가 필요한 경우 UTF-8 형식을 사용하도록 명시하고 있다.
- Clang 컴파일러의 동작 방식: Clang은 소스 파일을 항상 UTF-8로 가정하며, BOM 유무와 무관하게 이 원칙을 고수하는 것으로 알려져 있다. 이 설명은 Clang 공식 문서가 아니라, 이를 분석한 제3자 기술 블로그(Nutrient.io, "String Literals, Character Encodings, and Multiplatform C++")를 참고했다. 실행 문자집합 변환 역시 UTF-8을 기준으로 처리되어, MSVC와 같은 레거시 코드페이지 변환 단계가 없다는 점도 이 글에서 확인할 수 있다.
- 개별 프로젝트의 실무 절충안: 한 개발자가 자신의 프로젝트에 채택한 정책(Manski's Dev Log, "UTF-8 and BOM")을 보면, 기본은 BOM 없는 UTF-8로 하되, 컴파일러/인터프리터가 BOM 없이도 UTF-8을 기본으로 가정하는 환경에서는 이 원칙이 무리 없이 성립하고, 최종 사용자에게 직접 노출되는 텍스트가 포함된 파일에는 예외적으로 BOM을 허용해 인코딩 오류를 막는 방식을 취하고 있다. 이는 LLVM이나 특정 대형 프로젝트의 공식 정책이 아니라 한 개인 프로젝트의 사례이지만, 실무에서 통용되는 절충 논리를 잘 보여준다.
Microsoft 공식 문서의 입장
Microsoft는 /utf-8 컴파일러 옵션을 공식 문서에서 안내하며, 기본적으로 BOM이 없으면 소스 파일을 현재 사용자 코드 페이지로 가정한다는 점, 그리고 /utf-8 옵션으로 이를 UTF-8로 고정할 수 있다는 점을 명시하고 있다(Microsoft Learn, /utf-8(원본 및 실행 문자 집합을 UTF-8로 설정) 문서). 아울러 Windows 10 버전 1903부터는 애플리케이션 매니페스트에 activeCodePage를 UTF-8로 지정해, 프로세스 전체의 기본 코드페이지 자체를 UTF-8로 고정하는 방법도 공식적으로 제공하고 있다(Microsoft Learn, "Windows 앱에서 UTF-8 코드 페이지 사용" 문서).
정리
표준 문서와 업계 사례를 종합하면 방향은 비교적 뚜렷하다. 인코딩은 UTF-8을 쓰는 것이 사실상 이견 없는 정석이며, BOM에 대해서는 유니코드 표준 자체가 명확히 "쓰지 말라"고 강하게 못박고 있다기보다는 "굳이 필요하지 않다"는 쪽에 무게를 두고 있고, 실무에서는 도구의 특성에 따라 예외적으로 BOM을 허용하는 절충안이 흔히 쓰인다. Windows/MSVC 환경에서 BOM이 여전히 실질적으로 쓰이는 것은, 표준을 어기는 것이라기보다 MSVC라는 특정 도구가 BOM 없이는 UTF-8을 기본으로 가정하지 않는다는 특수한 사정에서 비롯된 실용적 선택에 가깝다.
4. 그래서 우리는 어떻게 설정해야 하는가
결론적으로, 오늘날 권장되는 조합은 다음과 같다.
소스 파일: BOM 없는 UTF-8로 저장 (또는 BOM 있는 UTF-8도 무방)
프로젝트 설정: /utf-8 컴파일러 옵션 추가
콘솔 출력이 필요한 경우: SetConsoleOutputCP(CP_UTF8) 추가
Visual Studio 설정 방법
1) 프로젝트에 /utf-8 옵션 추가
프로젝트 우클릭 → 속성 → 구성 속성 → C/C++ → 명령줄 → 추가 옵션
→ /utf-8 입력
이 옵션 하나로 소스 문자집합과 실행 문자집합이 모두 UTF-8로 고정된다. BOM 유무와 무관하게 소스를 정확히 UTF-8로 인식하게 되므로, 파일마다 저장 방식을 신경 쓸 필요가 없어진다. 즉 /utf-8을 걸어두면, MSVC 역시 "BOM 없이도 UTF-8을 기본으로 가정하는" 환경이 되어, 앞서 살펴본 절충안의 전제 조건을 스스로 만족시키게 된다.
2) 콘솔 출력 코드 추가
#include <stdio.h>
#include <Windows.h>
int main() {
SetConsoleOutputCP(CP_UTF8);
printf("안녕하세요.");
return 0;
}
/utf-8을 적용하면 실행 파일 안 문자열이 UTF-8로 유지되는데, 콘솔의 기본 코드페이지는 여전히 CP949이므로 이 한 줄이 없으면 콘솔에서 다시 깨진다. SetConsoleOutputCP는 해당 콘솔 창 전체의 설정을 바꾸는 것이므로, 필요하다면 프로그램 종료 전에 GetConsoleOutputCP()로 저장해둔 원래 값으로 복구해주는 것이 매너 있는 방식이다.
그럼에도 BOM이 여전히 유효한 경우
/utf-8 옵션을 걸 수 없거나, 프로젝트 전역 설정을 건드리기 어려운 상황(예: 파일 하나만 급하게 고쳐야 하는 경우)이라면, 앞서 소개한 절충안처럼 최종 사용자에게 직접 보이는 텍스트가 담긴 파일에 한해 BOM을 붙이는 것도 실무적으로 유효한 선택이다. 다만 이 경우 실행 파일 안 문자열이 시스템 코드페이지(CP949)로 변환되어 저장된다는 점을 유의해야 한다. 콘솔 출력만 놓고 보면 문제가 없지만, 네트워크로 들어오는 UTF-8 데이터(예: 채팅 메시지)와 소스에 적어놓은 한글 리터럴을 비교하는 로직에서는 두 값이 서로 다른 바이트가 되어 비교 자체가 실패한다. 콘솔에 보기 좋게 출력되는 것과 실제 데이터 처리 로직이 올바르게 동작하는 것은 별개의 문제이며, 후자를 보장하려면 BOM만으로는 부족하고 반드시 /utf-8이 필요하다.
결론
결국 이 모든 논의를 한 문장으로 요약하자면 글자가 깨지는 것은 "인코딩"과 "디코딩"이라는 두 단계가 서로 어긋났기 때문이며, 이를 막는 가장 확실한 방법은 소스 파일부터 콘솔 출력까지 모든 계층을 UTF-8 하나로 통일하는 것이다.
다만 이런 상황이 "BOM을 절대 쓰지 말라"는 의미는 아니다. 표준이 가리키는 방향은 "새 파일을 만들 때는 BOM 없는 UTF-8을 기본으로 하되, BOM이 붙은 파일을 만나더라도 도구가 오작동 없이 처리할 수 있어야 한다"는 것이고, 개인 학습이나 콘솔 출력 정도의 목적이라면 BOM을 쓰는 것도 여전히 실용적인 선택이다. 다만 네트워크로 들어오는 UTF-8 데이터와 소스 코드의 문자열을 직접 비교해야 하는 순간부터는, BOM만으로는 부족하고 반드시 /utf-8처럼 실행 문자집합까지 UTF-8로 고정하는 설정이 필요해진다.
참고 자료
- Google C++ Style Guide — https://google.github.io/styleguide/cppguide.html
- 해당 자료의 Non-ASCII부분에서 참고하였습니다.
- ISO/IEC JTC1/SC22/WG21, P2295R3 "Support for UTF-8 as a portable source file encoding" — https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2021/p2295r3.pdf
- PDF 안에서 "BOM" 검색 -> 해당 부분 참고
- Unicode Technical Committee, L2/21-038 "Clarify guidance for use of a BOM as a UTF-8 encoding signature" — https://www.unicode.org/L2/L2021/21038-bom-guidance.pdf
-
- PDF 첫 페이지 도입부, "Unicode 13, in the 'Byte Order' subsection of section 원문 인
-
- Wikipedia, "Byte order mark" — https://en.wikipedia.org/wiki/Byte_order_mark
- 문서 내 "UTF-8" 소제목에서 참
- Microsoft Learn, /utf-8(원본 및 실행 문자 집합을 UTF-8로 설정) — https://learn.microsoft.com/ko-kr/cpp/build/reference/utf-8-set-source-and-executable-character-sets-to-utf-8
- 페이지 상단, "설명(Remarks)" 또는 본문 도입부에서 참고
- Microsoft Learn, "Windows 앱에서 UTF-8 코드 페이지 사용" — https://learn.microsoft.com/ko-kr/windows/apps/design/globalizing/use-utf8-code-page
- UTF-8 코드 페이지" 관련 첫 단락 부분 참
- Manski's Dev Log, "UTF-8 and BOM" (개인 프로젝트 사례) — https://manski.net/articles/utf8-bom
- Nutrient.io, "String Literals, Character Encodings, and Multiplatform C++" (제3자 기술 블로그) — https://www.nutrient.io/blog/string-literals-character-encodings-and-multiplatform-cpp/
- msvc 인코딩 옵션 이해 (GitHub Gist) — https://gist.github.com/a9746b9ff99a76582c29ca6f956b219b
'공부' 카테고리의 다른 글
| 스파이럴(달팽이) 배열 구현하기 - switch/case 방향 전환 방식 (0) | 2026.07.08 |
|---|---|
| 선택정렬 (0) | 2026.07.07 |
| 버블정렬 (0) | 2026.07.06 |
| C/C++ 연산자 우선순위 (0) | 2026.07.05 |
| 이스케이프 시퀀스 / C 표준 입출력 (0) | 2026.07.05 |