홈으로

[JS] 강제변환

2020년 02월 03일

You don’t know JS 를 공부하고 정리한 글이다. 현재, 2판 작업중이지만 그렇게 바뀔 것 같지는 않아서 정리해본다.

추상 연산

암시적/명시적 강제변환을 살펴보기 전에, 자바스크립트 명세에서 각 데이터 타입간에 변환을 어떻게 시키는지에 대해 살펴보자.

ToString

자세한 사항은 ES5 9.8 ToString 을 참고하자.

  • undefined"undefined"
  • null"null"
  • true/false"true/false"
  • 숫자 → 너무 크지 않는 이상 그대로 변환되고 너무 크면 지수형태로 변환
  • 객체 → 내부의 toString()[[Class]] 반환
  • 배열 → 재정의된 toString() 이 콤마(,)로 구분된 형태 반환

ToNumber

자세한 사항은 ES5 9.3 ToNumber 를 참고하자.

  • undefinedNaN
  • null+0
  • true/false1/+0
  • 객체/배열 → ToPrimitive 추상연산 과정에서 valueOf() 메서드 구현을 확인하고 반환값이 원시값이면 그대로 강제변환한다. 만약 없으면 toString() 을 확인하고 없으면 TypeError 오류를 던진다.

ToBoolean

자세한 사항은 ES5 9.2 ToBoolean 을 참고하자.

해당 추상연산의 핵심은 true/false 가 아닌 값을 불리언으로 강제변환했을 때 어떻게 동작하느냐이다. 이 문제는 자바스크립트의 모든 값이 2개의 값 중 하나라는 것만 안다면 간단해진다.

  • 불리언으로 강제변환시 false 가 되는 값
  • 그 외의 값 = true

결국, 첫번째 값들의 목록만 외우고 있으면 나머지는 헷갈릴 걱정 없이 true 라는 것이다. 첫번째 값들의 성질을 가진 값들을 “falsy 값”이라고 하며 아래와 같다.

  • undefined
  • null
  • false
  • +0 , -0 , NaN
  • ""

이 경우만 알고있다면 다음과 같은 결과를 쉽게 납득할 수 있다.

var a = "false";
var b = "0";
Boolean(a && b); // true

var c = [];
var d = {};
Boolean(c && d); // true

명시적 강제변환

문자열 ↔ 숫자

var a = 3;
String(a); // "3"
var b = "15.23";
Number(b); // 15.23

각각 ToString과 ToNumber 추상연산로직을 따르기에, 원시 문자열/숫자로 강제변환한다. 이 방법 말고도 다른 방법이 있다.

var a = 3;
a.toString(); // "3"
var b = "15.23";
+b; // 15.23

a 를 변환할 때는 toString() 메서드를 사용했는데 당연히 a 에는 없기 때문에 객체 래퍼로 박싱해서 호출한다. b 를 변환할 때는 단항연산자(Unary Operator)를 사용하여 강제변환한다. 하지만 단항연산자의 경우, 가독성이 떨어지고 실수할 수 있는 확률이 높기 때문에 쓰지 않는 것이 좋다.

날짜 → 숫자

현재 시각을 타임스탬프로 바꾼다고 해보자. Date 객체를 활용하는데 보통 관용적으로 다음과 같이 했다.

var now = +(new Date());

다음 과정으로 변환된다.

  • new Date() 는 현재 날짜/시각을 가리키는 객체이다.
  • 단항연산자 + 를 사용해서 객체를 숫자로 강제변환하기 때문에 ToNumber 추상연산이 적용된다.
  • valueOf() 메서드를 확인하니, 있고 원시 숫자값을 반환한다.

이외에도 2가지 방법이 더 있다.

var now1 = (new Date()).getTime();
var now2 = Date.now();

틸드(~) 연산자 활용

자바스크립트의 비트 연산자는 32비트 연산만 가능하기 때문에 피연산자를 32비트로 바꾸는 ToInt32 추상 연산을 수행하고 NOT 연산을 수행한다. 이런 성질은 -1 과 같은 값에 활용될 수 있는데, 여기에 틸드연산을 적용하면 0 이 되기 때문이다. 문자열의 메서드인 indexOf() 는 대상을 못 찾으면 -1 을 반환하는데 보통 다음과 같이 사용한다.

if ("123".indexOf("3") !== -1) {
  console.log("찾았다!");
}

하지만 -1 이 “실패” 라는 의미를 가졌다는 것을 노출시키기 때문에 그다지 좋지 않고 여기에 틸드연산을 활용할 수 있다.

if (~("123".indexOf("3"))) {
  console.log("찾았다!");
}

만약에 "4" 를 못찾으면 -1 을 반환하는데 여기다 틸드연산을 적용할 경우 0 이 되어서 false 가 된다. 따라서 찾았을 경우는 true 이므로 위와 같이 사용하게 되는 것이다. 만약, if문 안에 직접적으로 불리언 값을 적어주고 싶다면 !! 를 적용하면 된다.

if (!!~("123".indexOf("3"))) {
  console.log("찾았다!");
}

틸드연산은 이외에도 비트 잘라내기에 유용한데, 소수의 소수점 이상을 잘라내기 위해서 사용되고는 한다.

~~123.14; // 123

이외의 방법으로는 Math.floor() 를 사용하지만 음수의 경우 2개의 작동방식이 다르다 는 점을 주의하자.

숫자형태의 문자열 파싱

앞서배운 문자열 강제변환과는 차이점이 있는데, 비 숫자형 문자를 허용한다는 것이다.

Number("123a"); // NaN
parseInt("123a"); // 123

단, parseInt() 의 경우에는 첫번째 인자로 문자열을 받는다. 비 문자열은 문자열로 강제변환되는데, 여기서 예측하기 힘들기 때문에 그냥 비 문자열은 인자로 전달하지 말자. 두번째 인자로는 기수(radix)를 전달하는데, 이 정보를 기반으로 첫번째 인자로 전달된 문자열을 파싱한다. 만약, 두번째 인자가 없다면 ES5 이후부턴 10진수로 처리하고 이전에는 첫번째 문자만 보고 추정한다.

비 불리언 → 불리언

ToBoolean 추상연산과 동일하게 엄청 간단하다.

Boolean([]); // true
Boolean({}); // true
Boolean("false"); // true
Boolean("0"); // true

Boolean(null); // false
Boolean(undefined); // false

이외에도 단항연산자 ! 를 사용할 수 있다.

!!null; // false

암시적 강제변환

숨겨진 형태로 일어나는 타입변환으로 명백하게 보이지 않는 타입변환의 총칭이다.

문자열 ↔ 숫자

피연산자 한쪽이 문자열이라면 + 연산자는 항상 문자열 붙이기를 한다. 만약, 피연산자가 객체라면 다음 과정을 거친다.

  • 객체를 ToPrimitive 추상연산으로 원시값으로 변환한다.
  • 원시값을 ToString 추상연산으로 문자열로 변환한다.
  • 문자열 붙이기를 한다.

따라서 아래와 같은 일이 일어난다.

var a = [1,2];
var b = [3,4];
a + b; // 1,23,4

보통 숫자를 문자열로 변환할 때의 가장 많이 쓰는 일반적인 방식은 다음과 같다.

var a = 4;
a + ""; // "4"

주의할 점은 String(a) 의 경우 toString() 을 바로 호출하지만 암시적 변환의 경우 valueOf() 를 먼저 호출한다는 사실이다. ES5 9.1 ToPrimitive 연산을 참고하면 보다 확실히 알 수 있다.

비 불리언 → 불리언

불리언으로의 암시적 강제변환이 일어나는 경우는 다음과 같다.

  • if 문의 조건 표현식
  • for 문의 2번째 조건 표현식
  • while 및 do~while문의 조건 표현식
  • 삼항연산자의 첫번째 조건 표현식
  • 논리연산자 및 좌측 피연산자

&&와 || 연산자

보통 다른 언어에선 &&와 ||의 반환값이 불리언 값이지만 자바스크립트에선 피연산자 중 하나로 귀결된다.

var a = 1;
var b = null;
a && b; // null
a || b; // 1

&&의 경우 앞쪽이 참일 경우에 뒤쪽을 실행하고 ||의 경우는 앞쪽이 거짓일 경우에 뒤쪽을 실행한다.

느슨한/엄격한 동등 비교

흔히 ===== 의 차이점을 타입을 비교하냐의 여부로 따지고는 한다. 하지만 엄격히 말하자면, 강제변환을 허용하는가의 여부 이다. 즉, == 는 강제변환을 허용하고 === 는 강제변환을 허용하지 않는다. 결국 == 에서 암시적 강제변환이 발생하며 이는 여러가지 예측하기 힘든 결과들을 내놓는다. 먼저 느슨한 동등 비교의 규칙부터 살펴보자.

  • 피연산자 중 하나가 문자열일 경우 : 문자열을 숫자로 강제변환한다.
  • 피연산자 중 하나가 불리언일 경우 : 불리언을 숫자로 강제변환한다.
  • 피연산자가 null 또는 undefined 일 경우 : 양쪽이 모두 null 이나 undefined 일 경우에만 참이다.
  • 피연산자가 비객체와 객체인 경우 : 객체를 원시값으로 강제변환한다.

따라서 다음이 성립한다.

"0" == false; // true
  • 피연산자 중 하나가 불리언이므로 숫자로 변환한다. "0" == 0
  • 피연산자 중 하나가 문자열이므로 숫자로 변환한다. 0 == 0

이런 결과들을 보고나면 무조건 엄격한 동등 비교인 === 를 쓰면 되는 것이 아닌가? 라고 생각할 수 있다. 저자의 의견은 무조건적으로 사용하지 말고 그 근본 원리를 이해하고 적절히 사용하는 것이 바람직하다고 한다. 예를 들어, 피연산자 중 하나가 null 이나 undefined 라면 == 를 쓰는 것이 훨씬 간단하다.

추상 관계 비교

< 연산에 대한 것으로, ToPrimitive 연산을 한 후의 피연산자가 모두 문자열일 경우와 그렇지 않은 경우로 나뉜다.

  • 피연산자가 모두 문자열 : 알파벳 순서로 비교한다.
var a = ["abc"];
var b = ["zd"];
a < b; // true
  • 피연산자가 모두 문자열 아님 : ToNumber를 통해 숫자로 변환해서 비교한다.
var a = [1];
var b = ["2"];
a < b; // true

단, <= 의 작동방식은 기존에 알고 있던 대로 동작하지 않는다. > 의 결과를 부정한 방식을 적용한다.

var a = {};
var b = {};
a < b; // false
a >= b; // true

여기서 ab 는 문자열 [object Object] 로 변환되기 때문에 a<b 의 결과는 거짓이 된다. 따라서, >= 의 결과는 그걸 부정한 참이 된다.

마무리

지금까지 자바스크립트에서 타입의 강제변환이 어떤 방식으로 일어나는지 살펴보았다. 예측하기 힘든 결과들은 거의 발생하지 않지만 그래도 어떻게 동작하는지에 대해 알게되니 한결 편해진 기분이다. 앞으로는 타입변환을 명시적으로 하거나, 아니면 암시적으로 발생하는 컨텍스트에서 확실히 이해하고 지나가도록 하자.

Loading script...