JavaScript의 `JSON` 클래스

JavaScript의 `JSON` 클래스

이 글은 JavaScript의 JSON 클래스에 대해 설명합니다.

실제 예제를 통해 JavaScript의 JSON 클래스를 설명합니다.

YouTube Video

JavaScript의 JSON 클래스

JSON 객체는 주로 두 가지 메서드를 가지고 있습니다. JSON.stringify()는 객체를 JSON 문자열로 변환하고, JSON.parse()는 JSON 문자열에서 객체를 만듭니다. JSON은 JavaScript 값 중 일부만 표현할 수 있는 데이터 교환 포맷입니다. 함수, undefined, Symbol, 순환 참조는 JSON으로 직접 변환할 수 없습니다.

JSON.stringify()

먼저, 객체를 JSON 문자열로 변환하는 기본 예제입니다.

1// Convert a JavaScript object to a JSON string.
2const obj = { name: "Alice", age: 30, active: true };
3const jsonString = JSON.stringify(obj);
4console.log(jsonString); // {"name":"Alice","age":30,"active":true}
  • 이 코드는 일반 객체를 JSON 문자열로 변환하는 가장 기본적인 예제입니다. JSON.stringify는 문자열을 동기적으로 반환합니다.

JSON.stringify의 두 번째 인수(replacer)

JSON.stringify의 두 번째 인수인 replacer를 지정하여 변환 규칙을 세밀하게 제어할 수 있습니다. 특정 프로퍼티만 남기기 위해 배열을 사용하는 예제입니다.

1// Use a replacer array to include only specific properties during stringification.
2const user = { id: 1, name: "Bob", password: "secret", role: "admin" };
3const safeJson = JSON.stringify(user, ["id", "name", "role"]);
4console.log(safeJson); // {"id":1,"name":"Bob","role":"admin"}
  • 이 예제에서는 안전한 로그 출력을 위해 password가 제외됩니다. 보안상의 이유로 민감한 정보를 제거하고 싶을 때 유용합니다.

replacer를 함수로 사용하기(값 변환 및 필터링 목적)

더 유연하게 제어하고 싶다면 각 키와 값을 처리하는 함수를 제공할 수 있습니다. 아래는 Date를 ISO 문자열로 유지하는 예제입니다.

 1// Use a replacer function to customize serialization.
 2const data = { name: "Carol", joined: new Date("2020-01-01T12:00:00Z") };
 3const jsonCustom = JSON.stringify(data, (key, value) => {
 4  // Convert Date objects to ISO strings explicitly
 5  if (this && this[key] instanceof Date) {
 6    return this[key].toISOString();
 7  }
 8  return value;
 9});
10console.log(jsonCustom); // {"name":"Carol","joined":"2020-01-01T12:00:00.000Z"}
  • 함수형 replacer에서는 키와 값이 인수로 전달되고 반환값이 최종 JSON에 포함됩니다. this는 부모 객체를 가리키므로, 중첩 변환에도 사용할 수 있습니다.

JSON.parse()의 기본

JSON.parse는 JSON 문자열을 다시 객체로 변환합니다. 잘못된 JSON을 처리할 수 있도록 try/catch로 감싸세요.

1// Parse a JSON string into an object and handle parsing errors.
2const jsonText = '{"title":"Example","count":42}';
3try {
4  const parsed = JSON.parse(jsonText);
5  console.log(parsed.title); // Example
6} catch (err) {
7  console.error("Invalid JSON:", err.message);
8}
  • 문자열이 올바르지 않으면 예외가 발생하므로 외부 데이터를 처리할 때 항상 예외 처리를 추가하세요.

reviver를 이용한 맞춤 복원(날짜 복원 예시)

JSON.parse의 두 번째 인수인 reviver를 이용해 값을 복원할 수 있습니다. 저장된 문자열 형식의 날짜를 다시 Date 객체로 변환하는 예제입니다.

 1// Use a reviver to turn ISO date strings back into Date objects.
 2const jsonWithDate = '{"name":"Dana","joined":"2021-06-15T09:00:00.000Z"}';
 3const obj = JSON.parse(jsonWithDate, (key, value) => {
 4  // A simple check for ISO date-like strings
 5  if (typeof value === "string" && /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z$/.test(value)) {
 6    return new Date(value);
 7  }
 8  return value;
 9});
10console.log(obj.joined instanceof Date); // true
  • reviver는 각 키마다 호출되며, 반환된 값이 최종 객체에 들어갑니다. 애플리케이션에 맞는 엄격한 날짜 포맷 확인을 해주세요.

이쁘게 출력하기와 space 인수

가독성을 위해 들여쓰기를 하려면 JSON.stringify의 세 번째 인수로 숫자나 문자열을 전달하세요.

1// Pretty-print an object with 2-space indentation for readability.
2const config = { host: "localhost", port: 3000, debug: true };
3const pretty = JSON.stringify(config, null, 2);
4console.log(pretty);
5/* {
6  "host": "localhost",
7  "port": 3000,
8  "debug": true
9} */
  • 로그, 환경설정 파일 등 사람이 읽을 JSON에서는 들여쓰기를 지정하면 도움이 됩니다. 다만, 이로 인해 데이터 크기가 커질 수 있습니다.

순환 참조 처리 방법

순환 참조가 있으면 JSON.stringifyTypeError를 던집니다. 일반적인 해결책으로는 seen 세트를 사용해 순환 참조를 피하는 replacer를 만드는 방법이 있습니다.

 1// Safely stringify objects that may contain circular references.
 2function safeStringify(value) {
 3  const seen = new WeakSet();
 4  return JSON.stringify(value, (key, val) => {
 5    if (val && typeof val === "object") {
 6      if (seen.has(val)) return "[Circular]";
 7      seen.add(val);
 8    }
 9    return val;
10  });
11}
12
13const a = { name: "A" };
14a.self = a;
15console.log(safeStringify(a)); // {"name":"A","self":"[Circular]"}
  • WeakSet을 이용하면 메모리 누수 없이 순환 참조를 감지할 수 있습니다. 순환 참조가 있을 경우 "[Circular]"를 반환합니다. "[Circular]" 대신 참조 ID를 할당하는 것도 가능합니다.

toJSON 메서드를 이용한 맞춤 직렬화

객체에 toJSON 메서드를 정의하면, JSON.stringify는 그 반환값을 사용합니다. 타입별 변환 규칙을 내장하고 싶을 때 유용합니다.

 1// Define toJSON on a class to customize its JSON representation.
 2class Point {
 3  constructor(x, y) {
 4    this.x = x;
 5    this.y = y;
 6  }
 7  toJSON() {
 8    // This will be used by JSON.stringify
 9    return { x: this.x, y: this.y, type: "Point" };
10  }
11}
12
13const p = new Point(10, 20);
14console.log(JSON.stringify(p)); // {"x":10,"y":20,"type":"Point"}
  • toJSON은 객체 단위로 직렬화 규칙을 정의할 수 있어, replacer보다 더 직관적이고, 지역적으로 제어할 수 있게 합니다.

JSON의 한계와 주의점

JSON으로 변환할 수 없는 대표적인 예는 undefined, 함수, Symbol, BigInt, 순환 참조 등이 있습니다. 큰 정수나 IEEE 부동소수점 반올림 등, 숫자 정밀도에도 주의하세요.

 1// Demonstrate values that can't be represented in JSON.
 2const sample = {
 3  a: undefined,
 4  b: function () {},
 5  c: Symbol("s"),
 6  d: 9007199254740993n // BigInt (note: JSON.stringify will throw on BigInt)
 7};
 8// JSON.stringify will throw for BigInt and will drop undefined, functions, symbols in objects.
 9try {
10  console.log(JSON.stringify(sample));
11} catch (err) {
12  console.error("Error during stringify:", err.message);
13}
  • BigInt를 다룰 때는 문자열로 변환하거나, 커스텀 replacer 또는 toJSON을 사용해 표현을 정의하세요.

보안(신뢰할 수 없는 JSON 처리 시)

JSON.parse 자체는 안전하며 eval보다 안전하지만, 파싱된 객체를 바로 신뢰하고 프로퍼티 접근이나 DB 쿼리에 사용하는 것은 위험합니다. 항상 데이터를 검증하여 기대하는 스키마에 부합하는지 확인하세요.

1// Parse external JSON and validate expected properties before use.
2const external = '{"username":"eve","role":"user"}';
3const parsed = JSON.parse(external);
4if (typeof parsed.username === "string" && ["user","admin"].includes(parsed.role)) {
5  console.log("Safe to use parsed.username and parsed.role.");
6} else {
7  throw new Error("Invalid payload");
8}
  • 입력(스키마) 검증에는 ajv와 같은 라이브러리를 사용하는 것이 강력합니다.

성능 고려사항

대용량 객체를 자주 직렬화·역직렬화하면 CPU 및 메모리에 부하가 걸립니다. 필요하다면 차이만 전송하거나, MessagePack 같은 이진 포맷 사용, 스트리밍(순차 처리) 등을 고려할 수 있습니다. 브라우저 환경에서는 structuredClone(복사용)이나 postMessage의 transferable 객체를 활용할 수도 있습니다.

구체적인 조언(간단 정리)

다음과 같은 점도 고려할 수 있습니다:.

  • 로그에는 예쁘게 출력하지 말고 한 줄짜리 JSON으로 사이즈를 줄이세요.
  • 자주 직렬화하는 객체는 미리 불필요한 프로퍼티를 제거해 경량화해 두세요.
  • replacer를 활용해 순회를 최소화하고, 불필요한 프로퍼티를 제외하세요.

실전 예제: API 요청 송·수신 흐름

마지막으로, 객체를 서버로 안전하게 전송하기 위해 JSON으로 변환하고, 응답에서 날짜를 복원하는 일련의 예시를 소개합니다.

 1// Prepare payload, stringify safely, send via fetch, and revive dates on response.
 2async function sendUserUpdate(url, user) {
 3  // Remove sensitive info before sending
 4  const payload = JSON.stringify(user, (k, v) => (k === "password" ? undefined : v));
 5  const res = await fetch(url, {
 6    method: "POST",
 7    headers: { "Content-Type": "application/json" },
 8    body: payload
 9  });
10
11  const text = await res.text();
12  // Reviver: convert ISO date strings back to Date
13  const data = JSON.parse(text, (key, value) => {
14    if (typeof value === "string" && /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z$/.test(value)) {
15      return new Date(value);
16    }
17    return value;
18  });
19  return data;
20}
  • 이 예제에서는 전송 전에 password를 제외하고, 수신 후 reviver로 날짜를 복원합니다. 실제 운영에서는 에러 처리와 타임아웃 처리도 추가해야 합니다.

요약

JSON.stringify()JSON.parse()를 사용해 객체와 문자열 간 변환이 가능합니다. replacerreviver로 변환 과정을 커스터마이즈할 수 있고, 클래스에 toJSON을 구현하면 개별 객체별 제어도 가능합니다. 순환 참조, BigInt, 함수 등은 그대로는 처리할 수 없으니 사전에 변환이 필요합니다.

항상 외부 입력을 검증하고, 민감 정보가 로그나 통신에 포함되지 않도록 하세요. 데이터 크기가 크다면, 예쁜 출력은 피하고, 차등 전송이나 이진 포맷 활용을 고려하는 것이 효율적입니다.

위의 기사를 보면서 Visual Studio Code를 사용해 우리 유튜브 채널에서 함께 따라할 수 있습니다. 유튜브 채널도 확인해 주세요.

YouTube Video