Lilyrs

Javascript에서의 Date 객체 비교할 때

지난 5월 경이었다. 아는 친구와 강변에서 맥주를 한 캔 까면서 나온 이야기 중 하나였다.

두 Date 객체가 있다. new Date(); 로 생성된 두 객체는 밀리초 단위까지 시간이 같다.
이 두 객체를, a와 b라고 하자.
이때 a < b 는 거짓, a > b 는 거짓이다. 그런데 a == b 도 a === b 도 거짓이다.
논리적 모순 아닌가?

확실히 일리 있는 말이다. 숫자 비교를 생각해보면 자명한 사실이다.

그러면 실제 코드로써 Date 를 비교해봐야 사실인지 확인할 수 있을 것이다. 아래는 해당 코드이다.

1
2
3
4
5
6
7
8
let a = new Date(), b = new Date();
console.info("a = " + a);
console.info("b = " + b);
console.info("milliseconds equality check: " + (a.getMilliseconds() === b.getMilliseconds()));
console.info("a > b : " + (a > b));
console.info("a < b : " + (a < b));
console.info("a ==b: " + (a == b));
console.info("a===b: " + (a === b));

이것의 출력은 다음과 같다.

a = Thu Sep 26 2019 20:35:15 GMT+0900 (Korean Standard Time)
b = Thu Sep 26 2019 20:35:15 GMT+0900 (Korean Standard Time)
milliseconds equality check: true
a > b : false
a < b : false
a ==b: false
a===b: false

Javascript의 동등 비교는 비범한 사례가 많다. 그 예로 속칭 자바스크립트의 삼위일체 라고 불리는 이미지가 많이 돌아다니곤 한다. 그리고 절대 == 대신 === 를 사용하라곤 한다. 하지만 구체적인 이유는 잘 언급하지 않는 경우가 많다.

이제 이 Date 객체의 비교 과정에서 일어나는 일을 추적해보자.

이 비교 연산은 ECMAScript Language Specification 에서 정의하는 동작에 따라 구현되어있다. 따라서 이 기괴한 동작을 이해하려면 언어의 사양을 봐야한다. 이 상황이 암시하는 건, 상기의 Date 객체 비교 동작이 절대 버그가 아니란 것이다.

ECMAScript 2015에서의 사양은 문서 번호 ECMA-262, 2015년 7월에 6판이 공개되어있다.[1]

먼저 대소비교 연산자의 스펙을 확인한다. 자명하지만, 어떻게 돌아가는지 검증해보는 것이 좋을 것 같다. 해당 문단은 7.2.11 절의 Abstract Relational Comparison 연산에 정의되어있다. 일단 보면 토할 것 같이 생겼다. 정신줄 잡고 쭉 읽어보면 다음과 같은 흐름이 이루어진다. 이 연산 절차는 x < y 를 기준으로 진행된다.

  1. 좌측 값을 x, 우측 값 y라고 한다.
  2. 좌측 우선(LeftFirst) 플래그가 켜진 경우, x 부터 Javascript의 기본 타입(Primitive Type)으로 변환한다. 이때 number 타입 힌트를 주기 때문에, 최대한 숫자로 변환하려 시도한다. 변환된 각 값들을 px, py라고 부른다.
  3. 좌측 우선(LeftFirst) 플래그가 없다면, y 부터 x 순서로 2번의 작업을 진행한다.
  4. 만약 pxpy 가 Javascript의 기본 타입으로 변환된 결과 문자열이라면, 문자열에 알맞는 별도의 절차를 통해 비교한다.
  5. 그 외의 경우에는, pxpy 가 숫자가 아니더라도 최대한 숫자로 변환한다. 숫자로 변환된 두 값을 각각 nxny 라고 한다.
  6. nxNaN(Not a Number) 라면 더이상 숫자로써 비교가 불가하므로 undefined 를 반환한다.
  7. nyNaN(Not a Number) 라면 더이상 숫자로써 비교가 불가하므로 undefined 를 반환한다.
  8. 만약에 nx+0 이고 ny-0 이라면 false 를 반환한다. (수학에서 말하는, 0으로 수렴하는 좌극한과 우극한을 의미)
  9. nx 가 양의 무한대라면 false 를 반환한다. 왜냐면 nxny 보다 커질 수 있기 때문이다.
  10. ny 가 양의 무한대라면 true 를 반환한다. 왜냐면 nynx 보다 커질 수 있기 때문에 항상 x < y 를 만족시킬 수 있기 때문이다.
  11. ny 가 음의 무한대라면 false 를 반환한다. nx 가 무엇이든 ny 가 더 작아질 수 있기 때문이다.
  12. nx 가 음의 무한대라면 true 를 반환한다. ny 가 무엇이든 nx 가 더 작아질 수 있기 때문에 항상 x < y 를 만족시킬 수 있기 때문이다.
  13. 극단적인 경우를 모두 배제하였으므로 정상적인 비교 경우만 남았다. nxny 보다 수학적으로 작다고 판단되면 true 를 반환하고, 아니면 false 를 반환한다. 수학적 판단의 기준은, 이때 nxny 는 모두 무한대로 향해가는 수도 아니고 0도 아니라는 가정을 한다.

13번 절차에 대한 설명을 부연하면, nxny의 대소비교는 0이 아닌 두 숫자에 대해 대소관계가 성립하면 true 를 반환한다는 의미로 해석된다. 즉, 0과 0의 비교에 대해선 false 를 반환하겠다는 것과 같다.

우리의 두 Date 객체에 대한 이야기로 돌아오면, Date 를 최대한 숫자로 바꿔야만 비교를 할 수 있을 것 같은 느낌이 드는 건 어쩔 수 없는 것 같다. 만약에 유닉스 타임스탬프[2]를 사용하고 있다면 왠지 가능할 것 같다. 더 깊게 들어가면 힘들기 때문에, 대략적으로 그렇게 변환될거라 가정을 했다.[3][4]

둘 다 유닉스 타임이라면, 수학적 대소비교를 거치는데 이때 밀리초 단위까지 같다면 두 시간은 수학적으로 같을 것이다. 따라서 a > ba < b 는 복잡한 과정을 거쳤지만 꽤나 확실하게 모두 false 로 평가됨을 확인할 수 있다.

그럼 a == ba === b 에서는 무슨 일이 일어났을까? 이 두 연산은 7.2.12 Abstract Equality Comparison7.2.13 Strict Equality Comparison 에 정의되어있다. 일단 == 의 동작부터 살펴본다. 미리 이야기하는거지만, 현재 두 Date 객체를 비교하는 상황에선 ===== 동작의 차이가 없다.

  1. xy 의 값을 받아 타입을 비교한다. 두 타입이 같으면 7.2.13의 Strict Equality Comparison(===)으로 넘어간다
  2. (이하 생략)

두 값의 타입이 같을 때 ===== 는 동작의 차이가 없으므로 7.2.13으로 이동해서 다시 흐름을 살펴본다.

  1. xy 의 타입이 다르면 false 를 반환한다.
  2. xundefined 이나 null 이라면 true를 반환한다.
  3. x 가 Number 타입일때, x 또는 yNaN(Not a Number) 일때 비교 불가 상태이므로 false 를 반환한다. 같은 숫자라면 true 를 반환하는데, +0-0 은 서로 같은 것으로 처리한다. 앞의 상황을 모두 처리했다면 숫자가 다른 경우이므로 false.
  4. x 가 String 타입이라면, 서로 같은 문자로 구성되어있는지 비교하여 같으면 true.
  5. x 가 Boolean 타입이라면, 둘 다 같은 불리언 상태면 true, 아니면 false.
  6. xy가 같은 Symbol 값이라면 true
  7. xy가 같은 Object 라면 true
  8. 이외에는 모두 false 를 반환한다.

2, 3, 4, 5에서 x 의 타입만을 언급하는 이유에 대해 의문을 가질 수 있는데, 생각보다 분명한 근거가 1번에 있다. 두 값의 타입이 다르면 false 를 반환하도록 1번 조건에서 거르고 있다. 즉, 2번부터는 xy 의 타입이 같다는 것이 암시되어있다.

Date 객체를 비교함에 있어서 포인트는 7번 조건이다.[5] 문서상에서는 8번 조건이다. Object가 서로 같으면 true 라는데, 여기에서 같은 Object를 식별하는 기준은 참조 주소(Reference)이다. 두 Date 객체 abnew 연산을 통해 생성된 다른 객체이므로 a === bfalse 이다.

대소비교 연산자 설명에서 많은 시간을 쏟았지만, 동등 비교는 생각보다 간결하게 끝났다. 결국 정리하면, 두 Date 객체는 크고 작음을 비교할 때는 정상적으로 비교할 수 있지만 동등(Equality)를 따져야할 때는 수학적 비교가 아니라 Javascript 객체 대 객체의 비교가 이루어진다는 점으로 정리할 수 있을 것이다.

꽤 오래 전에 말로는 이 문제에 대한 해설을 정리했지만, 글로 옮겨놓으니 정말 길고 힘들다. 이 글을 읽을 누군가가 이걸 계기로, Javascript의 기묘한 동작에 대해 웃고 넘기지만 말고 스펙을 확인해볼 수 있는 힘을 기른다면 큰 보람을 느끼지 않을까 싶다.


  1. 1.ECMAScript 2015 Language Specification
  2. 2.Unix Time
  3. 3.이 부분을 더 깊게 보려면 ToPrimitive() 연산과 이어서 나오는 ToNumber 연산을 살펴보아야한다
  4. 4.이것이 가능하다고 가정한 이유는, Date 객체간의 뺄셈(subtract)이 가능하기 때문이다. A < B 의 양쪽 항에서 -B 를 더하면 A - B < 0 이 된다. 따라서 직접적인 Date 객체 비교를 지원하지 않아도, 우회하여 비교할 방법이 있을 거라 확신했다.
  5. 5.문서상의 순서와는 다르게, 재구성한 점에 대한 양해를 구한다. 번역의 문제도 있는데, 모든 스텝을 적기에는 번거로운 점이 있었다.