Ray Book
타입과 형변환

암묵적 형변환

JavaScript 엔진이 타입을 자동으로 변환하는 규칙, ToPrimitive, ToNumber, ToString, ToBoolean 추상 연산을 시각화합니다

javascriptcoerciontype-conversionabstract-operations

형변환이란

이전 글에서 JavaScript의 타입 시스템을 살펴봤습니다. 동적 타입 언어인 JavaScript는 서로 다른 타입의 값이 만났을 때, 엔진이 자동으로 타입을 변환합니다. 이것을 암묵적 형변환 (implicit coercion) 이라고 합니다.

"5" - 3    // 2, 문자열 "5"가 숫자 5로 변환
"5" + 3    // "53", 숫자 3이 문자열 "3"으로 변환
!!"hello"  // true, 문자열이 boolean으로 변환

같은 "5"인데 -에서는 숫자로, +에서는 문자열로 취급됩니다. 이 규칙을 이해하려면 JavaScript 명세의 추상 연산 (abstract operations) 을 알아야 합니다.

네 가지 추상 연산

JavaScript 명세는 형변환을 네 가지 추상 연산으로 정의합니다.

  1. ToPrimitive , 객체를 원시값으로
  2. ToNumber , 값을 숫자로
  3. ToString , 값을 문자열로
  4. 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

코드
1Number("42") // 42
2Number("") // 0
3Number("hello") // NaN
4Number(true) // 1
5Number(false) // 0
6Number(null) // 0
7Number(undefined) // NaN
8Number([]) // 0 (ToPrimitive → "")
9Number({}) // NaN (ToPrimitive → "[object Object]")
변환
"42"string
ToNumber
42number
숫자로 파싱할 수 있는 문자열은 그대로 숫자로 변환됩니다. 앞뒤 공백은 무시됩니다.

핵심 규칙 정리:

Number("")          // 0, 빈 문자열은 0
Number(" ")         // 0, 공백만 있는 문자열도 0
Number("0x1A")      // 26, 16진수 리터럴 인식
Number(null)        // 0
Number(undefined)   // NaN, null과 다름!
Number(true)        // 1
Number(false)       // 0

ToString

코드
1String(42) // "42"
2String(0) // "0"
3String(-0) // "0" (주의!)
4String(NaN) // "NaN"
5String(true) // "true"
6String(false) // "false"
7String(null) // "null"
8String(undefined) // "undefined"
9String([1, 2, 3]) // "1,2,3"
10String({}) // "[object Object]"
변환
42number
ToString
"42"string
숫자는 그대로 문자열로 변환됩니다. 양수, 음수, 소수 모두 직관적으로 변환됩니다.

주목할 점:

String(-0)          // "0", 부호가 사라짐!
String([])          // "", 빈 배열은 빈 문자열
String([null, undefined]) // ",", null과 undefined는 빈 문자열 취급

ToBoolean

ToBoolean은 단순합니다. 8개의 falsy 값 을 외우면 됩니다. 나머지는 모두 truthy입니다. 이전 글의 마지막에서 다뤘으므로 여기서는 넘어갑니다.

+ 연산자의 이중성

+ 연산자는 JavaScript에서 가장 혼란스러운 연산자입니다. 숫자 더하기문자열 결합 두 가지 역할을 합니다.

규칙:

  1. 양쪽 피연산자를 ToPrimitive로 원시값으로 변환
  2. 둘 중 하나라도 문자열이면 → 나머지도 ToString → 문자열 결합
  3. 그 외 → 양쪽 모두 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

비교 연산자와 형변환

관계 연산자 (<, >, <=, >=)

관계 연산자는 양쪽을 원시값으로 변환한 뒤:

  1. 양쪽 다 문자열 → 사전식 (lexicographic) 비교
  2. 그 외 → 양쪽 모두 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 = true

null >= 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 ===) 를 살펴보겠습니다. ==가 내부적으로 어떤 형변환을 수행하는지 단계별로 추적합니다.