자바스크립트에서 타입 강제변환이란

Jin·2022년 3월 1일
0

Javascript

목록 보기
4/22

값 변환

어떤 값을 다른 타입의 값으로 바꾸는 과정이 명시적이면 타입 캐스팅이라 부르고 암시적이면 강제변환이라고 합니다.

자바스크립트 (이하 JS)에서는 대부분 모든 유형의 타입변환을 강제변환으로 일컫는 경향이 있으므로 '암시적 강제변환'과 '명시적 강제변환' 두 가지로 구별하려고 합니다.

명시적 강제변환은 코드만 봐도 의도적으로 타입변환을 일으킨다는 것이 명백합니다.
암시적 강제변환은 다른 작업 도중 불분명한 side effect로부터 발생하는 타입변환입니다.

let a = 24;
let b = a + ''; // 암시적 강제변환
let c = String(a); // 명시적 강제변환

추상 연산

ToString

문자열이 아닌 값을 문자열로 변환 작업은 ToString 추상 연산 로직이 담당합니다.
숫자는 그냥 문자열로 바뀌고 너무 작거나 큰 값은 지수 형태로 바뀝니다.

let a = 1.07 * 1000 * 1000 * 1000 * 1000 * 1000 * 1000 * 1000;
a.toString(); // "1.07e21"

배열은 문자열 변환시 모든 원소 값이 콤마로 분리된 형태로 변환됩니다.

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

JSON.stringify 함수도 toString()과 비슷하면서도 차이점이 존재합니다.

JSON.stringify(42); // "42"
JSON.stringify("42"); // ""42""
JSON.stringify(null); // "null"
JSON.stringify(true): // "true"

JSON 안전 값 (JSON으로 확실히 나타낼 수 있는 값)은 모두 해당 함수로 문자열화할 수 있습니다.

JSON.stringify()의 인자로 undefined, function, symbol 타입의 값이 들어가게 되면 자동으로 누락시키며 이런 값들이 배열에 포함되어 있으면 null이 바꾼 뒤 문자열화하게 됩니다. 객체의 프로퍼티로 존재하면 생략해버립니다.

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

객체를 stringify할 때에 객체 자체에 toJSON() 메서드가 정의되어 있다면, 먼저 이 메서드를 호출하여 직렬화한 값을 반환합니다.

const a = {
	val: [1, 2, 3],
	toJSON: function() {
		return this.val.slice(1);
	}
};

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

toJSON()의 역할은 문자열화하기 적당한 JSON 안전 값으로 바꾸는 것이지 JSON 문자열로 바꾸는 것이 아닙니다. 많은 개발자들이 혼동하는 부분입니다.

JSON.stringify를 좀 더 소개하자면 두 번째 인자로 배열과 함수가 들어갈 수 있습니다.

두 번째 인자가 배열이면 전체 원소는 문자열이어야 하고 각 원소 객체 직렬화 대상의 프로퍼티명과 일치하여야 합니다. 여기에 포함되지 않은 프로퍼티는 직렬화 과정에서 빠지게 됩니다.

let a = {
	b: 24,
	c: "24",
	d: [1, 2, 3]
}

JSON.stringify(a, ["b", "c"]); // "{"b":24,"c":"24"}"

두 번째 인자가 함수면 처음 한 번은 객체 자신에 대해, 다음부터는 객체 프로퍼티별로 한 번씩 실행하면서 매번 키와 값을 전달합니다. 직렬화 과정에서 빼고 싶으면 undefined를 반환하고, 그렇지 않으면 해당 값을 반환하면 됩니다.

let a = {
	b: 24,
	c: "24",
	d: [1, 2, 3]
}

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

JSON.stringify()의 세 번째 인자는 스페이스라고 하며 들여쓰기와 관련된 설정을 할 수 있습니다.

세 번째 인자로 숫자가 들어가면 들여쓰기를 할 빈 공간의 개수를 지정하는 것이고 문자열이 들어가면 각 들여쓰기 수준에 들어갈 문자열을 지정하게 됩니다.

let a = {
	b: 24,
	c: "24",
	d: [1, 2, 3]
}

JSON.stringify(a, null, 3);
// "{
//    "b": 24,
//    "c": "24,
//    "d": [
//       1,
// ...

JSON.stringify(a, null, "----");
// "{
// ----"b": 24,
// ----"c": "24",
// ----"d": [
// --------1,
// ...

ToString

숫자가 아닌 값을 숫자로 변환하는 로직은 ToNumber 추상 연산에서 정의되어 있습니다.

  • true -> 1
  • false -> 0
  • undefined -> NaN
  • null -> 0

변환이 실패하면 오류가 발생하지 않고 결과는 NaN입니다. 8진수는 숫자 리터럴이어도 8진수가 아닌 10진수로 처리된다는 점을 유의하여야 합니다.

해당 객체를 숫자로 변환하는 과정에서 valueOf()를 쓸 수 있고 반환 값이 원시 값이면 그대로 강제변환합니다. 그렇지 않을 경우 toString()를 이용하여 강제변환합니다.

어찌해도 원시 값으로 바꿀 수 없는 경우에는 TypeError를 반환합니다.

const a = {
	valueOf: function() {
		return "24";
	}
};

const b = {
	toString: function() {
		return "32";
	}
};

Number(a); // 24
Number(b); // 32
Number(""); // 0
Number([]); // 0
Number(["abc"]); // NaN

ToBoolean

JS에서 숫자는 숫자고, boolean은 boolean입니다.

재미있게도, JS에서는 falsy한 값들이 존재합니다.

  • undefined
  • null
  • false
  • +0, -0, NaN
  • ""
    이 목록에 있으면 falsy한 값이며, boolean으로 강제변환하면 false 값입니다.

반대로 이 목록에 없으면 truthy한 값이라는 것입니다.

여기서 함정은 0이든 false든 객체 래퍼(Wrapper)로 감싸고 있는 객체라면 그것은 true라는 점입니다.


명시적 강제변환

분명하고 확실한 타입변환을 말합니다.

개발자들이 흔히 사용하는 타입변환이 이 범주에 속합니다.

문자열에서 숫자, 혹은 숫자에서 문자열로의 명시적 강제변환은 String()과 Number()을 사용하는데, 앞에 new 키워드가 붙지 않기 때문에 객체 래퍼를 생성하는 것이 아니라는 점을 기억하시면 좋습니다.

여기서, 예외적인 강제 변환 방법을 소개하려고 합니다.

let a = 24;
let b = a.toString();

let c = +b;
c; // 24

문자열 앞에 +를 붙이면 숫자로 강제변환이 일어납니다.

날짜에서 타임스탬프로 타입을 강제변환할 때에도 +를 앞에 붙입니다. 하지만, 가독성과 혼동을 줄이기 위해서 이런 예외적인 방법을 쓰지 않는 것이 도움이 됩니다.

~는 사람들이 간과하는 JS의 강제변환 연산자입니다.
~ 연산자는 먼저 32비트 숫자로 강젠변환한 후 NOT 연산을 수행합니다. (각 비트를 거꾸로 뒤집는다)
결국, ~x는 대략 -(x + 1)과 같습니다.

JS의 indexOf() 메서드가 이 전례에 따라 특정 문자를 검색하다가 발견하면 0을, 못하면 -1을 반환합니다.
여기서, ~의 쓸모를 발견할 수 있습니다.

const str = "Hello world!";

if (str.indexOf("lo") >= 0) {
	...
}

if (~str.indexOf("lo")) {
	...
}

똑같은 조건문이지만 2번째 조건문이 더 불리언 값으로 적절해보입니다. 이는 lo의 인덱스가 3이므로 -(3 + 1) = -4이므로 true를 반환하기 때문에 1번째 조건문과 같은 효과를 가지기 때문입니다.

~~도 종종 사용할 때가 있습니다.

맨 앞의 ~는 ToInt32 강제변환을 적용한 후 각 비트를 거꾸로 합니다. 두 번째 ~는 비트를 또 한 번 뒤집는데, 결과적으로는 원래 상태로 뒤돌립니다. 결국 ToInt32 강제변환 (잘라내기)만 하는 셈이 됩니다.

많은 개발자들이 ~~는 Math.floor와 같은 효과가 있다고 생각합니다. 양수에서는 맞지만 음수에서는 틀린 이야기입니다.

Math.floor(-24.3); // -25
~~24.3; // -24

이런 차이점을 알고 쓰시면 도움이 됩니다. 또한, ~~ 연산 32비트 값에 한하여 안전합니다.

문자열에 포함된 숫자를 파싱하는 것은 문자열에서 숫자로 강제변환하는 것과 결과는 비슷하지만 분명한 차이가 존재합니다.

let a = "24";
let b = "24px";

Number(a); // 24
parseInt(a); // 24

Number(b); // NaN
parseInt(b); // 24

파싱은 숫자가 아닌 문자를 허용합니다. 좌에서 우로 파싱하다가 숫자가 아닌 문자를 만나면 즉시 멈추고 그 직전까지의 값을 반환합니다.

반면, 강제변환은 숫자가 아닌 문자를 허용하지 않기 때문에 숫자가 아닌 문자를 만나면 즉시 NaN을 반환해버립니다.

parseInt는 오로지 문자열에 쓰는 함수입니다.
불리언이 아닌 값을 불리언으로 강제변환하는 경우도 있습니다.
여기서 아까 falsy한 값 목록은 강제변환한 값이 false가 되고 나머지는 모두 true가 됩니다.
명시적으로 강제변환을 할 때에는 !! 연산자를 사용하여 이중부정 연산자를 사용합니다. 결국, 같은 상태가 되기 때문입니다.

let a = 24;
let b = a ? true : false; // true
b = Boolean(a); // true
b = !!a; // true

삼항연산자보다는 Boolean()이나 !!같은 명시적 강제변환이 훨씬 좋은 습관입니다.


암시적 변환

부수 효과가 명확하지 않게 숨겨진 형태로 일어나는 타입변환입니다. 즉, 분명하지 않은 타입변환은 모두 이 범주에 속합니다.

암시적 변환은 코드를 더 이해하기 어려운 측면도 분명 존재하지만 코드 가독성을 높이고 세세한 구현부를 추상화하거나 감추는 데 도움이 되기도 합니다. 일단, 암시적 강제변환이 무조건 나쁘지만은 않다는 생각과 함께 다음의 예를 보겠습니다.

let a = 24;
let b = a + "";
b; // "24"

a + ""는 a 값을 valueOf() 메서드에 전달하여 호출하고, 그 결괏값은 ToString 추상 연산을 하여 최종적인 문자열로 변환됩니다. 그러나, String(a)는 그저 toString()을 직접 호출할 뿐입니다.

const a = {
	valueOf: function() { return 24; }
	toString: function() { return 2; }
}

a + ""; // 24
String(a); // 2

대부분의 경우에는 이런 일이 없겠지만 만약 어느 객체가 valueOf, toString 메서드를 오버라이딩했다면 강제변환 과정에서 결괏값이 달라질 수 있으니 주의하여야 합니다.

  • 연산자도 값을 숫자로 강제변환합니다.
let a = [3];
let b = [1];

a - b; // 2

두 배열은 우선 문자열로 강제변환된 뒤 숫자로 강제변환되고 마지막에 - 연산을 하게 됩니다.

&&와 || 연산자도 boolean으로 타입을 강제변환합니다.

let a = 24;
let b = "abc";
let c = null;

a || b; // 24
a && b; // "abc"

c || b; // "abc"
c && b; // null

|| 연산자는 결과 true면 첫 번째 값을, false면 두 번째 값을 반환합니다.
&& 연산자는 true면 두 번째 값을, false면 첫 번째 값을 반환합니다.

쉽게 풀면 이런 식이 됩니다.

a || b
a ? a : b

a && b
a ? b : a

|| 연산자는 디폴트 값을 지정할 때 사용되기도 합니다.

function (a) {
	a = a || 24; // a 값이 falsy하면 24를 디폴트 값으로 할당
}

심벌은 절대 숫자로 변환되지는 않지만 boolean으로 변환하게 되면 항상 true입니다.


느슨한/엄격한 동등 비교

흔히, ==를 느슨한 동등 비교, ===를 엄격한 동등 비교로 일컫습니다.

정확히는 강제변환이 필요하다면 ==를, 필요하지 않다면 ===를 사용하는 것이 옳습니다.

let a = 24;
let b = "24";

a == b; // true
a === b; // false

이것은 우리가 쉽게 유추할 수 있는 비교입니다.

여기서 정확히 어떻게 강제변환이 일어나서 a == b이 true가 된 것일까요? 정답은 문자열이 숫자로 강제변환되어 숫자와 숫자가 비교된 것입니다.

다음의 경우를 살펴봅시다.

let a = true;
let b = "24";

a == b; // false

"24"는 falsy한 값이 아니므로 true와 동등하다고 생각하기 쉽습니다. 하지만, 여기서도 boolean이 숫자 타입으로 강제변환되고 "24" 역시 24로 변환되고 두 숫자가 비교하는 과정을 거칩니다.

true는 숫자로 강제변환될 시 1로 치환되므로 1 != 24이 되므로 false가 됩니다.

따라서,느슨한 동등 비교에서는 절대 == true, == false와 같은 코드는 사용하지 않는 것이 좋습니다. === 비교에서는 사용하여도 좋습니다.

null과 undefined는 느슨한 동등 비교에서 서로에게 타입을 맞춥니다.

null == undefined; // true

객체와 원시 값을 비교하는 경우를 살펴봅시다.

let a = 24;
let b = [24];

a == b; // true

[24]가 "24"가 되고 다시 24가 되므로 24 == 24는 true가 됩니다.

다음은 Object로 객체화하는 경우입니다.
객체끼리 비교하는 경우에는 언박싱을 하여서 그 안에 있는 원시 값끼리 비교하게 됩니다.

let a = null;
let b = Object(a);
a == b; // false

let c = undefined;
let d = Object(c);
c == d; // false

let e = NaN;
let f = Object(e);
e == f; // false

null과 undefined은 객체 래퍼가 따로 없으므로 박싱할 수 있으므로 Object(null)은 Object()로 해석되어 그냥 일반 객체가 만들어지므로 동등 비교시 false가 됩니다.

NaN 경우에는 Number로 박싱되지만 ==를 만나 언박싱하게 되면 자기자신과 동등하지 않으므로 false입니다.

이제, 느슨한 동등 비교시 의외인 경우 7가지를 소개합니다.

"0" == false; // true
false == 0; // true
false == ""; // true
false == []; // true
"" == 0; // true
"" == []; // true
0 == []; // true

다음의 경우를 사용할 일은 극히 드물겠지만, 만의 하나라는 경우가 있으므로 이런 경우에는 결괏값이 이렇구나 정도는 짚고 넘어가는 것이 좋겠습니다.

결국, == 연산자의 안전한 사용법을 위해서는

  • 피연산자 중 하나가 true나 false일 가능성이 있으면 절대로 == 연산자를 사용하지 말자
  • 피연산자 중 [], "", 0이 될 가능성이 있으면 가급적 == 연산자를 사용하지 말자

위의 2가지 원칙을 지켜야 합니다.


추상 관계 비교

어느 한쪽이라도 문자열이 아닐 경우 양쪽 모두 숫자로 강제변환하여 비교합니다. (+0, -0, NaN 조심)

비교 대상이 모두 문자열 값이면, 각 문자를 단순 어휘 비교 (알파벳 순서)합니다.
배열의 경우에도 단순 어휘 비교합니다.

let a = [2, 4];
let b = [0, 2, 4];

a < b; // false

a는 "2,4"로, b는 "0,2,4"로 변환되어 어휘 비교하게 되므로 a가 b보다 큽니다.

객체의 경우에는 어떨까요?

let a = { b: 24 };
let b = { b: 25 };

a < b; // false
a >= b; // true

문자열화하게 되면 a와 b 모두 [object Object]가 되므로 어휘 비교를 할 수 없기 때문입니다.

그런데 신기하게도 a >= b는 true로 나옵니다. 이것은 a < b가 false이기 때문에 그 반대의 경우인 a >= b는 true가 되도록 JS 엔진이 설정되어 있기 때문입니다.

따라서, 강제변환이 유용하고 어느 정도 안전한 관계 비교라면 그냥 쓰는 것이 좋습니다. 반면, 조심해서 관계 비교를 해야 할 것 같은 상황에서는 비교할 값들을 명시적으로 강제변환하고 비교하는 것이 안전합니다.

let a = [24];
let b = "025";

a < b; // false -- 문자열 비교
Number(a) < Number(b); // true -- 숫자 비교
profile
배워서 공유하기

0개의 댓글