프로필 사진
Sojin Park
Frontend Dev

JavaScript에서 일치 연산자
JavaScript에서 일치 여부를 판단하기 위해 사용하는 세 개의 알고리즘
2018. 12. 23.

JavaScript를 다룬다고 하면 늘 듣는 말이 있다. "예측 불허의 언어", "false와 0이 같은 언어", "true와 1이 같은 언어", ... 이와 관련한 유명한 사진도 있는데 JavaScript에 대한 사람들의 생각을 압축적으로 표현한 것이 아닐까 싶다. (사실 부분적으로 공감이 되는 부분도 있다.)

Thanks for inventing JavaScript

위의 그림에서 보듯이 true == 1, true + true + true === 3 등 이해하기 어려운 결과가 많다. 그러나 사실은 뒤에 숨겨져 있는 규칙을 알면 생각보다 단순하다.

JavaScript에서 동일성을 검증하는 세 가지 방법

JavaScript에는 동일성을 검증하기 위해 3가지 방법이 주어져 있다.

  1. 엄격 일치 비교Strict Equality Comparison 연산자 ===
  2. 추상 일치 비교Abstract Equality Comparison 연산자 ==
  3. Object.is로 사용하는 SameValue

엄격 일치 비교 연산자 ===

===는 생긴 것 때문에 "triple equals" 또는 우리말로 "는는는(...)"이라고 일컬어지곤 한다. 엄격 일치 연산이 이루어지는 규칙은 간단하다. 요약하자면

  1. xy의 타입이 같고 값(또는 참조)이 같은 경우에만 true.
  2. 아니면 false.

연산 규칙을 보면 무척 자연스럽다. 더 자세히 ECMAScript 5.1 명세서를 보면

x, y가 주어져서 x === y로 두 값을 비교할 때 그 결괏값은 true 또는 false이다.

  1. x의 타입과 y의 타입이 다르면 false.
  2. xy가 모두 undefined이면 true.
  3. xy가 모두 null이면 true.
  4. xy가 모두 number 타입이면
    1. xyNaN이면 false.
    2. xy가 같은 숫자 값을 가지면 true.
    3. x+0이고 y-0이면 true.
    4. x-0이고 y+0이면 true.
    5. 아무것에도 해당되지 않으면 (= xy의 값이 다르면) false.
  5. xy가 모두 string 타입인 경우, xy에 정확하게 같은 순서로 문자들이 배열하고 있는 경우 (= 같은 길이이고 같은 위치엔 같은 문자만 있을 경우) true. 아니면 false.
  6. xy가 모두 boolean 타입인 경우, xy가 모두 true 혹은 모두 false인 경우에만 true. 아니면 false.
  7. 남은 경우는 xy가 참조 타입인 경우인데, xy가 모두 같은 객체를 참조하고 있으면 true. 아니면 false.

실제로 연산을 해 보면 아래와 같이 예상 가능한 결과가 나온다.

// '0'을 감싼 string 객체
const obj = new String('0');

0 === 0; // true
'0' === '0'; // true
obj === obj; // true

0 === '0'; // false
0 === obj; // false
'0' === obj; // false

undefined === null; // false
obj === null; // false
obj === undefined; // false

엄격 일치 연산자는 일치 여부를 판단할 때 거의 대부분의 경우에 제일 알맞은 연산자이다. 타입이 다른 경우 바로 false를 반환하기 때문에 일치하는지 검증할 때 타입 변환을 수행하는 ==보다 효율적이기까지 하다.

그러나 ===에도 주의하여야 할 점이 있는데 첫 번째는 NaN이다. NaN은 숫자 연산에서 허용되지 않는 연산이 나왔다는 뜻이기에 정의상 어떤 값과도 일치하지 않는다. (이는 NaN이 포함된 다른 언어에서도 마찬가지이다.) 어떤 변수가 NaN인지 확인하기 위해서는 isNaN을 이용하거나 자신을 포함한 어떤 값과도 일치하지 않는 성질을 이용하여 x !== x와 같은 연산을 이용하여 확인해야 한다.

NaN === NaN; // false
NaN !== NaN; // true

두 번째로 IEEE 754에 규정된 두 값 +0-0의 경우 엄격하게 평가하지 않고 동등하게 취급한다는 것이다. 거의 대부분의 경우에는 +0-0을 구분하면 오히려 문제가 되기에 ===의 결과는 자연스럽다. 만약 +0-0을 구분해야 한다면 Object.is로 평가하는 SameValue를 사용하자.

추상 일치 비교 연산자 ==

추상 일치 비교 연산자는 ===에 자동 타입 변환이 들어간 것이다. 생긴 것 때문에 "double equals" 또는 한국어로 "는는"이라고 한다. == 동일성을 검증하기 위해 사용하는 알고리즘은 요약하자면 다음과 같다.

  1. 타입이 같은 경우에는 x === y와 동일.
  2. 타입이 다른데
    1. xy가 둘 다 값 타입이면 number로 변환하여 동등성을 비교한다.
    2. xy 중 하나가 참조 타입이면 참조 타입 변수를 toString, valueOf를 이용해 값 타입으로 바꾸어 다시 동등성을 평가한다.
  3. 아니면 false.

더 자세히 ECMAScript 5.1 명세서를 참고하면 다음과 같이 정의되어 있다.

x, y가 주어져서 x == y로 두 값을 비교할 때 그 결괏값은 true 또는 false이다.

  1. xy의 타입이 같을 때는 x === y와 결괏값이 같다.
  2. xnull이고 yundefined이면 true.
  3. xundefined이고 ynull이면 true.
  4. xnumber이고 ystring 이면, x == Number(y)와 결괏값이 같다.
  5. xstring이고 ynumber이면, Number(x) == y와 결괏값이 같다.
  6. xboolean이면 Number(x) == y와 결괏값이 같다.
  7. yboolean이면 x == Number(y)와 결괏값이 같다.
  8. xstring 또는 number이고 yobject이면, x == y.toString() (또는 y.toString이 없으면 x == y.valueOf()) 와 결괏값이 같다.
  9. xobject이고 ystring 또는 number이면, x.toString() == y (또는 x.toString이 없으면 x.valueOf() == y) 와 결괏값이 같다.
  10. false 반환.

즉, 값 타입끼리는 숫자로 바꾸어 비교하고, 값 타입과 참조 타입 사이 비교는 참조 타입을 값 타입으로 바꾸어 비교한다는 것이다.

이제 위에서 보았던 이상한 ==의 결과를 이해할 준비가 되었다.

true == 1인 이유는

Number(true) === 1 // 이므로

true == 1
--> Number(true) == 1
--> 1 == 1
--> true

true + true + true == 3인 이유는

Number(true) === 1

true + true + true
--> Number(true) + Number(true) + Number(true)
--> 1 + 1 + 1
--> 3

true + true + true == 3
--> 3 == 3
--> true

"" == 0인 이유는

Number("") === 0 // 이므로

"" == 0
--> Number("") == 0
--> 0 == 0
--> true

false == '0'인 이유는

Number(false) === 0
Number('0') === 0 // 이므로

false == '0'
--> false == Number('0')
--> false == 0
--> Number(false) == 0
--> 0 == 0
--> true

[1, 2] == 1,2인 이유는

[1, 2].toString() === `1,2` // 이므로

[1, 2] == '1,2'
--> [1, 2].toString() == '1,2'
--> '1,2' == '1,2'
--> true

'0' != ''인 이유는 두 피연산자 '0'''가 똑같이 string 타입이고 문자 배열이 일치하지 않기 때문이다.

==의 사용을 피하자

==은 가급적 사용하지 않는 편이 좋다. 왜냐하면 정해진 규칙 상으로는 맞지만 (규칙이 없는 것은 아니다) 규칙이 예상하기 힘들고 생각지 못했던 값들이 일치해 버리는 경우가 많기 때문이다. 때문에 가능하다면 ===을 쓰는 것이 프로그램 동작을 예상 가능하게 해 준다는 점에서 바람직하다.

그래도 ==을 사용하면 좋은 하나의 사용처가 있는데 undefined / null 검증이 그것이다. JavaScript에서 npm을 통해 외부 라이브러리들을 사용하다 보면 null을 반환하는 함수가 있고 undefined를 반환하는 함수가 있는데 의미 없는 빈 값임은 매한가지이므로, 함께 처리하고 싶을 때가 있다. 그러면서 0, ''와 같은 거짓으로 평가되는 값과 null, undefined를 구분짓고 싶을 때, == null 검증을 사용한다. ==의 규칙 상 null은 자기 자신과 undefined에만 일치하기 때문이다.

undefined == null; // true
0 == undefined; // false
0 == null; // false
'' == undefined; // false
'' == null; // false

function fn(optionalVal) {
  // optionalVal이 undefined 또는 null이면
  if (optionalVal == null) {
    // 함수를 실행하지 않고 반환
    return;
  }

  // 아니면 함수를 계속 실행, 이제 optionalVal은 undefined나 null이 아님
}

SameValue 연산 Object.is

Object.is===에서 앞서 말한 주의점 NaN, +0, -0에 대한 처리만 달라진 것이다. 즉, 이러한 값을 특별히 생각하지 않고 NaNNaN과 같고, +0-0과 같지 않다고 판단하는 것이다. 즉

Object.is(NaN, NaN); // true
Object.is(+0, -0); // false
Object.is(+0, 0); // true
Object.is(0, -0); // false

그러나 +0-0을 구분할 필요가 있는 것이 아니라면 Object.is의 사용은 지양하는 것이 좋다. 대부분의 경우 +0-0을 구분하면 생각이 복잡해지고 예기치 못한 결과를 가져오기 때문이다. NaN에 대한 처리는 전역 함수 isNaN을 사용하는 것이 좋다.

isNaN(NaN); // true

SameValueZero

가끔 JavaScript 글을 읽다 보면 SameValueZero에 대해 언급한 글들이 있다. 이것은 SameValue에서 다르게 생각했던 +0-0를 같게 생각하도록 바꾼 것이다. 즉, NaN이 서로 동일하다는 것만 제외하면 ===와 동작이 같다.

SameValueZero 일치 검증은 Array.prototype.includes, String.prototype.includes와 같은 언어 내부 함수에서 사용된다.

요약

  1. 엄격 일치 연산자 ===는 타입이 같고 값이 같은 (또는 참조가 같은) 경우에만 참을 반환한다.
  2. 추상 일치 연산자 ==는 타입이 같은 경우 ===와 같고, 타입이 다른 경우
    • 값 타입은 number로 바꾼 후 비교하고,
    • 참조 타입은 toString 혹은 valueOf로 값 타입으로 바꾸어 비교한다.
  3. Object.isNaN에 대한 일치나 +0, -0에 대한 불일치가 필요할 때 사용한다.