본문 바로가기
JAVASCRIPT

[JS][강제변환] 명시적 강제변환 : ~(틸드)

by KBS 2022. 2. 10.
728x90

명시적 강제변환

이상한 나라의 틸드(~)

~(틸드)는 종종 사람들이 간과하는 자바스크립트의 강제변환 연산자이자, 가장 헷갈리는 연산자의 대명사이다. 심지어 사용법을 터득한 개발자마저 사용을 꺼려한다고 한다.

 

예를들어보자 아무 연산도 하지 않는 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 ( 한빛 미디어 )
728x90

댓글