[20211228] 명시적 강제변환

이 포스팅은 카일 심슨 저자, “YOU DON’T KNOW JS” 타입과 문법, 스코프와 클로저편 4.3) 명시적 강제변환을 보고 정리하였습니다.

흔히 사용하는 타입변환은 대개 명시적 강제변환 범주에 속함. 코드는 명확할수록 다른 개발자들이 쓸데없이 내 의도를 추론할 필요가 없다.

문자열 <-> 숫자

String()과 Number()함수를 이용.

new 키워드가 붙지 않기 때문에 객체 래퍼를 생성하는 것이 아님.

var a = 42;
var b = String(a);

var c = "3.14";
var d = Number(c);

b; // "42"
d; // 3.14

String()은 ToString 추상 연산 로직에 따라 원시 문자열로 강제 변환한다.

Number() 역시 ToNumber 추상 연산 로직에 의해 어떤 값이든 원시 숫자 값으로 강제변환한다.

toString()과 +단항 연산자로도 강제변환이 가능하다.

var a = 42;
var b = a.toString();

var c = "3.14";
var d = +c;

b; // "42"
d; // 3.14

toString() 호출은 겉보기엔 명시적으로 보이지만 암시적인 요소가 감춰져있다.

원시 값 42는 toString() 메서드가 없으므로 엔진이 자동으로 42를 객체 래퍼로 박싱한다.

대부분의 자바스크립트 커뮤니티에서는 + 단항 연산자를 명시적 강제변환 형식으로 대부분 인정하는 분위기다. 하지만, 헷갈리는 경우가 더러 있다.

var c = "3.14";
var d = 5 + +c;

d; // 8.14

+단항 연산자를 사용하기 위해 증감연산자(++)와 구분하기 위해 위의 코드가 나올 수 있는데, 이는 혼란을 야기할 수 있다.

d = +c; // ?
d += c; // ?

그리고 위와 같은 코드도 서로 다른 결과를 야기하기 때문에 혼란을 야기하기 쉽다.

+단항연산자는 다른 연산자와 인접하여 사용하지 않는 것이 좋아보임.

날짜 → 숫자

+단항 연산자는 ‘Date 객체 → 숫자’ 강제변환 용도로도 쓰인다.

결과값이 날짜/시각 값을 유닉스 타임스탬프 표현형(1 Jan 1970 00:00:00 UTC 이후 시간을 ms 단위로 표시)이기 때문.

var d = new Date('Mon, 18 Aug 2014 08:53:06 CDT");
+d; // 1408369986000

하지만, +단항 연산자로 강제변환하는 것보다 강제변환을 하지 않는 쪽이 더 명시적이다.

var timestamp = new Date().getTime();

// 혹은 ES5에 추가된 정적함수 이용
var timestamp = Date.now();

틸드(~) 연산자

자바스크립트 비트 연산자는 오직 32비트 연산만 가능하다.

비트 연산을 하면 피연산자는 32비트 값으로 강제로 맞춰진다.

틸드(~) 연산자는 2의 보수를 구한다.

~42; // -(42+1) => -43

~x는 대략 -(x+1)와 같다.

틸드 연산의 결과를 0으로 만드는 유일한 값은 -1이다.

일정 범위 내의 숫자 값에 ~ 연산을 할 경우 입력 값이 -1이면 falsy한 0, 그 외엔 truthy한 숫자 값이 산출된다.

-1과 같은 성질의 값을 흔히 ‘경계 값’이라고 일컫는다.

자바스크립트는 문자열 메서드 indexOf()는 인자로 넘겨진 문자의 위치(인덱스)를 반환한다.

만약, 발견하지 못했을 경우 -1을 반환한다.

indexOf()는 단순히 위치를 확인하는 기능보단 문자열의 포함 여부를 조사하는 용도로 더 많이 쓰인다.

var a = 'Hello World';

// 아래의 표현은 내부 구현 방식을 내가 짠 코드에 심어놓은 꼴임
if(a.indexOf('lo') >= 0) {
	// found it
}
if(a.indexOf('lo') == -1) {
	// not found it
}

// indexOf()에 ~를 붙이면 불리언 값으로 적절하게 만들 수 있음
if(~a.indexOf('lo') {
	// found id
}

~의 또다른 용도로 비트 잘라내기가 있다.

숫자의 소수점 이상 부분을 잘라내기 위해 더블 틸드(~~)를 사용하는 개발자도 있다.

비트 연산자를 사용하면 피연산자를 부호있는 32비트 정수로 취급하기 때문이다.

하지만 흔한 착각이 Math.floor()와 같은 결과가 나온다고 생각하고 사용하는 것이다.

~~의 맨 앞의 ~는 ToInt32 강제변환을 적용한 후 각 비트를 거꾸로 한다.

두 번째 ~는 비트를 또 한 번 뒤집기 때문에, 결과적으로 원래 상태로 되돌린다.

~~ 연산도 비트 연산이기 때문에 32비트 값에 한하여 안전하다.

그 보다 Math.floor() 결과값이 음수에서 다르다는 사실을 조심하자.

Math.floor(-49.6); // -50
~~-49.6; // -49
x 0 도 상위 비트를 잘라내기 때문에 ~~x와 하는 일이 같으면서 속도더 빠른데 왜 ~~를 사용할까?

연산자 우선순위 때문임.

~~1e20 / 10; // 166199296

1e20 | (0 / 10); // 1661992960
(1e20 | 0) / 10; // 166199296

명시적 강제변환: 숫자 형태의 문자열 파싱

문자열에 포함된 숫자를 파싱하는 것은 ‘문자열 → 숫자’ 강제변환과 결과는 비슷하지만 차이가 있다.

var a = "42";
var b = "42px";

Number(a); // 42;
parseInt(a); // 42;

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

문자열로부터 숫자 값의 파싱은 비 숫자형 문자를 허용한다. 반면 강제변환은 비 숫자형 문자를 허용하지 않아 NaN를 반환한다.

parseInt(string, radix)의 첫 번째 인자로 문자열만 쓰는 것을 추천한다. 비 문자열이 인자로 주어지면 비 문자열을 문자열로 강제변환 하기 때문이다.

ES5 이전에는 parseInt() 두 번째 인자로 기수를 지정하지 않으면 문자열의 첫 번째 문자만 보고 마음대로 추정한다. (무수한 버그를 일으킴) 문자열의 첫 번재 문자가 x나 X면 16진수, 0이면 8진수로 해석을 한다.

var time = {
  hour: 08,
  min: 09,
};

var hour = parseInt(time.hour);
var min = parseInt(time.min);

console.log(hour, min); // 0 0 ??

ES5 이후부터 0x로 시작할 때만 16진수로 처리하고, 그 밖에 두 번째 인자가 없으면 10진수로 처리한다. ES5 이전에는 항상 두 번째 인자로 10을 전달해야 안전하다.

비 문자열 파싱

parseInt(1 / 0, 19); // 18

parseInt("Infinity", 19);

1/0은 비 문자열이기 때문에 문자열로 강제변환 하려고 노력한다.

1/0은 “Infinity”문자열로 변환된다.

2번째 인자로 19가 주어졌기 때문에 “Infinity는 19진법으로 변환된다.

19진법의 유효한 숫자는 0부터 9, a부터 i까지이다.

Infinity의 I부터 변환이 된다.

I는 19진법에서 18이다.

n은 19진법에서 유효하지 않은 숫자이므로 parseInt의 파싱이 멈추게된다.

그래서 위의 결과가 나온것…

명시적 강제변환: *(비 불리언) → 불리언

Boolean()은 분명히 명시적 강제변환이지만 그리 자주 쓰이지 않는다.

Number()보다 +단항 연산자를 명시적 강제변환으로 사용하는 것처럼

! 부정 단항 연산자도 값을 불리언으로 명시적으로 강제변환한다.

하지만, 그 과정에서 truthy, falsy까지 뒤바뀐다.

그래서 이중부정(!!) 연산자를 사용한다.

이 같이 ToBoolean 강제변환 모두 Boolean()이나 !!를 쓰지 않으면 if()문 등의 불리언 콘텍스트에서 암시적인 강제변환이 일어난다.

var a = 42;
var b = a ? true : false;

// 위의 암시적 강제변환보다 명시적 강제변환을 사용하자!
var c = Boolean(a);
var d = !!a;

true/false 중 하나가 연산 결과로 도출된다는 점에서 명시적인 ToBoolean 강제변환과 닮았지만,

내부적으로는 암시적 강제변환이 매복해 있다.

a를 불리언으로 강제변환해야 표현식의 true/false 여부를 따져볼 수 있기 때문이다.

결론

개인적으로 명시적 강제변환도 쉽지않고 알아야 할 내용도 많지만, JS 개발자라면 감내해야 하는 부분인 것 같다. 오히려 인지하지 못하고 암시적 강제변환이 일어난다면 어떤 일이 일어날지 모르니…