본문으로 건너뛰기

버그 제보

이상한 점을 알려 주시면 고칠게요. 지금 보고 있는 페이지와 브라우저 정보가 함께 담겨요.

페이지:

Frontend

JSON: stringify와 parse

작성 2026. 10. 17.추가
목차11개 항목
출처와 참고

javascript.info(모던 JavaScript 튜토리얼)의 「JSON과 메서드」 글을 바탕으로, 이후 추가된 기능은 MDN으로 확인해 재구성한 노트다. 파싱 속도 비교는 「왜 JSON.parse로 객체를 선언하는 방법이 더 빠를까?」를 바탕으로 더했다.

JSON이란

JSON(JavaScript Object Notation)은 값을 텍스트로 적는 언어 독립적인 데이터 형식이다. JavaScript 객체 리터럴에서 왔지만 지금은 RFC 8259와 ECMA-404로 따로 표준화되어 거의 모든 언어가 읽고 쓴다. 객체를 네트워크로 보내거나 저장소에 넣으려면 먼저 문자열로 바꿔야 하는데, 이것을 직렬화(serialize), 되돌리는 것을 역직렬화(deserialize) 라 한다.

메서드방향
JSON.stringify(value, replacer?, space?)값 → JSON 문자열
JSON.parse(text, reviver?)JSON 문자열 → 값

문법 규칙

JSON은 객체 리터럴보다 엄격하다. 파서를 단순하고 빠르게 만들기 위해서다.

규칙객체 리터럴에서는 되지만 JSON에서는 안 되는 것
키는 큰따옴표 문자열{ name: 1 }, { 'name': 1 }
문자열 값도 큰따옴표'Smith', `Smith`
값은 객체·배열·문자열·숫자·true·false·null만undefined, NaN, Infinity, new Date(), 함수
주석 없음// 설명
끝 쉼표 없음[1, 2,]

주석과 끝 쉼표를 허용하는 확장 형식(JSON5, VS Code 설정의 JSONC)은 표준 JSON이 아니라 각자의 파서가 필요하다. 형식을 벗어난 문자열을 JSON.parse에 넘기면 SyntaxError를 던지므로 외부 입력은 try...catch로 감싼다.

JSON.stringify가 하는 일

JSON.stringify({ name: 'John', courses: ['html'], wife: null })
// '{"name":"John","courses":["html"],"wife":null}'
JSON.stringify('hi')  // '"hi"': 원시값도 된다

중첩 객체·배열은 재귀적으로 따라 들어간다. 다만 JSON에 없는 값은 버리거나 바꾼다.

값객체 속성일 때배열 요소일 때
undefined, 함수, Symbol 값속성째 빠진다null
Symbol 키 속성빠진다-
NaN, Infinitynullnull
DatetoJSON()이 만든 ISO 문자열같다
Map, Set{}(내용이 사라진다)같다
BigIntTypeError같다
순환 참조TypeError: Converting circular structure to JSON같다
열거 불가 속성, 프로토타입의 속성빠진다-

그래서 JSON.stringify 왕복은 데이터를 조용히 잃는다. Date는 문자열이 되어 돌아오고, undefined 값을 가진 속성은 없던 일이 된다. Map은 Object.fromEntries(map)이나 [...map]으로 바꿔 보낸다(Map과 Set). BigInt는 문자열로 바꾸거나 아래 JSON.rawJSON을 쓴다(숫자와 Math).

replacer: 무엇을 어떻게 넣을지

두 번째 인자는 배열이나 함수다.

// 배열: 이 키만 넣는다. 중첩 객체 안의 키에도 같은 목록이 적용된다
JSON.stringify(meetup, ['title', 'participants', 'name'])
 
// 함수: 모든 (key, value)에 불린다. 반환값이 그 자리에 들어가고, undefined면 빠진다
JSON.stringify(meetup, (key, value) => (key === 'occupiedBy' ? undefined : value))
  • 함수는 중첩 객체와 배열 요소까지 재귀적으로 불린다. this는 지금 처리 중인 속성을 가진 객체다.
  • 첫 호출의 키는 빈 문자열 ''이고 값은 전체 객체다. 엔진이 { '': value }로 감싸서 시작하기 때문이다. 키로 거를 때 이 첫 호출을 걸러 내지 않도록 주의한다.
  • 순환 참조를 끊는 키를 빼거나, 비밀번호 같은 민감한 필드를 로그에서 지울 때 쓴다.

space: 사람이 읽을 모양으로

세 번째 인자는 들여쓰기다. 숫자(최대 10칸)나 문자열('\t')을 넘긴다. 전송용은 생략하고, 로그·디버깅·설정 파일 저장에 JSON.stringify(obj, null, 2)를 쓴다.

toJSON: 객체가 직접 정하기

객체에 toJSON() 메서드가 있으면 stringify는 그 반환값을 대신 직렬화한다. Date가 ISO 문자열이 되는 것도 내장 Date.prototype.toJSON 때문이다.

const room = { number: 23, toJSON() { return this.number } }
JSON.stringify({ title: 'Conference', room }) // '{"title":"Conference","room":23}'

클래스 인스턴스의 JSON 모양을 바꾸고 싶을 때는 toString이 아니라 toJSON을 재정의한다(TypeScript 코딩 컨벤션).

JSON.parse와 reviver

JSON.parse는 문자열을 값으로 되돌린다. JSON에는 Date 타입이 없어서 날짜는 문자열로 돌아온다. 두 번째 인자 reviver는 stringify의 replacer 함수처럼 모든 (key, value)에 불리고, 반환값이 그 자리를 대신한다. 안쪽 값부터 바깥으로 처리되므로 중첩된 날짜도 한 번에 바꾼다.

const text = '{"meetups":[{"title":"Conference","date":"2017-11-30T12:00:00.000Z"}]}'
 
const schedule = JSON.parse(text, (key, value) =>
  key === 'date' ? new Date(value) : value,
)
schedule.meetups[0].date.getUTCFullYear() // 2017

API 응답을 다룰 때는 reviver로 키 이름을 추측하기보다, 받은 뒤 스키마 검증 라이브러리로 타입을 확인하고 변환하는 편이 안전하다.

큰 정수 지키기: source 접근과 JSON.rawJSON

JSON.parse는 숫자를 모두 double로 읽으므로 2^53을 넘는 ID는 파싱하는 순간 반올림된다. reviver의 세 번째 인자 context.source로 원래 텍스트를 받아 BigInt로 만들고, 내보낼 때는 JSON.rawJSON으로 따옴표 없는 숫자 텍스트를 그대로 넣는다(Baseline 2025).

const data = JSON.parse('{"id":12345678901234567890}', (key, value, context) =>
  key === 'id' ? BigInt(context.source) : value,
)
data.id // 12345678901234567890n
 
JSON.stringify({ id: JSON.rawJSON(data.id.toString()) }) // '{"id":12345678901234567890}'

지원 범위가 넉넉하지 않은 환경에서는 처음부터 서버가 큰 ID를 문자열로 내려 주는 것이 가장 단순하다.

큰 데이터는 JSON.parse가 더 빨리 읽힌다

번들에 큰 정적 데이터(설정, 사전, 초기 상태)를 실어야 할 때는 객체 리터럴보다 JSON.parse('…') 문자열이 더 빨리 처리된다.

객체 리터럴 { "a": 1, … }JSON.parse('{"a":1,…}')
파서가 보는 것키·값·괄호마다 토큰. 중첩될수록 늘어난다호출식 하나와 문자열 리터럴 하나
문맥 판단{가 객체인지, 구조 분해인지, 블록인지 뒤를 봐야 안다JSON 문법은 단순해 「객체 시작이거나 오류」뿐이다

차이는 약 10KB 이상의 큰 데이터에서만 의미가 있다(V8 블로그 「The cost of parsing JSON」의 경험칙, 바꾸기 전에 측정한다). 손으로 바꾸지 않는다. webpack과 Vite는 .json 모듈을 이 형태로 바꿔 내보낼 수 있으므로(Vite는 json.stringify 옵션) 큰 데이터는 .json 파일로 분리해 import한다. Date·Map·undefined처럼 JSON이 표현하지 못하는 값이 있으면 적용할 수 없다. 서버가 그린 HTML에 초기 상태를 실을 때도 같은 이유로 JSON.parse와 문자열을 쓴다(SPA와 CSR·SSR).

JSON으로 복사하지 않는다

JSON.parse(JSON.stringify(obj))는 오랫동안 깊은 복사의 우회법이었다. 위 표처럼 Date·Map·undefined·순환 참조에서 데이터가 깨지므로, 지금은 structuredClone(obj)를 쓴다(변수와 데이터 타입의 「복사하기」).

정리

  • JSON은 큰따옴표·주석 없음·제한된 값 타입의 엄격한 텍스트 형식이다. 외부 입력의 parse는 try...catch로 감싼다.
  • stringify는 undefined·함수·Symbol을 빼고, NaN은 null, Date는 문자열, Map은 {}로 만든다. BigInt와 순환 참조는 에러다.
  • 넣을 것을 고르려면 replacer, 보기 좋게는 space, 객체가 스스로 정하려면 toJSON.
  • 날짜 복원은 reviver, 큰 정수는 context.source와 JSON.rawJSON 또는 문자열 ID.
  • 깊은 복사는 JSON 왕복이 아니라 structuredClone.

관련 노트: 배열·객체 다루기, 변수와 데이터 타입, Map과 Set, 숫자와 Math, AJAX와 HTTP 요청, 객체의 원시형 변환

원본 기록6

댓글

GitHub 계정으로 댓글을 남길 수 있어요.