자바스크립트 퀴즈북 리마인드 Day 3: + 연산자와.
객체가 원시값으로 바뀌는 ToPrimitive 흐름, + 연산자의 문자열 연결과 숫자 덧셈 분기, Symbol.toPrimitive가 코드 리뷰에서 왜 중요한지 정리합니다.
다음 코드는 전부 “같은 값인가?”를 묻는 것처럼 보입니다.
console.log(NaN === NaN);
console.log(Object.is(NaN, NaN));
console.log([-0].includes(0));
console.log([-0].indexOf(0));
console.log(
Post Q&A
자바스크립트 퀴즈북 리마인드 Day 2: 같은 값인지 묻는 네 가지 방법 전체를 기준으로 질문과 피드백을 받아요.답을 본 뒤에는 이 내용을 댓글로 달아서 서징에게도 물어볼 수 있어요. 작성자가 직접 볼 수 있어요!
결과는 이렇습니다.
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 | 느슨한 비교 연산자 |
=== |
이 표에서 중요한 것은 단순 암기가 아닙니다. 같은 데이터라도 어떤 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) {
// ...
}
명시적 변환은 코드가 조금 길어지지만, “언제 문자열을 숫자로 해석하는가”가 리뷰에 드러납니다.
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 알고리즘을 기대하는지까지 물어보는 편이 더 실용적입니다.
console.log([NaN].includes(NaN));
console.log([NaN].indexOf(NaN));==, ===, Object.is, SameValueZero가 각각 어떤 비교 알고리즘을 쓰는지 정리하고, includes와 indexOf, Map/Set 키 비교에서 생기는 프론트엔드 리뷰 포인트를 잡습니다.
| 없음 |
| false |
| true |
엄격 비교 연산자, indexOf |
Object.is | 없음 | true | false | SameValue 비교 |
| SameValueZero | 없음 | true | true | includes, Map, Set의 키 비교 |