본문 바로가기
JAVASCRIPT

[JS][강제변환] 추상 연산 ToString

by KBS 2022. 2. 8.
728x90

추상 연산

명시적/암시적 강제변환의 세계로 떠나기 전에 어떻게 값이 문자열, 숫자, 불리언 등의 타입이 되는지, 그 기본 규칙을 알아보자. ES5를 보면 변환 규칙의 '추상 연산'이 정의되어 있다. ToString, ToNumber, ToBoolean을 집중적으로 보고 ToPrimitive는 대략만 훑어 보자.

ToString

'문자열이 아닌 값 -> 문자열' 변환 작업은 ES5 $9.8의 ToString 추상 연산 로직이 담당한다. 내장 원시 값은 본연의 무자열화 방법이 정해져 있다(ex) null -> "null"). 숫자는 예상대로 그냥 문자열로 바뀌고 너무 작거나 큰 값은 지수 형태로 바뀐다.

 

일반 객체는 특별히 지정하지 않으면 기본적으로 toString() 메서드가 내부 [[class]]를 반환한다.

 

자신의 toString() 메서드를 가진 객체는 문자열 처럼 사용하면 자동으로 이 메서드가 기본 호출되어 toString()을 대체한다.

엄밀히는 '객체 -> 문자열'강제변환시 ToPrimitive 추상연산 과정을 거친다.

 

배열은 기본적으로 재정의된 toString()이 있다. 문자열 변환시 모든 원소 값이 콤마로 분리된 형태로 이어진다.

 

var a = [1, 2, 3];
a.toString(); //("1, 2, 3");

 

또한 toString() 메서드는 명시적으로도 호출 가능하며, 문자열 콘텍스트에서 문자열이 아닌 값이 있을 경우에도 자동 호출된다.

 

JSON 문자열화

 

ToStringJSON.stringify() 유틸리티를 사용하여 어떤 값을 JSON 문자열로 직렬화하는 문제와도 연관된다.

 

JSON 문자열화는 강제변환과 똑같지는 않지만, 방금 전 살펴본 ToString 규칙과 관련 잇으므로 JSON 문자열 화에 대해 알아보자.

 

대부분 단순 값들은 직렬화 결과가 반드시 문자열이라는 점을 제외하고는, JSON 문자열화나 toString() 변환이나 기본적으로 같은 로직이다.

 

JSON.stringify(42); // "42"
JSON.stringify("42"); // ""42"" (따옴표가 붙은 문자열 인자를 문자열화 한다.)
JSON.stringify(null); // "null"
JSON.stringify(true); // "true"

 

JSON 안전값(JSON 표현형으로 확실히 나타낼 수 있는 값)은 모두 JSON.stringify()로 문자열화 할 수 있다.

 

JSON 안전 값이 아닌 것들을 반대로 떠올리면 이해가 빠를 것이다. 이들은 모두 다른 언어로 인식하여 JSON 값으로 쓸 수 없는, 표준 JSON 규격을 벗어난 값이다.

 

JSON.stringify()는 인자가 undefined, 함수, 심벌 값이면 자동으로 누락시키며 이런 값들이 만약 배열에 포함되어 있으면 null로 바꾼다. 객체 프로퍼티에 있으면 간단히 지워버린다.

 

혹시라도 JSON.stringify()에 환형 참조 객체를 넘기면 에러가난다. 객체 자체에 toJSON() 메서드가 정의되어 있다면, 먼저 이 메서드를 호출하여 직렬화한 값을 반환한다.

 

부적절한 JSON 값이나 직렬화하기 곤란한 객체 값을 문자열화 하려면 toJSON() 메서드를 따로 정의해야 한다. 예를 들면,

 

var o = {};

var a = {
  b: 42,
  c: 0,
  d: function () {},
};

// 'a'를 환형 참조 객체로 만든다.
o.e = a;

// 환형 참조 객체는 JSON 문자열화 시 에러가 난다.
// JSON.stringify( a );

// JSON 값으로 직렬화하는 함수를 따로 정의한다.
a.toJSON = function () {
  // 직렬화에 프로퍼티 'b'만 포함시킨다.
  return { b: this.b };
};

JSON.stringify(a); // "{b : 42}"

 

toJSON()이 JSON 문자열 표현형을 반환하리라 넘겨짚는건 아주 흔한 오해다. 문자열을 문자열화할 의도가 아니라면 정확하지 않을 가능성이 높다. toJSON()은 적절히 평범한 실제 값을 반환하고 문자열화 처리는 JSON.stringify()이 담당한다.

 

다시말해 toJSON()의 역할은 '문자열화하기 적당한 JSON 안전 값으로 바꾸는 것'이지, 'JSON 문자열로 바꾸는 것'이 아니다. 많은 개발자가 잘못 알고 있는 부분이다.

 

var a = {
  val: [1, 2, 3],

  // 맞다!
  toJSON: function () {
    return this.val.slice(1);
  },
};

var b = {
  val: [1, 2, 3],

  // 틀렸다!
  toJSON: function () {
    return "[" + this.val.slice(1).join() + "]";
  },
};

JSON.stringify(a); // "[2, 3]"
JSON.stringify(b); // ""[2, 3]""

 

두 번째 호출 코드는 배열 자체가 아니라 반환된 문자열을 다시 문자열화 한다. 이건 개발자가 의도했던 바가 아닐 것이다.

 

JSON.stringify()를 얘기하다 보니, 잘 알려지지 않은 유용한 기능 하나를 보자. 배열 아니면 함수 형태의 대체자를 JSON.stringify()의 두 번째 선택 인자로 지정하여 객체를 재귀적으로 직렬화 하면서 필터링 하는 방법이 있다. toJSON()이 직렬화할 값을 준비하는 방식과 비슷하다.

 

대체자가 배열이면 전체 원소는 문자열이어야 하고 각 원소는 객체 직렬화의 대상 프로퍼티명이다. 즉, 여기에 포함되지 않은 프로퍼티는 직렬화 과정에서 빠진다.

 

대체자가 함수면 처음 한 번은 객체 자신에 대해, 그다음엔 각 객체 프로퍼티별로 한 번씩 실행하면서 매번 키와 값 두 인자를 전달한다. 직려로하 과정에서 해당 키를 건너뛰려면 undefiend를, 그외엔 해당 값을 반환한다.

 

var a = {
  b: 42,
  c: "42",
  d: [1, 2, 3],
};

JSON.stringify(a, ["b", "c"]); // "{"b" : 42, "c" : "42"}"
JSON.stringify(a, function (k, v) {
  if (k !== "c") return v;
});
//  "{"b" : 42, "d" : "[1, 2, 3]"}"

함수인 대체자는 최초 호축시 키 인자 k는 undefined다. 대체자는 if 문에서 키가 "c"인 프로퍼티를 솎아낸다. 문자열화는 재귀적으로 이루어지므로 배열 [1, 2, 3]의 각 원소는 v로, 인덱스는 k로 각각 대체자 함수에 전달된다.

JSON.stringify()은 세 번째 선택 인자는 스페이스라고 하며 사람이 읽기 쉽도록 들여쓰기를 할 수 있다. 들여 쓰기를 할 빈 공간의 개수를 숫자로 지정하거나 문자열을 지정하여 각 들여 쓰기 수준에 사용한다.

 

var a = {
  b: 42,
  c: "42",
  d: [1, 2, 3],
};

JSON.stringify(a, null, 3);
// "{
//    "b": 42,
//    "c": "42",
//    "d": [
//      1,
//      2,
//      3
//    ]
// }"

 

JSON.stringify()은 직접적인 강제변환의 형식은 아니지만 두 가지 이유로 ToString 강제 변환과 연관된다.

 

  1. 문자열, 숫자, 불리언, null 값이 JSON으로 문자열화 하는 방식은 ToString 추상 연산의 규칙에 따라 문자열 값으로 강제변환 되는 방식과 동일하다.
  2. JSON.stringify()에 전달한 객체가 자체 toJSON() 메서드를 가지고 있다면, 문자열화 전 toJSON()가 자동 호출되어 JSON 안전 값으로 '강제변환'된다.

참고

  • You Don't Know JS ( 한빛 미디어 )
728x90

댓글