본문 바로가기
JAVASCRIPT

[JS][강제변환] 동등비교 : 희귀 사례 (1)

by KBS 2022. 2. 12.
728x90

희귀 사례

지금까지 느슨한 동등 비교 == 이면의 암시적 강제변환에 대해 아주 자세히 살펴보았다. 이제는 우리가 강제변환 버그를 피해가도록 하기 위해서, 그중에서도 가장 골치 아프고 쓰지 알아야할 희귀 사례를 보자

먼저 내장 네이티브 프로토타입을 변경하면 어떤 참사가 빚어지는지 살펴보자.

알박힌 숫자 값

Number.prototype.valueOf = function () {
  return 3;
};

new Number(2) == 3; // true

 

2 === 3 비교는 이 예와 무관하다. 2, 3이 둘 다 이미 원시 숫자 값이고 곧바로 비교가 가능하므로 Number.prototype.valueOf() 내장 메서드는 호출되지 않는다. 그러나 new Number(2)는 무조건 ToPrimitive 강제변환 후 valueOf()를 호출한다.

 

정말 고약한 냄새가 나지 않는가? 감히 이런 코드는 흉내내지 말지어다. 이러 ㄴ코드를 짤 수 있기 때문에 강제변환과 == 이 욕을 먹고 있지만, 그렇더라도 불만의 근거를 여기에서 찾는건 잘못이다. 끔직한 일을 저지를 수 있는 자바스크립트가 나쁜 거이 아니라, 그런 짓을 저지르는 개발자가 나쁜 것이다. "프로그래밍 언어는 개발자를 지켜줘야해" 식의 빗나간 감상에 빠지지 말자

다음 사례는 아까보다 훨씬 까다롭다.

if (a == 2 && a == 3) {
  //..
}

 

가 동시에 2가 되고 3이 된다는게 말이 되나 싶겠지만, '동시에'란 전제부터가 틀렸다. 엄밀히 말해, 두 표현식 중 a == 2가 a == 3보다 먼저 평가된다.

a.valueOf()에 부수효과를 주면 어떨까? 이를테면 다음과 같이 처음 호출하면 2, 두 번째 호출하면 3을 반환하는 식으로 말이다.

var i = 2;
Number.prototype.valueOf = function () {
  return i++;
};

var a = new Number(42);

if (a == 2 && a == 3) {
  console.log("이게 작동하네..");
}

 

이런 코드는 그 자체로 공해니 생각조차 말고 강제변환을 비난하는 근거로 제시하지도 말자. 남용될 소지가 있다고 하여 비난받아 마땅한 정당한 근거가 되는 것은 아니다. 말도 안 되는 장난은 피하면 그만이고 올바르게, 적절하게 강제변환을 이용하자.

Falsy 비교

==의 암시적 강제변환을 비난하는 사람들은 falsy 값 비교에 관한 이상한 로직을 종종 거론한다.

falsy 값 비교에 관한 희귀 사례 목록을 보면서 정상과 비정상을 구별해보자.

"0" == null; // flase
"0" == undefined; // false
"0" == false; // true ..?
"0" == 0; // true
"0" == ""; // false

false == null; // false
false == undefined; // false
false == NaN; // false
false == 0; // true ..?
false == ""; // true ..?
false == []; // true ..?
false == {}; // false

"" == null; // false
"" == undefined; // false
"" == NaN; // false
"" == 0; // true ..?
"" == []; // true ..?
"" == {}; // false

0 == null; // false
0 == undefined; // false
0 == NaN; // false
0 == []; // true ..?
0 == {}; // false

 

여기에 열거된 24개의 비교 중 17개는 이치에 맞고 예측도 가능하다. 예컨데 " "와 NaN은 전혀 동등할 만한 값들이 아니며 실제로도 느슨한 동등 비교 시 강제변환 되지 않는다. 한편, "0"과 0은 그냥 봐도 같은 값이며 느슨한 동등 비교 시 강제변환 된다.

하지만 여기서 "..?"라고 주석을 붙인 7개의 비교는 긍정 오류이며, 개발자를 뜬눈으로 지새우게 할 소지가 다분하다. ""와 0은 분명히 다른 값이며 같은 값으로 취급할 경우 또한 거의 없기 때문에 상호 강제변환은 문제가 있다. 참고로 7개중에 부정 오류는 하나도 없다.

말도 안되는..

여기서 끝나지 않는다. 더 심각한 강제변환 사례도 있다.

[] == ![]; // true

 

오 또다른 차원의 경이로움이다.. 분명히 truthyfalsy의 비교가 아닌가 싶은데, 결과는 놀랍게도 true다. 어떤 값이 동시에 truthy도 되고 falsy도 될 수 있을까?

하지만 겉보기만 그럴 뿐 실제로 벌어지는 일은 다르다. 하나씩 파헤쳐보자. !단항 연산자를 기억하는가? ToBoolean으로 불리언 값으로 명시적 강제변환을 하는 연산자다. 다라서 [] == ![] 이전에 이미 [] == false로 바뀐다 24개 비교 목록 중 비슷한 코드가 있으니 왜 겨로가가 이렇게 나왔는지 짐작이 간다.

다음 사례도 비슷하다.

2 == [2]; // true
"" == [null]; // true

 

ToNumber를 설명할 때 앞에서 말했지만, 우변의 [2], [null]ToPrimitive가 강제변환을 하여 좌변과 비교 가능한 원시 값 으로 바꾼다. 배열의 valueOf() 메서드는 배열 자신을 반환하므로 강제변환 시 배열을 문자열화한다.

따라서 첫째 줄의 [2]sms "2"가 되고 다시 ToNumber 강제변환을 거쳐 2가된다. [null]은 바로 ""이 된다.

결국 2 == 2"" == ""로 해석된다.

이런 결과가 체질적으로 마음에 들지 않는다면 사실 우리가 생각하는 것과는 달리 불만의 원인은 강제변환이 아니다. '배열 -> 문자열'강제변환시 ToPrimitive이 수행하는 로직이 싫은 것이다. 즉, [2].toString()이 "2"를 반환하고 [null].toString()이 ""를 반환하는 형태 자체가 못마땅한 것이다.

그럼 과연 어떻게 문자열로 강제변환해야 맞는 걸까? 내 생각엔 [2] -> "2"만큼 적절한 변환은 아마 없을 것 같다. 아니면 "[2]" 이렇게..? 하지만 콘텍스트가 달라지면 아주 이상하게 작동할 지 모른다.

String(null) -> "null"이니 String([null]) 역시 "null" 이어야 맞지 않느냐고 이의를 제기할 수도 있다. 타당한 주장이지만, 바로 여기에 문제가 숨어 있다.

여기서 암시적 변환은 그 자체로 문제가 없다. [null]을 명시적 강제변환을 해도 결과는 ""이다. 배열을 그 내용과 동등하게 문자열화하는 게 맞는 것인지, 그리고 정확히 어떤 방법으로 문자열화 해야하는지, 이 두 문제가 서로 앞뒤가 맞지 않는다. 따라서 이 모든 말도 안되는 결과를 가져온 String([]) 규칙들이 비난의 대상이 되어야 할 것이다. 아예 배열에서 문자열 강제변환을 없애버리는 건 어떨까? 하지만 그렇게 되면 자바스크립트 언어의 다른 쪽에서 많은 흠집ㅈ이 생길 것이다.

요점만 정리하자면 우리가 맞닥뜨릴 가능성이 조금이라도 있는 평범한 값 사이에서 이상하게 작동하는 강제변환은 거진 이게 전부다.

 


참고

  • You Don't Know JS - 타입과 문법, 스코프와 클로저( 한빛 미디어 )
728x90

댓글