명시적 강제변환
이상한 나라의 틸드(~)
~(틸드)는 종종 사람들이 간과하는 자바스크립트의 강제변환 연산자이자, 가장 헷갈리는 연산자의 대명사이다. 심지어 사용법을 터득한 개발자마저 사용을 꺼려한다고 한다.
예를들어보자 아무 연산도 하지 않는 0 | x
의 'OR'연산자는 사실상 ToInt32
변환만 수행한다.
0 | -0; // 0
0 | NaN; // 0
0 | Infinity; // 0
0 | -Infinity; // 0
이러한 특수 숫자들은 32비트로 나타내는 것이 불가능 하므로 ToInt32
연산 결과는 0이다.
0 | __
이 강제적인 ToInt32
연산의 명시적인 형태냐 아니면 더 암시적인 형태냐 하는 문제는 논란의 여지가 있따. 명세서 관점에서 보면 두말할 것 없이 명시적인 것이지만, 이 수준의 비트 연산을 이해하지 못한 사람들 눈에는 일종의 암시적인 마법으로 보일 수 있다.
~로 돌아와서, ~연산자는 먼저 32비트 숫자로 '강제변환'한 후 NOT
연산을 한다.(각 비트를 거꾸로 뒤집는다.)
!이 불리언 값으로 강제변환 하는 것 뿐만아니라 비트를 거꾸로 뒤집는 것과 비슷하다.
그런데...? 비트를 거꾸로 한다는 둥 이런 얘기는 왜 하는 걸까? 아주 전문적이고 미묘한 주제인다... 자바스크립트 개발자가 비트 하나하나를 따져야 할 경우가 있을까..?
x는 대략
-(x + 1)`과 같다. 이상한 것 같지만 왜 그런지 금방 알 수 있다.
~42; // -(42 + 1) ==> -43
대관절 ~ 애기가 왜 나왔을까, 강제변환과 무슨 상관인지 어리둥절 할 것이다. 그럼 요점을 살펴보자.
-(x+1)
를 보자. 이 연산의 결과를 -으로 만드는 유일한 값은 무엇일까? 정답은 -1이다. 다시 말해, 일정범위 내의 숫자 값에 ~연산을 할 경우 입력 값이 -1이면(false로 쉽게 강제변환 할 수 있는) falsy
한 0, 그 외엔 truthy
한 숫자 값이 산출된다.
그래서 무슨 상관이란 말인가..?
여기서 -1과 같은 성징ㄹ의 값을 흔히 '경계 값'이라고 하는데, 동일 타입(숫자)의 더 확장된 값의 집합 내에서 임의의 어떤 의미를 부여한 값이다. 예를 들어 C언어의 함수는 대게 -1을 경계 값으로 사용하는데 return >= 0
는 성공, return -1
은 실패라는 의미를 각각 부여한다.
자바스크립트는 문자열 메서드 indexOf()
를 정의할 때 이 전례를 따라 특정 문자를 검색하고 발견하면 0부터 시작하는 숫자 값(인덱스)을, 발겸하지 못햇을 경우 -1을 반환한다.
사실, indexOf()
는 단순히 위치를 확인하는 기능보단 어떤 하위 문자열이 다른 문자열에 포함되어 있는지 조사하는 용도로 더 많이 쓰인다.
다음 코드를 보자.
var a = "Hello World";
// true
if (a.indexOf("lo") >= 0) {
// found it!
}
// true
if (a.indexOf("lo") != -1) {
// found it!
}
// true
if (a.indexOf("ol") < 0) {
// not found
}
// true
if (a.indexOf("ol") == -1) {
// not found
}
그런데 여러분의 눈에는 >=
이나 == -1
같은 코드가 좀 지저분해 보일 수 있다. 기본적으로 이런 부류의 코드는 '구멍난 추상화', 즉 내부 구현 방식을 내가 짠 코드에 심어 놓은 꼴이다. 이런 부분은 감추는 게 낫다고 생각한다.
indexOf()
에 ~를 붙이면 어떤 값을 '강제변환'(실제로는 단순히 변형)하여 불리언 값으로 적절하게 만들 수 있다.
var a = "Hello World";
~a.indexOf("lo"); // -4 <-- truthy!
// true
if (~a.indexOf("lo")) {
// 찼았어!
}
~a.indexOf("ol"); // 0 <-- falsy..
!~a.indexOf("ol"); // true
// true
if (!~a.indexOf("ol")) {
// 없네?
}
~은 indexOf()
로 검색 결과 '실패'시 -1을 falsy
한 0으로, 그 외에는 truthy
한 값으로 바꾼다.
-(x+1)은 ~의 의사 알고리즘으로, 내부적으로 ~-1을 -0으로 만들지만, 수학 연산이 아닌 비트 연산이므로 결괏값은 0이다.
굳이 따진다면 if(~a.indexOf())
문은 ~a.indexOf()
의 결괏값이 0이면 false, 그 외엔 true로 암시적인 강제변환을 하는 것이라 할 수도 있지만, 여기서 설명하고자 하는 의도를 잘 따라오고 있다면 ~이 오히려 명시적인 강제변환에 더 가깝지 않나 싶다.
이전의 >=이나 == -1 같은 잡동사니들과 견주어 보면 훨신 깔끔한거 같다.
비트 잘라내기
용도를 하나더 알아보자. 숫자의 소수점 이상 부분을 잘라내기 위해 더블 틸드(~~)를 사용하는 개발자들이 있다. 흔히들 이렇게 하면 Math.floor()
와 같은 결과가 나온다고 생각한다.
~~가 하는 일은 이렇다. 먼저 맨 앞의 ~는 ToInt32
'강제변환'을 적용한 후 각 비트를 거꾸로 한다. 그리고 두 번째 ~는 비트를 또한 번 뒤집는데, 결과적으로 원래 상태로 되돌린다. 결국 ToInt32
'강제변환'만 하는 셈이다.
~~의 비트 이중 뒤집기 묘미는 '명시적 강제변환 * -> 불리언'의 패리티 이중 부정(!!)과 유사하다.
그러나 ~~ 사용시 유의할 점이 있다. 우선 ~~ 연산은 32비트 값에 한하여 안전하다. 그런데 그보다도 음수에서는 Math.floor()
과 결괏값이 다르다는 사실을 꼭 인지하자.
Math.floor(-49.6); // -50
~~-49.6; // -49
Math.floor()
과의 다른 점은 차치하더라도 ~~x는 정수로 상위 비트를 잘라낸다. 하지만 같은 일을 하는 x | 0
가 더 빠를 것 같다.
그럼 굳이 왜 x | 0
대신 ~~x
를 써야할까? 바로 연산자의 우선순위 때문이다.
~~1e20 / 10; // 166199296
1e20 | (0 / 10); // 1661992960
(1e20 | 0) / 10; // 166199296
여러분의 코드를 읽을 주변 동료 개발자가 연산자의 작동원리를 적절히 이해하고 있다는 전제하에 ~와 ~~를 명시적인 '강제변환' 및 값 변형 장치로 잘 활용하기 바란다.
참고
- You Don't Know JS ( 한빛 미디어 )
'JAVASCRIPT' 카테고리의 다른 글
[JS][강제변환] 명시적 강제변환 : * -> 불리언 (0) | 2022.02.10 |
---|---|
[JS][강제변환] 명시적 강제변환 : 숫자 형태의 문자열 파싱 (0) | 2022.02.10 |
[JS][강제변환] 명시적 강제변환 : 날짜 <-> 숫자 (0) | 2022.02.10 |
[JS][강제변환] 명시적 강제변환 : 문자열 <-> 숫자 (0) | 2022.02.09 |
[JS][강제변환] 추상 연산 ToBoolean (0) | 2022.02.09 |
댓글