JSON Toolkit

JSON diff 도구 선택: 텍스트 diff, 구조 diff, 시각화

핵심 요약: 텍스트 diff는 파일이 어디서 다른지 찾고, 구조 diff는 내용이 동등한지 판단합니다. 올바른 계층을 고르면 키 순서를 사고로 오해하지 않습니다.

중급약 9분 소요2026-10-31 발행2026-10-31 업데이트
JSON diff 도구 선택 표지

핵심부터 말하면, 텍스트 diff는 변경을 발견하고(먼저 jq -S로 키를 정렬), 구조 diff는 동등성을 판단하며(업무 키로 배열 정렬), 시각화는 설명하거나 리뷰할 뿐입니다. CI에서는 세 계층을 분리하세요.

두 JSON 문서가 달라 보인다고 내용이 다른 것은 아닙니다. 도구를 고르기 전에 올바른 비교 계층을 고르세요.

1. 세 가지 "diff"는 같은 것이 아님

많은 팀이 "JSON 비교"를 단일 행동으로 취급한 뒤 시나리오마다 같은 문제에 부딪힙니다. 세 계층을 분리하면 도움이 됩니다:

계층 비교 대상 드러내는 것 대표 용도
텍스트 diff 원본 바이트와 줄 들여쓰기, 키 순서, 공백, 줄 바꿈 설정 드리프트, 코드 리뷰
구조 diff 파싱된 객체와 배열 키 추가/제거, 값 변경, 배열 요소 변경 API 회귀, 데이터 이전
시각 diff 차이를 제시하는 방식 변경이 어느 계층에 속하는지, 영향 범위 사람 리뷰, 팀 논의

텍스트 diff는 의존성이 없고 빠르며 Git과 자연스럽게 통합됩니다. 약점은 JSON 의미를 전혀 모른다는 것입니다. 완전히 동등한 두 문서도 키 순서나 들여쓰기가 다르면 빨강과 초록으로 켜집니다. 구조 diff는 반대입니다. 먼저 파싱한 뒤 비교해 값이 단지 이동했음을 알아봅니다. 하지만 올바른 배열 정렬 전략이 필요합니다. 아니면 단일 삽입이 전면 재작성으로 오해됩니다.

시각화는 세 번째 알고리즘이 아닙니다. 다른 두 계층의 출력을 렌더해 사람이 차이가 예상된 것인지 빠르게 확인하게 합니다. 인간 판단에는 좋고 자동 검사 대체로는 나쁩니다. 계층을 섞으면 대개 영구히 빨간 CI 파이프라인과 그것을 무시하도록 학습한 팀을 만듭니다.

2. 텍스트 diff: 먼저 jq -S로 키 순서 정리

두 파일을 빨리 보는 가장 저렴한 조합은 jq -S와 diff -u입니다. -S 플래그는 객체 키를 재귀적으로 정렬해 내용이 같지만 키 순서가 다른 문서를 정규화합니다:

jq -S . a.json > a.sorted.json
jq -S . b.json > b.sorted.json
diff -u a.sorted.json b.sorted.json

이것이 가장 흔한 거짓 양성을 제거합니다. RFC 8259는 객체를 순서 없는 컬렉션으로 정의하므로 {"a":1,"b":2}와 {"b":2,"a":1}은 같은 값입니다. 그러나 텍스트 diff는 모든 줄을 변경으로 표시합니다. 먼저 정렬하면 빨간 블록이 진짜 문제에 훨씬 가까워집니다.

jq -S는 키만 정렬하고 배열은 정렬하지 않습니다. JSON에서 배열 순서는 의미가 있고 [1,2]는 [2,1]과 같은 값이 아니므로 도구가 그 결정을 대신하지 않습니다. 배열이 개념상 순서 없는 집합이라면 비교 전에 업무 키로 직접 정렬하세요.

기억할 디테일 하나 더: diff는 차이를 찾으면 종료 코드 1로 끝나지만, 파일 누락이나 권한 오류도 0이 아닌 종료를 냅니다. CI에서 모든 0 아닌 코드를 "내용 변경"으로 취급하지 마세요. 아니면 잘못 입력한 경로가 데이터 변경으로 보고됩니다. 더 안전한 패턴은 파일을 먼저 확인하고 차이를 따로 처리합니다:

if ! diff -u a.sorted.json b.sorted.json; then
  echo "JSON content differs" >&2
  exit 1
fi

delta로 텍스트 diff를 읽기 좋게

원시 diff -u 출력도 동작하지만 긴 줄과 깊이 중첩된 JSON은 읽기 피곤합니다. delta는 구문 하이라이팅, 줄 번호, 더 선명한 배경색을 더하는 diff 필터입니다:

diff -u a.sorted.json b.sorted.json | delta

Git pager로 시작했기 때문에 git diff를 넘겨받을 수도 있습니다. 하루에 설정 diff 열 개를 읽는 사람에게 이런 도구는 알고리즘 정확도보다 읽기 속도를 높입니다. 또 다른 흔한 선택은 git diff --no-index --word-diff로, 단어 단위로 비교해 아주 긴 줄에 압축 저장된 JSON에 잘 맞습니다.

경고 하나: 미화는 거짓 차이를 제거하지 않습니다. 키 순서가 정규화되지 않았다면 delta는 소음을 더 예쁘게 만들 뿐입니다. 올바른 순서는 항상 먼저 정규화, 그다음 장식입니다.

3. 구조 diff: jsondiffpatch는 의미를 이해

두 데이터가 의미상 동등한지가 질문이라면 구조 diff를 쓰세요. JavaScript 생태계에서 가장 흔한 선택은 jsondiffpatch로, +와 - 마커가 붙은 텍스트 줄이 아니라 변경을 기술하는 델타 객체를 반환합니다:

import { create } from 'jsondiffpatch';

const instance = create();
const delta = instance.diff(
  { id: 1, tags: ['a', 'b'], enabled: true },
  { id: 1, tags: ['a', 'c'], enabled: false },
);

console.log(JSON.stringify(delta, null, 2));

델타는 키 추가, 키 제거, 값 교체, 배열 요소 이동을 구분합니다. 그래서 단언에서 델타를 바로 순회할 수 있습니다. 차이가 없으면 undefined, 있으면 경로별 설명입니다. 텍스트 diff와 달리 들여쓰기, 키 순서, 1e3 대 1000 같은 숫자 표기에 흔들리지 않습니다.

중요한 것은 구조 diff 출력이 패치 워크플로에 먹일 수 있다는 점입니다. jsondiffpatch는 patch와 reverse를 제공해 델타를 원본 객체에 적용하고 나중에 되돌릴 수 있습니다. 주의: 자동 생성 델타는 손으로 쓴 JSON Patch와 같은 형식이 아니고 변환에는 수고가 듭니다. 더 넓은 비교 접근은 JSON diff와 비교를 참고하세요.

4. 키 순서로 인한 거짓 차이를 없애는 방법

키 순서는 성가신 정도가 아니라 틀린 결론을 낼 수 있습니다. 조사할 때 세 단계를 따르세요:

  1. 비교 전에 jq -S나 동등 도구로 키 순서를 재귀 정규화합니다.
  2. timestamp, requestId, traceId처럼 실행마다 바뀌는 필드를 제거합니다.
  3. 중복 키가 없는지 확인합니다. 중복 키는 파싱 중 "마지막 승리"로 해결되어 텍스트 diff는 보고 구조 diff는 보지 못합니다.
jq -S 'del(.meta.timestamp, .meta.requestId)' a.json > /tmp/a.json
jq -S 'del(.meta.timestamp, .meta.requestId)' b.json > /tmp/b.json
diff -u /tmp/a.json /tmp/b.json

이 흐름은 설정 드리프트에 잘 맞습니다. 스테이징과 운영을 같은 정규화에 통과시키면 남는 것이 실제로 설명이 필요한 차이입니다. 필터 규칙은 기억에서 다시 타이핑하지 말고 버전 관리된 스크립트에 두세요. 도구 체인의 편집과 스크립트 쪽은 에디터, 명령행, 자동화를 참고하세요.

5. 배열: 인덱스로 정렬할지 id로 정렬할지

배열은 JSON 비교에서 가장 실수가 잦은 부분입니다. 기본 인덱스 정렬에서는 맨 앞에 레코드 하나를 삽입하면 이후 모든 요소가 밀려 도구가 모두 변경으로 표시합니다. 도구가 고장 난 것이 아니라 정렬 전략이 틀린 것입니다.

정렬 동작하는 경우 장점 위험
인덱스 배열 순서가 업무 의미를 지님 간단, 추가 필드 불필요 삽입 하나로 연쇄 거짓 양성
기본 키 요소가 유일 id를 가진 집합 삽입, 삭제, 이동 감지 안정적이고 유일한 키 필요
최장 공통 부분 수열 순서가 중요하지만 삽입 허용 순수 인덱싱보다 견고 설명이 어렵고 비용 큼

jsondiffpatch는 objectHash로 기본 키를 선언해 비교 전에 배열 요소를 인덱싱합니다:

const instance = create({
  objectHash: (item: { id?: string; sku?: string }) => item.id ?? item.sku,
});

순수 jq에서는 먼저 정렬한 뒤 텍스트 diff를 쓸 수 있습니다:

jq -S 'sort_by(.id)' items.json > items.sorted.json

id 정렬의 절충은 위치 변경이 더 이상 차이로 세지지 않는다는 것입니다. 업무가 레코드가 2번에서 5번 위치로 이동했음을 알아야 한다면 명시적 순서 필드를 함께 비교하세요. 집합 내용만 의존하지 마세요.

6. 시각 diff: 사람을 위해 만든 계층

구조 diff 출력은 회의실이 아니라 기계를 위해 설계됩니다. 다른 사람에게 변경을 설명해야 할 때 시각화가 더 효과적입니다:

  • VS Code diff 에디터는 파일 단위 리뷰에 좋고, 나란한 창과 인라인 하이라이트를 제공합니다.
  • JSON Crack, JSON Hero 같은 트리 뷰어는 변경이 어느 계층에 속하는지 보여 줍니다.
  • jsondiffpatch는 델타를 접이식 리치 뷰로 렌더하는 HTML 포매터를 제공합니다.

시각화의 가치는 맥락입니다. 텍스트 diff는 88줄이 바뀌었다고 말하고, 트리 뷰는 88줄이 data.items[3].price에 속한다고 말합니다. 그러나 시각화는 대개 전체 문서를 브라우저나 데스크톱 앱에 올립니다. 운영 페이로드를 열기 전에 도구가 로컬에서 계산하는지 서버로 업로드하는지 확인하세요.

7. diff를 CI에 넣는 세 가지 실천

첫째, 비교 전에 정규화. 들여쓰기를 고정하고 키를 재귀 정렬하며 변동 필드를 필터한 뒤 diff를 실행하세요. 아니면 CI가 소음으로 반복 실패합니다.

둘째, 의미에 따라 정렬을 고르기. 배열이 집합이면 기본 키로 정렬하고, 시퀀스면 인덱스 정렬을 유지하세요. 모든 API에 한 규칙을 적용하지 마세요.

셋째, 텍스트가 아니라 값을 단언. 테스트에서 파싱된 객체를 비교하는 것이 JSON.stringify 출력을 비교하는 것보다 훨씬 안정적입니다. 검사가 실패하면 전체 페이로드를 로그에 쏟지 말고 차이 경로와 전후 값을 출력하세요.

# Print only the differing paths and values, not the entire document
jq -S 'del(.timestamp)' actual.json > /tmp/actual.json
jq -S 'del(.timestamp)' expected.json > /tmp/expected.json
diff -u /tmp/expected.json /tmp/actual.json | head -n 80

차이를 장기 추적해야 하면 정규화 스크립트와 기대 파일을 함께 커밋하세요. 일회성 조사라면 임시 스크립트를 나중에 삭제해 조용히 업무 로직의 두 번째 구현이 되지 않게 하세요.

8. 도구 선택 표

시나리오 첫 선택 대안 핵심 알림
가끔 파일 비교 VS Code diff diff -u 먼저 형식화해 소음 제거
설정 드리프트 jq -S + diff delta 변동 필드 필터
API 응답 회귀 jsondiffpatch deepdiff 배열을 기본 키로 정렬
팀에 변경 설명 JSON Crack / JSON Hero HTML 포매터 데이터 업로드 여부 확인
아주 큰 파일 jq 하위 트리 추출 jless 탐색 전체 트리를 메모리에 올리지 말 것
pre-commit 검사 git diff --no-index pre-commit 종료 코드를 명시적으로 처리

단일 최고의 JSON diff 도구는 없고 현재 계층에 맞는 도구만 있습니다. 텍스트 diff는 변경을 발견하고, 구조 diff는 동등성을 판단하며, 시각화는 변경을 설명합니다. 세 계층을 분리하는 것이 모든 것을 하는 도구 하나를 찾는 것보다 훨씬 신뢰할 만합니다. 브랜치 간 충돌 처리는 JSON과 Git 병합에서 이어가세요.

FAQ

jq -S가 배열 순서도 바꾸나?

아니요. -S는 객체 키만 정렬하고 배열은 원래 순서를 유지합니다. JSON에서 배열은 순서가 있으므로 도구가 [1,2]를 [2,1]로 바꾸지 않습니다. 배열이 개념상 순서 없는 집합이라면 sort_by로 명시적으로 정렬하세요.

구조 diff에서 1과 1.0은 같은 값인가?

대부분 파서가 둘을 같은 숫자로 만들어 구조 diff는 대개 동등하다고 보고, 텍스트 diff는 다르다고 표시합니다. 동등성 판단에는 구조 diff, 파일이 바이트 그대로 같은지 판단에는 텍스트 diff를 쓰세요.

diff 출력을 바로 JSON Patch로 쓸 수 있나?

직접은 안 됩니다. diff 도구는 무엇이 다른지 보고하고, JSON Patch는 순서 있는 연산 목록을 기술하며 형식과 의미가 다릅니다. 자동 생성 패치도 배열 인덱스를 참조하는 경향이 있어 동시성에서 낡습니다. 적용 전에 test 연산을 더하세요.

수십 MB JSON 파일은 어떻게 비교하나?

먼저 관심 하위 트리를 추출하세요(예: jq '.data.items'). 그런 다음 기본 키로 정렬하고 비교하세요. 재귀 diff를 위해 전체 트리를 메모리에 올리지 마세요. 아주 큰 파일은 스트리밍이나 청크 비교가 낫고, 메모리 비용은 따로 평가해야 합니다.

온라인 diff 도구가 데이터를 유출할 수 있나?

구현에 달려 있습니다. 순수 프런트엔드 도구는 브라우저에서 계산하고 데이터를 업로드하지 않으며, 서버 렌더 도구는 내용을 백엔드로 보내 로그에 들어갈 수 있습니다. 운영 설정이나 고객 페이로드를 비교하기 전에 페이지가 로컬에서 계산하는지 확인하고, 필요하면 네트워크를 끊고 한 번 시험하세요.

텍스트 diff와 구조 diff 중 어떻게 결정하나?

두 파일이 어디서 다른지 빨리 보려면 jq -S로 키를 정렬하고 diff -u를 실행하세요. 두 문서가 의미상 동등한지 판단하려면 jsondiffpatch 같은 구조 diff를 쓰고 배열을 기본 키로 정렬하세요. 텍스트 diff는 의존성이 없고 Git과 통합되며, 구조 diff는 들여쓰기, 키 순서, 숫자 표기에 흔들리지 않습니다.

CI가 모든 0 아닌 diff 종료 코드를 차이로 취급하면 왜 안 되나?

diff는 차이를 찾으면 1로 끝나지만 파일 누락이나 권한 오류도 0 아닌 값을 냅니다. 모든 0 아닌 코드를 "내용 변경"으로 취급하면 잘못 입력한 경로가 데이터 변경으로 보고됩니다. 더 안전한 패턴은 파일을 먼저 확인하고 차이를 따로 처리합니다.

수십 MB 파일을 브라우저에서 diff해야 하나?

아니요. 먼저 jq로 관심 하위 트리를 추출한 뒤 기본 키로 정렬해 비교하세요. 재귀 diff를 위해 전체 트리를 메모리에 올리는 것은 메모리와 시간 비용이 훨씬 큽니다. 아주 큰 파일은 스트리밍이나 청크 비교가 낫습니다.