자바스크립트 퀴즈북 리마인드 Day 13: 모듈은 파일 묶음이.
ESM live binding, default/named export, circular dependency, dynamic import, CJS와 ESM 차이를 프론트엔드 코드 리뷰 관점에서 정리합니다.
다음 코드는 전부 “같은 값인가?”를 묻는 것처럼 보입니다.
console.log(NaN === NaN);
console.log(Object.is(NaN, NaN));
console.log([-0].includes(0));
console.log([-0].indexOf(0));
console.log([] == ![]);
결과는 이렇습니다.
false
true
true
0
true
처음 보면 마지막 줄이 제일 이상합니다. 그런데 실무에서는 [] == ![] 같은 장난 문제보다 더 현실적인 형태로 나타납니다.
const selectedIds = [NaN];
selectedIds.includes(NaN); // true
selectedIds.indexOf(NaN); // -1
둘 다 배열에서 값을 찾는 API인데 결과가 다릅니다. 이유는 간단합니다. JavaScript에는 “같다”가 하나만 있는 게 아닙니다. 비교 연산자와 API마다 쓰는 equality 알고리즘이 다릅니다.
===만 쓰면 equality 문제는 끝난다코드 리뷰에서 ==를 피하고 ===를 쓰자는 규칙은 대체로 좋은 기본값입니다. 문제는 여기서 멈추면 NaN, -0, includes, Map, Set 같은 차이를 설명하지 못한다는 점입니다.
===는 타입 변환을 하지 않습니다. 그래서 ==보다 예측하기 쉽습니다. 하지만 ===도 하나의 알고리즘일 뿐입니다.
NaN === NaN; // false
-0 === 0; // true
이 결과가 항상 실무 버그가 되는 것은 아닙니다. 대부분의 UI에서는 -0과 0을 구분하지 않아도 됩니다. 반대로 NaN을 상태값으로 다룬다면 === 비교만으로는 변화를 감지하지 못할 수 있습니다.
그러니 오늘의 목표는 “==는 나쁘고 ===는 좋다”가 아닙니다. 더 정확한 질문은 이겁니다.
지금 이 코드가 기대하는 “같다”는 어떤 규칙인가?
프론트엔드 코드 리뷰에서 자주 만나는 비교 규칙은 네 가지로 정리할 수 있습니다.
| 비교 방식 | 타입 변환 | NaN vs NaN | -0 vs 0 | 대표 사용처 |
|---|---|---|---|---|
== | 있음 | false | true | 느슨한 비교 연산자 |
=== | 없음 | false | true | 엄격 비교 연산자, indexOf |
Object.is | 없음 | true | false | SameValue 비교 |
| SameValueZero | 없음 | true | true | includes, Map, Set의 키 비교 |
이 표에서 중요한 것은 단순 암기가 아닙니다. 같은 데이터라도 어떤 API를 통과하느냐에 따라 “같다”의 의미가 달라진다는 점입니다.
==: 변환이 먼저 끼어드는 비교==는 Abstract Equality Comparison을 사용합니다. 타입이 다르면 비교 전에 변환을 시도합니다.
0 == false; // true
"" == false; // true
"42" == 42; // true
null == undefined; // true
이 규칙은 때로 편해 보이지만 리뷰에서는 위험합니다. 비교 코드만 봐서는 어떤 변환이 일어나는지 바로 드러나지 않기 때문입니다.
if (inputValue == 0) {
showEmptyState();
}
이 코드는 숫자 0만 잡는 것처럼 보입니다. 하지만 빈 문자열도 0처럼 비교될 수 있습니다. 사용자가 폼 입력을 지웠을 때 의도치 않게 empty state가 뜨는 식의 버그로 이어질 수 있습니다.
[] == ![]도 같은 맥락입니다.
[] == ![];
// [] == false
// "" == false
// 0 == 0
// true
이 예제 자체를 외울 필요는 없습니다. 대신 타입이 다른 값을 ==로 비교하면 변환 규칙까지 같이 읽어야 한다는 점만 기억하면 됩니다.
===: 타입 변환은 없지만 예외값은 남는다===는 타입 변환을 하지 않습니다. 그래서 리뷰 기본값으로 적합합니다.
"42" === 42; // false
0 === false; // false
null === undefined; // false
하지만 ===가 모든 equality 문제를 끝내지는 않습니다.
NaN === NaN; // false
-0 === 0; // true
NaN은 “숫자 계산이 실패했다”는 값을 표현합니다. 이 값은 자기 자신과도 ===로 같지 않습니다. 따라서 값이 NaN인지 확인하려면 Number.isNaN이나 Object.is를 써야 합니다.
Number.isNaN(NaN); // true
Object.is(NaN, NaN); // true
반대로 -0과 0은 ===에서 같습니다. 보통은 괜찮습니다. 다만 그래프 축, 수학 계산, 방향성 있는 0을 다루는 아주 좁은 영역에서는 Object.is(-0, 0) 차이가 의미를 가질 수 있습니다.
Object.is: 더 민감한 같음Object.is는 SameValue 비교를 사용합니다. ===와 거의 비슷하지만 두 가지가 다릅니다.
Object.is(NaN, NaN); // true
Object.is(-0, 0); // false
이 차이는 상태 비교에서 가끔 중요해집니다. 예를 들어 값이 NaN에서 NaN으로 유지되는 경우를 “변화 없음”으로 볼지, 계산 실패가 다시 발생했다고 볼지 판단해야 할 때가 있습니다.
React 내부에서도 과거부터 상태 변경 판단에 Object.is와 가까운 비교 감각이 중요하게 다뤄졌습니다. 그래서 프론트엔드 개발자는 Object.is를 “특이한 API”가 아니라 “NaN과 -0까지 구분하는 더 정확한 비교”로 기억해두는 편이 좋습니다.
그렇다고 모든 비교를 Object.is로 바꾸라는 뜻은 아닙니다. 실무 질문은 이쪽입니다.
이 값에서 NaN끼리 같게 보는 것이 맞는가?
-0과 0을 구분해야 하는 도메인인가?
대부분의 서비스 UI에서는 두 번째 질문의 답이 “아니오”입니다. 그러면 ===가 더 읽기 쉽습니다.
includes, Map, Set이 쓰는 감각SameValueZero는 Object.is와 비슷하지만 -0과 0을 같게 봅니다.
[NaN].includes(NaN); // true
[-0].includes(0); // true
const set = new Set([NaN, NaN, -0, 0]);
set.size; // 2
배열의 includes는 SameValueZero를 씁니다. 그래서 NaN을 찾을 수 있습니다. 반면 indexOf는 ===에 가까운 비교를 쓰기 때문에 NaN을 찾지 못합니다.
[NaN].includes(NaN); // true
[NaN].indexOf(NaN); // -1
이 차이는 검색 UI나 필터 상태에서 꽤 현실적인 버그가 됩니다. API에서 숫자 필터 값이 잘못 파싱되어 NaN이 섞였는데, indexOf 기반 로직은 못 찾고 includes 기반 로직은 찾는 식입니다.
Map과 Set의 키 비교도 SameValueZero 감각으로 이해하면 됩니다.
const cache = new Map();
cache.set(NaN, "invalid number");
cache.get(NaN); // "invalid number"
cache.set(-0, "minus zero");
cache.get(0); // "minus zero"
리뷰에서는 이런 질문을 하면 됩니다.
이 컬렉션의 키에 NaN이 들어올 수 있는가?
-0과 0을 같은 키로 봐도 되는가?
배열 검색 API가 indexOf인지 includes인지에 따라 결과가 달라지는가?
비교 코드는 짧아서 대충 지나가기 쉽습니다. 하지만 리뷰할 때는 다음 네 가지를 분리해보면 좋습니다.
if (route.params.page == currentPage) {
// ...
}
라우터 파라미터는 보통 문자열이고, 상태값은 숫자일 수 있습니다. 이때 ==로 맞추기보다 변환 지점을 명시하는 편이 낫습니다.
const pageFromRoute = Number(route.params.page);
if (pageFromRoute === currentPage) {
// ...
}
명시적 변환은 코드가 조금 길어지지만, “언제 문자열을 숫자로 해석하는가”가 리뷰에 드러납니다.
equality 문제는 비교 연산자에서만 생기지 않습니다. 비교 전에 이미 값이 바뀌는 경우가 많습니다.
const raw = input.value;
if (raw == 0) {
showZeroCase();
}
이 코드는 “입력값이 0인가?”처럼 보이지만, 실제로는 두 질문이 겹쳐 있습니다.
raw를 숫자로 해석해도 되는가?"", " ", false, [] 같은 값이 들어오면 첫 번째 질문의 답이 비교식 안에 숨어버립니다. 그래서 Day 2의 equality 리뷰는 Day 3의 ToPrimitive/숫자 변환 리뷰와 붙어야 합니다.
const raw = input.value.trim();
if (raw === "") {
showEmptyCase();
} else {
const amount = Number(raw);
if (Number.isNaN(amount)) {
showInvalidNumberCase();
} else if (amount === 0) {
showZeroCase();
}
}
코드는 길어졌지만, 리뷰할 수 있는 경계가 생깁니다. 빈 입력, 숫자 변환 실패, 숫자 0이 서로 다른 상태로 남습니다. “==를 ===로 바꿨다”보다 더 중요한 수리는 비교 전에 일어나는 해석을 밖으로 꺼내는 것입니다.
NaN이 들어올 수 있는가사용자 입력, URL query, CSV, 서버 응답을 숫자로 파싱하면 NaN 가능성이 생깁니다.
const amount = Number(input.value);
if (amount === NaN) {
showError();
}
이 코드는 의도대로 동작하지 않습니다. NaN === NaN은 false입니다.
if (Number.isNaN(amount)) {
showError();
}
if (values.indexOf(target) >= 0) {
// 포함됨
}
위치를 실제로 쓰지 않는다면 includes가 의도를 더 잘 드러냅니다. 게다가 NaN 처리도 다릅니다.
if (values.includes(target)) {
// 포함됨
}
큰 ID는 계산용 숫자가 아니라 식별자입니다. Day 1의 숫자 모델과 이어지는 지점입니다.
user.id === selectedUserId;
둘 다 number처럼 보이더라도 서버가 큰 정수 ID를 문자열로 보내는지, 클라이언트가 Number로 바꾸고 있지는 않은지 확인해야 합니다. equality 문제는 타입 변환 문제와 자주 붙어 있습니다.
===를 기본값으로 쓰는 습관은 좋습니다. 다만 그 규칙만으로 JavaScript equality를 다 이해했다고 보면 안 됩니다.
==는 타입 변환이 들어간다.===는 타입 변환은 없지만 NaN과 -0 예외가 있다.Object.is는 NaN끼리 같게 보고 -0과 0을 구분한다.NaN끼리 같게 보고 -0과 0도 같게 본다.includes, Map, Set은 ===와 완전히 같은 비교 감각이 아니다.리뷰에서는 “==를 쓰지 마세요”에서 끝내지 말고, 이 비교가 어떤 equality 알고리즘을 기대하는지까지 물어보는 편이 더 실용적입니다.
+ 연산자와 ToPrimitive 흐름은 equality 문제가 강제 변환과 붙는 지점을 이어서 다룹니다.string | number처럼 가능한 값의 범위를 표현한 뒤에도 런타임 비교 정책은 따로 정해야 한다는 쪽으로 연결됩니다.console.log([NaN].includes(NaN));
console.log([NaN].indexOf(NaN));Post Q&A
자바스크립트 퀴즈북 리마인드 Day 2: 같은 값인지 묻는 네 가지 방법 전체를 기준으로 질문과 피드백을 받아요.답을 본 뒤에는 이 내용을 댓글로 달아서 서징에게도 물어볼 수 있어요. 작성자가 직접 볼 수 있어요!
ESM live binding, default/named export, circular dependency, dynamic import, CJS와 ESM 차이를 프론트엔드 코드 리뷰 관점에서 정리합니다.
DOM 이벤트의 capture/bubble, delegation, default action, React synthetic event 경계를 프론트엔드 코드 리뷰 관점에서 정리합니다.
프로토타입 조회, class 문법, 인스턴스 필드와 prototype 메서드 차이를 프론트엔드 코드 리뷰 관점에서 정리합니다.
고차 함수, 순수 함수, 부수 효과, async 함수의 Promise 반환을 코드 리뷰에서 어떻게 읽을지 정리합니다.
this 바인딩을 정의 위치가 아니라 호출 형태, 메서드 분리, arrow function, bind 기준으로 읽는 방법을 코드 리뷰 관점에서 정리합니다.
콜 스택, 태스크 큐, 마이크로태스크 큐를 기준으로 Promise와 setTimeout 실행 순서를 코드 리뷰 관점에서 정리합니다.
클로저가 값 복사가 아니라 렉시컬 환경 참조라는 점을 stale closure, 루프 콜백, React 이벤트 리뷰 관점에서 정리합니다.
스코프 체인, 렉시컬 환경, var/let/const 호이스팅 차이를 콜백·상태 버그 리뷰 관점에서 다시 정리합니다.
객체 참조와 얕은 복사, Object와 Map/Set의 차이, WeakMap/WeakSet이 필요한 메모리 상황을 프론트엔드 코드 리뷰 관점에서 정리합니다.
객체 프로퍼티를 key/value가 아니라 descriptor로 읽는 법, getter/setter와 defineProperty, preventExtensions/seal/freeze의 경계를 코드 리뷰 관점에서 정리합니다.
객체가 원시값으로 바뀌는 ToPrimitive 흐름, + 연산자의 문자열 연결과 숫자 덧셈 분기, Symbol.toPrimitive가 코드 리뷰에서 왜 중요한지 정리합니다.
==, ===, Object.is, SameValueZero가 각각 어떤 비교 알고리즘을 쓰는지 정리하고, includes와 indexOf, Map/Set 키 비교에서 생기는 프론트엔드 리뷰 포인트를 잡습니다.
자바스크립트의 Number가 왜 정수처럼 보여도 부동소수점 모델 위에서 움직이는지, NaN과 -0, safe integer, BigInt를 프론트엔드 코드 리뷰 관점에서 다시 정리합니다.