문장을 한 글자도 바꾸지 않았는데 비교 도구가 모든 줄을 수정으로 표시할 수 있습니다. 줄 끝이 LF에서 CRLF로 바뀌었다면 눈에 보이는 내용과 원시 바이트의 동일성을 따로 확인해야 합니다. 반대로 공백을 전부 지워 비교하면 실제로 중요한 차이까지 사라질 수 있습니다. 이 글은 가상 두 줄 파일로 바이트 해시, 제한적인 줄바꿈 정규화, 마지막 개행·행 끝 공백·BOM 보존을 나눠 검사합니다. 모든 형식의 의미가 같다는 판정기가 아니라 비교 규칙이 지워도 되는 차이를 명시하는 연습입니다.

보이는 두 줄 뒤에 어떤 바이트가 있는지 확인합니다.

가상 원문은 alpha와 beta 두 줄이며 각 줄 끝에 LF 하나를 넣었습니다. 바이트 수는 11입니다. 같은 줄 끝을 CRLF 두 바이트로 바꾸면 13바이트가 됩니다. CR은 0D, LF는 0A이므로 JSON의 inputsHex에서 원시 배열을 직접 확인할 수 있습니다. 줄 끝이 두 군데라 한 바이트씩 늘어난 결과입니다. 문자의 색이나 편집기의 줄 표시만 보고 파일이 동일하다고 판단하지 않습니다.

원시 해시를 계산하면 LF 버전과 CRLF 버전은 다릅니다. 해시는 사람이 보는 문장이 아니라 입력 바이트 전체에 반응하기 때문입니다. 이 차이는 실제 바이트 변경을 정확히 나타내지만, 업무적으로 중요한 내용이 바뀌었는지는 설명하지 않습니다. 원본 보존이나 배포 파일 검증에는 그 차이가 중요할 수 있고, 문서 편집 검토에서는 줄바꿈만 달라진 것을 따로 묶고 싶을 수 있습니다. 비교 목적에 따라 원시 동일성과 정해 둔 텍스트 동일성을 각각 기록합니다.

CRLF를 LF로 바꾸는 규칙만 먼저 시험합니다.

예제의 normalize 함수는 바이트 열에서 CRLF를 LF로 바꿉니다. 다른 공백, 문자와 인코딩 표시는 건드리지 않습니다. 이 함수 뒤에 해시를 계산하면 두 파일의 normalizedHashEqual은 true입니다. 결과 문장은 “정해 둔 CRLF→LF 규칙을 적용한 후 바이트가 같다”가 정확합니다. 모든 문서 형식에서 의미가 같고 어디서나 안전하게 실행된다는 뜻은 아닙니다.

정규화는 원본을 덮어쓰는 작업과 구분합니다. 코드는 메모리의 사본을 비교하며 실제 파일을 다시 저장하지 않습니다. 원본 해시와 정규화 해시를 둘 다 남기면 전달된 파일의 변화와 텍스트 비교 결과를 추적할 수 있습니다. 정규화 뒤의 해시만 보관하면 나중에 CRLF였는지 LF였는지 알 수 없을 수 있습니다. 문서의 줄 끝 규칙을 바꾸기로 했다면 변환 정책과 적용 범위를 별도 변경으로 남기고 내용 편집과 섞이지 않게 검토하는 편이 좋습니다.

정규화 규칙을 적용한 결과가 같다는 것을 원시 파일의 해시와 함께 표에 둡니다. 원시 비교는 다름, CRLF만 LF로 변환한 비교는 같음이라는 두 행이 서로 모순되는 것은 아닙니다. 파일을 한 바이트도 바꾸면 안 되는 전송 검증에서는 첫 행이 기준이고, 줄 끝만 허용 차이인 텍스트 검토에서는 둘째 행을 참고합니다. 어떤 행을 승인에 쓸지 정하지 않으면 같은 데이터가 담당자마다 다른 통과 판정을 받을 수 있습니다.

또한 압축된 파일이나 이미지 바이트에 같은 치환 함수를 적용하지 않습니다. 우연히 0D 0A가 나타났다고 텍스트 줄 끝이라는 뜻이 아니기 때문입니다. 먼저 비교 대상이 어떤 인코딩의 텍스트인지 확인하고, 원시 자료는 따로 보존한 다음 허용 규칙을 적용합니다. 이 순서를 거꾸로 하면 정규화 과정 자체가 내용 손상을 만들 수 있습니다. 여기의 바이트 치환은 의도적으로 만든 작은 텍스트 배열에 한정했습니다.

마지막 개행이 없다는 사실을 자동으로 지우지 않습니다.

alpha 다음 LF, beta 다음 LF인 원문과 마지막 beta 뒤에 LF가 없는 자료를 비교했습니다. 이 예제의 normalize는 CRLF만 치환하므로 두 결과는 다릅니다. missingFinalNewlineStillDifferent가 true인 이유입니다. 어떤 도구는 두 문서를 같은 줄 목록으로 보여 줄 수 있지만, 원시 문서와 명령줄 도구의 출력·후속 이어쓰기에서는 마지막 줄 끝의 유무가 영향을 줄 수 있습니다.

마지막 개행 차이를 무시하고 싶은 업무라면 별도 규칙을 추가할 수 있습니다. 다만 이 글의 결과를 그대로 그 정책이라고 읽으면 안 됩니다. 코드에서 어느 부분이 동일성을 결정하는지 드러내고, 그 규칙을 적용하기 전후의 결과를 나눠야 합니다. “줄바꿈 무시”라는 모호한 옵션 이름만으로 마지막 개행, 빈 줄 여러 개, 파일 중간의 행 경계가 모두 어떻게 처리되는지 알 수 없습니다. 작은 입력으로 각 차이를 하나씩 바꿔 기대 결과를 먼저 적어 보세요.

행 끝 공백을 제거하면 실제 내용을 잃을 수 있습니다.

세 번째 입력에는 alpha 뒤에 공백 하나를 추가하고 줄 끝은 LF로 유지했습니다. CRLF 정규화 후에도 원문과 다르며 trailingSpaceStillDifferent가 true입니다. 반면 split()으로 공백을 전부 나누어 목록만 비교하면 두 입력은 같아집니다. allWhitespaceStripWronglyEqual은 바로 그 반례를 보여 줍니다. 비교 편의를 위해 지운 차이가 실제로 무시해도 되는 차이인지 결정하지 않았기 때문입니다.

들여쓰기가 문법인 코드, 정해진 폭의 텍스트, 마크다운의 줄바꿈 표현처럼 공백이 기능을 갖는 자료가 있습니다. 일반 문장에서도 단어 사이 공백을 전부 제거하면 다른 단어 경계를 같은 것으로 만들 수 있습니다. 모든 형식에 적용할 만한 “공백을 무시하면 의미가 같다”는 결론은 피합니다. 허용할 차이를 CRLF 한 쌍처럼 좁게 정의하고, 다른 차이는 검토 대상으로 남기는 방식이 문제를 추적하기 쉽습니다. 자동 수정까지 연결하기 전에 형식별 파서와 렌더링의 확인이 필요합니다.

원본 대비 변형CRLF→LF 후판정 범위
줄 끝만 CRLF같음허용 차이
마지막 개행 없음다름보존
행 끝 공백 추가다름보존
단독 CR 또는 BOM제거 안 함별도 정책 필요

단독 CR과 UTF-8 BOM은 그대로 남겼습니다.

a, CR, b만 있는 별도 바이트 열은 normalize 뒤에도 그대로입니다. 이 예제는 CR 하나를 모두 LF로 바꾸지 않기 때문입니다. 오래된 줄 끝 형식이나 제어문자 데이터에 대해 어떤 규칙을 쓸지는 추가 선택입니다. 처리하지 않은 것을 조용히 성공으로 표시하기보다 bareCRPreserved처럼 무엇이 남았는지 결과에 드러냅니다. 실제 코드가 한 가지 줄바꿈만 처리한다면 본문에서도 범위를 그만큼으로 한정해야 합니다.

UTF-8 BOM 바이트 EF BB BF가 앞에 있는 자료도 검사했습니다. CRLF는 LF로 바뀌지만 BOM은 유지됩니다. 인코딩 서명을 없애는 것은 줄 끝 변환과 다른 정책입니다. 디코딩 과정에서 BOM을 자동 제거하는 API를 사용했다면 원시 파일 비교와 텍스트 비교가 또 달라질 수 있습니다. 이 코드는 바이트 치환만 하므로 잘못된 UTF-8을 검증하지도 않습니다. “한글이 깨지지 않는다”나 “모든 텍스트 파일을 지원한다”는 주장을 붙이지 않는 이유입니다.

Python 텍스트 모드의 자동 변환을 고려합니다.

Python의 텍스트 입출력은 newline 설정에 따라 여러 줄 끝을 읽을 때 변환할 수 있습니다. 텍스트 모드로 읽은 뒤 문자열만 비교하면 원본 파일이 달랐는데 이미 같은 줄바꿈으로 바뀌었을 수 있습니다. 원시 증거가 필요할 때는 read_bytes 같은 바이트 API를 사용하고, 사람이 읽는 문서 비교 단계에서 디코딩·줄 끝 정책을 따로 적용합니다. 두 단계의 결과를 같은 필드에 덮어쓰지 않습니다.

저장 때도 운영체제 기본값과 도구 설정에 의해 줄 끝이 바뀔 수 있습니다. 따라서 다른 컴퓨터에서 다시 저장한 뒤 파일 전체가 수정으로 보이면 내용 편집 전에 인코딩·줄 끝부터 확인하는 순서가 유용합니다. 여기서는 실제 플랫폼별 저장 시험을 수행하지 않았습니다. Python io와 pathlib 문서에서 동작을 확인하고, 고정한 바이트 배열로 비교 규칙만 실행했습니다. 파일 시스템의 수정 시각이나 내용 해시가 달라졌다는 사실만으로 어느 도구가 잘못 저장했다고 단정할 수는 없습니다.

Git의 줄 끝 규칙은 프로젝트 계약으로 관리합니다.

Git의 gitattributes는 경로별 텍스트 처리와 eol 규칙을 지정할 수 있습니다. 저장소에 들어갈 때의 정규화와 작업 폴더에 나올 때의 줄 끝 처리가 다를 수 있으므로 저장소의 바이트와 작업 파일의 바이트를 비교하는 목적을 먼저 정합니다. 이 글은 Git 설정을 실제로 바꾸거나 저장소를 재정규화하지 않았습니다. 공식 문서의 동작을 참고해 변경이 많은 비교 화면을 어떻게 조사할지 설명하는 범위입니다.

프로젝트 전체를 한꺼번에 고칠 때는 줄 끝 변경과 기능 변경을 분리해 검토하는 편이 좋습니다. 정상적으로 줄 끝만 바뀌었는지 확인한 후 테스트와 렌더링을 이어갑니다. 이 정책은 코드 서명이나 바이너리 형식에 무차별 적용할 규칙이 아닙니다. 변환 대상 확장자와 예외 파일을 명시하고, 원시 바이트를 보존해야 하는 자산은 텍스트 정규화 경로에서 제외합니다. 도구가 text라고 추정했다는 이유만으로 업무 규칙까지 승인됐다고 읽지 않도록 주의하세요.

검수 결과는 어떤 차이를 무시했는지까지 남깁니다.

이번 실행은 원시 해시 불일치, CRLF 정규화 후 일치, 마지막 개행 차이 유지, 행 끝 공백 차이 유지, 단독 CR과 BOM 보존을 각각 반환했습니다. 따라서 “내용이 같다”라는 통과 하나보다 더 구체적인 판정이 가능합니다. 독자가 적용할 자료에도 같은 방식으로 입력 두 개, 허용한 변환과 남은 차이를 기록하면 비교 도구의 설정을 다시 확인하기 쉽습니다.

실무 문장은 “원시 바이트는 다르지만 CRLF를 LF로 바꾸는 규칙 아래서는 일치하며 공백·BOM·마지막 개행은 무시하지 않았다”처럼 씁니다. 그 다음 실제 문서 형식에서 의미가 유지되는지 확인합니다. 마크다운 표나 CSV처럼 행 경계가 문법에 영향을 주는 형식은 파서 결과도 따로 대조하세요. 파일 자체의 동일성을 증명해야 하는 배포·백업이라면 원시 해시를 유지해야 합니다. 정규화가 해결하는 문제와 원본 보존이 해결하는 문제를 나눠 쓰는 것이 이 검사의 목적입니다.

입력과 판정을 다시 확인하려면

이 글의 가상 입력과 코드는 2026년 10월 15일 로컬 Python 3에서 실행했습니다. 글의 발행일과 실행일은 다릅니다. 별도 설치 패키지 없이 Python 표준 라이브러리로 실행할 수 있습니다.

터미널에서 python3 lf-crlf-content-diff-20261015.py를 실행하면 JSON을 출력합니다. 사용자 자료나 비밀번호는 필요하지 않습니다. 입력과 실행 환경은 결과 JSON에 함께 남겼습니다.

계산 기준을 확인한 원문과 다음 작업

  • Git — gitattributes — text와 eol의 저장소·작업 폴더 정규화 규칙을 확인했습니다.
  • Python — io — 텍스트 스트림의 newline 변환 범위를 확인했습니다.
  • Python — pathlib — 바이트 읽기와 텍스트 읽기 API의 차이를 확인했습니다.

공식 원문은 2026-10-15에 확인했습니다. 문서가 설명하는 API·정의와 이 글의 가상 계산 결과는 구분해서 읽어 주세요.

함께 보면 좋은 글: 마크다운의 줄바꿈과 세로줄, CSV 왕복과 원시 데이터.