Study JavaScript 0531 - JSON과 메소드

변승훈·2022년 5월 31일
0

Study JavaScript

목록 보기
25/43

JSON과 메소드

객체를 문자열로 전환을 할 때 toString메소드를 사용한다. 그런데 개발 과정에서 해당 객체에서 프로퍼티가 추가되거나 삭제, 수정이 될 수 있다.
이렇게 되면 매번 수정을 해야하기 때문에 번거로울 것이다.
javascript에서는 이런 문제를 해결해주는 방법이 있는데 지금부터 알아보도록 하자.

1. JSON.stringify

JSON(JavaScript Object Notation)은 value나 객체를 나타내주는 범용 포맷이다. JSON은 본래 javascript에서 사용할 목적으로 만들어진 포맷이지만 라이브러리를 사용하면서 다른 언어에서도 사용하게 되었다.
JSON을 사용하는 주 목적은 데이터 교환이다.

Javascript가 제공하는 JSON관련 메소드는 다음과 같다.

  • JSON.stringify: 객체를 JSON으로 바꿔준다.
  • JSON.parse: JSON을 객체로 바꿔준다.

예시를 통해 JSON.stringify를 적용해보자!

let student = {
  name: 'Hun',
  age: 27,
  isAdmin: false,
  courses: ['html', 'css', 'js'],
  wife: null
};

let json = JSON.stringify(student);

console.log(typeof json); // 문자열

console.log(json);
/* JSON으로 인코딩된 객체:
{
  "name": "Hun",
  "age": 27,
  "isAdmin": false,
  "courses": ["html", "css", "js"],
  "wife": null
}
*/

이렇게 JSON.stringify로 변경된 문자열은 JSON으로 인코딩된, 직렬화 처리 된, 문자열로 변환 된, 결집 된 객체라고 부른다.
객체는 이렇게 문자열로 변환된 후에야 비로소 네트워크를 통해 전송하거나 저장소에 저장할 수 있다.

JSON으로 인코딩된 객체는 일반 객체와 다른 특징을 보인다.

  • 문자열은 큰따옴표로 감싸야 한다. JSON에선 작은따옴표나 백틱을 사용할 수 없다('Hun'이 "Hun"으로 변경된 것을 통해 이를 확인할 수 있다).
  • 객체 프로퍼티 이름은 큰따옴표로 감싸야 한다(age:27이 "age":27으로 변한 것을 통해 이를 확인할 수 있다).

JSON.stringify는 객체 뿐만 아니라 원시값에도 적용할 수 있다.
적용할 수 있는 자료형은 아래와 같다.

  • 객체 {...}
  • 배열 [...]
  • 원시형: 문자형, 숫자형, 불린형 값 true와 false, null
// 숫자를 JSON으로 인코딩하면 숫자
console.log( JSON.stringify(1) ) // 1

// 문자열을 JSON으로 인코딩하면 문자열(다만, 큰따옴표가 추가된다).
console.log( JSON.stringify('test') ) // "test"

console.log( JSON.stringify(true) ); // true

console.log( JSON.stringify([1, 2, 3]) ); // [1,2,3]

JSON은 데이터 교환을 목적으로 만들어진 언어에 종속되지 않는 포맷이다. 따라서 javascript 특유의 객체 프로퍼티는 JSON.stringify가 처리할 수 없다.

JSON.stringify 호출 시 무시되는 프로퍼티는 아래와 같다.

  • 함수 프로퍼티 (메서드)
  • 심볼형 프로퍼티 (키가 심볼인 프로퍼티)
  • 값이 undefined인 프로퍼티
let user = {
  sayHi() { // 무시
    alert("Hello");
  },
  [Symbol("id")]: 123, // 무시
  something: undefined // 무시
};

console.log( JSON.stringify(user) ); // {} (빈 객체가 출력됨)

대개 이 프로퍼티들은 무시 되도 괜찮다. 그런데 이들도 문자열에 포함시켜야 하는 경우가 생기곤 하는데 이에 대해선 아래에서 다루도록 하자.

JSON.stringify의 장점 중 하나는 중첩 객체도 알아서 문자열로 바꿔준다는 점이다.

let meetup = {
  title: "Conference",
  room: {
    number: 23,
    participants: ["john", "ann"]
  }
};

console.log( JSON.stringify(meetup) );
/* 객체 전체가 문자열로 변환되었습니다.
{
  "title":"Conference",
  "room":{"number":23,"participants":["john","ann"]},
}
*/

JSON.stringify를 사용할 때 주의해야 할 점이 하나 있는데 순환 참조가 있으면 원하는 대로 객체를 문자열로 바꾸는 게 불가능하다는 것이다.

let room = {
  number: 23
};

let meetup = {
  title: "Conference",
  participants: ["john", "ann"]
};

meetup.place = room;       // meetup은 room을 참조합니다.
room.occupiedBy = meetup; // room은 meetup을 참조합니다.

JSON.stringify(meetup); // Error: Converting circular structure to JSON

room.occupiedBymeetup을, meetup.placeroom을 참조하기 때문에 JSON으로의 변환이 실패했다.

2. replacer로 원하는 프로퍼티만 직렬화하기

JSON.stringify의 전체 문법은 아래와 같다.

let json = JSON.stringify(value[, replacer, space])
  • value
    인코딩 하려는 값
  • replacer
    JSON으로 인코딩 하길 원하는 프로퍼티가 담긴 배열. 또는 매핑 함수 function(key, value)
  • space
    서식 변경 목적으로 사용할 공백 문자 수

대다수의 경우 JSON.stringify엔 인수를 하나만 넘겨서 사용한다. 그런데 순환 참조를 다뤄야 하는 경우같이 전환 프로세스를 정교하게 조정하려면 두 번째 인수를 사용해야 한다.

JSON으로 변환하길 원하는 프로퍼티가 담긴 배열을 두 번째 인수로 넘겨주면 이 프로퍼티들만 인코딩할 수 있다.

let room = {
  number: 23
};

let meetup = {
  title: "Conference",
  participants: [{name: "John"}, {name: "Alice"}],
  place: room // meetup은 room을 참조
};

room.occupiedBy = meetup; // room references meetup

console.log( JSON.stringify(meetup, ['title', 'participants']) );
// {"title":"Conference","participants":[{},{}]}

배열에 넣어준 프로퍼티가 잘 출력된 것을 확인할 수 있다. 그런데 배열에 name을 넣지 않아서 출력된 문자열의 participants가 텅 비어버렸다. 규칙이 너무 까다로워서 발생한 문제다.

순환 참조를 발생시키는 프로퍼티 room.occupiedBy만 제외하고 모든 프로퍼티를 배열에 넣어보자.

let room = {
  number: 23
};

let meetup = {
  title: "Conference",
  participants: [{name: "John"}, {name: "Alice"}],
  place: room // meetup references room
};

room.occupiedBy = meetup; // room references meetup

console.log( JSON.stringify(meetup, ['title', 'participants', 'place', 'name', 'number']) );
/*
{
  "title":"Conference",
  "participants":[{"name":"John"},{"name":"Alice"}],
  "place":{"number":23}
}
*/

occupiedBy를 제외한 모든 프로퍼티가 직렬화되었습니다. 그런데 배열이 좀 길다는 느낌이 든다.

replacer 자리에 배열 대신 함수를 전달해 이 문제를 해결해 보자.

replacer에 전달되는 함수(replacer 함수)는 프로퍼티 (ket, value) 쌍 전체를 대상으로 호출되는데, 반드시 기존 프로퍼티 값을 대신하여 사용할 값을 반환해야 한다. 특정 프로퍼티를 직렬화에서 누락시키려면 반환 값을 undefined로 만들면 된다.

아래 예시는 occupiedBy를 제외한 모든 프로퍼티의 값을 변경 없이 “그대로” 직렬화하고 있다. occupiedBy는 undefined를 반환하게 해 직렬화에 포함하지 않은 것도 확인해 보길 바란다.

let room = {
  number: 23
};

let meetup = {
  title: "Conference",
  participants: [{name: "John"}, {name: "Alice"}],
  place: room // meetup은 room을 참조합니다
};

room.occupiedBy = meetup; // room은 meetup을 참조합니다

console.log( JSON.stringify(meetup, function replacer(key, value) {
  console.log(`${key}: ${value}`);
  return (key == 'occupiedBy') ? undefined : value;
}));

/* replacer 함수에서 처리하는 키:값 쌍 목록
:             [object Object]
title:        Conference
participants: [object Object],[object Object]
0:            [object Object]
name:         John
1:            [object Object]
name:         Alice
place:        [object Object]
number:       23
*/

replacer 함수가 중첩 객체와 배열의 요소까지 포함한 모든 key-value 쌍을 처리하고 있다는 점에 주목하자. replacer 함수는 재귀적으로 key-value 쌍을 처리하는데, 함수 내에서 this는 현재 처리하고 있는 프로퍼티가 위치한 객체를 가리킨다.

console.log에 예상치 못한 문자열(":[object Object]")이 뜨는걸 볼 수 있는데, 이는 함수가 최초로 호출될 때 {"": meetup} 형태의 "래퍼 객체"가 만들어지기 때문이다. replacer함수가 가장 처음으로 처리해야하는 (key, value) 쌍에서 key는 빈 문자열, value은 변환하고자 하는 객체(meetup) 전체가 되는 것이다.

이렇게 replacer 함수를 사용하면 중첩 객체 등을 포함한 객체 전체에서 원하는 프로퍼티만 선택해 직렬화 할 수 있다.

3. space로 가독성 높이기

JSON.stringify(value, replacer, space)의 세 번째 인수 space는 가독성을 높이기 위해 중간에 삽입해 줄 공백 문자 수를 나타낸다.

지금까진 space 없이 메서드를 호출했기 때문에 인코딩된 JSON에 들여쓰기나 여분의 공백문자가 하나도 없었다. space는 가독성을 높이기 위한 용도로 만들어졌기 때문에 단순 전달 목적이라면 space 없이 직렬화하는 편이다.

아래 예시처럼 space에 2를 넘겨주면 자바스크립트는 중첩 객체를 별도의 줄에 출력해주고 공백 문자 두 개를 써 들여쓰기 해준다.

let user = {
  name: "John",
  age: 25,
  roles: {
    isAdmin: false,
    isEditor: true
  }
};

console.log(JSON.stringify(user, null, 2));
/* 공백 문자 두 개를 사용하여 들여쓰기함:
{
  "name": "John",
  "age": 25,
  "roles": {
    "isAdmin": false,
    "isEditor": true
  }
}
*/

/* JSON.stringify(user, null, 4)라면 아래와 같이 좀 더 들여써진다.
{
    "name": "John",
    "age": 25,
    "roles": {
        "isAdmin": false,
        "isEditor": true
    }
}
*/

이처럼 매개변수 space는 로깅이나 가독성을 높이는 목적으로 사용된다.

4. 커스텀 “toJSON”

toString을 사용해 객체를 문자형으로 변환시키는 것처럼, 객체에 toJSON이라는 메서드가 구현되어 있으면 객체를 JSON으로 바꿀 수 있을 것이다. JSON.stringify는 이런 경우를 감지하고 toJSON을 자동으로 호출해준다.

let room = {
  number: 23
};

let meetup = {
  title: "Conference",
  date: new Date(Date.UTC(2017, 0, 1)),
  room
};

console.log( JSON.stringify(meetup) );
/*
  {
    "title":"Conference",
    "date":"2017-01-01T00:00:00.000Z",  // (1)
    "room": {"number":23}               // (2)
  }
*/

(1)에서 Date 객체의 내장 메서드 toJSON이 호출되면서 date의 값이 문자열로 변환된 걸 확인할 수 있다.

이번엔 room에 직접 커스텀 메서드 toJSON을 추가해보자. 그리고 (2)로 표시한 줄이 어떻게 변경되는지 확인해 봅시다.

let room = {
  number: 23,
  toJSON() {
    return this.number;
  }
};

let meetup = {
  title: "Conference",
  room
};

console.log( JSON.stringify(room) ); // 23

console.log( JSON.stringify(meetup) );
/*
  {
    "title":"Conference",
    "room": 23
  }
*/

위와 같이 toJSONJSON.stringify(room)를 직접 호출할 때도 사용할 수 있고, oom과 같은 중첩객체에도 구현하여 사용할 수 있다.

5. JSON.parse

JSON.parse를 사용하면 JSON으로 인코딩된 객체를 다시 객체로 디코딩 할 수 있다.

문법:

let value = JSON.parse(str, [reviver]);
  • str
    JSON 형식의 문자열
  • reviver
    모든 (key, value) 쌍을 대상으로 호출되는 function(key,value) 형태의 함수로 값을 변경시킬 수 있다.
// 문자열로 변환된 배열
let numbers = "[0, 1, 2, 3]";

numbers = JSON.parse(numbers);

console.log( numbers[1] ); // 

JSON.parse는 아래와 같이 중첩 객체에도 사용할 수 있다.

let userData = '{ "name": "John", "age": 35, "isAdmin": false, "friends": [0,1,2,3] }';

let user = JSON.parse(userData);

console.log( user.friends[1] ); // 1

중첩 객체나 중쳡 배열이 있다면 JSON도 복잡해지기 마련인데, 그렇더라도 결국엔 JSON 포맷 지켜야 한다.

아래에서 디버깅 등의 목적으로 직접 JSON을 만들 때 흔히 저지르는 실수 몇 개를 간추려보았다. 참고하여 이와 같은 실수를 저지르지 말자!

let json = `{
  name: "John",                     // 실수 1: 프로퍼티 이름을 큰따옴표로 감싸지 않았다.
  "surname": 'Smith',               // 실수 2: 프로퍼티 값은 큰따옴표로 감싸야 하는데, 작은따옴표로 감쌌다.
  'isAdmin': false                  // 실수 3: 프로퍼티 키는 큰따옴표로 감싸야 하는데, 작은따옴표로 감쌌다.
  "birthday": new Date(2000, 2, 3), // 실수 4: "new"를 사용할 수 없습니다. 순수한 값(bare value)만 사용할 수 있다.
  "friends": [0,1,2,3]              // 이 프로퍼티는 괜찮다.
}`;

JSON은 주석을 지원하지 않는다는 점도 기억해 놓자. 주석을 추가하면 유효하지 않은 형식이 된다.

key를 큰따옴표로 감싸지 않아도 되고 주석도 지원해주는 JSON5라는 포맷도 있는데, 이 포맷은 자바스크립트 명세서에서 정의하지 않은 독자적인 라이브러리이다.

JSON 포맷이 까다로운 규칙을 가지게 된 이유는 쉽고 빠르며 신뢰할 수 있을 만한 파싱 알고리즘을 구현하기 위해서이다.

6. reviver 사용하기

서버로부터 문자열로 변환된 meetup 객체를 전송받았다고 가정해보자.

전송받은 문자열은 아래와 같이생겼다.

// title: (meetup 제목), date: (meetup 일시)
let str = '{"title":"Conference","date":"2017-11-30T12:00:00.000Z"}';

이제 이 문자열을 역 직렬화(deserialize) 해서 자바스크립트 객체를 만들어보자.

JSON.parse를 호출해보자

let str = '{"title":"Conference","date":"2017-11-30T12:00:00.000Z"}';

let meetup = JSON.parse(str);

console.log( meetup.date.getDate() ); // 에러!

에러가 발생한다.

meetup.date의 값은 Date 객체가 아니고 문자열이기 때문에 발생한 에러이다. 그렇다면 문자열을 Date로 전환해줘야 한다는 걸 어떻게 JSON.parse에게 알릴 수 있을까?

이럴 때 JSON.parse의 두 번째 인수 reviver를 사용하면 된다. 모든 값은 “그대로”, 하지만 date만큼은 Date 객체를 반환하도록 함수를 구현해 보자.

let str = '{"title":"Conference","date":"2017-11-30T12:00:00.000Z"}';

let meetup = JSON.parse(str, function(key, value) {
  if (key == 'date') return new Date(value);
  return value;
});

console.log( meetup.date.getDate() ); // 이제 제대로 동작한다!

참고로 이 방식은 중첩 객체에도 적용할 수 있다.

let schedule = `{
  "meetups": [
    {"title":"Conference","date":"2017-11-30T12:00:00.000Z"},
    {"title":"Birthday","date":"2017-04-18T12:00:00.000Z"}
  ]
}`;

schedule = JSON.parse(schedule, function(key, value) {
  if (key == 'date') return new Date(value);
  return value;
});

console.log( schedule.meetups[1].date.getDate() ); // 잘 동작한다
profile
잘 할 수 있는 개발자가 되기 위하여

0개의 댓글