== vs ===: 무엇이 다를까?
자바스크립트를 사용하다 보면 ==와 === 연산자를 자주 마주칩니다. 두 연산자가 거의 동일하게 동작하는 경우가 많아 혼란스러울 수 있지만, 실제로는 중요한 차이점이 있습니다. 아래 예제를 살펴보겠습니다.
console.log(1 == '1');
console.log(1 === '1');
실행 결과는 각각 true와 false입니다. 이 차이의 핵심은 바로 타입 변환(Type Coercion)에 있습니다. == 연산자는 비교 전에 피연산자를 같은 타입으로 변환하려고 시도하는 반면, ===는 타입 변환 없이 엄격하게 비교합니다.
들어가며: 타입 변환의 이해
자바스크립트의 타입 변환은 코드를 간결하게 만들어주는 동시에 예상치 못한 결과를 초래할 수 있는 양날의 검입니다. 대표적인 예로 다음 코드를 보겠습니다.
[] == ![];
많은 개발자가 이 결과를 false라고 예상하지만, 실제로는 true입니다. 이 현상의 배경에는 자바스크립트의 암시적 타입 변환(Implicit Coercion)이 자리 잡고 있습니다. 이 글에서는 이러한 변환의 원리를 상세히 분석해 보겠습니다.
1. 타입 변환이란?
자바스크립트의 값은 크게 두 가지 범주로 나뉩니다:
- 원시값(Primitive): 숫자, 문자열, 불리언, undefined, null, Symbol, BigInt
- 참조값(Reference): 객체, 배열, 함수 등 (모든 객체 형태)
연산이나 비교를 수행할 때 피연산자의 타입이 일치하지 않으면 자바스크립트 엔진이 자동으로 타입을 변환합니다. 이는 두 가지 방식으로 발생합니다:
- 명시적 변환(Explicit):
Number(),String()등을 개발자가 직접 호출 - 암시적 변환(Implicit): 엔진이 자동으로 변환 (예:
1 == '1')
2. 원시값 간 변환: 기본 규칙
원시값 간의 변환은 세 가지 주요 규칙을 따릅니다.
2.1 숫자로 변환: ToNumber(x)
임의의 원시값을 숫자로 변환합니다.
// 숫자 → 숫자: 변환 없음
Number(123); // 123
// 불리언 → 숫자: true → 1, false → 0
Number(true); // 1
Number(false); // 0
// 문자열 → 숫자: 숫자 형태면 해당 숫자, 아니면 NaN, 빈 문자열은 예외
Number('456'); // 456
Number('hello'); // NaN
Number(''); // 0
// undefined → NaN, null → 0
Number(undefined); // NaN
Number(null); // 0
2.2 문자열로 변환: ToString(x)
값을 문자열 형태로 변환합니다.
// 숫자 → 문자열
String(789); // "789"
// 불리언 → 문자열
String(true); // "true"
String(false); // "false"
// undefined/null → 고정 문자열
String(undefined); // "undefined"
String(null); // "null"
2.3 불리언으로 변환: ToBoolean(x)
값을 true 또는 false로 변환합니다. 총 6개의 falsy 값만 false가 되고, 나머지는 모두 true입니다.
// 6개의 falsy 값
Boolean(0); // false
Boolean(''); // false
Boolean(NaN); // false
Boolean(undefined); // false
Boolean(null); // false
Boolean(false); // false
// 그 외는 모두 truthy
Boolean(1); // true
Boolean(' '); // true (공백 문자열은 falsy가 아님)
Boolean('abc'); // true
3. 참조값을 원시값으로 변환: ToPrimitive 알고리즘
객체, 배열 등의 참조값이 원시값을 기대하는 연산에 사용될 때, 자바스크립트 엔진은 내부적으로 ToPrimitive 알고리즘을 실행합니다. 이 알고리즘은 변환 방향에 따라 valueOf()와 toString() 메서드를 적절히 호출합니다.
3.1 불리언 변환: 가장 단순한 규칙
모든 참조값은 불리언으로 변환 시 항상 true를 반환합니다.
Boolean({}); // true
Boolean([]); // true
Boolean({ name: 'Kim' }); // true
Boolean([1, 2, 3]); // true
3.2 숫자로 변환: ToNumber(ToPrimitive(x, Number))
변환 순서는 다음과 같습니다:
valueOf()호출 → 원시값 반환 시 그 값을 숫자로 변환valueOf()가 참조값 반환 시toString()호출 → 반환된 문자열을 숫자로 변환- 둘 다 원시값을 반환하지 않으면 오류 발생
예제 1: 빈 배열 []
const arr = [];
// arr.valueOf() → [] (참조값)
// arr.toString() → "" (빈 문자열, 원시값)
// ToNumber("") → 0
Number([]); // 0
이제 처음 질문했던 [] == ![]의 동작을 단계별로 분석해 보겠습니다:
// 1단계: ![] → false (참조값을 불리언으로 변환하면 true → 부정 → false)
// 2단계: [] == false → 두 피연산자를 숫자로 변환
// [] → 0, false → 0
// 3단계: 0 == 0 → true
[] == ![]; // true
예제 2: 요소가 있는 배열 [1, 2]
const arr2 = [1, 2];
// arr2.valueOf() → [1, 2] (참조값)
// arr2.toString() → "1,2" (문자열, 원시값)
// ToNumber("1,2") → NaN
Number([1, 2]); // NaN
예제 3: 일반 객체 {}
const obj = {};
// obj.valueOf() → {} (참조값)
// obj.toString() → "[object Object]" (문자열, 원시값)
// ToNumber("[object Object]") → NaN
Number({}); // NaN
3.3 문자열로 변환: ToString(ToPrimitive(x, String))
숫자 변환과 반대 순서로 진행됩니다:
toString()호출 → 원시값 반환 시 그대로 사용toString()이 참조값 반환 시valueOf()호출- 둘 다 실패하면 오류 발생
예제 1: 배열 [1, 2]
const arr3 = [1, 2];
// arr3.toString() → "1,2" (원시값, 바로 사용)
String([1, 2]); // "1,2"
예제 2: 일반 객체 {}
const obj2 = {};
// obj2.toString() → "[object Object]" (원시값)
String({}); // "[object Object]"
예제 3: 커스텀 객체
const user = {
name: 'henry',
toString() {
return `이름: ${this.name}`;
},
valueOf() {
return { age: 20 }; // 참조값 반환
}
};
String(user); // "이름: henry" (toString() 결과 사용)
4. 암시적 변환의 주요 트리거
4.1 동등 비교 연산자 (==)
==는 양쪽 피연산자의 타입이 다를 경우, 주로 숫자 타입으로 통일하여 비교합니다.
4.2 덧셈 연산자 (+)
+ 연산자는 두 가지 역할을 수행합니다:
- 단항 연산자:
+x형태로 사용 시ToNumber(x)호출 - 이항 연산자:
a + b형태에서:- 두 값을 원시값으로 변환 후
- 하나라도 문자열이면 문자열 연결
- 그 외에는 숫자 덧셈 수행
// 단항 연산자
+[]; // 0 ([] → 0)
// 이항 연산자: 문자열 연결
[1, 2, 3] + '1'; // "1,2,31"
// 이항 연산자: 숫자 덧셈
true + 1; // 2 (true → 1)
4.3 조건문 (if, while)
조건식은 항상 불리언으로 변환됩니다. 모든 참조값은 true로 평가됨을 기억하세요.
if ({}) {
console.log('실행됨'); // 출력됨
}
if ('') {
console.log('실행 안 됨'); // 출력되지 않음
}
추가적인 주요 트리거로는 산술 연산자(-, *, /, %)와 비교 연산자(>, <, >=, <=)가 있습니다.
5. toString() 메서드의 다양한 동작
참조값과 원시값에서 toString()은 각각 다른 결과를 반환합니다.
- 일반 객체:
[object Object]반환 - 배열: 요소를 쉼표로 연결한 문자열 반환 (빈 배열은 빈 문자열)
- 원시값: 값을 그대로 문자열로 변환 (예:
(123).toString()→"123")
6. 실전 팁: 타입 변환 함정 피하기
- 일관된 비교 연산자 사용:
==대신===를 기본으로 사용하여 예상치 못한 타입 변환을 방지하세요. - 명시적 변환 선호: 참조값을 연산에 사용할 때는
Number(),String()등으로 명시적으로 변환하세요. - 핵심 규칙 숙지: 참조값 변환 시 숫자는 "valueOf → toString", 문자열은 "toString → valueOf" 순서를 기억하세요.
- NaN 조심: 참조값을 숫자로 변환할 때
NaN이 자주 발생하며,NaN은 자기 자신과도 같지 않습니다 (NaN === NaN→false).