수정 이력(6건, 최종 수정 2026년 09월 03일)
이 글에 적용한 변경 사항의 기록입니다. 보관해 둔 수정 전 버전은 DOI가 부여된 고정 URL에서 읽을 수 있습니다.
- permalink·저자 표기·지식 맵 래퍼·깨진 내부 링크 등 CI가 지적한 표시용 수정을 반영했습니다. 본문의 기술적인 주장은 바꾸지 않았습니다.
- encode/decode 전제의 어긋남부터 복구 절차·운영 규칙까지 그림으로도 따라갈 수 있도록 Mermaid 그림을 18점 추가했습니다(본문 500〜750자당 1그림 규약에 맞춘 것입니다). 기존 경로도에도 캡션을 붙이고, 그림 번호를 일련번호로 다시 매겼습니다. 본문 문장은 바꾸지 않았습니다.
- 기사 맨 앞에 「이 기사의 지식 맵」 절을 추가했습니다. 본문에서 다루는 개념과 그 관계를 요약·그림·상세 페이지 링크로 정리한 것입니다. 본문의 주장은 바꾸지 않았습니다.
- 외부 리뷰(1283건)에 대한 대응으로 본문을 갱신했습니다. 개별 변경 내용은 아래 이력을 참조해 주십시오.
- 깨진 파일을 어떻게 고칠지 장을 신설했습니다(`iconv`와 PowerShell, 변환 후 확인. `Get-Content`와 `Set-Content`를 이으면 개행이 맞춰져 버리는 부작용도 명시합니다). 용어 미니 사전, PowerShell 버전 차에 따른 기본 encoding 비교표(읽기 쪽이 비대칭인 점을 포함), 반대 방향의 바이트 예를 추가하고, 어느 층에서 깨지는지의 경로도를 넣었습니다.
- 참고 링크 등에서 세로줄(파이프) 기호가 들어간 줄이 표로 표시되어 링크를 누를 수 없었던 표시 오류를 수정했습니다. 본문 내용은 그대로입니다.
- 최초 공개
이 글을 인용하기(DOI: 10.5281/zenodo.21635206)
이 글은 Zenodo에 보관되어 있습니다. 항상 최신 버전으로 연결되는 DOI와 지금 보고 있는 버전에 고정된 DOI를 아래에 함께 제시합니다.
Go Komura (2026). 「Windows 문자 코드 입문 - Linux 연동에서 일어나는 문자 깨짐」. 합동회사 코무라소프트. https://doi.org/10.5281/zenodo.21635206 https://comcomponent.com/ko/blog/windows-text-encoding-mojibake-linux/
- DOI(최신 버전)
- 10.5281/zenodo.21635206
- DOI(이 버전)
- 10.5281/zenodo.21635207
Windows의 문자 깨짐은 일본어가 어려워서 일어나는 것이 아닙니다. 대부분은 같은 바이트 열을 다른 문자 코드로 읽었거나, 잘못 읽은 결과를 다른 문자 코드로 저장했기 때문입니다.
특히 Windows와 Linux를 가로지르면 Windows 쪽에는 CP932, UTF-8, UTF-16, console의 code page, PowerShell의 버전 차이 등 여러 문맥이 남고, Linux 쪽은 UTF-8 전제로 흐르는 경우가 많아서, 평소 보이지 않던 전제의 어긋남이 한 번에 표면화됩니다.
flowchart TB
accTitle: Windows와 Linux에서 전제가 어긋나는 구도
accDescr: Windows 쪽에는 CP932나 UTF-16, console의 code page, PowerShell의 버전 차이 등 여러 문맥이 남고, UTF-8 전제로 흐르는 경우가 많은 Linux 쪽과 조합하면 전제의 어긋남이 표면화됨을 나타내는 그림.
w1["Windows 쪽"] --> w2["CP932 / UTF-16 / code page / 버전 차이"]
l1["Linux 쪽"] --> l2["UTF-8 전제가 강함"]
w2 --> z1["전제의 어긋남이 표면화"]
l2 --> z1
그림1: Windows 쪽에 남는 여러 문맥과 Linux 쪽의 UTF-8 전제가 만나는 지점에서 어긋남이 한 번에 표면화됩니다.
이 이야기는 일본어 처리의 어려움이라기보다 bytes를 어떤 전제로 다루고 있는가를 맞출 수 있는지의 이야기입니다. 이 글에서는 Windows의 문자 코드 주변을 「왜 문자 깨짐이 일어나는가」라는 관점에서 정리하고, 특히 Linux와 조합했을 때 사고가 늘어나는 지점을 실무 쪽으로 정리합니다.
대상 독자는 Windows에서 만든 CSV / 로그 / 설정 파일을 Linux 쪽으로 넘기거나, 그 반대를 하면서, 문자 깨짐의 원인 분리와 복구를 직접 하고 싶은 분입니다. 특정 언어나 프레임워크 지식은 전제로 하지 않습니다. 명령 예는 iconv와 PowerShell을 사용합니다.
1. 먼저 잡아 두고 싶은 것
먼저 요점만 쓰면 중요한 것은 다음 6점입니다.
- 문자 깨짐은 「문자」의 문제가 아니라 「바이트 열을 어떻게 해석했는가」의 문제입니다.
- Windows에는 Unicode 계와 legacy code page 계가 공존하고 있어, 1대 안에서도 문맥마다 전제가 다릅니다.
- Linux 쪽은 UTF-8 전제가 강하므로 Windows 쪽의 CP932나 UTF-16이 섞이면 사고가 되기 쉽습니다.
- 표시가 무너진 것뿐인 단계와 깨진 내용을 저장해 버린 단계는 나눠 생각해야 합니다.
- 신규 텍스트는 UTF-8을 1순위로 두고, 기존 legacy 파일은 명시적인 이행 태스크까지 그대로 유지하는 편이 안전합니다.
- file의 encoding, editor의 encoding, console의 code page, 앱 내부의 문자열 형식은 별개입니다. 여기를 혼동하면 조사가 길을 잃습니다.
「Windows에서 문자 깨짐이 있었다」는 말만으로는 원인을 특정할 수 없습니다. 적어도 다음 중 어느 것이 어긋나 있는지를 나눌 필요가 있습니다.
- 파일 자체의 문자 코드
- 저장 시의 문자 코드
- 에디터의 해석
- console의 input/output code page
- 앱 내부의 문자열 형식
- Linux 쪽의 locale과 상정 encoding
flowchart TB
accTitle: 문자 깨짐의 원인 분리에서 나누는 층
accDescr: 「Windows에서 문자 깨짐이 있었다」는 말만으로는 원인을 특정할 수 없고, 파일 자체·저장 시·에디터·console의 code page·앱 내부·Linux의 locale 중 어디가 어긋났는지를 나눠야 함을 나타내는 그림.
s0["Windows에서 문자 깨짐이 있었다"] --> q1{"어디가 어긋났는가"}
q1 --> a1["파일 자체의 문자 코드"]
q1 --> a2["저장 시의 문자 코드"]
q1 --> a3["에디터의 해석"]
a1 --> a4["console의 code page"]
a2 --> a5["앱 내부의 문자열 형식"]
a3 --> a6["Linux의 locale"]
그림2: 「문자 깨짐이 있었다」만으로는 원인이 정해지지 않으며, 적어도 이 6개 층 중 어디가 어긋났는지를 나눠 봅니다.
1.1 먼저 나오는 용어
본문에서 설명 없이 나오는 약어를 먼저 짧게 정리합니다.
| 용어 | 풀네임 | 이 기사에서의 의미 |
|---|---|---|
| BOM | Byte Order Mark | 파일 선두에 두는 수 바이트의 표시. 어느 Unicode encoding인지를 읽는 쪽에 전하는 것으로, UTF-16에서는 바이트 순서도 함께 나타냅니다. UTF-8에서는 붙여도 붙이지 않아도 됩니다 |
| code page | - | Windows가 「어느 legacy 문자 코드로 해석할지」를 번호로 갖고 있는 장치. 일본어 Windows의 CP932는 그 번호 중 하나입니다 |
| ANSI | - | Windows에서 「그때의 active code page」를 가리키는 말. 실체는 환경에 따라 바뀌며, 일본어 환경이라면 CP932입니다 |
| locale | - | 언어·지역·문자 코드의 기본값을 모은 설정. Linux에서는 LANG이나 LC_ALL로 지정하고, ja_JP.UTF-8처럼 encoding까지 포함합니다 |
| WSL | Windows Subsystem for Linux | Windows 위에서 Linux를 돌리는 장치. Windows 쪽과 Linux 쪽의 전제가 1대 안에서 같이 있으므로, 이 기사의 사고가 일어나기 쉬운 장소입니다 |
| ETL | Extract / Transform / Load | 데이터를 꺼내 변환하고 다시 쓰는 처리. 중간에 파일을 다시 읽어 저장하므로 encoding이 바뀌는 지점이 됩니다 |
이 글의 지식 맵
Windows의 문자 깨짐은 CP932로 저장한 바이트열을 UTF-8로 읽거나, 그 반대처럼 encode와 decode의 전제가 어긋나서 생기며, 방향에 따라 깨지는 증상이 다릅니다. Windows PowerShell은 5.1과 7 이후에서 기본 encoding이 갈라져 있으며, Out-File이 UTF-16LE를, Set-Content가 CP932를 쓰는 식의 경로별 차이가 사고의 온상이 됩니다. Linux 쪽은 locale을 따라 UTF-8을 전제로 읽기 때문에, CP932나 BOM이 붙은 UTF-8을 넘기면 문자 깨짐이나 데이터 손상으로 이어지고, WSL처럼 둘이 공존하는 환경에서는 특히 일어나기 쉽습니다. 원래 바이트열이 남아 있으면 iconv나 PowerShell로 올바른 encoding으로 다시 읽어 쓰면 복구할 수 있지만, 잘못 읽은 내용을 저장해 버리면 복원할 수 없습니다. 기사는 새 파일을 UTF-8 제1 후보로 두고, encoding 자체를 I/O 계약으로 명문화하는 운영을 권장합니다.
flowchart LR
accTitle: Windows의 문자 깨짐과 Linux 연동의 지식 맵
accDescr: 같은 바이트열을 CP932와 UTF-8 중 어느 쪽으로 읽느냐에 따라 문자 깨짐 증상이 달라진다는 점, BOM·UTF-16LE·콘솔 코드 페이지가 독립된 층이라는 점, PowerShell 버전에 따라 기본 encoding이 다르다는 점, iconv나 PowerShell로 복구하는 방법과 UTF-8을 제1 후보로 두는 운영 방침의 관계를 보여주는 그림
mojibake["문자 깨짐"]
cp932["CP932"]
utf_8["UTF-8"]
utf_16le["UTF-16LE"]
bom["BOM(Byte Order Mark)"]
encoding_overwrite_corruption["오독 내용 덮어쓰기 저장에 의한 데이터 손상"]
console_code_page["콘솔 코드 페이지"]
windows_powershell_5_1["Windows PowerShell 5.1"]
powershell_7["PowerShell 7"]
iconv["iconv"]
powershell["PowerShell"]
linux_locale["Linux의 locale"]
wsl["WSL(Windows Subsystem for Linux)"]
encoding_as_interface["encoding을 I/O 계약으로 명문화하는 운영"]
utf8_first_policy["UTF-8 우선 운영 방침"]
cp932 -.->|"원인이 될 수 있다"| mojibake
utf_8 -.->|"원인이 될 수 있다"| mojibake
utf_16le -.->|"원인이 될 수 있다"| mojibake
bom -.->|"원인이 될 수 있다"| mojibake
mojibake -.->|"원인이 될 수 있다"| encoding_overwrite_corruption
console_code_page -.->|"원인이 될 수 있다"| mojibake
windows_powershell_5_1 -->|"이용한다"| utf_16le
windows_powershell_5_1 -->|"이용한다"| cp932
powershell_7 -->|"이용한다"| utf_8
iconv -.->|"완화한다"| mojibake
powershell -.->|"완화한다"| mojibake
mojibake -->|"에서 확인할 수 있다"| iconv
linux_locale -.->|"이용한다"| utf_8
wsl -->|"이용한다"| linux_locale
wsl -.->|"원인이 될 수 있다"| mojibake
encoding_as_interface -->|"권장되는 대응"| mojibake
utf8_first_policy -->|"권장되는 대응"| mojibake
그림의 실선은 항상 성립하는 관계, 점선은 조건이 붙는 관계입니다(성립 조건은 상세 페이지의 관계별 설명에 적혀 있습니다). 관계 전체 목록(총 17건, 근거와 확신도 포함)과 주요 개념의 정의는 지식 맵 상세 페이지에 정리되어 있습니다(일본어). 데이터: JSON-LD / Turtle
2. 문자 깨짐의 정체
문자 깨짐의 정체는 꽤 단순합니다.
- 문자열을 어떤 문자 코드로 encode해 바이트 열로 만든다
- 그 바이트 열을 어떤 문자 코드로 decode해 문자열로 되돌린다
- encode와 decode의 전제가 일치하지 않으면 다른 문자열로 읽힌다
flowchart TB
accTitle: encode와 decode 전제의 일치
accDescr: 문자열을 encode해 만든 바이트 열을 decode로 문자열로 되돌릴 때, 둘의 전제가 일치하면 원래 문자열로 돌아가고, 어긋나면 다른 문자열로 읽힘을 나타내는 그림.
e1["문자열을 encode해 바이트 열로"] --> e2["바이트 열을 decode해 문자열로"]
e2 --> j1{"전제는 일치하는가"}
j1 -->|"일치"| r1["원래 문자열로 돌아간다"]
j1 -->|"어긋남"| r2["다른 문자열로 읽힌다"]
그림3: 문자 깨짐의 정체는 encode와 decode의 전제가 일치하지 않는 것입니다.
예를 들어 あ를 UTF-8로 저장하면 바이트 열은 다음이 됩니다.
E3 81 82
이 바이트 열을 UTF-8로 읽으면 あ이지만, CP932 쪽 문맥으로 읽으면 縺� 같은 다른 문자열로 보입니다. 이것이 문자 깨짐입니다.
중요한 것은 여기서 일어나고 있는 일이 「일본어가 망가졌다」가 아니라 같은 bytes에 대한 해석이 어긋났을 뿐이라는 점입니다.
반대 방향도 봅니다. あ를 CP932로 저장하면 바이트 열은 이렇게 됩니다.
82 A0
이 바이트 열을 UTF-8로 읽으려 하면 0x82도 0xA0도 UTF-8의 선두 바이트로 성립하지 않으므로, 둘 다 치환 문자가 되어 ��처럼 보입니다. UTF-8 쪽에서 보면 애초에 문자로 성립하지 않는 것입니다.
조금 더 긴 예가 특징이 잘 드러납니다. 日本語를 CP932로 저장하면 바이트 열은 이렇습니다.
93 FA 96 7B 8C EA
이것을 UTF-8로 읽으면 ���{��처럼 됩니다. 여기서 보고 싶은 것은 4바이트째의 0x7B만 ASCII의 {로 그대로 통과한다는 점입니다. CP932의 2바이트째에는 ASCII 범위 값이 올 수 있으므로, 깨진 결과에 {나 \ 같은 기호가 섞여 들어갑니다.
즉 방향에 따라 증상이 다릅니다.
| 실제 바이트 열 | 읽는 쪽의 상정 | 보이는 모습 |
|---|---|---|
| UTF-8 | CP932 | 縺처럼 그럴듯한 한자나 가타카나가 늘어선다 |
| CP932 | UTF-8 | 치환 문자 �투성이가 되고, 가끔 { 같은 ASCII 기호가 섞인다 |
「읽을 수 없는 한자가 늘어서 있으면 UTF-8을 CP932로 읽고 있다」「치환 문자투성이면 CP932를 UTF-8로 읽고 있다」고 감을 잡을 수 있으므로, 이 비대칭을 기억해 두면 원인 분리가 빨라집니다.
2.1 표시만 무너진 상태라면 아직 되돌릴 수 있는 경우가 있다
문자 깨짐에는 아직 되돌릴 수 있는 단계가 있습니다. 예를 들어 원래 bytes가 바뀌지 않았다면 올바른 encoding으로 다시 열어 되돌릴 수 있는 경우가 있습니다.
반대로 위험한 것은 다음 같은 흐름입니다.
- UTF-8의 file을 CP932로 잘못 읽는다
- 화면에서는
縺�처럼 보인다 - 그대로 「보이는 문자열」을 저장한다
- 원래 UTF-8 bytes가 사라진다
이 단계에 들어가면 단순한 표시 붕괴가 아니라 데이터 파손입니다.
flowchart TB
accTitle: 표시 붕괴가 데이터 파손으로 바뀌는 흐름
accDescr: UTF-8의 file을 CP932로 잘못 읽어 화면에서 깨져 보이는 단계에서, 보이는 문자열을 그대로 저장해 원래 UTF-8 바이트 열이 사라지는 단계로 나아가면 표시 붕괴가 아니라 데이터 파손이 됨을 나타내는 그림.
d1["UTF-8의 file을 CP932로 잘못 읽음"] --> d2["화면에서는 깨져 보인다"]
d2 --> d3["보이는 문자열을 그대로 저장"]
d3 --> d4["원래 UTF-8 바이트 열이 사라진다"]
d2 -.-> n1["여기까지면 다시 열면 되돌릴 수 있다"]
d4 -.-> n2["여기서부터는 데이터 파손"]
그림4: 잘못 읽기만 했다면 되돌릴 수 있지만, 잘못 읽은 내용을 저장한 시점에 데이터 파손으로 바뀝니다.
2.2 더 위험한 것은 「표현할 수 없는 문자」를 좁은 code page로 떨어뜨릴 때
또 하나의 전형 사고는 Unicode 문자열을 CP932 같은 legacy code page로 떨어뜨릴 때입니다.
예를 들어 상대 code page에 없는 문자가 들어 있으면,
?로 바뀐다- 치환 문자
�가 들어간다 - 가까운 다른 문자로 변환된다
- 변환 실패가 된다
같은 일이 일어납니다.
이 사고는 읽을 수 있다·없다만이 아니라 왕복 변환해서 원래로 돌아오는가로 봐야 합니다. 한 번 사라진 문자는 나중에 올바른 encoding을 알아도 복원할 수 없습니다.
flowchart TB
accTitle: 좁은 code page로 떨어뜨릴 때의 사고
accDescr: Unicode 문자열을 CP932처럼 좁은 code page로 변환하면, 상대 code page에 없는 문자가 치환이나 변환 실패가 되고, 왕복 변환해도 원래로 돌아오지 않으며, 한 번 사라진 문자는 올바른 encoding을 알아도 복원할 수 없음을 나타내는 그림.
u1["Unicode 문자열"] --> u2["CP932처럼 좁은 code page로 변환"]
u2 --> j1{"상대에 없는 문자는"}
j1 --> r1["?나 치환 문자로 바뀐다"]
j1 --> r2["다른 문자화·변환 실패"]
r1 --> k1["왕복 변환해도 원래로 돌아오지 않는다"]
r2 --> k1
k1 -.-> n1["사라진 문자는 복원할 수 없다"]
그림5: 좁은 code page로 떨어뜨리는 사고는 읽을 수 있다·없다가 아니라 왕복 변환해서 돌아오는가로 봅니다.
3. 왜 Windows에서는 복잡해지기 쉬운가
Windows가 복잡한 것은 단순히 오래되어서만이 아닙니다. Unicode의 세계와 legacy code page의 세계가 지금도 같이 있기 때문입니다.
3.1 Windows API에는 Unicode 계와 code page 계가 공존한다
Windows API에는 크게 2계통이 있습니다.
W계: wide character. Unicode를 UTF-16으로 다루는 계A계: ANSI라고 불리는 code page 계
즉 Windows 안에는 처음부터 「Unicode로 다루는 길」과 「그때의 active code page로 다루는 길」이 둘 다 있습니다. 그래서 같은 Windows 위에서도 어느 API나 어느 도구를 거쳤는지에 따라 전제가 바뀝니다.
flowchart TB
accTitle: Windows API의 2계통
accDescr: Windows API에는 Unicode를 UTF-16으로 다루는 W계와, 그때의 active code page로 다루는 A계가 처음부터 공존하며, 어느 경로를 거쳤는지에 따라 전제가 바뀜을 나타내는 그림.
api["Windows API"] --> w1["W계 (wide character)"]
api --> a1["A계 (ANSI라고 불리는 계)"]
w1 --> w2["Unicode를 UTF-16으로 다룬다"]
a1 --> a2["active code page로 다룬다"]
w2 --> z1["거친 경로에 따라 전제가 바뀐다"]
a2 --> z1
그림6: Windows 안에는 Unicode의 길과 code page의 길이 처음부터 둘 다 있습니다.
3.2 「Windows의 일본어」는 하나가 아니다
Windows의 일본어 주변에서 실무상 자주 섞이는 것은 다음 4가지입니다.
- CP932: 일본어 Windows의 legacy text에서 자주 나온다
- UTF-8: 새로운 text 자산, web, cross-platform 계에서 늘고 있다
- UTF-16LE: Windows 계 도구나 API 문맥에서 지금도 흔히 나온다
- console의 code page:
cmd.exe나 일부 console tool의 입출력에 영향을 주는 별도 레이어
여기서 중요한 것은 chcp 65001을 했다고 해서 file도 UTF-8이 된 것은 아니다라는 점입니다. console의 code page를 바꾸는 것과 기존 file의 bytes가 무엇인지는 별개입니다.
flowchart TB
accTitle: chcp 65001과 기존 file은 별개
accDescr: chcp 65001은 console의 code page만 바꿀 뿐 기존 file의 바이트 열은 아무 것도 바뀌지 않으며, console 설정과 file 내용은 별개임을 나타내는 그림.
c1["chcp 65001을 실행"] --> c2["console의 code page가 바뀐다"]
f1["기존 file의 바이트 열"] --> f2["아무 것도 바뀌지 않는다"]
c2 --> n1["console과 file은 별개"]
f2 --> n1
그림7: chcp 65001로 바뀌는 것은 console의 해석뿐이며, file의 바이트 열은 그대로입니다.
덧붙여 일본어 Windows의 legacy text를 대충 「Shift_JIS」라고 부르는 일은 많지만, 실무에서는 CP932라는 이름으로 의식해 두는 편이 대화가 덜 흔들립니다. 적어도 「Windows에서 온 일본어 legacy encoding 이야기를 하고 있다」고 명시할 수 있습니다.
3.3 file 이름과 file 내용은 별개
Windows에서 일본어 file 이름이 평범하게 보이면 「그럼 내용도 괜찮겠지」라고 생각하기 쉽습니다. 여기가 위험합니다.
- path / file name을 다루는 층
- file 내용을 읽는 층
- console에 표시하는 층
이 3가지는 별개입니다.
예를 들어 일본어 path는 문제없이 다뤄도, file 내용이 CP932로 저장되어 있어 Linux 쪽에서 UTF-8로 읽히면 깨집니다. 반대로 file 내용이 UTF-8이어도 console의 code page가 맞지 않으면 표시만 무너집니다.
층의 관계를 그림으로 그리면 이렇습니다.
flowchart LR
W["쓰는 쪽<br/>앱 / 에디터 / 스크립트"] --> FB["파일의 바이트 열<br/>여기만 사실"]
FB --> R1["읽는 쪽A: 에디터<br/>자동 판정 또는 지정 encoding"]
FB --> R2["읽는 쪽B: console<br/>입출력의 code page"]
FB --> R3["읽는 쪽C: 앱 내부<br/>라이브러리의 기본 encoding"]
FB --> R4["읽는 쪽D: Linux 쪽<br/>locale에 따라 UTF-8 전제"]
R1 --> S1["표시만 무너진다<br/>다시 저장하면 파손으로 바뀐다"]
R2 --> S2["표시만 무너진다<br/>파일은 무사"]
R3 --> S3["처리 결과가 깨진다<br/>하류로 전파된다"]
R4 --> S4["decode error 또는 치환 문자"]
그림8: 사실은 파일의 바이트 열뿐이며, 에디터·console·앱·Linux 쪽의 읽는 쪽은 서로 독립입니다.
볼 지점은 2가지입니다. 하나는 깨져 있는 것이 가운데 바이트 열인지, 오른쪽의 읽는 쪽인지를 나누는 것입니다. 다른 하나는 오른쪽 4개가 서로 독립이라 하나로 확인돼도 나머지 3개의 보증이 되지 않는다는 점입니다. 「console에서 읽혔으니 에디터에서도 괜찮다」가 성립하지 않는 것은 이 형태 때문입니다.
3.4 PowerShell이나 주변 도구의 기본값도 맞춰져 있지 않다
Windows에서 은근히 사고를 늘리는 것이, 같은 「텍스트를 썼다」고 생각해도 경로에 따라 출력 bytes가 다르다는 점입니다.
특히 주의할 것은 이 부근입니다.
- Windows PowerShell 5.1은 기본 encoding이 일관되지 않다
- 일부 cmdlet이나 redirection은 UTF-16LE를 만든다
- 다른 경로에서는 active ANSI code page가 쓰인다
- PowerShell 7 이후는 UTF-8 no BOM이 기본이 되어 있다
즉 「PowerShell에서 낸 text」만으로는 encoding이 정해지지 않습니다. 어느 버전에서, 어느 cmdlet으로, 어느 쓰기 경로를 썼는지까지 봐야 합니다.
어느 경로가 어떤 bytes를 만드는지는 Microsoft Learn의 about_Character_Encoding에 정리되어 있습니다. 자주 쓰는 것만 꺼내면 이렇습니다.
| 쓰기 경로 | Windows PowerShell 5.1의 기본 | PowerShell 7의 기본 |
|---|---|---|
Out-File, >, >> |
UTF-16LE (BOM 있음) | UTF-8 no BOM |
Set-Content, Add-Content (신규 또는 빈 파일) |
ANSI = active code page. 일본어 환경이라면 CP932 | UTF-8 no BOM |
Export-Csv |
ASCII. 비 ASCII는 빠집니다 | UTF-8 no BOM |
Export-Clixml, New-ModuleManifest |
UTF-16LE | UTF-8 no BOM |
New-Item -Type File -Value |
UTF-8 no BOM | UTF-8 no BOM |
Start-Transcript |
UTF-8 with BOM | UTF-8 no BOM |
읽기 쪽에도 차이가 있습니다. BOM 없는 파일을 읽을 때 5.1의 Get-Content는 ANSI로 보지만, Import-Csv와 Select-String은 UTF-8로 봅니다. 같은 세션 안에서 전제가 갈라져 있는 것입니다.
여기서 실무상 가장 아픈 것은 같은 「텍스트를 썼다」여도 Out-File은 UTF-16LE, Set-Content는 CP932가 된다는 점입니다. 4.3에서 든 「NUL byte가 대량으로 섞인 바이너리 같은 text」는 대개 >나 Out-File의 기본에서 옵니다.
flowchart TB
accTitle: Windows PowerShell 5.1 읽기 쪽의 비대칭
accDescr: BOM 없는 파일을 읽을 때 Windows PowerShell 5.1의 Get-Content는 ANSI로 보는 반면 Import-Csv와 Select-String은 UTF-8로 보아, 같은 세션 안에서 전제가 갈라져 있음을 나타내는 그림.
b1["BOM 없는 파일"] --> g1["Get-Content로 읽는다"]
b1 --> i1["Import-Csv나 Select-String으로 읽는다"]
g1 --> g2["ANSI로 본다"]
i1 --> i2["UTF-8로 본다"]
g2 --> z1["같은 세션 안에서 전제가 갈라진다"]
i2 --> z1
그림9: 5.1에서는 같은 BOM 없는 파일이라도 읽는 cmdlet에 따라 상정 encoding이 다릅니다.
하나 더, 5.1에서는 -Encoding UTF8을 지정해도 BOM 있음이 됩니다. 5.1에서 BOM 없는 UTF-8을 쓰려면 .NET 쪽으로 쓰게 됩니다.
# Windows PowerShell 5.1에서 UTF-8 no BOM을 쓴다
$text = "日本語を含む本文"
[System.IO.File]::WriteAllText(
"C:\work\output.txt", $text,
(New-Object -TypeName System.Text.UTF8Encoding -ArgumentList $false))
UTF8Encoding의 인수를 $false로 두는 것이 BOM을 붙이지 않는 지정입니다. PowerShell 7이라면 -Encoding utf8NoBOM으로 같은 결과가 됩니다.
4. Linux와 조합했을 때의 전형 사고
Windows 단독에서는 그럭저럭 돌아가던 것이 Linux를 끼운 순간에 깨지는 일은 드물지 않습니다. 이유는 단순합니다. Linux 쪽에서는 UTF-8 전제가 강하기 때문입니다.
4.1 Windows에서 CP932로 저장한 text를 Linux가 UTF-8로 읽는다
가장 흔한 사고입니다.
- Windows의 legacy app이나 오래된 운영이 CP932로 CSV / TXT / log를 쓴다
- Linux 쪽 script나 tool은 locale에 따라 UTF-8 전제로 읽는다
- 결과적으로 decode error,
�, 의미 없는 문자열이 된다
이때 Linux 쪽 tool이 나쁜 것이 아니라, 받은 bytes에 encoding 약속이 붙어 있지 않은 것이 근본 원인입니다.
flowchart TB
accTitle: CP932 text를 Linux가 UTF-8로 읽는 사고
accDescr: Windows의 legacy app이나 오래된 운영이 CP932로 CSV나 log를 쓰고, encoding 약속 없이 Linux 쪽 script나 tool이 locale에 따라 UTF-8 전제로 읽으면 decode error나 치환 문자가 됨을 나타내는 그림.
p1["legacy app이 CP932로 CSV / log를 쓴다"] --> p2["encoding 약속 없이 넘긴다"]
p2 --> p3["Linux 쪽은 locale에 따라 UTF-8 전제로 읽는다"]
p3 --> p4["decode error / 치환 문자"]
p2 -.-> n1["근본 원인은 약속의 부재"]
그림10: 나쁜 것은 Linux 쪽 tool이 아니라, bytes에 encoding 약속이 붙어 있지 않은 것입니다.
4.2 Linux / VS Code에서 만든 UTF-8 no BOM을 Windows 쪽이 ANSI로 본다
반대 방향 사고도 있습니다.
- Linux나 VS Code에서 UTF-8 no BOM의 script / config / text를 만든다
- Windows PowerShell 5.1이나 legacy tool이 BOM 없는 file을 ANSI 쪽 code page로 본다
- 일본어나 non-ASCII를 포함한 행만 깨진다
여기서 나쁜 쪽으로 몰리기 쉬운 것은 UTF-8이지만, 실제 원인은 BOM 없는 UTF-8을 올바로 추정해 주지 않는 읽는 쪽이 섞여 있는 것입니다.
flowchart TB
accTitle: UTF-8 no BOM을 ANSI로 보게 되는 사고
accDescr: Linux나 VS Code에서 만든 UTF-8 no BOM file을 Windows PowerShell 5.1이나 legacy tool이 ANSI 쪽 code page로 보고 읽으면, 일본어 등 non-ASCII를 포함한 행만 깨짐을 나타내는 그림.
v1["Linux / VS Code에서 UTF-8 no BOM을 만든다"] --> v2["5.1이나 legacy tool이 읽는다"]
v2 --> v3["BOM 없는 file을 ANSI 쪽으로 본다"]
v3 --> v4["non-ASCII를 포함한 행만 깨진다"]
v3 -.-> n1["원인은 추정하지 않는 읽는 쪽"]
그림11: 반대 방향 사고에서는 BOM 없는 UTF-8을 추정하지 못하는 읽는 쪽이 원인이 됩니다.
4.3 Windows 쪽이 UTF-16LE를 쓰고, Linux 쪽에서는 「텍스트처럼 보이지 않는다」
이것도 꽤 있습니다.
- Windows PowerShell 5.1의 일부 출력이나 legacy tool이 UTF-16LE를 쓴다
- Linux 쪽 text tool은 UTF-8의 1 byte stream을 상정한다
- 결과적으로 NUL byte가 대량으로 섞인 「바이너리 같은 text」가 된다
UTF-16LE 자체는 나쁘지 않습니다. 다만 Linux의 text processing tool에 그대로 흘려 넣는 전제와는 잘 맞지 않는 장면이 많습니다.
flowchart TB
accTitle: UTF-16LE가 Linux 쪽에서 바이너리처럼 보이는 사고
accDescr: Windows PowerShell 5.1의 일부 출력이나 legacy tool이 쓴 UTF-16LE를, UTF-8의 1 byte stream을 상정하는 Linux text tool에 흘리면 NUL byte가 대량으로 섞인 바이너리 같은 text로 보임을 나타내는 그림.
u1["5.1의 일부 출력이나 legacy tool"] --> u2["UTF-16LE를 쓴다"]
u2 --> u3["Linux의 text tool은 1 byte stream을 상정"]
u3 --> u4["NUL byte가 섞여 바이너리처럼 보인다"]
그림12: UTF-16LE 자체는 나쁘지 않지만, Linux text 처리의 전제와는 잘 맞지 않습니다.
4.4 BOM 유무로도 friction이 생긴다
BOM은 encoding 그 자체는 아니지만 실무에서는 꽤 효과가 큽니다.
- Windows 쪽 일부 tool은 BOM이 있으면 도움이 된다
- Linux 쪽 일부 tool은 BOM을 선두의 여분 bytes로 다룬다
- 결과적으로 1열째나 1행째 선두만 깨지거나, 보이지 않는 쓰레기가 붙거나, 비교 결과가 어긋난다
특히 UTF-8에서는 같은 UTF-8이어도 BOM 있음 / 없음으로 bytes는 별개입니다. 「UTF-8로 했다」만으로는 운영 규칙으로서 아직 절반밖에 정해지지 않았습니다.
flowchart TB
accTitle: BOM 유무로 생기는 friction
accDescr: 같은 UTF-8이어도 BOM 있음·없음으로 바이트 열은 별개이며, Windows 쪽 일부 tool은 BOM이 있으면 도움이 되는 반면 Linux 쪽 일부 tool은 BOM을 선두의 여분 bytes로 다루므로 선두만 깨지는 등의 friction이 생김을 나타내는 그림.
b0["같은 UTF-8이어도 BOM 있음 / 없음은 별bytes"] --> w1["Windows 쪽 일부 tool"]
b0 --> l1["Linux 쪽 일부 tool"]
w1 --> w2["BOM이 있으면 도움이 된다"]
l1 --> l2["선두의 여분 bytes 취급"]
l2 --> l3["선두만 깨진다· 보이지 않는 쓰레기가 붙는다"]
그림13: 「UTF-8로 했다」만으로는 부족하고, BOM 유무까지 정해야 비로소 규칙이 됩니다.
4.5 console의 보이는 모습을 믿으면 헤맨다
Windows와 Linux를 가로지를 때 또 하나 위험한 것이 console입니다.
- Windows console에는 input / output의 code page가 있다
- Linux terminal 쪽은 UTF-8 locale 전제로 움직이는 경우가 많다
- WSL, SSH, container, CI를 거치면 표시 경로가 늘어난다
이 상태에서 「console에서는 읽혔으니 file도 괜찮다」「console에서는 무너졌으니 file이 깨져 있다」고 판단하면 빗나가기 쉽습니다. 보이는 것이 깨져 있는지, 저장된 bytes가 깨져 있는지는 따로 확인하는 편이 안전합니다.
4.6 전형 사고를 표로 정리하면 이렇다
| 장면 | 실제 bytes | 읽는 쪽의 상정 | 전형 증상 |
|---|---|---|---|
| Windows의 legacy app이 저장한 CSV | CP932 | Linux 쪽은 UTF-8 | �, decode error, 의미 없는 일본어 |
| Linux / VS Code에서 만든 file | UTF-8 no BOM | Windows PowerShell 5.1이 ANSI 취급 | 일본어 행만 깨진다 |
| Windows PowerShell 5.1의 일부 출력 | UTF-16LE 또는 ANSI | Linux 쪽은 UTF-8 text를 기대 | NUL byte 혼입, 바이너리 같은 동작 |
| UTF-8 with BOM의 file | UTF-8 + BOM | Unix 계 tool은 plain UTF-8 전제 | 선두 열만 깨진다, 여분 문자가 붙는다 |
| console 표시만 믿는다 | file과 console이 다른 전제 | 조사자가 표시만으로 판단 | 원인 분리를 빗나간다 |
5. 문자 깨짐 조사는 이 4가지 질문으로 진행한다
문자 깨짐 조사에서 헤매면 다음 4가지 질문으로 돌아가는 것이 가장 빠릅니다.
5.1 원래 bytes는 무엇인가
먼저 봐야 할 것은 「지금 이 file이 어떤 bytes인가」입니다. 겉모습이 아니라 bytes를 보는 의식이 필요합니다.
- UTF-8인가
- UTF-8 with BOM인가
- CP932인가
- UTF-16LE인가
- 도중에 다시 저장되어 다른 것이 되지 않았는가
5.2 처음에 누가, 어떤 전제로 썼는가
다음에 「처음 쓴 쪽」을 특정합니다.
- Windows의 legacy app인가
- PowerShell 5.1인가 7인가
- Linux의 script인가
- VS Code인가
- Excel에서 온 export인가
- 어떤 middleware / batch / CI인가
여기가 모호하면 encoding 추정이 운에 맡기게 됩니다.
5.3 지금 누가, 어떤 전제로 읽고 있는가
쓴 쪽만이 아니라 읽는 쪽의 전제도 필요합니다.
- editor가 auto-detect하고 있는가
- PowerShell이 BOM을 보고 있는가
- Linux 쪽이 locale에 따라 UTF-8 취급을 하고 있는가
- library가 기본 encoding을 쓰고 있는가
- 명시적으로
Encoding.UTF8이나cp932를 지정하고 있는가
문자 깨짐은 거의 여기서 발생합니다.
5.4 잘못 읽은 내용이 이미 저장되었는가
마지막으로 피해가 표시만으로 멈추어 있는지를 확인합니다.
- 아직 bytes는 원래 그대로인가
- 깨져 보이는 내용을 누군가 저장했는가
?나�가 차분에 들어가 있지 않은가- 전문이 다른 encoding으로 다시 쓰이지 않았는가
이 4가지 질문이 채워지면 대개 원인이 보입니다.
flowchart TB
accTitle: 문자 깨짐 조사의 4가지 질문
accDescr: 원래 bytes는 무엇인가, 처음에 누가 어떤 전제로 썼는가, 지금 누가 어떤 전제로 읽고 있는가, 잘못 읽은 내용이 이미 저장되었는가의 4가지 질문을 순서대로 채우면 대개 문자 깨짐의 원인이 보임을 나타내는 그림.
q1["질문1: 원래 bytes는 무엇인가"] --> q2["질문2: 처음에 누가 어떤 전제로 썼는가"]
q2 --> q3["질문3: 지금 누가 어떤 전제로 읽고 있는가"]
q3 --> q4["질문4: 잘못 읽은 내용은 이미 저장되었는가"]
q4 --> r1["원인이 보인다"]
그림14: 조사에서 헤매면 이 4가지 질문으로 돌아가 순서대로 채우는 것이 가장 빠릅니다.
6. 깨진 file을 어떻게 고칠까
원인이 보이면 다음은 복구입니다. 여기서 먼저 정하는 것은 원래 bytes가 아직 남아 있는가입니다.
- 원래 bytes가 남아 있다: 올바른 encoding으로 다시 읽어 목적 encoding으로 쓰면 되돌릴 수 있습니다. 이 장의 절차입니다
- 잘못 읽은 결과를 이미 저장해 버렸다: 백업이나 Git 이력에서 되돌리는 수밖에 없습니다. 치환 문자
�나?가 된 문자는 올바른 encoding을 알아도 복원할 수 없습니다
그래서 가장 먼저 해야 할 일은 작업 대상의 복사본을 만드는 것입니다. 변환은 복사본에 대해 하고, 원 file은 건드리지 마십시오.
flowchart TB
accTitle: 복구의 첫 분기
accDescr: 복구에서는 먼저 원래 bytes가 남아 있는지를 확인하고, 남아 있으면 올바른 encoding으로 다시 읽어 쓰며, 잘못 읽은 결과를 이미 저장했다면 백업이나 Git 이력에서 되돌리는 수밖에 없고, 작업은 복사본에 대해 함을 나타내는 그림.
s1["먼저 복사본을 만든다"] --> j1{"원래 bytes는 남아 있는가"}
j1 -->|"남아 있다"| r1["올바른 encoding으로 다시 읽어 쓴다"]
j1 -->|"저장되어 상실"| r2["백업이나 Git 이력에서 되돌린다"]
r2 -.-> n1["치환 문자가 된 문자는 복원할 수 없다"]
그림15: 복구는 복사본을 만든 뒤, 원래 bytes가 남아 있는지에 따라 길이 갈립니다.
6.1 Linux 쪽에 있다면 iconv
CP932의 file을 UTF-8로 만들려면 iconv가 가장 솔직합니다.
# CP932 -> UTF-8
iconv -f CP932 -t UTF-8 input.csv > output.csv
변환 원본 encoding을 틀리면 다음처럼 도중에 멈춥니다.
iconv: illegal input sequence at position 0
멈추는 것 자체가 「그 encoding이 아니다」라는 정보이므로, 후보를 바꿔 시험하는 편이 빠릅니다. 반대로 무엇을 넘겨도 멈추지 않는다면 그 file이 ASCII만으로 되어 있을 가능성이 있습니다.
UTF-16LE에서 UTF-8로 떨어뜨릴 때는 -f UTF-16LE를 씁니다. 다만 BOM 있는 file을 -f UTF-16LE로 명시해 변환하면 BOM이 U+FEFF라는 1문자로 출력에 남는 경우가 있습니다. BOM 취급을 iconv 쪽에 맡기고 싶다면 -f UTF-16을 쓰고, 변환 뒤에는 선두 1문자를 반드시 확인하십시오.
6.2 Windows 쪽에 있다면 PowerShell
PowerShell 6.2 이후에서는 -Encoding에 code page 번호를 직접 넘길 수 있습니다. CP932는 932입니다.
# PowerShell 6.2 이후. CP932 -> UTF-8 no BOM
Get-Content -Path .\input.csv -Encoding 932 |
Set-Content -Path .\output.csv -Encoding utf8NoBOM
반대로 Linux 쪽에서 만든 UTF-8 file을 CP932만 읽을 수 있는 상대에게 넘긴다면 이렇습니다.
Get-Content -Path .\input.csv -Encoding utf8 |
Set-Content -Path .\output.csv -Encoding 932
다만 이 쓰기에는 부작용이 2가지 있습니다.
Get-Content는 행 단위로 나누고,Set-Content는 각 행 뒤에 개행을 다시 붙입니다. 즉 개행 코드가 맞춰져 버립니다- 원 file 끝에 개행이 없어도 출력에는 붙습니다
개행까지 포함해 bytes를 유지하고 싶다면 행으로 나누지 말고 통째로 다룹니다.
# 개행을 그대로 유지한 채 CP932 -> UTF-8 no BOM
$text = [System.IO.File]::ReadAllText(
"C:\work\input.csv", [System.Text.Encoding]::GetEncoding(932))
[System.IO.File]::WriteAllText(
"C:\work\output.csv", $text,
(New-Object -TypeName System.Text.UTF8Encoding -ArgumentList $false))
GetEncoding(932)는 Windows PowerShell 5.1이라면 그대로 쓸 수 있습니다. PowerShell 7에서 여기가 예외가 되는 환경에서는 먼저 다음을 한 번 실행한 뒤에 쓰십시오.
[System.Text.Encoding]::RegisterProvider(
[System.Text.CodePagesEncodingProvider]::Instance)
BOM을 붙일지는 7.1대로 상대 사정으로 정합니다. 위 예에서 UTF8Encoding의 인수를 $true로 하면 BOM 있음이 됩니다.
6.3 변환한 뒤에 반드시 볼 곳
변환은 「오류가 나지 않았다」만으로 끝나지 않습니다. 최소한 다음을 봅니다.
- 대표적인 일본어 행을 2〜3개, 올바로 읽을 수 있는가
?나�가 늘지 않았는가. 늘었다면 그 문자는 이미 사라진 것입니다- 행 수가 바뀌지 않았는가
- BOM과 개행 코드가 넘기는 상대의 상정과 맞는가
- file 크기가 극단적으로 바뀌지 않았는가. CP932에서 UTF-8로 하면 일본어 부분은 2 bytes에서 3 bytes로 늘어나므로, 조금 커지는 것이 정상입니다
특히 두 번째가 중요합니다. CP932에 없는 문자를 포함한 UTF-8을 CP932로 떨어뜨릴 때는 반드시 정보가 떨어집니다. 2.2에서 쓴 대로 왕복해서 원래로 돌아오는지까지 확인하십시오.
flowchart TB
accTitle: 변환 뒤에 반드시 볼 곳
accDescr: 변환은 오류가 나지 않았다는 것만으로 끝나지 않으며, 대표적인 일본어 행을 읽을 수 있는지, ?나 치환 문자가 늘지 않았는지, 행 수나 BOM·개행·file 크기가 상정대로인지를 확인하고, 치환 문자가 늘었다면 그 문자는 이미 사라졌음을 나타내는 그림.
c1["변환이 오류 없이 끝난다"] --> c2["대표적인 일본어 행을 2〜3개 읽어 확인"]
c2 --> c3["?나 치환 문자가 늘지 않았는가"]
c3 --> c4["행 수·BOM·개행· 크기를 확인"]
c3 -.-> n1["늘었다면 그 문자는 사라진 것이다"]
그림16: 변환은 「오류가 나지 않았다」로 끝나지 않고, 내용 확인까지가 한 세트입니다.
6.4 일괄 변환은 「이행 태스크」로 잘라 낸다
마지막으로 운영 이야기입니다. 깨진 파일 하나를 고치는 것과 리포지토리 전체를 CP932에서 UTF-8로 모으는 것은 다른 작업입니다. 후자는 다음 장의 7.2대로 일상 기능 수정에 곁들여 하지 말고, 독립된 이행 태스크로 계획하십시오.
7. 사고를 줄이는 운영 규칙
여기서부터는 실무 쪽 이야기입니다. Windows와 Linux를 가로지르는 업무에서는 다음 규칙을 먼저 정해 두면 사고가 꽤 줄어듭니다.
7.1 신규 file은 UTF-8을 1순위로 둔다
신규 text file은 우선 UTF-8을 1순위로 두는 것이 무난합니다. 다만 여기서 멈추면 안 됩니다. BOM을 어떻게 할지까지 포함해 정해야 합니다.
추천하는 정하는 법은 이렇습니다.
- Linux 쪽에서 읽는 경우가 많은 text: UTF-8 no BOM을 기본으로 한다
- Windows의 legacy tool이나 Windows PowerShell 5.1이 읽는 script: BOM 유무를 상대 사정으로 명시한다
- UTF-16LE가 필요한 명확한 상대가 있다면 그 요건을 사양으로 쓴다
「UTF-8로 통일」이라고만 쓰면 나중에 BOM으로 다툽니다.
flowchart TB
accTitle: 신규 file encoding을 정하는 법
accDescr: 신규 text file은 UTF-8을 1순위로 둔 뒤 멈추지 않고, Linux 쪽에서 읽는 경우가 많으면 UTF-8 no BOM을 기본으로 하며, legacy tool이나 Windows PowerShell 5.1이 읽으면 BOM 유무를 상대 사정으로 명시하고, UTF-16LE가 필요한 상대가 있으면 요건을 사양으로 쓰기까지 정함을 나타내는 그림.
s1["신규 text file"] --> s2["UTF-8을 1순위로 둔다"]
s2 --> j1{"누가 읽는가"}
j1 -->|"Linux 쪽이 많다"| r1["UTF-8 no BOM을 기본으로"]
j1 -->|"5.1이나 legacy tool"| r2["BOM 유무를 상대 사정으로 명시"]
j1 -->|"UTF-16LE가 필요한 상대"| r3["요건을 사양으로 쓴다"]
그림17: 신규 file은 UTF-8을 1순위로 둔 뒤, BOM 취급까지 상대 사정으로 끝까지 정합니다.
7.2 기존 legacy file은 명시적인 이행 태스크까지 유지한다
기존 file이 CP932라면 일상 기능 수정에 곁들여 마음대로 UTF-8화하지 않는 편이 안전합니다.
운영으로는 이 형태가 안전합니다.
- 기존 file은 원래 encoding / BOM / 개행을 유지한다
- encoding 변경은 이행 태스크로 분리한다
- 변환 대상, 영향 범위, 하류 consumer를 확인한 뒤에 일괄 변환한다
문자 깨짐 사고의 상당수는 선의의 「곁들인 UTF-8화」에서 시작합니다.
7.3 encoding을 interface의 일부로 다룬다
CSV, TXT, log, 설정 file, 간이 protocol은 내용만이 아니라 encoding 자체가 interface입니다.
예를 들어 사양으로 최소한 여기까지는 쓰고 싶습니다.
- 이 file은 UTF-8 / CP932 / UTF-16LE 중 어느 것인가
- UTF-8인 경우 BOM은 붙는가
- 개행은 LF / CRLF 중 어느 쪽인가
- Linux / Windows 중 어느 쪽이 producer / consumer인가
- 중간의 batch나 ETL이 다시 저장하지 않는가
「text로 넘긴다」는 사양이 되어 있지 않습니다.
7.4 기본값을 믿지 말고, 쓸 때는 명시한다
code 위에서도 script 위에서도 encoding은 명시한 편이 안전합니다.
위험한 것은 이런 생각입니다.
- 기본값 그대로 저장한다
- OS에 맞춰 아마 잘 될 것이라고 생각한다
- console에서 읽혔으니 file도 괜찮겠지
- auto-detect가 있으니 괜찮다
기본값은 Windows / Linux, PowerShell 5.1 / 7, editor, runtime에서 평범하게 바뀝니다. 명시하지 않는 한 우연히 움직이고 있을 뿐이 되기 쉽습니다.
7.5 console과 file을 나눠 확인한다
은근히 효과가 큰 것이 이 규칙입니다.
- console에서의 표시 확인
- file을 다시 열어 확인
이 2가지를 나눕니다.
chcp나 terminal 표시가 맞아도 저장 file이 다른 encoding이면 의미가 없습니다. 반대로 file은 정상이어도 console의 표시 code page가 맞지 않으면 겉모습만 깨집니다.
7.6 Git은 encoding을 고쳐 주지 않는다
소소하지만 중요합니다.
Git은 기본적으로 bytes를 추적할 뿐입니다. 즉 깨진 bytes도 그대로 성실하게 이력에 넣습니다.
그래서,
- 아무 것도 바꾸지 않았는데 거대한 차이가 나왔다
- 일본어 행만 알 수 없는 차이가 됐다
- 선두 행만 바뀌었다
- 개행과 encoding이 함께 바뀌었다
같은 때는 내용 변경보다 먼저 re-encoding 사고를 의심하는 편이 좋습니다.
flowchart TB
accTitle: Git은 encoding을 고쳐 주지 않는다
accDescr: Git은 기본적으로 bytes를 추적할 뿐이며 깨진 bytes도 그대로 이력에 들어가므로, 아무 것도 바꾸지 않았는데 거대한 차이가 나거나 일본어 행만 알 수 없는 차이가 될 때는 내용 변경보다 먼저 re-encoding 사고를 의심해야 함을 나타내는 그림.
g1["Git은 bytes를 추적할 뿐이다"] --> g2["깨진 bytes도 그대로 이력에 들어간다"]
g2 --> g3["거대한 차이·일본어 행만의 알 수 없는 차이가 나온다"]
g3 --> g4["내용 변경보다 먼저 re-encoding 사고를 의심한다"]
그림18: Git은 깨진 bytes도 성실히 기록하므로, 알 수 없는 차이는 먼저 re-encoding을 의심합니다.
8. 최소한의 체크리스트
Windows와 Linux가 섞이는 일에서 먼저 고정하고 싶은 체크리스트를 둡니다.
flowchart TB
accTitle: 체크리스트의 흐름
accDescr: 편집 전에 현재 encoding이나 BOM·개행을 확인하고, 편집 중에는 기본값이나 auto-detect 맡김을 피하며, 편집 뒤에 다시 열기와 차이 확인을 하는 흐름과, 일괄 변환 등은 이행 태스크로 따로 다룸을 나타내는 그림.
c1["편집 전: 현재 encoding· BOM·개행을 확인"] --> c2["편집 중: 기본값이나 auto-detect 맡김을 피한다"]
c2 --> c3["편집 후: 다시 열기와 차이를 확인"]
c3 -.-> t1["일괄 변환 등은 이행 태스크로 분리"]
그림19: 체크는 편집 전·편집 중·편집 후로 나누고, 일괄 변환은 별도 태스크로 잘라 냅니다.
8.1 편집 전
- 이 file의 현재 encoding은 무엇인가
- BOM은 있는가
- 개행은 LF / CRLF 중 어느 쪽인가
- 대표적인 일본어 행을 2〜3개 메모했는가
- Linux 쪽 / Windows 쪽 중 어느 쪽이 최종 consumer인지 알고 있는가
8.2 편집 중
- 기본 encoding에 의존한 쓰기를 하지 않았는가
- auto-detect에 맡겨 save하지 않았는가
- PowerShell이나 shell redirection 경로를 대충 쓰지 않았는가
- 「표시가 읽힌다」만으로 안심하지 않았는가
8.3 편집 후
- 저장 뒤에 다시 열어 확인했는가
- Linux 쪽에서도 Windows 쪽에서도 대표 행이 무너지지 않았는가
?나�가 차이에 늘지 않았는가- 선두 행이나 선두 열만 깨지지 않았는가
- BOM / 개행만의 큰 차이가 되지 않았는가
8.4 이행 태스크로 해야 할 것
- CP932 → UTF-8의 일괄 변환
- UTF-8 BOM policy의 통일
- PowerShell 5.1 전제 script의 재고
- CI / container / WSL / SSH 경유의 text pass의 명문화
- editor / formatter / batch의 저장 설정 통일
9. 정리
Windows의 문자 코드 문제를 한 마디로 말하면, Unicode의 세계와 legacy code page의 세계가 지금도 같이 있는 것이 본질입니다.
그리고 Linux와 조합했을 때 사고가 늘어나는 것은 Linux 쪽이 UTF-8 전제로 흐르는 경우가 많고, Windows 쪽의 CP932나 UTF-16, console code page, PowerShell의 버전 차이가 한 번에 겉으로 나오기 때문입니다.
기억해 두고 싶은 것은 다음 5점입니다.
- 문자 깨짐은 bytes의 해석 어긋남
- 표시 붕괴와 데이터 파손은 별개
- Windows에서는 file / editor / console / API의 층을 나눠 생각한다
- Linux와 주고받는 text는 UTF-8을 1순위로 둔다
- 기존 legacy file의 변환은 통상 수정과 나눈다
「Windows에서 문자 깨짐이 있었다」를 그대로 다루면 이야기가 너무 넓습니다. 하지만,
- 원래 bytes는 무엇인가
- 누가 어떻게 썼는가
- 누가 어떻게 읽었는가
- 이미 저장되었는가
의 4가지 질문으로 자르면 꽤 정리할 수 있습니다.
문자 코드는 눈에 잘 띄지 않지만 Windows와 Linux 사이에서는 I/O 계약 그 자체입니다. 여기를 애매하게 두지 않는 것이 가장 효과가 있는 대책입니다.
10. 참고 자료
Windows / Microsoft
- Code Pages - Win32 apps | Microsoft Learn
- Code Page Identifiers - Win32 apps | Microsoft Learn
- Unicode in the Windows API - Win32 apps | Microsoft Learn
- Console Code Pages - Windows Console | Microsoft Learn
- chcp | Microsoft Learn
- Use UTF-8 code pages in Windows apps | Microsoft Learn
PowerShell / VS Code
- about_Character_Encoding | Microsoft Learn
- Understanding file encoding in VS Code and PowerShell | Microsoft Learn
GNU / Linux locale
관련 기사
같은 태그를 공유하는 최신 기사입니다. 더 가까운 주제로 지식을 넓힐 수 있습니다.
Windows의 문자 인코딩과 줄바꿈 - 문자 깨짐과 CRLF/LF의 기본
Windows에서 섞이기 쉬운 Shift_JIS / UTF-8 / UTF-16, 문자 깨짐, CRLF / LF의 차이를 실무에서 판단하기 쉽게 정리합니다.
Windows에서 Codex의 문자 깨짐 사고를 줄이는 지시 규칙
Windows에서 Codex에 일본어 파일을 다루게 할 때, 추측 저장을 피하고 기존 encoding을 유지하며 다시 읽어 검증하기 위한 실무 지시 규칙을 정리합니다.
일본어 글꼴과 문자의 함정 ── 업무 앱에서 JIS2004·이체자 선택자·외자를 다루는 법
「화면과 장표에서 葛 자의 모양이 다르다」「성명의 글자가 나오지 않는다」── 업무 시스템의 문자 문제는 문자 코드(데이터)와 글꼴(겉모습)의 층을 나누면 정리할 수 있습니다. JIS2004의 자형 변경, 이체자 선택자, 외자의 실무 대응을 설명합니다.
WMI/CIM을 C#·PowerShell에서 쓰기 ── 하드웨어 정보 가져오기·프로세스 모니터링·원격 조회의 실무 가이드
PC 시리얼 번호 조회, 디스크 여유 공간 모니터링, 프로세스 시작 감지의 흔한 답이 WMI/CIM입니다. Get-CimInstance 등 CIM cmdlet 사용법과 구 Get-WmiObject에서의 이전, C#의 System.Managemen...
그룹 정책(GPO) 실무 입문 ── 동작 방식·적용 확인·Intune과의 역할 분담
「GPO로 배포한다」는 말의 뜻을 모른 채 AD 환경을 다루고 있지는 않은지요. 그룹 정책의 동작 방식과 LSDOU 적용 순서, gpupdate·gpresult로 적용 여부를 확인하는 방법, Intune과의 역할 분담, 고객사 GPO가 앱 동작을...
관련 토픽
이 기사와 가까운 토픽 페이지입니다. 기사를 출발점 삼아 관련 서비스와 다른 기사로 이어집니다.
Windows 기술 토픽
Windows 개발, 장애 조사, 기존 자산 활용에 관한 KomuraSoft LLC 기사를 모은 토픽 허브입니다.
이 주제와 연결되는 서비스
이 기사는 다음 서비스 페이지로 이어집니다. 가까운 입구부터 확인해 주세요.
기술 상담 & 설계 리뷰
Windows와 Linux 사이에서 CSV, 로그, 설정 파일의 문자 코드 전제가 어긋나는 업무에서는 I/O 계약과 운영 규칙을 먼저 정리하면 사고를 줄이기 쉽습니다.
Windows 앱 개발
Windows용 업무 도구에서는 CP932와 UTF-8이 섞인 현장이 많아, 문자 코드 취급을 설계에 넣어 두는 것이 유지보수성으로 직결됩니다.
자주 묻는 질문
이 기사 주제에 대해 상담 시 자주 나오는 질문을 모았습니다.
- Windows에서 문자 깨짐이 일어나는 원인은 무엇인가요?
- 대부분은 같은 바이트 열을 다른 문자 코드로 읽었거나, 잘못 읽은 결과를 다른 문자 코드로 저장했기 때문입니다. 예를 들어 「あ」를 UTF-8로 저장한 바이트 열(E3 81 82)을 CP932 문맥으로 읽으면 「縺」 같은 다른 문자열로 보입니다. 일본어가 어려워서 일어나는 것이 아니라, encode와 decode의 전제가 일치하지 않는 것이 정체입니다.
- 문자 깨짐이 난 파일은 되돌릴 수 있나요?
- 원래 바이트 열이 바뀌지 않았다면 올바른 encoding으로 다시 여는 것만으로 되돌릴 수 있는 경우가 있습니다. 위험한 것은 잘못 읽혀 깨져 보이는 내용을 그대로 저장하는 흐름이며, 이 단계에 들어가면 표시 붕괴가 아니라 데이터 파손이 됩니다. 또한 Unicode 문자열을 CP932처럼 좁은 code page로 떨어뜨려 「?」나 치환 문자로 바뀐 경우, 한 번 사라진 문자는 나중에 올바른 encoding을 알아도 복원할 수 없습니다.
- chcp 65001을 하면 파일도 UTF-8이 되나요?
- 되지 않습니다. console의 code page를 바꾸는 것과 기존 파일의 바이트 열이 무엇인지는 별개입니다. Windows에서는 파일 자체의 문자 코드, 에디터의 해석, console의 input/output code page, 앱 내부의 문자열 형식이 서로 다르므로 이 층을 나눠 생각해야 합니다. console에서 읽혔으니 파일도 괜찮다는 판단은 빗나가기 쉬우므로, console 표시 확인과 파일을 다시 열어 확인하는 작업은 나눠야 합니다.
- Windows와 Linux 사이에서 텍스트를 주고받을 때의 안전한 규칙은 무엇인가요?
- 신규 파일은 UTF-8을 1순위로 두고 BOM 취급까지 정하는 것입니다. Linux 쪽에서 읽는 경우가 많은 텍스트는 UTF-8 no BOM을 기본으로 하고, Windows PowerShell 5.1 같은 legacy 도구가 읽는 경우에는 BOM 유무를 상대 사정에 맞게 명시합니다. 기존 CP932 파일은 일상 수정에 곁들여 변환하지 말고 명시적인 이행 태스크로 분리합니다. 또한 CSV나 로그는 encoding 자체가 interface이므로 encoding·BOM·개행 코드를 사양으로 적어 두면 사고가 줄어듭니다.