본문으로 건너뛰기
yceffort
PostsSeriesTagsAbout🧪 Research
EN

Tweaks

theme
accent palette
film grain
minimal mode
BACK TO INDEX
◆ ESSAY
--min
--year
KOoriginal

mailMail icongithubtwitter
yceffort
•
© 2026
•
https://yceffort.kr
BACK TO INDEX
◆ ESSAY

JSON.stringify 만들어보기

avatar
yceffort
2022-06-17 · 7분
7min
2022year
KOoriginal
javascript

Table of Contents

  • JSON이 지원하는 타입
  • 현기증 나는 JSON.stringify
    • JSON이 지원하지 않는 타입은 undefined
    • Map, Regex, Set은 빈 JSON
    • Array와 Object 내부에 지원하지 않는 타입이 있는 경우
    • BigInt와 순환참조는 throw error
    • NaN과 Infinity는 null
    • 날짜는 ISO String
    • 열거불가능, Symbol 키는 무시
    • 요약
  • 구현해보기
  • 참고

JSON이 지원하는 타입

JSON 무려 공식 홈페이지가 존재하는데, 여기서 어떤 데이터 타입을 지원하는지 나와있다. JSON은 우리가 매일 쓰고 또 그다지 어렵지 않기 때문에 그렇게 복잡하게 생각해본적이 없는데, 공식 문서의 그래프를 보면 살짝 어지러워진다. 이래저래 읽는게 귀찮고 복잡하므로, 타입스크립트로 간단하게 요약해보자면 다음과 같다.

type JSONType =
  null | boolean | number | string | JSONType[] | {[key: string]: JSONType}

JSON은 언어에 종속적이지 않기 때문에, 자바스크립트에만 있는 고유의 타입, undefined Symbol BigInt 등과 Function Class Map 등도 지원하지 않는다.

현기증 나는 JSON.stringify

JSON.stringify를 계속 쓰다보면, 이 함수의 동작은 참 일관적이지 않다는 것을 깨닫게 된다.

JSON.stringify(1) // '1'
JSON.stringify(null) // 'null'
JSON.stringify('foo') // '"foo"'
JSON.stringify({foo: 'bar'}) // '{"foo":"bar"}'
JSON.stringify(['foo', 'bar']) // '["foo","bar"]'

JSON이 지원하지 않는 타입은 undefined

여기까지는 우리가 모두 이해하는 수준이다. 그러나 앞서 언급했던, JSON이 지원하지 않는 일부 타입에 대해서는 다음과 같이 반환된다.

JSON.stringify(undefined) // undefined
JSON.stringify(Symbol('foo')) // undefined
JSON.stringify(() => {}) // undefined

모두 undefined가 나온다면 그래도 행복할 것 같다. 그러나

Map, Regex, Set은 빈 JSON

JSON.stringify(/foo/) // '{}'
JSON.stringify(new Map()) // '{}'
JSON.stringify(new Set()) //'{}'

....?

Array와 Object 내부에 지원하지 않는 타입이 있는 경우

더 골 때리는 것은 serialize가 가능한 값, 예를 들어 array나 object에서 더 일관성 없이 동작한다는 것이다. undefined Symbol Function 이 배열안에 있으면 'null'로 변환된다. 그리고 객체 안에 속성이 있다면 그 속성 전체는 완전히 무시되고 빈 객체 (정확히는 빈 JSON) 가 된다.

JSON.stringify([undefined]) // '[null]'
JSON.stringify({foo: undefined}) // '{}'

JSON.stringify([Symbol()]) // '[null]'
JSON.stringify({foo: Symbol()}) // '{}'

JSON.stringify([() => {}]) // '[null]'
JSON.stringify({foo: () => {}}) // '{}'

이와 다르게, Map Set Regex가 배열이나 객체 내부에 있다면, 이들은 모두 일관되게 {}으로 변환된다. 그리고, 당연히 값도 날아간다.

JSON.stringify([/foo/]) // '[{}]'
JSON.stringify({foo: /foo/}) // '{"foo":{}}'

JSON.stringify([new Set()]) // '[{}]'
JSON.stringify({foo: new Set()}) // '{"foo":{}}'

JSON.stringify([new Map()]) // '[{}]'
JSON.stringify({foo: new Map()}) // '{"foo":{}}'

BigInt와 순환참조는 throw error

여기에 추가로, BigInt가 내부에 오게 되면 TypeError를 리턴하게 된다.

bigint = BigInt(9007199254740991)
JSON.stringify(bigint) //  Uncaught TypeError: Do not know how to serialize a BigInt

그리고 우리가 잘 알고 있는 것 처럼, 순환참조를 하는 객체의 경우에도 에러가 난다.

const foo = {}
foo.a = foo

JSON.stringify(foo) // Uncaught TypeError: Converting circular structure to JSON

한가지 유념에 두어야 할 것은, BigInt와 Cyclic Object 이 딱 두가지 경우에만 error를 던진다. JSON.stringify는 우리가 아는 함수 중에서 가장 관대한 편에 속한다.

NaN과 Infinity는 null

숫자 중에서도 NaN과 Infinity는 null로 리턴된다.

JSON.stringify(NaN) // null
JSON.stringify(Infinity)

날짜는 ISO String

Date의 경우에는 ISO string으로 변환된다. 그 이유는 Date.prototype.toJSON의 동작 때문이다.

JSON.stringify(new Date()) // '"2022-06-18T03:43:12.133Z"'

열거불가능, Symbol 키는 무시

JSON.stringify는 오직 열거 가능한, 비 심볼키 속성에 대해서만 처리한다. 즉, 심볼키로 되어 있거나, 열거 불가능한 속성은 무시하게 된다.

const foo = {}
foo[Symbol('p1')] = 'bar'
Object.defineProperty(foo, 'p2', {value: 'baz', enumerable: false})

JSON.stringify(foo) // '{}'

이 코드 조각을 보고 나니 왜 JSON.parse와 JSON.stringify로 객체를 깊은 복사하는 것이 불가능한지 이해할 수 있게 되었다.

요약

UnSupported typepass directlyarrayobject
undefinedundefined'null'omitted
symbolundefined'null'omitted
functionundefined'null'omitted
NaN'null''null''null'
Infinity'null''null''null'
Regex'{}''{}''{}'
Map{}'{}''{}'
Set'{}''{}''{}'
WeakMap'{}''{}''{}'
WeakSet'{}''{}''{}'
BigIntTypeErrorTypeErrorTypeError
Cyclic objectsTypeErrorTypeErrorTypeError

구현해보기

가장 먼저 해야할 것은 순환 참조인지 확인하는 함수를 만든 것이다.

function isCyclic(input: unknown): boolean {
  const seen = new Set()

  function dfs(obj: unknown) {
    if (typeof obj !== 'object' || obj === null) {
      return false
    }
    seen.add(obj)

    return Object.entries(obj).some(([key, value]) => {
      const result = seen.has(value) ? true : isCyclic(value)
      seen.delete(result)
      return result
    })
  }

  return dfs(input)
}

이제 본격적으로 stringify를 구현해보자.

function isCyclic(input: unknown): boolean {
  const seen = new Set()

  function dfs(obj: unknown) {
    if (typeof obj !== 'object' || obj === null) {
      return false
    }
    seen.add(obj)

    return Object.entries(obj).some(([key, value]) => {
      const result = seen.has(value) ? true : isCyclic(value)
      seen.delete(result)
      return result
    })
  }

  return dfs(input)
}

function JSONStringify(data: unknown): string {
  if (isCyclic(data)) {
    throw new TypeError('순환참조 객체는 stringify 할 수 없습니다.')
  }

  if (typeof data === 'bigint') {
    throw new TypeError('Bigint는 stringify로 변환할 수 없습니다.')
  }

  if (data === null) {
    return String(null)
  }

  if (typeof data !== 'object') {
    if (Number.isNaN(data) || data === Infinity) {
      return String(null)
    } else if (['function', 'undefined', 'symbol'].includes(typeof data)) {
      return undefined
    } else if (typeof data === 'string') {
      return `"${data}"`
    } else {
      return String(data)
    }
  } else {
    if (data instanceof Date) {
      return JSONStringify(data.toJSON())
    } else if (data instanceof Array) {
      const result = data.map((item) => {
        if (
          typeof item === 'undefined' ||
          typeof item === 'function' ||
          typeof item === 'symbol'
        ) {
          return String(null)
        } else {
          return JSONStringify(item)
        }
      })

      return `[${result}]`.replace(/'/g, '"')
    } else {
      const result = Object.entries(data).reduce((result, [key, value]) => {
        if (
          value !== undefined &&
          typeof value !== 'function' &&
          typeof value !== 'symbol'
        ) {
          result.push(`"${key}":${JSONStringify(value)}`)
        }
        return result
      }, [] as string[])

      return `{${result}}`.replace(/'/g, '"')
    }
  }
}

테스트 해보기

const test = [
  1,
  null,
  'foo',
  {'foo': 'bar'},
  ['foo', 'bar'],
  undefined,
  new Map(),
  new Set(),
  [undefined],
  {foo: undefined},
  [Symbol()],
  {foo: Symbol()},
  [() => {}],
  {foo: () => {}},
  [/foo/],
  {foo: /foo/},
  [new Set()],
  {foo: new Set()},
  [new Map()],
  {foo: new Map()},
]

for (const tc of test) {
  const result1 = JSON.stringify(tc)
  const result2 = JSONStringify(tc)


  if (result1===result2) {
    console.log(tc, 'TRUE')
  } else if (result1 === undefined && result2 === undefined) {
    console.log(tc, 'TRUE')
  } else {
    console.log(tc, 'FALSE')
  }
}

/**
 1 TRUE
null TRUE
foo TRUE
{ foo: 'bar' } TRUE
[ 'foo', 'bar' ] TRUE
undefined TRUE
Map {} TRUE
Set {} TRUE
[ undefined ] TRUE
{ foo: undefined } TRUE
[ Symbol() ] TRUE
{ foo: Symbol() } TRUE
[ [Function] ] TRUE
{ foo: [Function: foo] } TRUE
[ /foo/ ] TRUE
{ foo: /foo/ } TRUE
[ Set {} ] TRUE
{ foo: Set {} } TRUE
[ Map {} ] TRUE
{ foo: Map {} } TRUE
 * /

참고

  • JSON.stringify의 공식 문서에서
  • fast-json-stringify
  • How to improve the performance of JSON. stringify ()?

관련 글

  • #turbopack#nextjs#bundler

    Next.js turbopack에서 싱글톤이 두 개가 됐다: scope hoisting 버그와 순환 import

    Next.js 16 turbopack 프로덕션 빌드에서 모듈 스코프 싱글톤이 런타임에 두 개가 됐다. 같은 동기 구간에서 조건 판정이 뒤집히고, 응답이 도착해도 타임아웃이 나는 증상을 번들 산출물로 추적한 기록. scope hoisting의 부분 병합, 순환 import, 이미 고쳐져 있던 upstream 버그, 그리고 뒤늦게 돌린 단일 변수 실험까지.

    2026-08-19·28분
  • #javascript#animation#web-animations-api

    number-flow를 구형 브라우저로 이식하기: 다섯 가지 결정과 두 가지 번복

    number-flow가 애니메이션을 켜는 최소 버전은 Chrome 125, Safari 17.2다. 이 하한을 Chrome 66과 WebKit 16.4까지 내리는 포크를 만들면서 내린 결정들과, 뒤집게 된 판단 두 가지, 그리고 자동 강등을 포기한 Safari 버그 조사의 기록.

    2026-08-11·44분
  • #nodejs#security#javascript

    Node.js vm 모듈의 함정: 샌드박스가 아닌 이유

    집필 중인 Node.js Deep Dive의 5.2장(vm 모듈의 함정) 일부를 미리 공개합니다.

    2026-02-27·25분
  • #nodejs#javascript

    Node.js Deep Dive (가제) 베타 리더를 모십니다.

    많관부22

    2026-02-19·5분

새 글을 놓치고 싶지 않으시다면 RSS로 구독해 주세요.

RSS 구독 →

yceffort — 프론트엔드 엔지니어입니다. 발표·기술 자문·기고 문의는 이곳에서 받고 있습니다.

← Back to the blogIssue on GitHub →