LogoSEO Jing
  • All Posts
  • SEO Jing
  • okayJing
  • KD Team
  • CLAB Coreteam
  • Study

Contact Me

© 2026 SEOJing. All rights reserved.

자바스크립트 퀴즈북 리마인드 Day 2: 같은 값인지 묻는 네 가지 방법

2026년 6월 28일·8분 읽기

오늘의 문제

다음 코드는 전부 “같은 값인가?”를 묻는 것처럼 보입니다.

js
console.log(NaN === NaN);
console.log(Object.is(NaN, NaN));
console.log([-0].includes(0));
console.log([-0].indexOf(0));
console.log([] == ![]);

결과는 이렇습니다.

text
false
true
true
0
true

처음 보면 마지막 줄이 제일 이상합니다. 그런데 실무에서는 [] == ![] 같은 장난 문제보다 더 현실적인 형태로 나타납니다.

js
const selectedIds = [NaN];

selectedIds.includes(NaN); // true
selectedIds.indexOf(NaN); // -1

둘 다 배열에서 값을 찾는 API인데 결과가 다릅니다. 이유는 간단합니다. JavaScript에는 “같다”가 하나만 있는 게 아닙니다. 비교 연산자와 API마다 쓰는 equality 알고리즘이 다릅니다.

---

흔한 착각: `===`만 쓰면 equality 문제는 끝난다

코드 리뷰에서 ==를 피하고 ===를 쓰자는 규칙은 대체로 좋은 기본값입니다. 문제는 여기서 멈추면 NaN, -0, includes, Map, Set 같은 차이를 설명하지 못한다는 점입니다.

===는 타입 변환을 하지 않습니다. 그래서 ==보다 예측하기 쉽습니다. 하지만 ===도 하나의 알고리즘일 뿐입니다.

js
NaN === NaN; // false
-0 === 0; // true

이 결과가 항상 실무 버그가 되는 것은 아닙니다. 대부분의 UI에서는 -0과 0을 구분하지 않아도 됩니다. 반대로 NaN을 상태값으로 다룬다면 === 비교만으로는 변화를 감지하지 못할 수 있습니다.

그러니 오늘의 목표는 “==는 나쁘고 ===는 좋다”가 아닙니다. 더 정확한 질문은 이겁니다.

text
지금 이 코드가 기대하는 “같다”는 어떤 규칙인가?

---

equality 알고리즘을 한 장으로 보기

==, ===, Object.is, SameValueZero의 차이를 정리한 JavaScript equality 다이어그램

프론트엔드 코드 리뷰에서 자주 만나는 비교 규칙은 네 가지로 정리할 수 있습니다.

비교 방식타입 변환NaN vs NaN-0 vs 0대표 사용처
==있음falsetrue느슨한 비교 연산자
===없음falsetrue엄격 비교 연산자, indexOf
Object.is없음truefalseSameValue 비교
SameValueZero없음truetrueincludes, Map, Set의 키 비교

이 표에서 중요한 것은 단순 암기가 아닙니다. 같은 데이터라도 어떤 API를 통과하느냐에 따라 “같다”의 의미가 달라진다는 점입니다.

---

1. `==`: 변환이 먼저 끼어드는 비교

==는 Abstract Equality Comparison을 사용합니다. 타입이 다르면 비교 전에 변환을 시도합니다.

js
0 == false; // true
"" == false; // true
"42" == 42; // true
null == undefined; // true

이 규칙은 때로 편해 보이지만 리뷰에서는 위험합니다. 비교 코드만 봐서는 어떤 변환이 일어나는지 바로 드러나지 않기 때문입니다.

js
if (inputValue == 0) {
  showEmptyState();
}

이 코드는 숫자 0만 잡는 것처럼 보입니다. 하지만 빈 문자열도 0처럼 비교될 수 있습니다. 사용자가 폼 입력을 지웠을 때 의도치 않게 empty state가 뜨는 식의 버그로 이어질 수 있습니다.

[] == ![]도 같은 맥락입니다.

js
[] == ![];
// [] == false
// "" == false
// 0 == 0
// true

이 예제 자체를 외울 필요는 없습니다. 대신 타입이 다른 값을 ==로 비교하면 변환 규칙까지 같이 읽어야 한다는 점만 기억하면 됩니다.

---

2. `===`: 타입 변환은 없지만 예외값은 남는다

===는 타입 변환을 하지 않습니다. 그래서 리뷰 기본값으로 적합합니다.

js
"42" === 42; // false
0 === false; // false
null === undefined; // false

하지만 ===가 모든 equality 문제를 끝내지는 않습니다.

js
NaN === NaN; // false
-0 === 0; // true

NaN은 “숫자 계산이 실패했다”는 값을 표현합니다. 이 값은 자기 자신과도 ===로 같지 않습니다. 따라서 값이 NaN인지 확인하려면 Number.isNaN이나 Object.is를 써야 합니다.

js
Number.isNaN(NaN); // true
Object.is(NaN, NaN); // true

반대로 -0과 0은 ===에서 같습니다. 보통은 괜찮습니다. 다만 그래프 축, 수학 계산, 방향성 있는 0을 다루는 아주 좁은 영역에서는 Object.is(-0, 0) 차이가 의미를 가질 수 있습니다.

---

3. `Object.is`: 더 민감한 같음

Object.is는 SameValue 비교를 사용합니다. ===와 거의 비슷하지만 두 가지가 다릅니다.

js
Object.is(NaN, NaN); // true
Object.is(-0, 0); // false

이 차이는 상태 비교에서 가끔 중요해집니다. 예를 들어 값이 NaN에서 NaN으로 유지되는 경우를 “변화 없음”으로 볼지, 계산 실패가 다시 발생했다고 볼지 판단해야 할 때가 있습니다.

React 내부에서도 과거부터 상태 변경 판단에 Object.is와 가까운 비교 감각이 중요하게 다뤄졌습니다. 그래서 프론트엔드 개발자는 Object.is를 “특이한 API”가 아니라 “NaN과 -0까지 구분하는 더 정확한 비교”로 기억해두는 편이 좋습니다.

그렇다고 모든 비교를 Object.is로 바꾸라는 뜻은 아닙니다. 실무 질문은 이쪽입니다.

text
이 값에서 NaN끼리 같게 보는 것이 맞는가?
-0과 0을 구분해야 하는 도메인인가?

대부분의 서비스 UI에서는 두 번째 질문의 답이 “아니오”입니다. 그러면 ===가 더 읽기 쉽습니다.

---

4. SameValueZero: `includes`, `Map`, `Set`이 쓰는 감각

SameValueZero는 Object.is와 비슷하지만 -0과 0을 같게 봅니다.

js
[NaN].includes(NaN); // true
[-0].includes(0); // true

const set = new Set([NaN, NaN, -0, 0]);
set.size; // 2

배열의 includes는 SameValueZero를 씁니다. 그래서 NaN을 찾을 수 있습니다. 반면 indexOf는 ===에 가까운 비교를 쓰기 때문에 NaN을 찾지 못합니다.

js
[NaN].includes(NaN); // true
[NaN].indexOf(NaN); // -1

이 차이는 검색 UI나 필터 상태에서 꽤 현실적인 버그가 됩니다. API에서 숫자 필터 값이 잘못 파싱되어 NaN이 섞였는데, indexOf 기반 로직은 못 찾고 includes 기반 로직은 찾는 식입니다.

Map과 Set의 키 비교도 SameValueZero 감각으로 이해하면 됩니다.

js
const cache = new Map();

cache.set(NaN, "invalid number");
cache.get(NaN); // "invalid number"

cache.set(-0, "minus zero");
cache.get(0); // "minus zero"

리뷰에서는 이런 질문을 하면 됩니다.

text
이 컬렉션의 키에 NaN이 들어올 수 있는가?
-0과 0을 같은 키로 봐도 되는가?
배열 검색 API가 indexOf인지 includes인지에 따라 결과가 달라지는가?

---

프론트엔드 코드 리뷰에서 보는 법

비교 코드는 짧아서 대충 지나가기 쉽습니다. 하지만 리뷰할 때는 다음 네 가지를 분리해보면 좋습니다.

1. 타입이 다른 값을 비교하고 있는가

js
if (route.params.page == currentPage) {
  // ...
}

라우터 파라미터는 보통 문자열이고, 상태값은 숫자일 수 있습니다. 이때 ==로 맞추기보다 변환 지점을 명시하는 편이 낫습니다.

js
const pageFromRoute = Number(route.params.page);

if (pageFromRoute === currentPage) {
  // ...
}

명시적 변환은 코드가 조금 길어지지만, “언제 문자열을 숫자로 해석하는가”가 리뷰에 드러납니다.

2. 값에 `NaN`이 들어올 수 있는가

사용자 입력, URL query, CSV, 서버 응답을 숫자로 파싱하면 NaN 가능성이 생깁니다.

js
const amount = Number(input.value);

if (amount === NaN) {
  showError();
}

이 코드는 의도대로 동작하지 않습니다. NaN === NaN은 false입니다.

js
if (Number.isNaN(amount)) {
  showError();
}

3. 배열 검색의 목적이 위치인지 포함 여부인지

js
if (values.indexOf(target) >= 0) {
  // 포함됨
}

위치를 실제로 쓰지 않는다면 includes가 의도를 더 잘 드러냅니다. 게다가 NaN 처리도 다릅니다.

js
if (values.includes(target)) {
  // 포함됨
}

4. 식별자는 숫자인가 문자열인가

큰 ID는 계산용 숫자가 아니라 식별자입니다. Day 1의 숫자 모델과 이어지는 지점입니다.

js
user.id === selectedUserId;

둘 다 number처럼 보이더라도 서버가 큰 정수 ID를 문자열로 보내는지, 클라이언트가 Number로 바꾸고 있지는 않은지 확인해야 합니다. equality 문제는 타입 변환 문제와 자주 붙어 있습니다.

---

오늘의 정리

===를 기본값으로 쓰는 습관은 좋습니다. 다만 그 규칙만으로 JavaScript equality를 다 이해했다고 보면 안 됩니다.

==는 타입 변환이 들어간다. ===는 타입 변환은 없지만 NaN과 -0 예외가 있다. Object.is는 NaN끼리 같게 보고 -0과 0을 구분한다. SameValueZero는 NaN끼리 같게 보고 -0과 0도 같게 본다. includes, Map, Set은 ===와 완전히 같은 비교 감각이 아니다.

리뷰에서는 “==를 쓰지 마세요”에서 끝내지 말고, 이 비교가 어떤 equality 알고리즘을 기대하는지까지 물어보는 편이 더 실용적입니다.

---

퀴즈

Quiz1 / 4
Q.다음 코드의 결과로 맞는 것은 무엇일까요?
js
"console.log([NaN].includes(NaN));\nconsole.log([NaN].indexOf(NaN));"

Post Q&A

오케이징에게 물어보기

자바스크립트 퀴즈북 리마인드 Day 2: 같은 값인지 묻는 네 가지 방법 전체를 기준으로 질문과 피드백을 받아요.답을 본 뒤에는 이 내용을 댓글로 달아서 서징에게도 물어볼 수 있어요. 작성자가 직접 볼 수 있어요!

0/500

포스트 목록

/study/javascript-quizbook
파일 13개, 폴더 0개
자바스크립트 퀴즈북 리마인드 Day 1: 숫자는 왜 가끔 믿을 수 없을까자바스크립트 퀴즈북 리마인드 Day 2: 같은 값인지 묻는 네 가지 방법자바스크립트 퀴즈북 리마인드 Day 3: + 연산자와 ToPrimitive 흐름자바스크립트 퀴즈북 리마인드 Day 4: 프로퍼티 descriptor와 freeze의 경계자바스크립트 퀴즈북 리마인드 Day 5: 참조, 복사, Map/Set의 메모리 감각자바스크립트 퀴즈북 리마인드 Day 6: 스코프 체인과 호이스팅자바스크립트 퀴즈북 리마인드 Day 7: 클로저 버그와 오래된 값자바스크립트 퀴즈북 리마인드 Day 8: 이벤트 루프와 Promise 타이밍자바스크립트 퀴즈북 리마인드 Day 9: this는 호출한 쪽이 정한다자바스크립트 퀴즈북 리마인드 Day 10: 함수는 값이고 경계다자바스크립트 퀴즈북 리마인드 Day 11: class를 봐도 프로토타입을 읽는다자바스크립트 퀴즈북 리마인드 Day 12: 이벤트는 target에서 끝나지 않는다자바스크립트 퀴즈북 리마인드 Day 13: 모듈은 파일 묶음이 아니라 의존성 그래프다

같은 섹션의 대표 이미지

13 posts · latest first
ESM live binding과 dynamic import, circular dependency 리뷰 포인트를 보여주는 module graph 다이어그램
Study26. 07. 09.

자바스크립트 퀴즈북 리마인드 Day 13: 모듈은 파일 묶음이.

ESM live binding, default/named export, circular dependency, dynamic import, CJS와 ESM 차이를 프론트엔드 코드 리뷰 관점에서 정리합니다.

26. 07. 09.SEOJing
DOM 이벤트가 capture 단계로 내려가고 bubble 단계로 올라가는 경로 다이어그램
Study26. 07. 08.

자바스크립트 퀴즈북 리마인드 Day 12: 이벤트는.

DOM 이벤트의 capture/bubble, delegation, default action, React synthetic event 경계를 프론트엔드 코드 리뷰 관점에서 정리합니다.

26. 07. 08.SEOJing
인스턴스에서 프로토타입과 Object.prototype으로 이어지는 프로퍼티 조회 경로 다이어그램
Study26. 07. 07.

자바스크립트 퀴즈북 리마인드 Day 11: class를 봐도.

프로토타입 조회, class 문법, 인스턴스 필드와 prototype 메서드 차이를 프론트엔드 코드 리뷰 관점에서 정리합니다.

26. 07. 07.SEOJing
입력, 순수 계산, 출력, 부수 효과 경계를 나누어 함수 설계를 보여주는 다이어그램
Study26. 07. 06.

자바스크립트 퀴즈북 리마인드 Day 10: 함수는 값이고 경계다.

고차 함수, 순수 함수, 부수 효과, async 함수의 Promise 반환을 코드 리뷰에서 어떻게 읽을지 정리합니다.

26. 07. 06.SEOJing
메서드 호출, 분리된 함수, 이벤트 콜백, bind와 arrow function의 this 차이를 보여주는 다이어그램
Study26. 07. 05.

자바스크립트 퀴즈북 리마인드 Day 9: this는 호출한 쪽이.

this 바인딩을 정의 위치가 아니라 호출 형태, 메서드 분리, arrow function, bind 기준으로 읽는 방법을 코드 리뷰 관점에서 정리합니다.

26. 07. 05.SEOJing
콜 스택이 비워진 뒤 마이크로태스크 큐가 먼저 실행되고 다음 태스크로 넘어가는 이벤트 루프 다이어그램
Study26. 07. 04.

자바스크립트 퀴즈북 리마인드 Day 8: 이벤트 루프와.

콜 스택, 태스크 큐, 마이크로태스크 큐를 기준으로 Promise와 setTimeout 실행 순서를 코드 리뷰 관점에서 정리합니다.

26. 07. 04.SEOJing
클로저가 생성 위치의 바인딩을 기억해 나중 실행에서 오래된 값을 읽을 수 있음을 보여주는 다이어그램
Study26. 06. 29.

자바스크립트 퀴즈북 리마인드 Day 7: 클로저 버그와 오래된.

클로저가 값 복사가 아니라 렉시컬 환경 참조라는 점을 stale closure, 루프 콜백, React 이벤트 리뷰 관점에서 정리합니다.

26. 06. 29.SEOJing
스코프 체인과 호이스팅 관계를 단순화한 다이어그램
Study26. 06. 28.

자바스크립트 퀴즈북 리마인드 Day 6: 스코프 체인과 호이스팅.

스코프 체인, 렉시컬 환경, var/let/const 호이스팅 차이를 콜백·상태 버그 리뷰 관점에서 다시 정리합니다.

26. 06. 28.SEOJing
JavaScript 객체 참조와 Map, Set, WeakMap의 참조 구조를 비교한 다이어그램
Study26. 06. 27.

자바스크립트 퀴즈북 리마인드 Day 5: 참조, 복사,.

객체 참조와 얕은 복사, Object와 Map/Set의 차이, WeakMap/WeakSet이 필요한 메모리 상황을 프론트엔드 코드 리뷰 관점에서 정리합니다.

26. 06. 27.SEOJing
JavaScript 프로퍼티 descriptor가 data descriptor와 accessor descriptor로 갈라지는 구조 다이어그램
Study26. 06. 26.

자바스크립트 퀴즈북 리마인드 Day 4: 프로퍼티.

객체 프로퍼티를 key/value가 아니라 descriptor로 읽는 법, getter/setter와 defineProperty, preventExtensions/seal/freeze의 경계를 코드 리뷰 관점에서 정리합니다.

26. 06. 26.SEOJing
객체가 ToPrimitive를 거쳐 + 연산자의 문자열 연결 또는 숫자 덧셈으로 갈라지는 흐름 다이어그램
Study26. 06. 25.

자바스크립트 퀴즈북 리마인드 Day 3: + 연산자와.

객체가 원시값으로 바뀌는 ToPrimitive 흐름, + 연산자의 문자열 연결과 숫자 덧셈 분기, Symbol.toPrimitive가 코드 리뷰에서 왜 중요한지 정리합니다.

26. 06. 25.SEOJing
==, ===, Object.is, SameValueZero의 차이를 정리한 JavaScript equality 다이어그램
Study26. 06. 24.

자바스크립트 퀴즈북 리마인드 Day 2: 같은 값인지 묻는 네.

==, ===, Object.is, SameValueZero가 각각 어떤 비교 알고리즘을 쓰는지 정리하고, includes와 indexOf, Map/Set 키 비교에서 생기는 프론트엔드 리뷰 포인트를 잡습니다.

26. 06. 24.SEOJing
JavaScript Number, safe integer, NaN, -0, BigInt를 한 장으로 정리한 다이어그램
Study26. 06. 23.

자바스크립트 퀴즈북 리마인드 Day 1: 숫자는 왜 가끔 믿을.

자바스크립트의 Number가 왜 정수처럼 보여도 부동소수점 모델 위에서 움직이는지, NaN과 -0, safe integer, BigInt를 프론트엔드 코드 리뷰 관점에서 다시 정리합니다.

26. 06. 23.SEOJing