uniflow
KO / EN
Dev·판단·2026-10-19

AI가 고친 .js 파일만 CRLF 줄바꿈으로 바뀌는 이유 — BOM·LF가 깨지는 경우 실측 비교

BOM·LF 로 잘 있던 .js 파일이 AI 에게 한 줄 고쳐 달라고 한 뒤 CRLF 로 바뀌어 있었습니다. 범인은 AI 가 아니라 AI 가 파일을 쓴 경로였습니다. 같은 파일을 도구 13가지로 고쳐 보고, 실제 저장소에서 24개 파일이 뒤집힌 과정을 추적했습니다.

AI 에게 .js 파일 한 줄을 고쳐 달라고 했는데 커밋해 보니 파일 전체가 바뀐 것으로 나오는 경우가 있습니다. 열어 보면 내용은 그대로인데 줄 끝이 LF 에서 CRLF 줄바꿈으로 바뀌어 있습니다. 결론부터 말하면 원인은 AI 가 아니라 AI 가 파일을 저장한 경로입니다. Windows 에서 AI 가 Python 스크립트(open(p, 'w'))나 PowerShell(Set-Content)로 파일을 고치면 줄바꿈이 CRLF 로 바뀌고, 경우에 따라 BOM 까지 사라집니다. 반대로 AI 코딩 도구의 부분 편집 기능(Claude Code 의 Edit 도구)으로 고치면 원래의 BOM·줄바꿈이 그대로 유지됩니다. 같은 도구 안에서도 파일 전체를 새로 쓰는 Write 도구는 원래 줄바꿈과 상관없이 LF 로 씁니다.

필자가 운영하는 사이트 저장소에서 한 달 사이 24개 파일이 이렇게 뒤집혔습니다. 같은 파일을 도구 13가지로 직접 고쳐 보면서 어떤 경우에 바뀌고 어떤 경우에 안 바뀌는지 비교했습니다.

BOM·LF·CRLF 차이 — 눈에 안 보이는 3바이트와 1바이트

먼저 용어부터 짧게 정리합니다. 셋 다 화면에서는 보이지 않고 파일의 바이트에만 있습니다.

이름실제 바이트어디에 있나주로 쓰는 곳
LF0A (\n)매 줄 끝macOS·Linux, 대부분의 웹 프로젝트
CRLF0D 0A (\r\n)매 줄 끝Windows 메모장·PowerShell·Python 기본값
UTF-8 BOMEF BB BF파일 맨 앞 3바이트Windows 일부 편집기, 일부 레거시 코드

BOM 은 "이 파일은 UTF-8 이다"라고 알려 주는 표식인데, UTF-8 에서는 없어도 되는 값입니다. 그래서 어떤 도구는 붙이고, 어떤 도구는 떼고, 어떤 도구는 있는 그대로 둡니다. 줄바꿈도 마찬가지로 도구마다 기본값이 다릅니다. 문제는 이 기본값이 파일을 읽을 때와 쓸 때 다를 수 있다는 점입니다. 읽을 때는 무엇이든 받아 주고, 쓸 때는 자기 기본값으로 쓰는 도구가 있습니다. 그런 도구를 한 번만 거쳐도 파일의 줄바꿈이 통째로 바뀝니다.

실제로 겪은 일 — 한 줄 고쳤는데 diff 는 824줄

필자의 사이트 저장소에는 .gitattributes 가 없습니다. 원래 모든 .js 파일이 LF 였는데, 2026년 9~10월 사이 AI 와 작업한 커밋 몇 개를 거치면서 텍스트 파일 107개 중 24개가 CRLF 로 바뀌어 있었습니다. 커밋별로 줄 끝의 CR 개수를 세어 보니 바뀐 시점이 정확히 잡혔습니다.

파일실제로 바꾼 줄git 에 찍힌 변경
계산기 스크립트(.js)+26 / −9+824 / −807
README.md+10 / −1+205 / −196
콘텐츠 로더(.js)+25 / −0+197 / −172
계산기 목록(.js)+3 / −1+95 / −93

"실제로 바꾼 줄"은 git diff --ignore-cr-at-eol 로 줄바꿈 차이를 무시하고 센 값입니다. 서른 줄 남짓 고쳤는데 git 은 파일 전체를 지우고 다시 쓴 것으로 기록했습니다.

AI 작업 기록에서 찾은 범인

Claude Code 는 작업 기록을 로컬에 남깁니다. 해당 커밋 직전의 기록을 열어 보니, 줄바꿈이 뒤집힌 파일은 모두 AI 가 아래와 같은 Python 스크립트로 고친 파일이었습니다.

p = 'lib/content.js'
s = open(p, encoding='utf-8').read()
# ... 문자열 치환 ...
open(p, 'w', encoding='utf-8').write(s)

AI 는 여러 파일을 한 번에 고치거나, 치환할 문자열이 정확히 한 번 나오는지 assert 로 확인하면서 고치고 싶을 때 이런 스크립트를 자주 썼습니다.

더 눈에 띄는 건 메타태그 검사 스크립트였습니다. AI 가 Write 도구로 새로 만들 때는 LF 였는데, 1분 뒤 정규식 한 줄을 Python 스크립트로 고치면서 CRLF 가 됐고, 그 상태로 첫 커밋에 들어갔습니다. 같은 세션, 같은 AI 인데 어떤 도구로 저장했느냐에 따라 결과가 갈린 것이죠.

도구 13가지로 직접 고쳐 본 결과 — CRLF 줄바꿈이 생기는 경우

원인을 확인하려고 실험을 했습니다. BOM + LF 로 된 두 줄짜리 .js 파일을 만들고, 도구마다 한 줄을 바꿔 저장한 뒤 바이트를 확인했습니다. 원본 줄바꿈에 따라 결과가 갈리는 도구는 BOM + CRLF 파일로도 시험해 행을 나눴습니다.

측정 환경은 Windows 11, Python 3.10, Node 22, Windows PowerShell 5.1, Git 2.53(Git Bash 의 GNU sed 4.9·GNU grep 3.0), 2026년 10월의 Claude Code 입니다. 다른 환경에서는 결과가 다를 수 있고, 특히 Claude Code 의 Write·Edit 동작은 버전이 바뀌면 달라질 수 있습니다.

고친 방법원본BOM줄바꿈
Claude Code Edit 도구BOM + LF유지LF 유지
Claude Code Edit 도구BOM + CRLF유지CRLF 유지 (새로 넣은 줄도 CRLF)
Claude Code Write 도구 (기존 파일 덮어쓰기)BOM + LF유지LF 유지
Claude Code Write 도구 (기존 파일 덮어쓰기)BOM + CRLF유지 ※LF 로 바뀜
Claude Code Write 도구 (새 파일)—없음LF
Python open(p, 'w', encoding='utf-8')BOM + LF유지전부 CRLF
Python utf-8-sig 로 읽고 utf-8 로 쓰기BOM + LF사라짐전부 CRLF
Python open(p, 'w', encoding='utf-8', newline='')BOM + LF유지LF 유지
Node fs.writeFileSyncBOM + LF유지LF 유지
Git Bash sed -iBOM + LF유지LF 유지
Git Bash sed -iBOM + CRLF유지CR 이 지워져 LF
PowerShell Get-Content -Raw → Set-ContentBOM + LF사라짐LF + 끝에 CRLF 한 번 (혼합)
PowerShell Out-File -Encoding utf8BOM + LF유지LF + 끝에 CRLF 한 번 (혼합)
PowerShell (Get-Content) 줄 단위 → Set-Content -Encoding UTF8BOM + LF유지전부 CRLF
.NET [IO.File]::WriteAllTextBOM + LF사라짐LF 유지
core.autocrlf=true 로 git checkoutBOM + LF유지작업 폴더만 CRLF

※ 필자의 PC 에서는 BOM 이 남았지만, 별도로 측정한 결과에서는 같은 조작으로 BOM 까지 사라졌습니다(파일 첫 바이트가 BOM ef bb bf 가 아니라 한글 '가'의 ea b0 80). 줄바꿈이 LF 로 바뀌는 것은 두 측정에서 같았습니다.

"원래 BOM·LF 였는데 BOM·CRLF 가 됐다"는 증상과 정확히 맞는 건 Python 기본 텍스트 모드 행입니다.

Edit 냐 Write 냐 — 같은 AI, 같은 파일인데 갈린다

추가 실험에서 가장 눈에 띈 건 Claude Code 의 두 도구 차이였습니다.

  • Edit 는 파일에서 바꿀 부분만 갈아 끼웁니다. 원래 BOM·줄바꿈을 그대로 두고, 새로 넣은 줄도 원래 파일의 줄바꿈(CRLF 파일이면 CRLF)을 따랐습니다.
  • Write 는 파일 전체를 새 내용으로 씁니다. 원래 파일이 CRLF 였어도 결과는 LF 였습니다. 원본이 LF 인 파일에서는 아무 차이가 보이지 않아, 처음 실험에서는 이 차이를 놓쳤습니다.

같은 AI 가 같은 파일을 고쳐도 어느 도구로 저장했느냐에 따라 결과가 달라진다는 뜻입니다. 이 글의 결론(원인은 AI 가 아니라 저장 경로)을 오히려 더 분명하게 보여 주는 결과입니다. 정리하면 "LF 로 바뀐다"도 "CRLF 로 바뀐다"도 아니고, 원래 상태를 지키지 않는 저장 경로를 거치면 그 경로의 기본값으로 바뀐다가 정확합니다.

sed -i — 원본 줄바꿈에 따라 결과가 다르다 (정정)

(정정) 처음에는 "Git Bash sed -i 는 BOM·LF 를 유지한다"고만 적었습니다. 원본이 LF 인 파일만 시험한 결과였습니다. 원본이 CRLF 인 파일에 sed -i 를 쓰면 CR 이 모두 지워져 LF 가 됩니다(BOM 은 남습니다). LF 파일은 그대로입니다.

직접 확인해 볼 수 있습니다.

printf '\xef\xbb\xbfconst a = 1;\r\nconst b = 2;\r\n' > crlf.js
printf '\xef\xbb\xbfconst a = 1;\nconst b = 2;\n' > lf.js
sed -i 's/const b = 2;/const b = 3;/' crlf.js lf.js

for f in crlf.js lf.js; do
  echo "$f  BOM=$(head -c3 $f | od -An -tx1)  CR=$(tr -cd '\r' < $f | wc -c)"
done
# Git Bash 에서는 crlf.js 의 CR 이 0 으로 나온다

Python 텍스트 모드 — 가장 흔한 경우

Python 은 텍스트 모드로 파일을 열면 읽을 때와 쓸 때 줄바꿈을 다르게 다룹니다.

  • 읽을 때: \r\n 이든 \n 이든 전부 \n 으로 바꿔서 줍니다. 그래서 원래 파일이 무엇이었는지 코드에서는 알 수 없습니다.
  • 쓸 때: \n 을 운영체제 기본값으로 바꿔서 씁니다. Windows 에서는 \r\n, macOS·Linux 에서는 \n 입니다.

그래서 같은 스크립트를 맥에서 돌리면 아무 일도 안 생기고, Windows 에서 돌리면 파일 전체가 CRLF 가 됩니다. 여러 PC 를 오가며 일한다면 "어제는 괜찮았는데 오늘은 바뀐다"는 일이 여기서 생깁니다.

BOM 이 유지되는 이유도 같은 맥락입니다. encoding='utf-8' 로 읽으면 BOM 3바이트가 보이지 않는 문자(U+FEFF) 하나로 문자열 맨 앞에 남고, 쓸 때 그 문자가 다시 BOM 으로 저장됩니다. 반대로 utf-8-sig 로 읽으면 BOM 을 떼고 읽으니, utf-8 로 쓰는 순간 BOM 이 사라집니다.

막는 법은 한 단어입니다. 쓸 때 newline='' 를 주면 Python 이 줄바꿈을 건드리지 않습니다.

s = open(p, encoding='utf-8', newline='').read()
open(p, 'w', encoding='utf-8', newline='').write(s)

PowerShell 5.1 — BOM 이 빠지고 꼬리에 CRLF

Windows 에 기본으로 깔린 PowerShell 5.1 은 더 까다롭습니다. Set-Content 는 인코딩을 지정하지 않으면 시스템 기본 코드페이지로 저장합니다. 한국어 Windows 에서는 CP949 입니다. 실험에서 // 한글 주석 이 들어간 UTF-8 파일을 Get-Content -Raw | Set-Content 로 다시 쓰니 BOM 이 사라지고 한글이 CP949 바이트로 바뀌었습니다. 이 파일을 UTF-8 로 읽는 빌드 도구에서는 한글이 깨집니다. 별도 측정에서도 같은 결과가 재현됐습니다('가'가 UTF-8 ea b0 80 이 아니라 CP949 b0 a1 로 저장됨).

게다가 Set-Content·Out-File 은 내용 끝에 CRLF 를 하나 더 붙입니다. 본문은 LF, 마지막 줄만 CRLF 인 혼합 줄바꿈 파일이 됩니다. 줄 단위로 읽어서(Get-Content 에 -Raw 없이) 다시 쓰면 줄마다 CRLF 로 이어 붙여 파일 전체가 CRLF 가 됩니다.

git autocrlf — 파일을 안 건드렸는데 바뀌는 경우

마지막 줄은 성격이 다릅니다. AI 도 편집기도 아닌 git 이 바꾸는 경우입니다. core.autocrlf=true 이면 git 은 저장소에는 LF 로 두고, 작업 폴더로 꺼낼 때만 CRLF 로 바꿔 줍니다. 커밋할 때 다시 LF 로 되돌리니 보통은 문제가 없습니다.

함정은 PC 마다 설정이 다를 때입니다. Git for Windows 는 설치할 때 시스템 설정에 autocrlf=true 를 넣는 경우가 많고, 사용자 설정에서 false 로 덮어쓴 PC 도 있습니다. 필자의 PC 도 시스템은 true, 사용자는 false 였습니다. 한 PC 에서 CRLF 로 꺼낸 파일을 다른 설정의 PC 로 복사하거나, 설정이 다른 PC 에서 커밋하면 CRLF 가 저장소에 들어갑니다. BOM 은 git 이 건드리지 않습니다.

Advertisement본문 중간 · 반응형본 도메인에서만 게재

CRLF 로 바뀌면 문제가 되나 — 판단 기준

대부분의 .js 파일은 CRLF 로 바뀌어도 실행에는 문제가 없습니다. 브라우저와 Node 는 두 줄바꿈을 똑같이 읽습니다. 그래서 "동작은 하는데 뭔가 찜찜한" 상태로 오래 남기 쉽습니다. 문제가 되는 건 실행이 아니라 그 주변입니다.

바로 고쳐야 하는 경우

  • 셸 스크립트·#! 로 시작하는 실행 파일: 첫 줄 끝의 \r 까지 인터프리터 이름으로 읽혀 bad interpreter 오류가 납니다. Linux 서버에서 돌리는 배포 스크립트라면 바로 멈춥니다.
  • 한글이 CP949 로 바뀐 경우: 줄바꿈이 아니라 인코딩 문제입니다. 화면·빌드에서 깨지니 즉시 되돌립니다.
  • 코드 리뷰를 받는 저장소: 서른 줄 고친 PR 이 800줄 변경으로 보이면 리뷰어가 실제 변경을 찾을 수 없습니다.

당장은 괜찮지만 정리해 두는 게 좋은 경우

  • 혼자 쓰는 저장소의 .js·.md: 실행은 되지만 git blame 이 "이 줄은 그날 AI 가 썼다"로 덮여 이력을 추적하기 어려워집니다.
  • 혼합 줄바꿈 파일: 편집기가 어느 쪽으로 저장할지 파일마다 달라져, 다음 수정 때 또 diff 가 부풀어 오릅니다.

그냥 둬도 되는 경우

  • Windows 전용 배치 파일(.bat·.cmd)은 오히려 CRLF 가 맞습니다. 저장소 전체를 LF 로 맞출 때 이 파일들은 예외로 둡니다.

CRLF 줄바꿈 확인하는 법 — 그리고 속기 쉬운 명령

가장 정확한 건 git 에게 직접 묻는 겁니다.

# 파일마다 저장소(i/)와 작업 폴더(w/)의 줄바꿈
git ls-files --eol -- '*.js'

# 집계만 보고 싶을 때
git ls-files --eol | awk '{print $1}' | sort | uniq -c

# 커밋 전에 "진짜로 바뀐 줄"만 보기
git diff --stat
git diff --ignore-cr-at-eol --stat

마지막 두 명령의 숫자가 크게 다르면 줄바꿈이 바뀐 겁니다. 커밋하기 전에 이 둘만 비교해도 대부분 잡힙니다.

Git Bash 의 grep 은 어느 쪽으로든 틀린다 (정정)

조사하다가 한 번 속았습니다. Windows 의 Git Bash 에서 grep -c $'\r$' 파일 로 CR 로 끝나는 줄을 세었더니, LF 파일도 모든 줄이 CR 로 끝난다고 나왔습니다. git ls-files --eol 은 같은 파일을 LF 라고 했고, od -c 로 바이트를 직접 열어 보니 LF 가 맞았습니다.

(정정) 처음에는 "grep 은 LF 파일을 CR 로 잘못 센다"고 한 방향으로만 적었습니다. 그런데 별도 측정에서는 반대로 CRLF 파일에서 0 이 나왔습니다. CR 이 있는데 없다고 센 것입니다. 틀리는 방향이 하나가 아니므로, 줄바꿈·BOM 판정에는 grep 을 쓰지 말고 바이트로만 판정하는 것이 맞습니다.

이 오판은 실제로 엉뚱한 보고로 이어졌습니다. AI 에이전트가 이 grep 명령으로 원본을 "LF"로 판정한 뒤, 고친 파일을 다시 세고는 "수정 후 CRLF 로 바뀌었다"고 보고했습니다. 바이트로 세어 보니 수정 전후 모두 CRLF 였습니다. 바뀐 것은 파일이 아니라 측정이었던 셈입니다.

셸에서 직접 세려면 줄 단위 도구 대신 바이트를 세는 쪽이 안전합니다.

tr -cd '\r' < 파일 | wc -c    # 0 이면 CR 이 하나도 없다
head -c3 파일 | od -An -tx1    # ef bb bf 면 BOM 이 있다

막는 법 — 저장소·AI 지침·습관 세 군데

1. 저장소에 .gitattributes 를 둔다

가장 확실한 방법입니다. 저장소 맨 위에 .gitattributes 를 만들고 아래처럼 적습니다.

* text=auto eol=lf
*.bat text eol=crlf
*.cmd text eol=crlf

이렇게 해 두면 AI 가 Python 으로 CRLF 를 써도 git 에 들어갈 때 LF 로 정규화됩니다. PC 마다 autocrlf 설정이 달라도 이 파일이 우선합니다. 필자가 관리하는 다른 저장소에는 이 설정이 있는데, 같은 방식으로 AI 와 작업해 왔는데도 .js 157개가 전부 LF 였습니다. 설정이 없는 저장소에서만 24개가 뒤집혔습니다.

이미 섞여 버린 저장소라면 .gitattributes 를 추가한 뒤 한 번 정리합니다.

git add --renormalize .
git commit -m "줄바꿈 LF 로 정규화"

이 커밋은 diff 가 크게 나오니 다른 변경과 섞지 말고 단독 커밋으로 남깁니다. 단, .gitattributes 는 BOM 을 관리하지 않습니다. BOM 을 꼭 유지해야 하는 파일이 있다면 아래 2번의 지침에 따로 적어야 합니다.

2. AI 지침 파일에 한 줄 적는다

AI 에게 일을 시킬 때 쓰는 지침 파일(CLAUDE.md 같은)에 저장 방식을 적어 둡니다. 지침 파일을 쓰는 법은 Codex vs Claude Code — 한글 환경 비교에서도 다뤘습니다.

기존 파일은 전용 편집 도구(Edit)나 "읽고 고쳐 쓰기"로만 수정한다. 파일 전체 쓰기(Write)는 새 파일을 만들 때만 쓴다. 여러 파일을 스크립트로 고쳐야 하면 Python 은 읽기·쓰기 모두 newline='' 를 붙이고, PowerShell 의 Set-Content·Out-File 이나 sed -i 로는 소스 파일을 쓰지 않는다.

기존 파일의 BOM·줄바꿈은 그 파일의 원래 상태가 기준이다. 이웃 파일의 관례나 저장소 일괄 규칙보다 우선한다.

줄바꿈·BOM 판정은 grep 이 아니라 바이트 단위(tr -cd '\r' < 파일 | wc -c, head -c3 파일 | od -An -tx1)로 한다.

AI 는 지침에 적힌 것을 따르지만 적히지 않은 것은 자기 기본 습관대로 합니다. 이번 사례에서도 AI 는 편집 도구가 있는데도 여러 파일을 한 번에 고칠 때 Python 을 택했습니다. 습관을 바꾸는 것보다 지침 한 줄이 빠릅니다.

다만 문장 지침만으로는 완전히 막히지 않습니다. 확실히 하려면 수정 전에 BOM·줄바꿈을 기록해 두고, 수정 후 같은 값인지 비교하는 단계를 도구로 강제합니다. 아래처럼 수정 전후에 같은 명령을 돌려 두 출력이 다르면 멈추는 식입니다.

# 수정 전후에 같은 명령을 돌려 두 출력을 비교한다
eolsnap() {
  for f in "$@"; do
    printf '%s BOM=%s CR=%s\n' "$f" "$(head -c3 "$f" | od -An -tx1 | tr -d ' ')" "$(tr -cd '\r' < "$f" | wc -c)"
  done
}
eolsnap src/*.js > before.txt
# ... AI 가 파일을 고친다 ...
eolsnap src/*.js > after.txt
diff before.txt after.txt   # 줄 수가 바뀐 파일은 CR 수가 달라지는 게 정상 — BOM 과 "CR 0 ↔ 0 아님" 의 변화를 본다

3. 커밋 직전에 diff 숫자를 본다

git diff --stat 의 숫자가 생각보다 훨씬 크면 커밋하기 전에 멈춥니다. 고친 줄은 서른 줄인데 변경이 수백 줄이면 거의 줄바꿈 아니면 인코딩입니다.

정리 — CRLF 줄바꿈은 AI 가 아니라 저장 경로의 문제

AI 로 고친 .js 파일이 BOM·LF 에서 BOM·CRLF 로 바뀌는 건, Windows 에서 AI 가 Python 기본 텍스트 모드로 파일을 쓸 때 생기는 일입니다. 같은 AI 라도 부분 편집 도구(Edit)로 고치면 바뀌지 않고, 같은 스크립트라도 맥에서 돌리면 바뀌지 않습니다. 반대 방향도 있습니다. 원래 CRLF 였던 파일은 Write 도구나 sed -i 를 거치면 LF 로 바뀝니다. 어느 쪽이든 기준은 "그 파일의 원래 상태"이고, 판정은 바이트로 합니다. 한글 인코딩 문제와 마찬가지로, 원인은 대개 "누가 썼느냐"가 아니라 "어떤 도구의 기본값을 거쳤느냐"입니다. 한글 깨짐의 경우는 Codex 한글 깨짐 원인과 대응 전략에 정리해 두었습니다.

지금 바로 해 볼 수 있는 순서는 이렇습니다.

  1. git ls-files --eol | awk '{print $1}' | sort | uniq -c 로 저장소에 CRLF 가 섞였는지 확인한다
  2. 섞였으면 .gitattributes 에 * text=auto eol=lf 를 넣고 git add --renormalize . 로 단독 커밋한다
  3. AI 지침 파일에 "기존 파일은 Edit 로만, Write 는 새 파일만", "Python 은 newline='', PowerShell Set-Content·sed -i 금지", "판정은 바이트로" 를 넣는다
  4. 커밋 전에 git diff --stat 과 git diff --ignore-cr-at-eol --stat 의 숫자를 비교한다
Advertisement글 최하단 · 띠배너본 도메인에서만 게재