JSON 파싱은 웹 개발자라면 수백 번은 하게 되는 작업입니다. 다행히 JavaScript에는 내장 지원이 있어 라이브러리가 필요 없습니다. 하지만 JSON.parse()JSON.stringify()에는 문서가 알려주는 것보다 더 많은 뉘앙스가 있습니다. 제대로 살펴보겠습니다.

JSON.parse() — 문자열을 객체로 변환하기

JSON.parse()는 JSON 형식의 문자열을 받아 JavaScript 값으로 변환합니다. 그 값은 보통 객체나 배열이지만 문자열, 숫자, 불리언, 또는 null이 될 수도 있습니다:

js
const jsonString = '{"name": "Alice", "age": 30, "active": true}';
const user = JSON.parse(jsonString);

console.log(user.name);    // "Alice"
console.log(user.age);     // 30
console.log(user.active);  // true
console.log(typeof user);  // "object"

입력값은 반드시 문자열이어야 합니다. 흔한 실수는 이미 객체인 값을 파싱하려는 것입니다 — JSON.parse({})는 객체를 먼저 "[object Object]"로 변환하는데, 이것은 유효한 JSON이 아니므로 SyntaxError를 던집니다.

항상 JSON.parse()를 try/catch로 감싸세요

아무도 알려주지 않는 사실이 있습니다: JSON.parse()는 입력이 유효하지 않은 JSON이면 SyntaxError를 던집니다. API, 사용자 입력, 또는 파일에서 데이터를 파싱하는 경우 항상 해당 오류를 처리해야 합니다. 잘못된 응답 하나가 처리하지 않으면 앱을 크래시시킬 수 있습니다:

js
function safeParseJSON(str) {
  try {
    return { data: JSON.parse(str), error: null };
  } catch (err) {
    return { data: null, error: err.message };
  }
}

const { data, error } = safeParseJSON('{"name": "Alice"}');
if (error) {
  console.error('Invalid JSON:', error);
} else {
  console.log(data.name); // "Alice"
}

// Handles bad input gracefully
const result = safeParseJSON('not json at all');
console.log(result.error); // "Unexpected token 'o', "not json "... is not valid JSON"

저는 거의 모든 프로젝트에서 이런 래퍼를 사용합니다. 오류 처리를 일관되게 만들고 UI까지 버블링되는 지저분한 캐치되지 않은 예외를 방지합니다.

JSON.stringify() — 객체를 문자열로 변환하기

JSON.stringify()는 반대 작업을 합니다: JavaScript 값을 JSON 문자열로 변환합니다. API로 데이터를 보내거나, localStorage에 저장하거나, 파일에 쓸 때 사용합니다:

js
const user = {
  name: "Bob",
  age: 25,
  roles: ["admin", "editor"],
  password: "secret123" // we'll handle this later
};

// Basic usage
const jsonString = JSON.stringify(user);
// '{"name":"Bob","age":25,"roles":["admin","editor"],"password":"secret123"}'

// Pretty-printed (great for logs and file output)
const prettyJson = JSON.stringify(user, null, 2);
console.log(prettyJson);
// {
//   "name": "Bob",
//   "age": 25,
//   "roles": ["admin", "editor"],
//   "password": "secret123"
// }

JSON.stringify()의 세 번째 인수는 들여쓰기 수준입니다. 24를 사용하면 읽기 쉬운 출력이 됩니다. 기본값(세 번째 인수 없음)은 압축된 최소화 JSON을 줍니다 — 네트워크 전송에 더 좋습니다.

stringify()가 조용히 삭제하는 것들

이것은 개발자들이 자주 당하는 문제입니다. JSON.stringify()는 JSON이 지원하지 않는 특정 값들을 조용히 생략합니다:

js
const data = {
  name: "Alice",
  greet: function() { return "hi"; },   // Functions → dropped
  undef: undefined,                      // undefined → dropped
  sym: Symbol("key"),                    // Symbols → dropped
  nan: NaN,                              // NaN → null
  inf: Infinity,                         // Infinity → null
  date: new Date("2024-01-15")           // Dates → ISO string
};

console.log(JSON.stringify(data, null, 2));
// {
//   "name": "Alice",
//   "nan": null,
//   "inf": null,
//   "date": "2024-01-15T00:00:00.000Z"
// }
// greet, undef, sym are GONE
주의: undefined 값이 있는 객체를 stringify하고 나중에 파싱하면 해당 키들이 완전히 사라집니다. 코드에서 나중에 obj.key === undefined를 확인한다면 미묘한 버그를 일으킬 수 있습니다.

API에서 JSON 가져오기 — 실전 패턴

실제로 대부분의 JSON 파싱은 API에서 데이터를 가져올 때 발생합니다. Fetch API는 이것을 간단하게 만들어 줍니다 — response.json()이 파싱을 대신 처리합니다:

js
async function getUser(userId) {
  try {
    const response = await fetch(`https://api.example.com/users/${userId}`);

    if (!response.ok) {
      throw new Error(`HTTP error: ${response.status}`);
    }

    const user = await response.json(); // parses JSON automatically
    return user;
  } catch (err) {
    console.error('Failed to fetch user:', err);
    return null;
  }
}

const user = await getUser(42);
if (user) {
  console.log(`Hello, ${user.name}!`);
}

response.json()은 본질적으로 JSON.parse(await response.text())입니다. 응답 본문이 유효한 JSON이 아니면 오류를 던지므로, 프로덕션 코드에서는 별도로 캐치하는 것이 좋습니다.

중첩된 JSON 데이터 다루기

실제 API에서는 깊게 중첩된 JSON이 흔합니다. 키가 없을 때 크래시하지 않고 안전하게 탐색하는 방법입니다:

js
const apiResponse = {
  "user": {
    "profile": {
      "address": {
        "city": "Berlin"
      }
    }
  }
};

// Unsafe — throws if any level is undefined
const city1 = apiResponse.user.profile.address.city; // "Berlin"

// Safe with optional chaining (ES2020+) — returns undefined instead of throwing
const city2 = apiResponse?.user?.profile?.address?.city; // "Berlin"
const zip   = apiResponse?.user?.profile?.address?.zip;  // undefined (not a crash)

// With a fallback using nullish coalescing
const country = apiResponse?.user?.profile?.address?.country ?? "Unknown";

옵셔널 체이닝(?.)과 null 병합 연산자(??)는 2020년 이후 ECMAScript 명세의 일부인 현대 JavaScript 기능으로, 불확실한 JSON 구조를 훨씬 안전하게 다룰 수 있게 해줍니다. 모든 최신 브라우저와 Node.js 14+에서 지원되니 자유롭게 사용하세요.

replacer와 reviver 함수

JSON.stringify()JSON.parse() 모두 변환을 커스터마이즈할 수 있는 두 번째 인수를 받습니다. 실제 시나리오에서 진정으로 유용합니다:

js
// replacer: filter or transform values during stringify
const user = { name: "Alice", password: "s3cr3t", age: 30 };
const safe = JSON.stringify(user, ["name", "age"]); // only include these keys
// '{"name":"Alice","age":30}'  — password is excluded

// reviver: transform values during parse
const dateJson = '{"name": "Alice", "createdAt": "2024-01-15T09:30:00Z"}';
const parsed = JSON.parse(dateJson, (key, value) => {
  if (key === "createdAt") return new Date(value); // convert string to Date
  return value;
});
console.log(parsed.createdAt instanceof Date); // true
console.log(parsed.createdAt.getFullYear());   // 2024

reviver 패턴은 JSON을 거쳐 Date 객체를 복원할 때 특히 유용합니다. JSON에는 네이티브 날짜 타입이 없기 때문입니다.

유용한 도구들

JavaScript 프로젝트에서 JSON을 다룰 때 시간을 절약해 주는 도구들입니다: 압축된 API 응답을 예쁘게 출력해주는 JSON 포매터, 문법 오류를 확인하는 JSON 검증기, 대용량 페이로드에서 특정 필드를 쿼리하는 JSON Path, 그리고 JSON을 문자열에 임베드해야 할 때의 JSON Escape. 더 깊이 알고 싶다면 MDN JSON 레퍼런스가 훌륭합니다.

마무리

JSON.parse()JSON.stringify(), 이 두 함수가 전부입니다. 쌓아야 할 핵심 습관: 항상 parse()를 try/catch로 감싸고, 중첩 접근에는 옵셔널 체이닝을 사용하며, stringify()가 조용히 삭제하는 것들을 파악하세요. 이 세 가지를 제대로 익히면 현실 JSON 시나리오의 99%를 놀라움 없이 처리할 수 있습니다.