1. 개요
==와 ===의 차이를 “타입까지 검사하느냐”로 외우고 넘어가는 경우가 많다. 맞는 말이지만 그것만으로는 왜 ==이 위험한지 감이 안 온다. ==은 타입이 다르면 암묵적으로 형변환을 한 뒤 비교한다. 그 형변환 규칙이 직관과 어긋나는 경우가 꽤 있다. 몇 가지만 알아도 왜 팀 컨벤션에서 ===을 강제하는지 이해가 된다.
2. 핵심 내용
2-1. == 은 타입이 다르면 변환부터 한다
1 == "1"; // true, 문자열을 숫자로 변환
0 == false; // true, boolean 을 숫자로 변환
null == undefined; // true, 이 둘만 예외적으로 서로 같다고 취급규칙 자체는 정해져 있지만(ECMA-262의 추상 동등 비교 알고리즘), 외우기엔 항목이 많고 직관과 어긋나는 조합이 섞여 있다.
2-2. 흔히 틀리는 조합들
"" == 0; // true, 빈 문자열이 숫자로 변환되면 0
"" == false; // true
[] == false; // true, 배열이 문자열을 거쳐 숫자로 이중 변환된다
[] == ![]; // true, ![] 는 false, 그리고 [] == false
"0" == false; // true
null == 0; // false, null 은 undefined 하고만 같다고 취급된다
NaN == NaN; // false, NaN 은 자기 자신과도 같지 않다[] == ![]가 true인 게 대표적인 예다. 배열을 논리 부정(!)하면 false가 되고, 그다음 [] == false에서 배열이 다시 빈 문자열로, 빈 문자열이 다시 0으로 변환돼 false(0)와 같아진다. 이런 걸 실무에서 의도하고 쓰는 사람은 없다.
2-3. === 은 변환을 안 한다
1 === "1"; // false, 타입이 다르면 그냥 false
0 === false; // false
null === undefined; // false타입이 다르면 무조건 false다. 예측 가능하다. 팀 컨벤션에서 ==을 금지하고 ===을 강제하는 이유가 이거다. 형변환 규칙을 매번 떠올리며 코드를 읽을 필요가 없어진다.
TypeScript에서 typeof x === "string"처럼 항상 ===으로 타입을 좁히는 것도 같은 이유다. 여기서 ==을 썼다간 형변환이 끼어들어 좁혀지는 타입 자체가 흔들린다. 이 좁히기(narrowing) 얘기는 TypeScript 기초 2 - 유니온과 타입 좁히기에서 이어간다.
2-4. NaN 과 -0, 그리고 Object.is
===도 만능은 아니다. NaN === NaN은 false고, +0 === -0은 true다. 값 비교가 정말 엄밀해야 할 때(캐시 키 비교 등)는 Object.is를 쓴다.
Object.is(NaN, NaN); // true
Object.is(+0, -0); // false
[NaN].includes(NaN); // true, includes 는 내부적으로 SameValueZero 사용
[NaN].indexOf(NaN); // -1, indexOf 는 === 사용같은 “NaN을 찾는다”인데 includes와 indexOf가 다른 결과를 낸다는 걸 모르면 디버깅에서 시간을 꽤 쓴다.
2-5. 예외적으로 ==을 허용하는 한 곳
value == null은 value가 null이거나 undefined일 때만 true다. 두 값을 한 번에 걸러야 할 때 의도적으로 쓰는 관용구다.
if (value == null) { /* null 또는 undefined */ }
// 위 한 줄이 아래와 같다
if (value === null || value === undefined) { /* ... */ }이 경우를 빼면 ==을 쓸 이유는 거의 없다.
3. 마무리
요약
==은 타입이 다르면 암묵적 형변환 후 비교한다, 규칙이 많고 직관과 어긋난다===은 타입이 다르면 무조건 false다, 예측 가능하다NaN === NaN은 false, 엄밀 비교가 필요하면Object.isvalue == null은 null/undefined 를 한 번에 거르는 유일하게 허용되는 관용구다
다음은 CommonJS 와 ESM 이다.