🌈 Chapter 4 : 강제변환
Table of Contents
🎯 값 변환
- 어떤 값을 다른 타입의 값으로 바꾸는 과정이 명시적이면 타입 캐스팅(Type Casting), 암시적이면 강제변환(Coercion) 이라고 한다.
- 항상 그렇지 않을 수도 있지만 자바스크립트에서 강제변환을 하면 문자열, 숫자, 불리언 같은 스칼라 원시 값 중 하나가 되며, 객체, 함수 같은 합성 값 타입으로 변환될 일은 없다.
- 이렇게도 구분할 수 있는데, 타입 캐스팅(타입 변환)은 정적 타입 언어에서 컴파일 시점에, 강제변환은 동적 타입 언어에서 런타임 시점에 발생한다.
- 그러나 자바스크립트에서는 대부분 모든 유형의 타입변환을 강제변환으로 뭉뚱그려 일컽는 경향이 있어서, 암시적 강제변환과 명시적 강제변환 두 가지로 구별할 수도 있다.
- 명시적 강제변환은 의도적으로 타입변환을 일으킨다는 사실이 명백한 반면, 암시적 강제변환은 다른 작업 도중 불분명한 부수 효과(Side Effect)로부터 발생하는 타입변환이다.
var a = 42;
var b = a + ''; // 암시적 강제변환
var c = String(a); // 명시적 강제변환
- 둘의 차이는 엄밀히 말해 스타일의 차이뿐 아니라 작동상에도 미묘한 차이가 있다.
- 명시적(Explicit) : 암시적(Implicit) = 명백한(Obvious) : 숨겨진 부수 효과(Hidden Side Effect) 용어상으로는 이러한 대응 관계가 성립
🎯 추상 연산
📚 ToString
- 문자열이 아닌 값 -> 문자열 변환 작업은 ES5 9.8의
ToString
추상 연산 로직이 담당한다.
var a = 1.07 * 1000 * 1000 * 1000 * 1000 * 1000 * 1000 * 1000;
a; // 1.07e+21
a.toString(); // "1.07e+21"
- 일반 객체는 특별히 지정하지 않으면 기본적으로
toString()
메서드가 내부[[Class]]
를 반환한다. - 자신의
toString()
메서드를 가진 객체는 문자열처럼 사용하면 자동으로 이 메서드가 기본 호출되어toString()
을 대체한다. - 배열은 기본적으로 재정의된
toString()
이 있다. 문자열 변환 시 모든 원소 값이 콤마(.)로 분리된 형태로 이어진다.
var a = [1, 2, 3];
a.toString(); // "1,2,3"
- 또한
toString()
메서드는 명시적으로도 호출 가능하며, 문자열 콘텍스트에서 문자 열 아닌 값이 있을 경우에도 자동 호출된다.
📌 JSON 문자열화
ToString
은JSON.stringify()
유틸리티를 사용하여 어떤 값을JSON
문자열로 직렬화하는 문제와도 연관된다.JSON
문자열화는 강제변환과 똑같지는 않지만,ToString
규칙과 관련이 있다.- 대부분 단순 값들은 직렬화 결과가 반드시 문자열이라는 점을 제외하고는,
JSON
문자열화나toString()
변환이나 기본적으로 같은 로직이다.
JSON.stringify(42); // "42"
JSON.stringify("42"); // ""42""
JSON.stringify(null); // "null"
JSON.stringify(true); // "true"
JSON
안전 값(JSON
표현형으로 확실히 나타낼 수 있는 값)은 모두JSON.stringify()
로 문자열화할 수 있다.JSON
안전 값이 아닌 것들은undefined
, 함수, 심벌, 환형 참조 객체(프로퍼티 참조가 무한 순환되는 구조의 객체) 같은 것들이다.- 만약
JSON.stringify()
는 안전 값이 아닌 것들은 자동으로 누락시키며 만약 배열에 포함되어 있으면null
로 바꾼다. 객체 프로퍼티에 있으면 지워버린다.
JSON.stringify(undefined); // undefined
JSON.stringify(function(){}); // undefined
JSON.stringify([1, undefined, function(){}, 4]); // "[1,null,null,4]"
JSON.stringify({ a:2, b: function(){} }) // "{"a":2}"
- 환형 참조 객체를 넘기면 에러가 발생한다.
- 객체 자체에
toJSON()
메서드가 정의되어 있다면 먼저 이 메서드를 호출하여 직렬화한 값을 반환한다. - 부적절한
JSON
값이나 직렬화하기 곤란한 객체 값을 문자열화하려면toJSON()
메서드를 따로 정의해야 한다.
var o = {};
var a = {
b: 42,
c: o,
d: function(){}
};
// a를 환형 참조 객체로 만든다.
o.e = a;
// 에러 발생
JSON.stringify(a); // Uncaught TypeError: Converting circular structure to JSON
// JSON 값으로 직렬화하는 함수를 따로 정의한다.
a.toJSON = function() {
return { b: this.b }; // 직렬화에 b만 포함시킨다.
}
JSON.stringify(a); // "{"b":42}"
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()
의 두 번째 선택 인자로 지정하여 객체를 재귀적으로 직렬화하면서 필터링 하는 방법이 있다.toJSON()
이 직렬화할 값을 준비하는 방식과 비슷하다. - 대체자가 배열이면 전체 원소는 문자열이어야 하고 각 원소는 객체 직렬화의 대상 프로퍼티명이다. 즉, 여기에 포함되지 않은 프로퍼티는 직렬화 과정에서 빠진다.
- 대체자가 함수면 처음 한 번은 객체 자신에 대해, 그 다음엔 각 객체 프로퍼티별로 한 번씩 실행하면서 매번 키와 값 두 인자를 전달한다. 직렬화 과정에서 해당 키를 건너뛰려면
undefined
를 그 외엔 해당 값을 반환한다.
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
(1,2,3)로, 인덱스는k
(0,1,2)로 각각 대체자 함수에 전달된다. JSON.stringify()
의 세 번째 선택 인자는 스페이스라고 하며 사람이 읽기 쉽도록 들여쓰기를 할 수 있다.- 들여 쓰기를 할 빈 공간의 개수를 숫자로 지정하거나 문자열(10자 이상이면 앞에서 10자까지만 잘라 사용한다)을 지정하여 각 들여 쓰기 수준에 사용한다.
var a = {
b: 42,
c: '42',
d: [1, 2, 3]
};
JSON.stringify(a, null, 3);
// "{
// "b": 42,
// "c": "42",
// "d": [
// 1,
// null,
// 3
// ]
// }"
JSON.stringify(a, null, '-----');
// "{
// -----"b": 42,
// -----"c": "42",
// -----"d": [
// ----------1,
// ----------null,
// ----------3
// -----]
// }"
JSON.stringify()
는 직접적인 강제변환의 형식은 아니지만 두 가지 이유로ToString
강제변환과 연관된다.- 문자열, 숫자, 불리언,
null
값이JSON
으로 문자열화하는 방식은ToString
추상 연산의 규칙에 따라 문자열 값으로 강제변환되는 방식과 동일하다. JSON.stringify()
에 전달한 객체가 자체toJSON()
메서드를 갖고 있다면, 문자열화 전toJSON()
이 자동 호출되어JSON
안전 값으로 강제변환된다.
- 문자열, 숫자, 불리언,
📚 ToNumber
- 숫자가 아닌 값 -> 수식 연산이 가능한 숫자 변환 로직은 예를 들어
true
는 1,false
는 0이 되고,undefined
는NaN
으로,null
은 0으로 바뀐다. - 문자열 값에
ToNumber
를 적용하면 대부분 숫자 리터럴 규칙/구문과 비슷하게 작동한다. - 변환이 실패하면 결과는
NaN
이다. - 한 가지 차이는 0이 앞에 붙은 8진수는
ToNumber
에서 올바른 숫자 리터럴이라도 8진수로 처리하지 않는다.(10진수로 처리) - 객체(또는 배열)는 동등한 원시 값으로 변환 후 그 결괏값(아직 숫자가 아닌 원시 값)을 앞서 설명한
ToNumber
규칙에 의해 강제변환한다. - 동둥한 원시 값으로 바꾸기 위해
ToPrimitive
추상 연산 과정에서 해당 객체가valueOf()
메서드를 구현했는지 확인한다. valueOf()
를 쓸 수 있고 반환 값이 원시 값이면 그대로 강제변환하되, 그렇지 않을 경우 (toString()
메서드가 존재하면)toString()
을 이용하여 강제변환한다.- 원시 값으로 바꿀 수 없을 땐
TypeError
오류를 던진다. - ES5부터는
[[Prototype]]
가null
인 경우 대부분Object.create(null)
를 이용하여 강제변환이 불가능한 객체(valueOf()
,toString()
메서드가 없는 객체)를 생성할 수 있다.
var a = {
valueOf: function() {
return '42';
}
};
var b = {
toString: function() {
return '42';
}
};
var c = [4, 2];
c.toString = function() {
return this.join(''); // '42'
};
Number(a); // 42
Number(b); // 42
Number(c); // 42
Number(''); // 0
Number([]); // 0
Number(['abc']); // NaN
📚 ToBoolean
- 1을
true
로, 0을false
로 강제변환할 수는 있지만 그렇다고 두 값이 똑같은 건 아니다.
📌 Falsy 값
true/false
가 아닌 값을 불리언에 상당한 값으로 강제변환했을 때, 이 값들은?- 자바스크립트 모든 값은 다음 둘 중 하나다.
- 불리언으로 강제변환하면
false
가 되는 값 - 1번을 제외한 나머지(즉, 명백히
true
인 값)
- 불리언으로 강제변환하면
- 명세가 정의한
falsy
값은undefined
,null
,false
,+0
,-0
,NaN
,''
이다. - 위
falsy
값은 불리언으로 강제변환하면false
이다. - 반대로 위 값들 중 없으면
truthy
이다. 하지만 자바스크립트 명세에는truthy
값 목록 같은 건 없다. - 모든 객체는 명백히
truthy
하다는 식의 몇 가지 예시만 있을 뿐falsy
값 목록에 없으면 응당truthy
값이 되는 것이다.
📌 Falsy 객체
var a = new Boolean(false);
var b = new Number(0);
var c = new String('');
var d= Boolean(a && b && c);
d; // true
- a, b, c는 명백히
falsy
값을 감싼 객체이다. - d가
true
인 것으로 봐서 세 변수는 모두true
이다. - 일반적인 자바스크립트의 의미뿐 아니라 브라우저만의 특이한 작동 방식을 가진 값을 생성하는 경우가 있는데, 이것이 바로
falsy
객체의 정체이다. falsy
객체는 불리언으로 강제변환하면false
이다.
📌 truthy 값
var a = 'false';
var b = '0';
var c = "''";
var d = Boolean(a && b && c);
d; // true
- 문자열 값을 보면
falsy
처럼 보이지만 문자열 값 자체는 모두truthy
이기 때문이다.
var a = []; // 빈 배열
var b = {}; // 빈 객체
var c = function(){}; // 빈 함수
var d = Boolean(a && b && c);
d; // true
- 외향은
falsy
처럼 생겼지만 어떻든[]
,{}
,function(){}
는falsy
값 목록에 없으므로 모두truthy
값이다. truthy
값 목록은 사실상 무한하여 일일이 작성하는 게 불가능하다.truthy/falsy
개념은 어떤 값을 불리언 타입으로 (명시적/암시적) 강제변환 시 해당 값의 작동 방식을 이해 한다는 점에서 중요하다.
🎯 명시적 강제변환
- 명시적 강제변환(Explicit Coercion)은 분명하고 확실한 타입변환이다.
📚 명시적 강제변환: 문자열 ↔ 숫자
- 문자열 ↔ 숫자 강제변환은
String()
과Number()
함수를 이용하는데, 앞에new
키워드가 붙지 않기 때문에 객체 래퍼를 생성하는 것이 아니란 점이다.
var a = 42;
var b = String(a);
var c = '3.14';
var d = Number(c);
b; // '42'
d; // 3.14
ToString
추상 연산 로직에 따라String()
은 값을 받아 원시 문자열로 강제 변환한다.Number()
역시 마찬가지로ToNumber
추상 연산 로직에 의해 어떤 값이든 원시 숫자 값으로 강제변환한다.String()
과Number()
이외에도 문자열 ↔ 숫자의 명시적인 타입변환 방법은 또 있다.
var a = 42;
var b = a.toString();
var c = '3.14';
var d = +c;
b; // '42'
d; // 3.14
a.toString()
호출은 겉보기엔 명시적이지만, 몇 가지 암시적인 요소가 감춰져 있다.- 원시 값 42에는
toString()
메서드가 없으므로 엔진은toString()
를 사용할 수 있게 자동으로 42를 객체 래퍼로 박싱한다. (말하자면 명시적으로, 암시적인 작동이다.) +c
의+
는 단항연산자로 피연산자c
를 숫자로, 명시적 강제변환한다.
var c = '3.14';
var d = 5+ +c;
d; // 8.14
-
단항 연산자 역시+
처럼 강제변환을 하지만 숫자의 부호를 뒤바꿀 수도 있다.
1 + - + + + - + 1; // 2
- 명시적으로 변환하여 문제를 악화시키지 말고 혼동을 줄이는 게 좋다.
📌 날짜 ➡ 숫자
+
단항 연산자는Date
객체 ➡ 숫자 강제변환 용도로 쓰인다.- 결괏값이 날짜/시각 값을 유닉스 타임스탬프 표현형이기 때문이다.
var d = new Date();
+d; // 1604492699962
// 현재 시각을 타임스탬프로 바꿀 때 관용적으로 사용하는 방법
var timestamp = +new Date();
- 그러나 강제변환을 하지 않고도
Date
객체로부터 타임스탬프를 얻는 방법이 있다. - 오히려 강제변환을 하지 않은 쪽이 더 명시적이므로 권장할 만하다.
var timestamp = new Date().getTime();
// ES5에 추가된 정적 함수 Date.now()를 쓰는 게 더 낫다.
var timestamp = Date.now();