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

JavaScript에서 JSON 포맷하는 방법 — 5가지 방법

Toova

JSON은 어디에나 있습니다: API 응답, 설정 파일, 데이터베이스 내보내기, 로그 파이프라인. 그러나 원시 JSON 출력은 종종 컴팩트한 블롭입니다 — 줄 바꿈 없음, 들여쓰기 없음, 직렬화기가 뱉어내는 순서대로의 키. 이는 디버깅을 고통스럽게 만들고 협업을 필요한 것보다 더 어렵게 만듭니다.

JavaScript는 내장 JSON.stringify부터 메모리에 로드하기에 너무 큰 파일을 위한 스트리밍 라이브러리까지 JSON을 포맷하는 여러 방법을 제공합니다. 이 가이드는 가장 일반적인 경우와 개발자가 방심하게 되는 엣지 케이스를 다루는 다섯 가지 실용적인 방법을 살펴봅니다.

방법 1: 들여쓰기가 있는 JSON.stringify

가장 간단한 접근 방식은 이미 언어에 내장되어 있습니다. JSON.stringify는 세 개의 인수를 받습니다: 직렬화할 값, replacer(방법 2에서 자세히), 그리고 들여쓰기를 제어하는 space 매개변수.

const data = {
  name: "Alice",
  role: "engineer",
  skills: ["JavaScript", "TypeScript", "Node.js"],
  active: true,
};

// 2칸 들여쓰기(일반적인 기본값)
console.log(JSON.stringify(data, null, 2));

// 4칸 들여쓰기
console.log(JSON.stringify(data, null, 4));

// 탭 들여쓰기
console.log(JSON.stringify(data, null, "\t"));

null, 2로 출력은 다음과 같습니다:

{
  "name": "Alice",
  "role": "engineer",
  "skills": [
    "JavaScript",
    "TypeScript",
    "Node.js"
  ],
  "active": true
}

2칸은 JavaScript 프로젝트에서 가장 일반적인 관례입니다 — ESLint, Prettier 및 대부분의 스타일 가이드의 기본값과 일치합니다. 4칸은 Python 인접 도구에서 일반적입니다. space가 생략되거나 0으로 설정되면 출력은 컴팩트한 단일 라인 문자열입니다 — 와이어 전송에 이상적입니다.

JSON Formatter 도구를 사용하여 브라우저에서 JSON을 즉시 포맷할 수도 있습니다 — 원시 JSON을 붙여넣고, 보기 좋게 출력된 결과를 얻고, 코드를 작성하지 않고 결과를 복사하세요.

방법 2: 사용자 정의 Replacer 함수

JSON.stringify의 두 번째 인수 — replacer — 는 직렬화 전에 값을 필터링하고 변환하는 함수나 배열입니다. 여기서 출력에 무엇이 들어갈지에 대한 실제 제어를 얻습니다.

배열 형식은 키 화이트리스트를 위한 가장 간단한 접근 방식입니다:

const user = {
  id: 42,
  name: "Bob",
  password: "s3cr3t",
  email: "bob@example.com",
  createdAt: new Date(),
};

// 배열 replacer: 나열된 키만 포함
const safeJson = JSON.stringify(user, ["id", "name", "email"], 2);
console.log(safeJson);
// {
//   "id": 42,
//   "name": "Bob",
//   "email": "bob@example.com"
// }

함수 형식은 값별 제어를 제공합니다. 포함하려면 값을 반환하고, 제거하려면 undefined를 반환하거나, 변환된 값을 반환하세요:

function replacer(key, value) {
  // 밑줄로 시작하는 키 제거(비공개 관례)
  if (key.startsWith("_")) return undefined;

  // 민감한 필드 마스킹
  if (key === "password" || key === "token") return "[REDACTED]";

  // Date 객체를 명시적으로 ISO 문자열로 변환
  if (value instanceof Date) return value.toISOString();

  return value;
}

const payload = {
  id: 1,
  name: "Carol",
  password: "hunter2",
  _internalFlag: true,
  lastLogin: new Date("2026-05-01"),
};

console.log(JSON.stringify(payload, replacer, 2));
// {
//   "id": 1,
//   "name": "Carol",
//   "password": "[REDACTED]",
//   "lastLogin": "2026-05-01T00:00:00.000Z"
// }

replacer는 모든 중첩된 객체와 배열에 재귀적으로 실행됩니다. 이 패턴은 디스크에 쓰거나 관측 가능성 서비스로 보내기 전에 자격 증명을 제거하려는 로깅 파이프라인에 유용합니다.

방법 3: 정렬된 키로 보기 좋게 출력

JavaScript 객체는 키 순서를 보장하지 않습니다(V8과 대부분의 엔진이 문자열 키에 대해 삽입 순서를 보존하긴 합니다). 결정적 출력이 필요할 때 — diffing, 캐싱 또는 표준 표현을 위해 — 키를 알파벳순으로 정렬하는 것이 올바른 방법입니다.

function sortedStringify(value, indent = 2) {
  return JSON.stringify(value, sortReplacer, indent);
}

function sortReplacer(key, value) {
  if (value !== null && typeof value === "object" && !Array.isArray(value)) {
    return Object.keys(value)
      .sort()
      .reduce((sorted, k) => {
        sorted[k] = value[k];
        return sorted;
      }, {});
  }
  return value;
}

const config = {
  version: "1.0",
  author: "Dave",
  dependencies: { typescript: "^5.4", eslint: "^9.0", astro: "^5.0" },
  name: "my-project",
};

console.log(sortedStringify(config));
// {
//   "author": "Dave",
//   "dependencies": { "astro": "^5.0", "eslint": "^9.0", "typescript": "^5.4" },
//   "name": "my-project",
//   "version": "1.0"
// }

정렬된 키는 git diff가 다른 직렬화기의 임의적인 재정렬이 아닌 실제로 변경된 라인만 표시함을 의미합니다. 이는 버전 제어에 체크인된 package.json 및 유사한 설정 파일에 특히 유용합니다.

JSON을 다른 형식으로 변환하기 위해 JSON to YAML 도구JSON to CSV 도구도 출력에서 키 순서를 처리합니다.

방법 4: 문자열에서 포맷(파싱 + Stringify)

실제 JSON은 일반적으로 문자열로 도착합니다 — fetch 응답, 파일 읽기, 클립보드 붙여넣기 또는 데이터베이스 TEXT 컬럼에서. 먼저 파싱한 다음 다시 포맷해야 합니다. 중요한 부분은 적절한 오류 처리입니다: 잘못된 JSON은 throw하며 그것을 우아하게 잡고 싶을 것입니다.

function formatJsonString(rawString, indent = 2) {
  try {
    const parsed = JSON.parse(rawString);
    return { ok: true, result: JSON.stringify(parsed, null, indent) };
  } catch (err) {
    return { ok: false, error: err.message };
  }
}

const raw = '{"name":"Eve","scores":[100,95,88],"active":true}';
const { ok, result, error } = formatJsonString(raw);

if (ok) {
  console.log(result);
  // {
  //   "name": "Eve",
  //   "scores": [100, 95, 88],
  //   "active": true
  // }
} else {
  console.error("파싱 실패:", error);
}

// 잘못된 입력
const bad = '{"name": "Eve", "broken":}';
const r2 = formatJsonString(bad);
// { ok: false, error: "Unexpected token '}'" }

파싱 오류를 구조화된 반환 객체로 감싸면 이 함수는 모든 호출 사이트를 try/catch로 둘러싸지 않고 UI 컴포넌트와 빌드 스크립트에서 안전하게 사용할 수 있습니다. JSON.stringify에 대한 MDN 문서는 전체 매개변수 명세를 다루고, RFC 8259는 프로토콜 수준에서 유효한 JSON이 어떻게 생겼는지 정의합니다.

방법 5: 큰 파일을 위한 스트리밍

방법 1–4는 모두 포맷 전에 전체 JSON 구조를 메모리에 로드합니다. 수백 메가바이트나 멀티 기가바이트 범위의 파일의 경우 이는 Node.js 이벤트 루프를 차단하고 프로세스를 완전히 충돌시킬 수 있습니다.

스트리밍 접근 방식은 파일을 청크로 읽고 포맷된 출력을 점진적으로 씁니다. NDJSON(라인당 하나의 JSON 객체, 로그 파일과 데이터베이스 내보내기에서 일반적)의 경우 readline 기반 접근 방식은 추가 종속성 없이 작동합니다:

import { createReadStream, createWriteStream } from "node:fs";
import { createInterface } from "node:readline";

async function formatNdjsonFile(inputPath, outputPath) {
  const rl = createInterface({
    input: createReadStream(inputPath),
    crlfDelay: Infinity,
  });

  const output = createWriteStream(outputPath);

  for await (const line of rl) {
    if (!line.trim()) continue;
    try {
      const obj = JSON.parse(line);
      output.write(JSON.stringify(obj, null, 2) + "\n---\n");
    } catch (e) {
      output.write("[잘못된 JSON 라인: " + e.message + "]\n---\n");
    }
  }

  output.end();
}

NDJSON은 가장 간단한 스트리밍 형식입니다: 각 라인이 유효하고 완전한 JSON 객체입니다. 많은 내보내기 도구가 정확히 사소하게 스트리밍 가능하기 때문에 이를 지원합니다. 큰 데이터 내보내기의 형식을 제어한다면 단일 거대한 JSON 배열보다 NDJSON을 선호하세요.

주의해야 할 엣지 케이스

이것들은 표준 JSON 포맷이 조용히 실패하거나 예기치 않게 throw되는 시나리오입니다.

순환 참조

객체가 직접 또는 간접적으로 자신을 참조하면 JSON.stringifyTypeError를 throw합니다. WeakSet을 사용하여 방문한 객체를 추적하는 replacer로 수정하세요:

function safeStringify(obj, indent = 2) {
  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;
  }, indent);
}

const a = { name: "circular" };
a.self = a;
console.log(safeStringify(a));
// { "name": "circular", "self": "[Circular]" }

WeakSet은 가비지 컬렉션을 방지하지 않고 참조를 보유하여 장기 실행 프로세스에서 메모리 누수를 피합니다.

BigInt 값

JSON 명세에 64비트 정수 타입이 없기 때문에 JSON.stringifyBigInt 값에 대해 TypeError를 throw합니다. replacer에서 문자열로 변환하세요:

const data = { id: 9007199254740993n, value: 42 };

JSON.stringify(data, (key, value) =>
  typeof value === "bigint" ? value.toString() : value
, 2);
// { "id": "9007199254740993", "value": 42 }

Map과 Set 값

Map은 빈 객체로 직렬화되고 Set은 빈 배열로 직렬화됩니다 — 그 내용이 아닙니다. replacer에서 명시적으로 변환하세요:

const data = {
  tags: new Set(["json", "javascript"]),
  meta: new Map([["source", "api"]]),
};

JSON.stringify(data, (key, value) => {
  if (value instanceof Set) return [...value];
  if (value instanceof Map) return Object.fromEntries(value);
  return value;
}, 2);
// { "tags": ["json", "javascript"], "meta": { "source": "api" } }

undefined 값

undefined 값을 가진 객체 속성은 조용히 제거됩니다. undefined를 가진 배열 슬롯은 null이 됩니다. 모든 키를 보존해야 할 때 undefinednull로 변환하기 위해 replacer를 사용하세요:

const obj = { a: 1, b: undefined, c: null };
JSON.stringify(obj, null, 2);
// { "a": 1, "c": null }  — "b"는 조용히 제거됨

// 수정:
JSON.stringify(obj, (key, value) =>
  value === undefined ? null : value
, 2);
// { "a": 1, "b": null, "c": null }

브라우저에서 JSON을 즉시 포맷

코드를 작성하지 않고 지금 JSON 블롭을 포맷해야 한다면 Toova JSON Formatter가 한 번의 클릭으로 처리합니다 — 원시 JSON을 붙여넣고, 2 또는 4칸 들여쓰기로 보기 좋게 출력된 결과를 얻고, 결과를 복사하세요. 가입 없음, 파일 업로드 없음, 모든 것이 브라우저에서 로컬로 실행됩니다.

형식 간 변환을 위해 JSON to YAMLJSON to CSV는 동일한 개인정보 우선 접근 방식을 따릅니다 — 데이터가 기기를 떠나지 않습니다.

결론

대부분의 사용 사례에서 JSON.stringify(obj, null, 2)면 충분합니다. 필터링, 마스킹 또는 정렬된 키가 필요할 때 replacer 함수를 추가하세요. 외부 입력을 처리할 때 JSON.parse를 try/catch로 감싸세요. 파일 크기가 동기적 파싱을 비실용적으로 만들 때만 스트리밍에 손을 뻗으세요. 그리고 엣지 케이스 — 순환 참조, BigInt, Map/Set, undefined — 를 비정상적인 데이터 모양으로 작업할 때 마음 한 구석에 두세요.