자바스크립트에서 두 값을 비교할 때 예상과 다른 결과가 나온 경험이 있을까. 가장 흔한 원인은 == 연산자가 자동으로 타입을 변환하기 때문이다. 다만 이 타입 강제 변환 규칙이 정확히 뭔지, 왜 일어나는지, 어떻게 피하는지 제대로 아는 개발자는 많지 않다. 이번에는 타입 강제 변환이 정확히 어떻게 동작하는지, 언제 예상 밖의 결과가 나오는지, 그리고 버그를 방지하려면 어떻게 해야 하는지 완벽하게 정리해서 소개하겠다.

 

타입 강제 변환이란 뭔가
JavaScript는 동적 타입 언어다. 변수 선언 시 타입을 지정하지 않고, 런타임에 값의 타입이 결정된다. 문제는 == 연산자가 이 자유도를 악용해서 비교 전에 양쪽 값의 타입을 같게 맞추려고 시도한다는 것이다.
console.log(5 == "5");     // true (문자열이 숫자로 변환됨)
console.log(5 === "5");    // false (타입까지 비교)
console.log(true == 1);    // true (불린이 숫자로 변환됨)
console.log(true === 1);   // false
console.log(null == undefined); // true (느슨한 같음)
console.log(null === undefined); // false

== 연산자는 "느슨한 같음(loose equality)"이라고 부르고, === 는 "일치(strict equality)"라고 부른다. 차이는 간단해 보이지만 실무에서 버그를 만드는 주범이 바로 이 타입 강제 변환이다.

 

타입 강제 변환 규칙 5가지
변환 상황 규칙 예시
숫자와 문자열 문자열을 숫자로 변환 5 == "5" → true
불린과 다른 타입 불린을 숫자로 변환 (true=1, false=0) true == 1 → true, false == 0 → true
null과 undefined null == undefined는 true, 나머지는 false null == 0 → false (null은 0이 아님)
객체와 원시값 객체를 toString() 또는 valueOf()로 변환 [] == false → true ([] → "" → 0)
NaN NaN은 자기 자신과도 같지 않음 NaN == NaN → false

 

실무에서 자주 터지는 버그들
버그 1: 폼 입력값 검증 실패

✗ 잘못된 코드. 사용자가 폼에 입력한 값은 항상 문자열인데, 숫자 0과 비교하면 타입 강제 변환이 일어난다.

const userInput = "0"; // 폼에서 받은 입력

if (userInput == 0) {
  console.log("입력값이 0이므로 거절");
}
// 출력: "입력값이 0이므로 거절" (잘못된 분기, "0" 문자열을 의도했음)

✓ 올바른 코드. === 를 쓰거나 명시적으로 타입을 변환해서 비교한다.

const userInput = "0";

if (userInput === "0" || parseInt(userInput) === 0) {
  console.log("입력값이 문자 0 또는 숫자 0");
}
// 더 명확함

// 또는
if (parseInt(userInput, 10) === 0) {
  console.log("숫자로 변환해서 0인지 확인");
}
버그 2: 배열 비교의 함정

✗ 잘못된 코드.

const arr = [0];

if (arr == 0) {
  console.log("배열이 0과 같다고?!"); // 실행됨
}
// 배열 [0]이 문자열 "0"으로 변환 → 숫자 0으로 변환 → true

if (arr == false) {
  console.log("배열이 false와 같다고?!"); // 이것도 실행됨
}

✓ 올바른 코드.

const arr = [0];

if (Array.isArray(arr) && arr.length === 1 && arr[0] === 0) {
  console.log("배열 [0]임을 명확하게 확인");
}

// 또는 배열 내용 자체를 검사
if (arr[0] === 0) {
  console.log("배열의 첫 요소가 0");
}
버그 3: 데이터베이스 ID 검증

✗ 잘못된 코드. API에서 받은 user_id가 "0"이거나 0일 수 있다. 이건 유효한 ID인데 == 로 비교하면 거짓으로 판단될 수 있다.

const userId = "0"; // 또는 0

if (userId) {
  console.log("ID 존재");
} else {
  console.log("ID 없음"); // 이 분기가 실행됨 (0, "0", null, undefined 모두 falsy)
}

✓ 올바른 코드. null, undefined만 명시적으로 체크하거나 값 자체를 검증한다.

const userId = "0";

if (userId !== null && userId !== undefined && userId !== "") {
  console.log("ID 존재 및 유효");
}

// 또는 더 명확하게
if (typeof userId === "number" || typeof userId === "string") {
  if (userId !== "" && !Number.isNaN(Number(userId))) {
    console.log("유효한 ID");
  }
}

 

== vs === 성능 차이는 미미하다

일부 개발자들은 === 가 더 느릴 거라고 생각한다. 하지만 현대 JavaScript 엔진(V8, SpiderMonkey, JavaScriptCore)은 이미 == 의 타입 강제 변환 자체가 비용이 크다고 판단하고 === 를 쓰는 코드를 최적화한다. 다시 말해 성능 때문에 == 를 써야 할 이유는 전혀 없다는 뜻이다.

오히려 타입 강제 변환이 일어날 때마다 숨겨진 연산이 늘어나기 때문에 == 가 더 느릴 수 있다. 성능은 === 를 쓰는 것이 낫고, 코드 가독성도 명확하다.

 

ESLint로 자동으로 버그 방지하기

모든 == 를 === 로 바꾸는 건 번거롭다. ESLint의 eqeqeq 규칙을 설정하면 == 를 쓸 때마다 경고가 나온다.

// .eslintrc.json
{
  "rules": {
    "eqeqeq": ["error", "always"],
    "no-implicit-coercion": "error"
  }
}

// 또는 airbnb 스타일 가이드를 쓰면 기본으로 포함됨
{
  "extends": "airbnb-base"
}

이렇게 설정하면 == 를 발견할 때마다 에러를 던지고, prettier --fix 로 자동 수정도 가능하다.

 

요약: 무조건 === 를 쓰자

타입 강제 변환은 JavaScript의 가장 헷갈리는 부분 중 하나다. 처음 배울 때는 "오, 유연하네"라고 생각하지만, 프로젝트 규모가 커질수록 이 유연성이 버그의 온상이 된다. == 는 성능이나 간결함에서 이득이 없고, 오직 혼동만 일으킨다.

모던 JavaScript 코드는 사실상 모두 === 를 쓴다. 사용 중인 라이브러리나 레거시 코드에 == 가 남아있다면 그건 리팩토링 대상이다. 이 글의 "ESLint 설정" 부분을 참고해 프로젝트에 eqeqeq 규칙을 추가하면, 팀 전체가 일관되게 === 를 사용하게 되고 타입 관련 버그를 크게 줄일 수 있을 것이다.