타입과 형변환 — == 대신 ===를 쓰는 진짜 이유
[] == false는true인데,[] == true도false입니다. 도대체 빈 배열은 참인가요, 거짓인가요?
자바스크립트의 == 연산자는 비교 전에 암묵적 형변환을 수행합니다. 이 변환 규칙을 이해하지 못하면 조건문에서 의도와 다른 분기가 실행되는 버그를 만들게 됩니다.
원시 타입 7종
자바스크립트에는 7개의 원시 타입(Primitive Type) 이 있습니다.
| 타입 | 예시 | 비고 |
|---|---|---|
string | 'hello' | 불변(immutable) |
number | 42, 3.14, NaN, Infinity | IEEE 754 부동소수점 |
boolean | true, false | |
undefined | undefined | 선언만 하고 할당하지 않은 변수 |
null | null | "의도적으로 비어있음" |
symbol | Symbol('id') | ES2015, 유일한 값 |
bigint | 9007199254740993n | ES2020, 임의 정밀도 정수 |
원시 타입의 핵심 특징은 불변(immutable) 이라는 점입니다. 값 자체를 변경할 수 없고, 새 값을 할당하는 것뿐입니다.
let str = 'hello';
str[0] = 'H'; // 무시됨
console.log(str); // 'hello' — 변하지 않음
str = 'Hello'; // 새 문자열을 할당한 것
참조 타입
원시 타입이 아닌 모든 것은 참조 타입(Reference Type) 입니다. 객체, 배열, 함수 등이 여기에 해당합니다.
const a = { name: 'Alice' };
const b = a; // 참조 복사
b.name = 'Bob';
console.log(a.name); // 'Bob' — 같은 객체를 가리킴
원시 타입은 값을 복사하고, 참조 타입은 참조(주소)를 복사합니다. 깊은 복사와 얕은 복사의 차이를 이해하려면 이 구분이 기초가 됩니다.
typeof 연산자
typeof는 피연산자의 타입을 문자열로 반환합니다.
typeof 'hello' // 'string'
typeof 42 // 'number'
typeof true // 'boolean'
typeof undefined // 'undefined'
typeof Symbol() // 'symbol'
typeof 42n // 'bigint'
typeof {} // 'object'
typeof [] // 'object' — 배열도 객체
typeof null // 'object' — 유명한 버그!
typeof function(){} // 'function'
typeof null === 'object'
이것은 자바스크립트 초기 구현의 버그입니다. 첫 번째 버전에서 값의 타입을 하위 비트로 판별했는데, null은 값이 0이어서 객체로 판별되었습니다. 호환성 때문에 지금까지 수정되지 않았습니다.
null 체크에는 === null을 사용합니다.
const value = null;
// 잘못된 방법
typeof value === 'null' // 이런 타입은 없음
// 올바른 방법
value === null // true
instanceof 연산자
instanceof는 프로토타입 체인을 따라가면서 타입을 확인합니다.
[] instanceof Array // true
[] instanceof Object // true — Array의 프로토타입 체인에 Object가 있음
'' instanceof String // false — 원시 타입은 객체가 아님
typeof는 원시 타입에, instanceof는 객체 타입에 적합합니다.
정확한 타입 체크
가장 정확한 방법은 Object.prototype.toString.call()입니다.
Object.prototype.toString.call('hello') // '[object String]'
Object.prototype.toString.call([]) // '[object Array]'
Object.prototype.toString.call(null) // '[object Null]'
Object.prototype.toString.call(undefined) // '[object Undefined]'
배열 체크에는 Array.isArray()가 가장 깔끔합니다.
Array.isArray([]); // true
Array.isArray({}); // false
암묵적 형변환 (Type Coercion)
자바스크립트는 타입이 다른 값끼리 연산할 때 자동으로 형변환 을 수행합니다. 이것을 암묵적 형변환(coercion)이라고 합니다.
문자열 변환
+ 연산자에서 한쪽이 문자열이면 나머지도 문자열로 변환됩니다.
'5' + 3 // '53' — 숫자가 문자열로 변환
'5' + true // '5true'
'5' + null // '5null'
'5' + undefined // '5undefined'
숫자 변환
-, *, /, % 연산자는 피연산자를 숫자로 변환합니다.
'5' - 3 // 2
'5' * '3' // 15
true + 1 // 2 — true → 1
false + 1 // 1 — false → 0
null + 1 // 1 — null → 0
undefined + 1 // NaN — undefined → NaN
불리언 변환
조건문에서 자동으로 불리언 변환이 일어납니다.
truthy와 falsy
falsy 값 은 불리언 변환 시 false가 되는 값입니다. 나머지는 전부 truthy 입니다.
falsy 값 (전부 외우기)
false
0
-0
0n // BigInt의 0
'' // 빈 문자열
null
undefined
NaN
이 8개가 전부입니다. 나머지는 모두 truthy입니다.
자주 틀리는 truthy 값
// 전부 truthy!
Boolean('0') // true — 비어 있지 않은 문자열
Boolean('false') // true
Boolean([]) // true — 빈 배열
Boolean({}) // true — 빈 객체
Boolean(new Boolean(false)) // true — 객체는 항상 truthy
빈 배열 []이 truthy라는 점은 자주 혼동되는 포인트입니다.
== vs === (동등 vs 일치)
=== (일치 연산자)
타입 변환 없이 타입과 값이 모두 같을 때만 true입니다.
5 === 5 // true
5 === '5' // false — 타입이 다름
null === undefined // false
== (동등 연산자)
타입이 다르면 ** 암묵적 형변환을 거친 뒤** 비교합니다. 변환 규칙이 복잡합니다.
5 == '5' // true — '5' → 5로 변환
null == undefined // true — 특수 규칙
0 == false // true — false → 0
'' == false // true — '' → 0, false → 0
==의 동작 알고리즘 (Abstract Equality Comparison)
ECMAScript 스펙에 정의된 핵심 규칙들을 정리하면 이렇습니다.
- 타입이 같으면
===와 동일하게 비교 null == undefined→true- 숫자와 문자열 → 문자열을 숫자로 변환
- 불리언이 있으면 → 숫자로 변환 (
true → 1,false → 0) - 객체와 원시 타입 → 객체의
valueOf()/toString()호출
악명 높은 예제들
[] == false // true
// [] → '' → 0, false → 0 → 0 == 0 → true
[] == ![] // true (!)
// ![] → false (빈 배열은 truthy이므로 !truthy → false)
// [] == false → 위와 동일 → true
'' == 0 // true
// '' → 0
'0' == false // true
// false → 0, '0' → 0
' \t\n' == 0 // true
// 공백 문자열 → 0
이런 결과들이 나오기 때문에 ** 항상 ===를 사용 **하는 것이 좋습니다. ==는 null 체크에서만 의도적으로 쓰는 경우가 있습니다.
// null 또는 undefined 체크 — 유일하게 == null이 유용한 경우
if (value == null) {
// value === null || value === undefined
}
명시적 형변환
암묵적 형변환에 의존하지 말고, 명시적으로 변환하는 것이 좋습니다.
// 문자열로
String(123) // '123'
(123).toString() // '123'
// 숫자로
Number('123') // 123
parseInt('123') // 123
+'123' // 123 — 단항 + 연산자
// 불리언으로
Boolean(1) // true
!!1 // true — 이중 부정
parseInt vs Number
Number('123abc') // NaN — 전체가 숫자가 아니면 NaN
parseInt('123abc') // 123 — 앞에서부터 파싱
Number('') // 0
parseInt('') // NaN
Number('0x1A') // 26
parseInt('0x1A', 16) // 26
Symbol
Symbol은 ES2015에서 추가된 원시 타입으로, ** 유일한(unique) 값 **을 만듭니다.
const s1 = Symbol('id');
const s2 = Symbol('id');
console.log(s1 === s2); // false — 설명이 같아도 다른 값
주요 용도
객체의 고유한 프로퍼티 키로 사용하여 이름 충돌을 방지합니다.
const ID = Symbol('id');
const user = {
[ID]: 1234,
name: 'Alice'
};
console.log(user[ID]); // 1234
console.log(Object.keys(user)); // ['name'] — Symbol은 열거되지 않음
Well-Known Symbols
자바스크립트 엔진이 내부적으로 사용하는 심볼들도 있습니다.
// Symbol.iterator — 이터러블 프로토콜
const iterable = {
[Symbol.iterator]() {
let i = 0;
return {
next() {
return { value: i++, done: i > 3 };
}
};
}
};
for (const v of iterable) {
console.log(v); // 0, 1, 2
}
BigInt
BigInt는 ES2020에서 추가된 원시 타입으로, ** 임의 정밀도 정수 **를 다룹니다.
// Number의 한계
console.log(9007199254740992 === 9007199254740993); // true — 정밀도 손실!
// BigInt로 해결
console.log(9007199254740992n === 9007199254740993n); // false — 정확!
주의사항
// Number와 혼합 연산 불가
1n + 1; // TypeError
// 명시적 변환 필요
1n + BigInt(1); // 2n
Number(1n) + 1; // 2
// 비교는 가능
1n == 1; // true (==는 형변환)
1n === 1; // false (타입이 다름)
주의할 점
typeof null === 'object' 함정
typeof로 null 체크를 하려고 typeof value === 'null'이라고 쓰면 항상 false입니다. null 체크에는 반드시 value === null을 사용해야 합니다.
==의 암묵적 형변환으로 인한 의도치 않은 비교
==를 사용하면 '' == 0이 true, '0' == false가 true가 됩니다. 조건문에서 이런 비교가 의도치 않게 통과하면 디버깅이 어려운 버그가 됩니다. ==는 null 체크(value == null)에서만 의도적으로 사용하고, 나머지는 ===를 기본으로 사용하는 것이 안전합니다.
BigInt와 Number 혼합 연산
1n + 1은 TypeError를 던집니다. 두 타입 간 연산에는 반드시 명시적 변환(BigInt(1) 또는 Number(1n))이 필요합니다. 비교 연산(==, <, >)은 혼합 가능하지만, ===는 타입이 다르므로 항상 false입니다.
정리
| 항목 | 설명 |
|---|---|
| 원시 타입 | string, number, boolean, undefined, null, symbol, bigint 7종 |
| typeof null | 'object' (역사적 버그) |
== vs === | ==는 암묵적 형변환 후 비교, ===는 타입+값 일치 비교 |
| falsy 값 | false, 0, -0, 0n, '', null, undefined, NaN (8개) |
| Symbol 용도 | 유일한 프로퍼티 키 생성, Well-Known Symbols |
| BigInt 주의 | Number와 혼합 연산 불가, 명시적 변환 필요 |