형변환이란
이전 글에서 JavaScript의 타입 시스템을 살펴봤습니다. 동적 타입 언어인 JavaScript는 서로 다른 타입의 값이 만났을 때, 엔진이 자동으로 타입을 변환합니다. 이것을 암묵적 형변환 (implicit coercion) 이라고 합니다.
"5" - 3 // 2, 문자열 "5"가 숫자 5로 변환
"5" + 3 // "53", 숫자 3이 문자열 "3"으로 변환
!!"hello" // true, 문자열이 boolean으로 변환같은 "5"인데 -에서는 숫자로, +에서는 문자열로 취급됩니다. 이 규칙을 이해하려면 JavaScript 명세의 추상 연산 (abstract operations) 을 알아야 합니다.
네 가지 추상 연산
JavaScript 명세는 형변환을 네 가지 추상 연산으로 정의합니다.
- ToPrimitive , 객체를 원시값으로
- ToNumber , 값을 숫자로
- ToString , 값을 문자열로
- ToBoolean , 값을 boolean으로
ToPrimitive
객체를 원시값으로 변환할 때 호출됩니다. hint 에 따라 valueOf()와 toString() 중 어느 것을 먼저 시도할지 결정합니다.
// hint: "number" → valueOf() 먼저, 실패하면 toString()
// hint: "string" → toString() 먼저, 실패하면 valueOf()
// hint: "default" → valueOf() 먼저 (number와 동일)
const obj = {
valueOf() { return 42; },
toString() { return "hello"; },
};
obj + 1 // 43, hint: "default" → valueOf() → 42
`${obj}` // "hello", hint: "string" → toString()배열의 경우 valueOf()는 배열 자체를 반환하므로 (원시값이 아님), toString()이 사용됩니다.
[].toString() // ""
[1, 2].toString() // "1,2"ToNumber
핵심 규칙 정리:
Number("") // 0, 빈 문자열은 0
Number(" ") // 0, 공백만 있는 문자열도 0
Number("0x1A") // 26, 16진수 리터럴 인식
Number(null) // 0
Number(undefined) // NaN, null과 다름!
Number(true) // 1
Number(false) // 0ToString
주목할 점:
String(-0) // "0", 부호가 사라짐!
String([]) // "", 빈 배열은 빈 문자열
String([null, undefined]) // ",", null과 undefined는 빈 문자열 취급ToBoolean
ToBoolean은 단순합니다. 8개의 falsy 값 을 외우면 됩니다. 나머지는 모두 truthy입니다. 이전 글의 마지막에서 다뤘으므로 여기서는 넘어갑니다.
+ 연산자의 이중성
+ 연산자는 JavaScript에서 가장 혼란스러운 연산자입니다. 숫자 더하기와 문자열 결합 두 가지 역할을 합니다.
규칙:
- 양쪽 피연산자를 ToPrimitive로 원시값으로 변환
- 둘 중 하나라도 문자열이면 → 나머지도 ToString → 문자열 결합
- 그 외 → 양쪽 모두 ToNumber → 숫자 더하기
// 문자열이 있으면 문자열 결합
"3" + 4 // "34"
4 + "3" // "43"
"" + 42 // "42", 숫자를 문자열로 변환하는 트릭
// 문자열이 없으면 숫자 더하기
true + true // 2
true + false // 1
null + 1 // 1, Number(null) = 0
undefined + 1 // NaN, Number(undefined) = NaN객체가 포함된 경우:
[] + [] // "", 둘 다 "" → "" + ""
[] + {} // "[object Object]", "" + "[object Object]"
{} + [] // 0 (콘솔에서), {}를 빈 블록으로 해석, +[]만 평가{} + []의 결과는 실행 컨텍스트에 따라 다릅니다. 콘솔에서는 {}를 코드 블록으로, 표현식 위치에서는 객체 리터럴로 해석합니다.
단항 + 연산자
+를 단항으로 사용하면 ToNumber를 적용합니다.
+"42" // 42
+"" // 0
+true // 1
+null // 0
+undefined // NaN
+[] // 0비교 연산자와 형변환
관계 연산자 (<, >, <=, >=)
관계 연산자는 양쪽을 원시값으로 변환한 뒤:
- 양쪽 다 문자열 → 사전식 (lexicographic) 비교
- 그 외 → 양쪽 모두 ToNumber → 숫자 비교
"10" > "9" // false, 문자열 비교: "1" < "9"
"10" > 9 // true, 숫자 비교: 10 > 9
null > 0 // false, Number(null) = 0, 0 > 0 = false
null == 0 // false, == 에서 null은 특별 취급!
null >= 0 // true, Number(null) = 0, 0 >= 0 = truenull >= 0이 true인데 null == 0이 false인 것은 직관에 반합니다. 이는 >=는 ToNumber를 적용하지만, ==에서 null은 undefined와만 같도록 별도 규칙이 있기 때문입니다.
템플릿 리터럴과 ToString
템플릿 리터럴 (` 백틱) 안의 ${...}는 내부 값에 ToString을 적용합니다.
const n = 42;
const arr = [1, 2, 3];
const obj = { a: 1 };
`value: ${n}` // "value: 42"
`arr: ${arr}` // "arr: 1,2,3"
`obj: ${obj}` // "obj: [object Object]"디버깅 시 객체를 템플릿 리터럴에 넣으면 [object Object]가 되므로, JSON.stringify()를 사용하세요.
`obj: ${JSON.stringify(obj)}` // "obj: {"a":1}"명시적 변환을 사용하세요
암묵적 형변환은 코드를 짧게 만들 수 있지만, 의도를 불분명하게 합니다. 명시적 변환 을 권장합니다.
// ✕ 암묵적
const str = "" + value;
const num = +value;
const bool = !!value;
// ✓ 명시적
const str = String(value);
const num = Number(value);
const bool = Boolean(value);명시적 변환은 "이 값을 이 타입으로 변환하겠다"는 의도를 분명히 전달합니다.
다음 단계
형변환 규칙을 이해했으니, 다음 글에서는 이 규칙이 가장 직접적으로 드러나는 동등 비교 연산자 (== vs ===) 를 살펴보겠습니다. ==가 내부적으로 어떤 형변환을 수행하는지 단계별로 추적합니다.