자바스크립트에서 두 값을 비교할 때 예상과 다른 결과가 나온 경험이 있을까. 가장 흔한 원인은 == 연산자가 자동으로 타입을 변환하기 때문이다. 다만 이 타입 강제 변환 규칙이 정확히 뭔지, 왜 일어나는지, 어떻게 피하는지 제대로 아는 개발자는 많지 않다. 이번에는 타입 강제 변환이 정확히 어떻게 동작하는지, 언제 예상 밖의 결과가 나오는지, 그리고 버그를 방지하려면 어떻게 해야 하는지 완벽하게 정리해서 소개하겠다.
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" → 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 |
✗ 잘못된 코드. 사용자가 폼에 입력한 값은 항상 문자열인데, 숫자 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인지 확인");
}
✗ 잘못된 코드.
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");
}
✗ 잘못된 코드. 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");
}
}
일부 개발자들은 === 가 더 느릴 거라고 생각한다. 하지만 현대 JavaScript 엔진(V8, SpiderMonkey, JavaScriptCore)은 이미 == 의 타입 강제 변환 자체가 비용이 크다고 판단하고 === 를 쓰는 코드를 최적화한다. 다시 말해 성능 때문에 == 를 써야 할 이유는 전혀 없다는 뜻이다.
오히려 타입 강제 변환이 일어날 때마다 숨겨진 연산이 늘어나기 때문에 == 가 더 느릴 수 있다. 성능은 === 를 쓰는 것이 낫고, 코드 가독성도 명확하다.
모든 == 를 === 로 바꾸는 건 번거롭다. ESLint의 eqeqeq 규칙을 설정하면 == 를 쓸 때마다 경고가 나온다.
// .eslintrc.json
{
"rules": {
"eqeqeq": ["error", "always"],
"no-implicit-coercion": "error"
}
}
// 또는 airbnb 스타일 가이드를 쓰면 기본으로 포함됨
{
"extends": "airbnb-base"
}
이렇게 설정하면 == 를 발견할 때마다 에러를 던지고, prettier --fix 로 자동 수정도 가능하다.
타입 강제 변환은 JavaScript의 가장 헷갈리는 부분 중 하나다. 처음 배울 때는 "오, 유연하네"라고 생각하지만, 프로젝트 규모가 커질수록 이 유연성이 버그의 온상이 된다. == 는 성능이나 간결함에서 이득이 없고, 오직 혼동만 일으킨다.
모던 JavaScript 코드는 사실상 모두 === 를 쓴다. 사용 중인 라이브러리나 레거시 코드에 == 가 남아있다면 그건 리팩토링 대상이다. 이 글의 "ESLint 설정" 부분을 참고해 프로젝트에 eqeqeq 규칙을 추가하면, 팀 전체가 일관되게 === 를 사용하게 되고 타입 관련 버그를 크게 줄일 수 있을 것이다.