Skip to content

==와 ===는 뭐가 다른가 ​

비교 연산은 안다고 생각하기 쉬운데, 자바스크립트는 다른 언어와 차이가 있다. 아래 다섯 줄을 전부 맞히고 나서야 안다고 말할 수 있다? 라고 생각했다.

javascript
console.log(1 == '1');          // ?
console.log(null == undefined); // ?
console.log(NaN === NaN);       // ?
console.log(0.1 + 0.2 === 0.3); // ?
console.log({} === {});         // ?

답은 순서대로 true, true, false, false, false다. 하나씩 이유를 따라가면 이 글의 목차가 된다.

==는 비교 전에 값을 바꾼다 ​

==(동등 비교)는 두 값의 타입이 다르면 먼저 타입을 암묵적으로 변환한 뒤 비교한다. 1 == '1'은 문자열을 숫자로 바꿔 1 == 1로 만들기 때문에 true다. 편해 보이지만, 변환 규칙을 전부 외우지 않으면 결과를 예측할 수 없다는 게 문제다.

javascript
console.log('' == 0);        // true — 빈 문자열이 0으로 변환된다
console.log('0' == 0);       // true
console.log('' == '0');      // false — 같은 타입이라 변환이 없다
console.log(null == 0);      // false — null은 특별 취급
console.log(null == undefined); // true — 이 둘만 서로 같다고 규칙에 박혀 있다

''와 '0'은 각각 0과 같은데 서로는 다르다 — 동등 비교인데 이행이 안 된다. 개인적으로 이 규칙들은 외울 가치가 없다고 생각한다. ===를 기본으로 쓰고, ==는 x == null(null과 undefined를 한 번에 거르는 관용구) 정도만 허용하는 게 사실상 업계 표준이다.

===(일치 비교)는 변환 없이 타입이 다르면 그냥 false다. 예측 가능하다. 남은 문제들은 전부 ===인데도 직관을 배신하는 경우들이다.

NaN은 자기 자신과도 다르다 ​

javascript
console.log(NaN === NaN); // false

NaN(Not-a-Number)은 자바스크립트에서 유일하게 자기 자신과 일치하지 않는 값이다(IEEE 754 표준이 그렇게 정했다). 그래서 어떤 값이 NaN인지 === NaN으로는 영원히 확인할 수 없고, 전용 도구를 써야 한다.

javascript
console.log(Number.isNaN(NaN)); // true

역으로 이 성질을 이용해 x !== x가 true면 x는 NaN이라는 트릭도 있다 — 실제로 폴리필들이 쓰던 방법이다.

0.1 + 0.2는 0.3이 아니다 ​

javascript
console.log(0.1 + 0.2);         // 0.30000000000000004
console.log(0.1 + 0.2 === 0.3); // false

자바스크립트 버그가 아니라, IEEE 754 배정밀도 부동소수점을 쓰는 모든 언어의 공통 현상이다. 우리가 10진수로 1/3을 유한하게 못 쓰듯(0.3333…), 0.1과 0.2는 2진수로 무한소수라서 저장되는 순간 이미 미세한 오차를 품는다. 그 오차가 덧셈에서 드러난 것뿐이다.

실수를 비교할 땐 "같은가"가 아니라 "충분히 가까운가"를 물어야 한다.

javascript
console.log(Math.abs(0.1 + 0.2 - 0.3) < Number.EPSILON); // true

단, Number.EPSILON은 1 근처 크기의 수에서만 유효한 허용 오차라서 만능 관용구는 아니다(큰 수끼리 비교하면 오차도 커져서 상대 오차로 비교해야 한다). 돈 계산처럼 오차가 허용되지 않는 도메인에서 정수(원 단위, 센트 단위)로 환산해 계산하는 관례도 여기서 나온다.

객체의 ===는 내용이 아니라 참조를 묻는다 ​

javascript
console.log({} === {}); // false — 내용이 같아도
const a = { v: 1 };
const b = a;
console.log(a === b); // true — 같은 객체를 가리켜야만

원시 vs 객체 글의 결론이 비교에서 그대로 재현된다. 변수에 담긴 게 참조이니, ===가 비교하는 것도 참조다. "내용이 같은가"가 아니라 "같은 객체인가"를 묻는 연산이라서, 내용 비교가 필요하면 직접 프로퍼티를 순회하거나 라이브러리(lodash isEqual 등)를 써야 한다.

이 성질이 실 프젝에서 가장 크게 작동하는 곳이 React다. React는 상태가 바뀌었는지를 내용 비교가 아니라 Object.is 기반의 참조 비교로 판단한다. 내용을 아무리 바꿔도 참조가 그대로면 "안 바뀜"으로 처리된다.

javascript
const [items, setItems] = useState([1, 2]);

items.push(3);      // 내용은 바뀌었지만
setItems(items);    // 참조가 같다 → React: "안 바뀌었네" → 리렌더 없음

setItems([...items, 3]); // 새 참조 → 변경 감지 → 리렌더

지난 글에서 "바뀔 경로만 새 객체로 만든다"던 불변 업데이트 패턴은 결국 이 비교 방식에 맞추기 위한 것이다. 값싼 참조 비교로 변경을 감지하려면, 변경이 반드시 새 참조를 만들어야 한다.

마지막으로 Object.is는 ===와 거의 같고 두 곳만 다르다 — 사람 직관에 가깝게 고친 버전이다.

javascript
Object.is(NaN, NaN); // true  (===는 false)
Object.is(0, -0);    // false (===는 true)

비교의 함정들은 전부 한 가지 질문으로 정리된다. "지금 무엇과 무엇을 비교하고 있는가" — ==는 변환된 값끼리, ===의 원시 값은 값끼리, 객체는 참조끼리. 비교 대상을 정확히 알면 다섯 문제에 더는 안 속는다.

여기까지가 "값" 이야기다. 그런데 그 값을 담는 그릇(변수)은 언제 만들어지고 언제부터 쓸 수 있는 걸까 — 시리즈 첫 질문이었던 호이스팅으로 이어진다. → 호이스팅은 왜 일어나는가