지난 5월 경이었다. 아는 친구와 강변에서 맥주를 한 캔 까면서 나온 이야기 중 하나였다.
두 Date 객체가 있다. new Date(); 로 생성된 두 객체는 밀리초 단위까지 시간이 같다.
이 두 객체를, a와 b라고 하자.
이때 a < b 는 거짓, a > b 는 거짓이다. 그런데 a == b 도 a === b 도 거짓이다.
논리적 모순 아닌가?
확실히 일리 있는 말이다. 숫자 비교를 생각해보면 자명한 사실이다.
그러면 실제 코드로써 Date
를 비교해봐야 사실인지 확인할 수 있을 것이다. 아래는 해당 코드이다.
1 | let a = new Date(), b = new Date(); |
이것의 출력은 다음과 같다.
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
를 기준으로 진행된다.
- 좌측 값을
x
, 우측 값y
라고 한다. - 좌측 우선(LeftFirst) 플래그가 켜진 경우,
x
부터 Javascript의 기본 타입(Primitive Type)으로 변환한다. 이때number
타입 힌트를 주기 때문에, 최대한 숫자로 변환하려 시도한다. 변환된 각 값들을px
,py
라고 부른다. - 좌측 우선(LeftFirst) 플래그가 없다면,
y
부터x
순서로 2번의 작업을 진행한다. - 만약
px
와py
가 Javascript의 기본 타입으로 변환된 결과 문자열이라면, 문자열에 알맞는 별도의 절차를 통해 비교한다. - 그 외의 경우에는,
px
와py
가 숫자가 아니더라도 최대한 숫자로 변환한다. 숫자로 변환된 두 값을 각각nx
와ny
라고 한다. nx
가NaN(Not a Number)
라면 더이상 숫자로써 비교가 불가하므로undefined
를 반환한다.ny
가NaN(Not a Number)
라면 더이상 숫자로써 비교가 불가하므로undefined
를 반환한다.- 만약에
nx
가+0
이고ny
가-0
이라면false
를 반환한다. (수학에서 말하는, 0으로 수렴하는 좌극한과 우극한을 의미) nx
가 양의 무한대라면false
를 반환한다. 왜냐면nx
는ny
보다 커질 수 있기 때문이다.ny
가 양의 무한대라면true
를 반환한다. 왜냐면ny
는nx
보다 커질 수 있기 때문에 항상x < y
를 만족시킬 수 있기 때문이다.ny
가 음의 무한대라면false
를 반환한다.nx
가 무엇이든ny
가 더 작아질 수 있기 때문이다.nx
가 음의 무한대라면true
를 반환한다.ny
가 무엇이든nx
가 더 작아질 수 있기 때문에 항상x < y
를 만족시킬 수 있기 때문이다.- 극단적인 경우를 모두 배제하였으므로 정상적인 비교 경우만 남았다.
nx
가ny
보다 수학적으로 작다고 판단되면true
를 반환하고, 아니면false
를 반환한다. 수학적 판단의 기준은, 이때nx
와ny
는 모두 무한대로 향해가는 수도 아니고 0도 아니라는 가정을 한다.
13번 절차에 대한 설명을 부연하면, nx
와 ny
의 대소비교는 0이 아닌 두 숫자에 대해 대소관계가 성립하면 true
를 반환한다는 의미로 해석된다. 즉, 0과 0의 비교에 대해선 false
를 반환하겠다는 것과 같다.
우리의 두 Date
객체에 대한 이야기로 돌아오면, Date
를 최대한 숫자로 바꿔야만 비교를 할 수 있을 것 같은 느낌이 드는 건 어쩔 수 없는 것 같다. 만약에 유닉스 타임스탬프[2]를 사용하고 있다면 왠지 가능할 것 같다. 더 깊게 들어가면 힘들기 때문에, 대략적으로 그렇게 변환될거라 가정을 했다.[3][4]
둘 다 유닉스 타임이라면, 수학적 대소비교를 거치는데 이때 밀리초 단위까지 같다면 두 시간은 수학적으로 같을 것이다. 따라서 a > b
와 a < b
는 복잡한 과정을 거쳤지만 꽤나 확실하게 모두 false
로 평가됨을 확인할 수 있다.
그럼 a == b
와 a === b
에서는 무슨 일이 일어났을까? 이 두 연산은 7.2.12 Abstract Equality Comparison 과 7.2.13 Strict Equality Comparison 에 정의되어있다. 일단 ==
의 동작부터 살펴본다. 미리 이야기하는거지만, 현재 두 Date
객체를 비교하는 상황에선 ==
와 ===
동작의 차이가 없다.
x
와y
의 값을 받아 타입을 비교한다. 두 타입이 같으면 7.2.13의 Strict Equality Comparison(===)으로 넘어간다- (이하 생략)
두 값의 타입이 같을 때 ==
와 ===
는 동작의 차이가 없으므로 7.2.13으로 이동해서 다시 흐름을 살펴본다.
x
와y
의 타입이 다르면false
를 반환한다.x
가undefined
이나null
이라면true
를 반환한다.x
가 Number 타입일때,x
또는y
가NaN(Not a Number)
일때 비교 불가 상태이므로false
를 반환한다. 같은 숫자라면true
를 반환하는데,+0
과-0
은 서로 같은 것으로 처리한다. 앞의 상황을 모두 처리했다면 숫자가 다른 경우이므로false
.x
가 String 타입이라면, 서로 같은 문자로 구성되어있는지 비교하여 같으면true
.x
가 Boolean 타입이라면, 둘 다 같은 불리언 상태면true
, 아니면false
.x
와y
가 같은 Symbol 값이라면true
x
와y
가 같은 Object 라면true
- 이외에는 모두
false
를 반환한다.
2, 3, 4, 5에서 x
의 타입만을 언급하는 이유에 대해 의문을 가질 수 있는데, 생각보다 분명한 근거가 1번에 있다. 두 값의 타입이 다르면 false
를 반환하도록 1번 조건에서 거르고 있다. 즉, 2번부터는 x
와 y
의 타입이 같다는 것이 암시되어있다.
두 Date
객체를 비교함에 있어서 포인트는 7번 조건이다.[5] 문서상에서는 8번 조건이다. Object가 서로 같으면 true
라는데, 여기에서 같은 Object를 식별하는 기준은 참조 주소(Reference)이다. 두 Date
객체 a
와 b
는 new
연산을 통해 생성된 다른 객체이므로 a === b
는 false
이다.
대소비교 연산자 설명에서 많은 시간을 쏟았지만, 동등 비교는 생각보다 간결하게 끝났다. 결국 정리하면, 두 Date
객체는 크고 작음을 비교할 때는 정상적으로 비교할 수 있지만 동등(Equality)를 따져야할 때는 수학적 비교가 아니라 Javascript 객체 대 객체의 비교가 이루어진다는 점으로 정리할 수 있을 것이다.
꽤 오래 전에 말로는 이 문제에 대한 해설을 정리했지만, 글로 옮겨놓으니 정말 길고 힘들다. 이 글을 읽을 누군가가 이걸 계기로, Javascript의 기묘한 동작에 대해 웃고 넘기지만 말고 스펙을 확인해볼 수 있는 힘을 기른다면 큰 보람을 느끼지 않을까 싶다.
- 1.ECMAScript 2015 Language Specification ↩
- 2.Unix Time ↩
- 3.이 부분을 더 깊게 보려면 ToPrimitive() 연산과 이어서 나오는 ToNumber 연산을 살펴보아야한다 ↩
- 4.이것이 가능하다고 가정한 이유는, Date 객체간의 뺄셈(subtract)이 가능하기 때문이다. A < B 의 양쪽 항에서 -B 를 더하면 A - B < 0 이 된다. 따라서 직접적인 Date 객체 비교를 지원하지 않아도, 우회하여 비교할 방법이 있을 거라 확신했다. ↩
- 5.문서상의 순서와는 다르게, 재구성한 점에 대한 양해를 구한다. 번역의 문제도 있는데, 모든 스텝을 적기에는 번거로운 점이 있었다. ↩