JSON 포매터 & 검증기

JSON.parse 성능: V8 엔진은 큰 페이로드를 어떻게 처리하는가

현대 V8(Node.js / Chrome / Deno의 엔진)은 JSON.parse에 일반적인 재귀 하강 파서를 쓰지 않습니다. 2019년 이후 V8은 C++로 작성된 전용 fast-path 파서를 탑재하고 있으며, UTF-16 문자열을 직접 다루고, young generation에 객체를 할당하며, transition tree(hidden class)로 배열 내 형제 객체들이 형태(shape)를 공유하도록 만듭니다. [{"id":1,"name":"a"},{"id":2,"name":"b"}, ...] 같은 페이로드라면 첫 번째 이후의 모든 객체는 동일한 hidden class로 생성되며, 이후 속성 접근은 monomorphic하게 인라이닝됩니다. JSON.parse 비용은 (1) 문자열 토큰화/언이스케이프, (2) 결과 객체 그래프로 인한 GC 압력, (3) reviver 콜백 비용 — 이 세 가지가 지배합니다. reviver 함수를 추가하는 것은 가장 큰 성능 함정입니다. fast path를 강제로 포기시키고, 트리의 모든 키마다 JS 함수가 호출됩니다. 10MB 페이로드 기준 보통 4~8배 느려집니다. 필드 몇 개만 변환하면 된다면 먼저 parse한 뒤 결과를 직접 순회하는 게 낫습니다. 동질적인 객체로 이루어진 매우 큰 배열의 경우 V8이 종종 스트리밍 JSON 라이브러리보다 빠릅니다 — fast path가 그만큼 잘 튜닝되어 있기 때문입니다. 하지만 단일 문서가 100MB를 넘기면 JSON.parse는 원본 문자열과 결과 객체 트리를 동시에 메모리에 들고 있으므로 peak RSS가 파일 크기의 3~4배까지 치솟습니다. 이때는 simdjson, oboe.js, stream-json 같은 스트리밍 파서로 전환해야 합니다. V8은 호출 간에 파서의 토큰화 버퍼를 캐싱하기 때문에, 많은 작은 페이로드를 타이트한 루프로 파싱하는 게 동일 총량을 한 번에 파싱하는 것보다 빠릅니다. 와이어 포맷을 통제할 수 있다면 NDJSON(라인당 JSON 1개)을 쓰세요. 점진적으로 파싱되고, 라인 단위로 에러에서 복구되며, 단순한 라인 버퍼 IO와도 잘 어울립니다.
// SLOW: reviver kills the fast path
const data = JSON.parse(raw, (key, val) => {
  if (key === 'createdAt') return new Date(val);
  return val;
});

// FAST: parse first, walk later
const data = JSON.parse(raw);
function walk(o) {
  for (const k in o) {
    if (k === 'createdAt' && typeof o[k] === 'string') o[k] = new Date(o[k]);
    else if (o[k] && typeof o[k] === 'object') walk(o[k]);
  }
}
walk(data);

// FASTER for huge files: NDJSON
import readline from 'node:readline';
const rl = readline.createInterface({ input: fs.createReadStream('big.ndjson') });
for await (const line of rl) {
  const row = JSON.parse(line); // 1 record at a time
  process(row);
}

실서비스에서 반드시 마주치는 JSON 함정들

JSON은 단순해 보이고, 바로 그래서 뒷발을 칩니다. RFC 8259는 문법을 매우 엄격히 정의합니다 — 숫자는 IEEE-754 double 한 종류, 리터럴은 true / false / null, 문자열은 큰따옴표 UTF-8, 날짜·주석·트레일링 콤마·Infinity·NaN 모두 없음. 실서비스의 "이상한 JSON 버그"는 이 6개 규칙 중 하나가 현실에 의해 깨진 것입니다. NaN과 Infinity는 valid JSON이 아닙니다. JavaScript의 JSON.stringify는 이 값들을 조용히 null로 바꿉니다 — { "p99": Infinity }가 { "p99": null }로 직렬화되고, 대시보드는 조용히 0을 표시합니다. 직렬화 전에 clamp / 특수 처리하세요. round-trip을 절대 신뢰하지 마세요. Date 객체는 비대칭하게 round-trip 됩니다. JSON.stringify(new Date())는 Date.prototype.toJSON을 통해 ISO-8601 문자열을 출력하지만, JSON.parse는 그 문자열이 날짜라는 걸 모릅니다 — 문자열이 돌아옵니다. 네트워크 왕복 후 data.createdAt.getTime()을 호출하면 throw 합니다. zod, valibot, ajv 같은 스키마로 감싸거나, 타임스탬프가 문자열로 돌아온다는 점을 명시적으로 문서화해야 합니다. BigInt는 하드 에러입니다 — JSON.stringify(1n)은 TypeError: Do not know how to serialize a BigInt를 던집니다. 64비트 ID를 BigInt로 반환하는 DB 드라이버(Postgres bigint, MySQL UNSIGNED BIGINT)에서 끊임없이 만나는 문제입니다. 실용적 해법은 (a) 앱 부팅 시 BigInt.prototype.toJSON = function() { return this.toString() }을 정의해 수신측이 문자열로 받는 걸 받아들이거나, (b) superjson 같이 타입을 보존하는 직렬화기를 쓰는 것입니다. 순환 참조도 TypeError입니다. DOM 트리든, parent 포인터를 가진 그래프든 JSON.stringify를 폭발시킵니다. replacer 파라미터로 알려진 순환 키를 건너뛸 수 있습니다(undefined 반환 시 drop). 디버깅용으로는 util.inspect나 structuredClone(내부적으로 cycle을 감지)이 더 친절합니다. 마지막으로 키 순서: 스펙은 객체가 unordered라고 하지만 V8의 JSON.stringify는 정수형 키를 먼저, 그다음 삽입 순서대로 키를 emit 합니다. 테스트가 정확한 JSON 문자열을 검증한다면 그건 구현 디테일을 테스트하는 것입니다 — 파싱된 객체 단위로 assert 하세요.
// NaN/Infinity silently become null
JSON.stringify({ p99: NaN, max: Infinity });
// => '{"p99":null,"max":null}'  (BUG: silent data loss)

// Date asymmetric round-trip
const o = { at: new Date() };
const back = JSON.parse(JSON.stringify(o));
back.at.getTime(); // TypeError: back.at.getTime is not a function

// BigInt fix at app boot
BigInt.prototype.toJSON = function () { return this.toString(); };

// Circular reference handler
function safeStringify(obj) {
  const seen = new WeakSet();
  return JSON.stringify(obj, (k, v) => {
    if (typeof v === 'object' && v !== null) {
      if (seen.has(v)) return '[Circular]';
      seen.add(v);
    }
    return v;
  });
}

JSON vs JSON5 vs JSONC — 올바른 방언 선택

JSON 자체는 frozen 표준입니다 — RFC 8259가 스펙이고, 사람을 배려하는 기능은 의도적으로 빠져있습니다. 이 엄격함은 와이어 포맷에는 좋지만 손으로 편집하는 설정 파일엔 끔찍하기 때문에 비공식 방언 두 개가 설정 영역을 양분하고 있습니다. JSONC("JSON with comments")는 VS Code, TypeScript의 tsconfig.json, 대부분의 Microsoft 도구가 쓰는 방언입니다. RFC 8259에 정확히 두 가지를 더합니다 — 라인 주석(//)과 블록 주석(/* */). 트레일링 콤마는 VS Code 파서에서 허용되지만 어떤 명문 스펙에도 들어있지 않습니다. JSONC는 의도적으로 미니멀합니다 — 모든 JSONC 문서는 주석을 제외하면 JSON 문서입니다. TypeScript 프로젝트에선 별도 라이브러리가 필요 없습니다 — 컴파일러가 자체 tolerant 파서를 들고 있고, 작은 정규식으로 주석을 제거(문자열 처리 주의)하면 valid JSON이 됩니다. JSON5는 더 야심찹니다 — 작은따옴표 문자열, 따옴표 없는 ES5 식별자 키, 멀티라인 문자열, 16진수, 앞뒤 소수점(.5, 5.), 숫자 앞 양의 부호, NaN과 Infinity, 트레일링 콤마를 모두 추가합니다. 대신 JSON5는 더 이상 JSON의 subset이 아닙니다 — JSON5 문서를 JSON.parse에 먹일 수 없습니다. 사람이 이기고 기계가 json5 npm 패키지를 쓸 수 있는 설정 파일에 쓰세요. 절대 와이어 포맷으로는 쓰지 마세요. 세 번째 후보 HJSON은 키와 짧은 문자열 값에서까지 따옴표를 뺍니다. JSON5보다 더 관대하지만 생태계가 좁습니다. 도구가 이미 표준화하지 않은 한 피하세요. 규칙: API와 IPC 메시지는 호환성을 위해 RFC 8259 strict JSON을 유지하세요(모든 언어에 안정된 파서가 있음). 사람이 읽고 편집하는 에디터/빌드 설정은 JSONC가 적합합니다. JSON5는 더 풍부한 기능이 필요하고 writer/reader를 모두 통제할 때만 꺼내세요. JSON5 문서가 JSON을 기대하는 시스템에 샐 경우 가장 나쁜 타이밍에 "Unexpected token" 오류로 터집니다.
// JSON (RFC 8259) — strict
{
  "name": "app",
  "ports": [3000, 3001]
}

// JSONC — comments + (de facto) trailing commas
{
  // tsconfig.json style
  "compilerOptions": {
    "target": "ES2022", /* matches Node 18+ */
    "strict": true,
  },
}

// JSON5 — humans first
{
  name: 'app',          // unquoted key, single-quoted string
  port: 0xABCD,         // hex
  rate: .5,             // leading decimal
  retries: +Infinity,   // signed Infinity
  // multi-line string
  banner: "line1\
line2",
}
최종 수정:

도구 소개

JSON 포매터는 원본 JSON을 파싱한 뒤 일관된 들여쓰기로 다시 출력하고, 입력이 잘못된 경우 명확한 오류 메시지를 보여줍니다. 미니파이된 API 응답을 빠르게 읽거나 누락된 콤마를 찾아내거나 깔끔한 페이로드를 버그 리포트에 첨부할 때 가장 빠른 방법입니다. JSON은 REST API와 설정 파일의 표준이므로 좋은 포매터는 매일 쓰는 도구입니다.

사용 방법

  1. 입력 패널에 JSON을 붙여넣습니다.
  2. 들여쓰기가 적용된 결과가 오른쪽에 표시됩니다.
  3. 파싱이 실패하면 첫 번째 문제의 라인과 컬럼을 가리키는 오류 메시지가 표시됩니다.
  4. Minify 버튼으로 프로덕션용 페이로드의 공백을 제거할 수 있습니다.
  5. Copy 버튼으로 결과를 클립보드에 복사해 다른 곳에 사용합니다.

주요 사용 사례

  • Stripe, GitHub, Slack 등의 미니파이된 웹훅 페이로드 읽기
  • curl 응답을 깔끔히 정리해 슬랙 스레드에 공유
  • 직접 수정한 package.json / tsconfig.json이 여전히 파싱되는지 검증
  • OpenAPI/Swagger 문서 구조 점검
  • 두 JSON 페이로드를 동일한 형식으로 정리한 뒤 비교
  • 대형 설정 파일을 환경 변수에 담기 전에 미니파이

자주 묻는 질문

Q. JSON에 주석을 넣을 수 있나요?

A. 표준 JSON은 주석을 허용하지 않습니다. 주석이 필요하면 JSONC(VS Code), JSON5, YAML 등을 사용하세요. 순수 JSON이라면 파싱 전에 주석을 제거해야 합니다.

Q. "Unexpected token" 오류는 왜 나나요?

A. 대부분 콤마 누락/중복, 키 따옴표 누락, 작은따옴표 사용 때문입니다. 키와 문자열 값은 반드시 큰따옴표를 사용해야 합니다.

Q. 뒤에 오는 콤마(trailing comma)는 허용되나요?

A. 표준 JSON에서는 허용되지 않습니다. JSON5나 JavaScript 객체 리터럴은 허용하지만 JSON.parse와 대부분 API는 거부합니다.

Q. 날짜는 어떻게 표현하나요?

A. JSON에는 날짜 타입이 없습니다. 사실상 표준은 ISO-8601 문자열(예: 2026-05-01T10:00:00Z)입니다.