본문으로 건너뛰기
Toova
모든 도구

시간을 절약해주는 7가지 JSON 트릭

Toova

모든 JavaScript 개발자는 일주일에 수십 번 JSON.parseJSON.stringify를 사용합니다. 하지만 대부분은 기본에서 멈춥니다 — API 응답을 파싱하고 객체를 문자열로 직렬화하는 것. API는 더 많은 것을 제공하며, 덜 사용되는 일부 기능은 그렇지 않으면 서드파티 라이브러리나 몇 시간의 디버깅이 필요한 문제를 해결합니다.

이 가이드는 기본값을 넘어선 일곱 가지 트릭을 다룹니다. 각각은 실제 코드베이스에 즉시 적용할 수 있습니다. 추상화 없이, 발명된 예제 없이 — 이것들은 프로덕션 시스템에서 나타나는 패턴입니다.

이 문서의 모든 코드 예제는 브라우저에서 완전히 JSON을 검증하고 보기 좋게 출력하는 Toova JSON Formatter를 사용하여 검증하고 탐색할 수 있습니다.

1. JSON 라운드트립을 통한 딥 클론(그리고 그 한계)

JavaScript 플레이북에서 가장 오래된 트릭: 평범한 객체의 딥 클론을 생성하려면 JSON.parse(JSON.stringify(obj))를 사용하세요.

const original = { a: 1, b: { c: 2 } };

// 단순한 접근 — 깊이 중첩된 객체는 여전히 공유됨
const shallowCopy = { ...original }; // b는 여전히 같은 객체를 가리킴

// JSON 딥 클론 — 완전히 독립적인 복사본 생성
const deepClone = JSON.parse(JSON.stringify(original));

deepClone.b.c = 99;
console.log(original.b.c); // 2 — 원본은 변경되지 않음

이는 문자열로 직렬화하고 다시 파싱하면 공유 참조가 없는 완전히 새로운 객체 트리가 생성되기 때문에 작동합니다. 빠르고, 종속성이 필요하지 않으며, ES5 이후로 사용 가능했습니다.

메모리 내 사용을 위한 현대적 대안은 structuredClone()입니다:

// 현대적 대안: structuredClone() — 더 많은 타입 처리
// Node.js 17+와 모든 에버그린 브라우저에서 지원
const clone = structuredClone(original);

의존하기 전에 JSON 접근 방식의 한계를 알아두세요:

// JSON.parse/stringify는 다음을 처리할 수 없음:
const broken = {
  date: new Date(),      // 문자열이 됨 — Date 프로토타입 잃음
  fn: () => 'hello',    // 조용히 제거됨
  undef: undefined,     // 조용히 제거됨
  inf: Infinity,        // null이 됨
  map: new Map(),       // {}가 됨
  cycle: null,          // 순환 참조는 throw
};

객체가 평범한 데이터 — 문자열, 숫자, 불리언, 배열 및 중첩된 평범한 객체 — 만 포함하는 경우 JSON 라운드트립은 안전하고 빠릅니다. 그 외의 경우 structuredClone() 또는 전용 라이브러리를 선호하세요.

2. 필터링과 마스킹을 위한 커스텀 Replacer

대부분의 개발자는 JSON.stringify가 두 번째 인수를 받는다는 것을 알지만 거의 사용하지 않습니다. 그 두 번째 인수는 replacer입니다: 포함할 키의 배열 또는 각 값이 직렬화되는 방법을 정확하게 제어하는 함수입니다.

배열 replacer — 특정 키를 화이트리스트:

const user = {
  id: 'u_001',
  name: 'Alice',
  password: 'hunter2',        // 로그에 나타나면 안 됨
  creditCard: '4111111111111111', // 로그에 나타나면 안 됨
  role: 'admin',
};

// 배열 replacer: 이 키만 포함
JSON.stringify(user, ['id', 'name', 'role']);
// '{"id":"u_001","name":"Alice","role":"admin"}'

함수 replacer — 값 변환 또는 편집:

// 함수 replacer: 키/값에 대한 완전한 제어
const masked = JSON.stringify(user, (key, value) => {
  if (key === 'password' || key === 'creditCard') return '[REDACTED]';
  if (key === '' ) return value; // 루트 객체 — 항상 반환
  return value;
});
// '{"id":"u_001","name":"Alice","password":"[REDACTED]","creditCard":"[REDACTED]","role":"admin"}'

더 정교한 버전은 키 이름이 아닌 모양에 따라 값을 마스킹합니다:

// 타입 기반 마스킹을 위한 replacer
const sanitize = (key, value) => {
  if (typeof value === 'string' && value.match(/^4[0-9]{15}$/)) {
    return '****-****-****-' + value.slice(-4);
  }
  return value;
};

이 기법은 로깅 미들웨어에 필수적입니다: 전체 객체 컨텍스트가 있는 구조화된 로그를 원하지만 특정 필드는 로그 집계기에 도달해서는 안 됩니다. replacer는 코드베이스 전반에 편집 로직을 분산시키는 대신 직렬화 경계에서 이를 처리할 수 있게 합니다.

3. 키를 결정적으로 정렬

JavaScript의 객체 키 순서는 (문자열 키의 경우) 삽입 순서입니다. 동일한 키를 가지지만 다른 순서로 생성된 두 객체는 다른 JSON 문자열을 생성하며, 이는 단순한 동등성 검사, 캐시 키 및 콘텐츠 해시를 깨뜨립니다.

function sortedStringify(obj) {
  return JSON.stringify(obj, Object.keys(obj).sort());
}

const a = { z: 1, a: 2, m: 3 };
const b = { a: 2, m: 3, z: 1 };

sortedStringify(a) === sortedStringify(b); // true — 키 순서 정규화됨

깊이 중첩된 객체의 경우 정렬을 재귀적으로 적용하세요:

// 중첩된 객체를 위한 재귀적 키 정렬
function sortKeys(value) {
  if (Array.isArray(value)) return value.map(sortKeys);
  if (value !== null && typeof value === 'object') {
    return Object.fromEntries(
      Object.keys(value).sort().map((k) => [k, sortKeys(value[k])])
    );
  }
  return value;
}

const sorted = JSON.stringify(sortKeys(deepNested));

정렬된 JSON은 다음과 같은 경우에 필수적입니다:

  • 요청 본문에서 캐시 키 생성
  • JSON 페이로드에 대한 체크섬이나 서명 계산
  • 필드 순서와 관계없이 테스트에서 API 응답 비교
  • 순서가 동등성에 영향을 주지 않아야 하는 설정 객체 저장

정렬하고 보기 좋게 출력한 후, 두 정규화된 JSON 문자열을 비교하고 버전 간에 어떤 값이 변경되었는지 정확히 보려면 Text Diff 도구를 사용하세요.

4. 정밀도를 잃지 않고 BigInt 처리

JavaScript의 Number 타입은 최대 253 − 1까지의 정수를 안전하게 표현할 수 있습니다. 분산 시스템에서 생성된 ID, 소수 단위의 금융 금액 또는 나노초 단위의 타임스탬프의 경우 이것은 충분하지 않습니다. BigInt는 임의 정밀도 정수를 다루지만 JSON.stringify는 그것들로 무엇을 해야 할지 모릅니다.

const data = {
  amount: 9007199254740993n, // Number.MAX_SAFE_INTEGER보다 큼
};

// 이는 throw: TypeError: Do not know how to serialize a BigInt
JSON.stringify(data); // ERROR

표준 우회 방법:

// 해결책 1: replacer로 문자열로 변환
JSON.stringify(data, (key, value) =>
  typeof value === 'bigint' ? value.toString() : value
);
// '{"amount":"9007199254740993"}'

// 해결책 2: BigInt 프로토타입의 toJSON() (monkey-patch — 주의해서 사용)
BigInt.prototype.toJSON = function () { return this.toString(); };
JSON.stringify(data); // '{"amount":"9007199254740993"}'

파싱 측에서는 reviver 함수가 문자열 표현에서 BigInt 값을 복원할 수 있습니다:

// 파싱 시 BigInt를 복원하는 reviver
const revived = JSON.parse('{"amount":"9007199254740993"}', (key, value) => {
  if (key === 'amount') return BigInt(value);
  return value;
});
console.log(typeof revived.amount); // 'bigint'

일관되게 적용되도록 BigInt-to-string 변환을 공유 직렬화 레이어에 보관하세요. BigInt가 코드베이스 가장자리의 임시 JSON.stringify 호출로 새어나가게 두면 추적하기 어려운 예측할 수 없는 오류로 이어집니다.

5. 순환 참조 감지

순환 참조는 객체가 자신이나 객체 그래프의 조상에 대한 참조를 포함할 때 발생합니다. 생각보다 흔합니다: 이벤트 이미터, DOM 노드, React fiber 노드, ORM 엔티티 모두 자주 역참조를 가집니다.

// 순환 참조 예제
const obj = { name: 'node' };
obj.self = obj; // obj가 자신을 참조

JSON.stringify(obj); // throws: TypeError: Converting circular structure to JSON

방문한 객체를 추적하는 커스텀 replacer로 처리하세요:

// 수동 순환 참조 핸들러
function safeStringify(obj) {
  const seen = new WeakSet();
  return JSON.stringify(obj, (key, value) => {
    if (typeof value === 'object' && value !== null) {
      if (seen.has(value)) return '[Circular]';
      seen.add(value);
    }
    return value;
  });
}

safeStringify(obj); // '{"name":"node","self":"[Circular]"}'

WeakSet은 여기서 올바른 데이터 구조입니다: 가비지 컬렉션을 방지하지 않고 객체 참조를 보유하며 조회는 O(1)입니다. 이 패턴은 직렬화 중에 throw하기보다 오류가 우아하게 저하되기를 원하는 로깅 미들웨어에서도 작동합니다.

일반적인 변형: 순환 참조를 [Circular]로 표시하는 대신 [Circular: $.config.parent] 같은 경로 문자열로 대체하여 디버깅 중 참조 위치를 명시적으로 만듭니다.

6. 단일 라인에 배열을 보기 좋게 출력

JSON.stringify의 세 번째 인수는 들여쓰기입니다. 2 또는 4를 전달하면 모든 것이 여러 줄로 확장되는데, 이는 객체에 좋지만 태그, ID 또는 좌표 같은 기본 타입의 배열에는 장황할 수 있습니다.

const mixed = {
  title: 'Report',
  tags: ['json', 'api', 'debug'],
  config: { indent: 2, sortKeys: true },
  count: 42,
};

// 기본값: 모든 것이 멀티라인
JSON.stringify(mixed, null, 2);
// {
//   "title": "Report",
//   "tags": [
//     "json",
//     "api",
//     "debug"
//   ],
//   "config": {
//     "indent": 2,
//     "sortKeys": true
//   },
//   "count": 42
// }

단순한 배열을 한 줄로 축소하기 위해 출력을 사후 처리할 수 있습니다:

// 단일 라인 배열을 위한 커스텀 replacer
function prettyMixed(obj) {
  const raw = JSON.stringify(obj, null, 2);
  // 기본 타입만 포함하는 배열을 한 줄로 압축
  return raw.replace(
    /\[\n\s+([\s\S]*?)\n\s+\]/g,
    (match, inner) => {
      const items = inner.split(',\n').map((s) => s.trim());
      if (items.every((s) => !/^[{\[]/.test(s))) {
        return '[' + items.join(', ') + ']';
      }
      return match;
    }
  );
}

결과: 객체는 가독성을 위해 멀티라인으로 유지되고, 기본 타입의 배열은 콤팩트함을 위해 한 줄로 축소됩니다. 이는 사람과 기계 모두 동일한 데이터를 읽어야 하는 많은 설정 파일과 구조화된 로그 출력에서 사용되는 형식입니다. JSON FormatterJSON to YAML 변환기는 이 축소를 자동으로 처리합니다.

7. 큰 JSON 스트리밍

파싱 전에 큰 JSON 파일을 메모리에 완전히 로드하는 것은 JSON 처리 파이프라인에서 가장 흔한 단일 성능 실수입니다. 100 MB JSON 응답은 원시 문자열에 대해 최소 100 MB의 힙을 할당한 다음 파싱된 객체에 대해 두 번째 할당을 합니다. 몇 메가바이트 이상의 파일의 경우 스트리밍 파서가 데이터를 점진적으로 처리합니다.

스트리밍 라이브러리가 있는 Node.js에서:

// node --experimental-vm-modules (또는 스트리밍 JSON 라이브러리 사용)
// JSON Source Map 제안과 함께하는 네이티브 스트리밍 (Stage 3, 2026):

// 지금은 @streamparser/json이나 jsonstream2 같은 라이브러리 사용
import { createReadStream } from 'fs';
import parser from '@streamparser/json';

const jsonParser = new parser.JSONParser();
jsonParser.onValue = ({ value, key, parent }) => {
  if (key === 'id') {
    console.log('Found id:', value);
  }
};

createReadStream('large.json').pipe(jsonParser);

Fetch Streams API를 사용한 브라우저에서:

// Fetch + JSON 스트림 디코더를 사용한 브라우저 측 스트리밍:
const response = await fetch('/api/large-data');
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';

while (true) {
  const { done, value } = await reader.read();
  if (done) break;
  buffer += decoder.decode(value, { stream: true });
  // 버퍼에서 완전한 JSON 객체를 처리...
}

스트리밍은 배치 처리 파이프라인, 큰 내보내기 엔드포인트 및 로그 분석 스크립트에서 가장 유용합니다. 1 MB 미만의 일반적인 API 응답의 경우 표준 JSON.parse가 충분히 빠르고 훨씬 더 간단합니다. 스트리밍이 실제로 가치를 발휘하기 시작하는 임계값은 파싱된 구조의 복잡성과 대상 환경의 힙 크기에 따라 약 5–10 MB입니다.

스트리밍 파서를 구축하기 전에 큰 JSON 응답의 구조를 탐색하려면 샘플을 JSON Formatter에 붙여넣어 필요한 데이터가 포함된 경로를 확인한 다음 추가 분석을 위해 CSV 또는 YAML로 변환하세요.

모두 종합하기

이 일곱 가지 기법은 JSON API의 가장 영향력 있는 부분을 다룹니다:

  • 딥 클론 라운드트립을 통해 — 평범한 데이터에 빠름, 한계 알기
  • Replacer — 직렬화 경계에서 키 필터링 및 민감한 값 마스킹
  • 키 정렬 — 캐시 키, 서명 및 테스트 단언을 위한 결정적 출력
  • BigInt 처리 — 문자열로 stringify, 파싱 시 복원
  • 순환 참조 감지 — 안전한 직렬화를 위한 WeakSet 기반 replacer
  • 콤팩트 배열과 함께 보기 좋게 출력 — 과도한 공백 없이 사람이 읽을 수 있는 출력
  • 큰 JSON 스트리밍 — 큰 파일 및 API 응답을 위한 점진적 파싱

JSON.stringify의 replacer와 space 인수의 전체 문서는 MDN JSON.stringify 참조를 참조하세요. 현대적 딥 클론 대안에 대해서는 지원되는 모든 타입과 엣지 케이스를 다루는 structuredClone() 문서를 참조하세요.