JavaScript에서 JSON 포맷하는 방법 — 5가지 방법
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.stringify는 TypeError를 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.stringify는 BigInt 값에 대해 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이 됩니다. 모든 키를 보존해야 할 때 undefined를 null로 변환하기 위해 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 YAML과 JSON to CSV는 동일한 개인정보 우선 접근 방식을 따릅니다 — 데이터가 기기를 떠나지 않습니다.
결론
대부분의 사용 사례에서 JSON.stringify(obj, null, 2)면 충분합니다. 필터링, 마스킹 또는 정렬된 키가 필요할 때 replacer 함수를 추가하세요. 외부 입력을 처리할 때 JSON.parse를 try/catch로 감싸세요. 파일 크기가 동기적 파싱을 비실용적으로 만들 때만 스트리밍에 손을 뻗으세요. 그리고 엣지 케이스 — 순환 참조, BigInt, Map/Set, undefined — 를 비정상적인 데이터 모양으로 작업할 때 마음 한 구석에 두세요.