해당 시리즈는 Leonardomso의 33 Concepts Every JavaScript Developer Should Know 를 보고 공부, 정리한 시리즈이며, 자세한 내용은 링크를 확인하길 바란다.
자바스크립트에서 많은 에러를 발생시키면서 한편으로는 편리함을 가져다 주는 형변환에 대해서 알아보자.
이번 장에서는 형변환에 대해서만 얘기해보자.
개인적으로 자바스크립트가 갖고 있는 계륵(鷄肋) 같은 특징이라고 생각한다.
3가지 용어 모두 형변환을 의미하는 단어이다.
보통 정적 언어에서는 type casting 이나 type coversion, 자바스크립트와 같은 언어에서는 type coercion 이라 한다.
특히, 자바스크립트에서는 주로 type coercion 이라 하기에 앞으로 형변환에 대해서는 coercion 이라는 용어로 사용하겠다. (MDN 사이트에서는 type conversion 은 명시적 형변환과 암묵적 형변환을 모두 포함하는 것이고 type coercion 은 암묵적 형변환을 의미한다고 간단하게 설명한다.)
앞서 말한 것처럼 보통 coercion 하면 암묵적 형변환을 대게 가리키지만, 여기서는 형변환 전반에 대해서 다루기 위해 명시적, 암묵적 형변환을 모두 다루고자 하며, 명확히 하기 위해 명시적 형변환 (=explicit coercion) / 암묵적 형변환 (=implicit coercion) 로 분류해서 설명하겠다.
앞에서도 간단하게 언급했지만, 형변환은 크게 명시적 형변환과 암묵적 형변환 2개로 나뉜다.
명시적 형변환 (=explicit coercion)은 말 그대로, 코드에서 형변환을 의도적으로 발생시키는 것이 명확히 보이는 것을 의미한다.
반대로, 암묵적 형변환 (=implicit coercion)은 코드에서 다른 연산을 통해서 형변환을 발생시키지만, 그 과정이 명확하지 않은 것을 의미한다.
아래 코드를 보자
let a = 42;
let b = a + ""; // 암묵적 형변환
let c = String( a ); // 명시적 형변환
b
와 c
모두 string 타입의 "42"
를 갖고 있는 변수다.
c
의 경우, String
생성자 함수를 활용해서 숫자 타입이었던 a
를 string으로 변환시킴을 명확하게 보여준다. 반면에, b
는 +
를 활용해서 다소 의아하게? 형변환을 발생시키는 것을 볼 수 있다.
얼핏 보면, b
처럼 암묵적으로 형변환하는 방식이 이해하기 어렵고 헷갈려서 지양해야겠다고 생각할 수도 있다.
하지만, 꼭 그렇지만은 않다.
암묵적 형변환이 코드를 더 간결하고 이해하기 쉬울 수 있다. 이부분은 마지막에 정리할 때 다시 얘기하자.
그렇다면 각각의 형변환에 대해서 좀 더 자세히 알아보자.
암묵적 형변환은 개발자의 의도가 명확하지 않지만 다른 액션으로 인해 발생하는 형변환을 의미한다.
자바스크립트는 Weak Type Language
이기에 암묵적 형변환이 일어난다.
암묵적 형변환되는 데이터는 크게 String
, Number
, Boolean
이 3가지로 변환된다. 차례대로 알아보자.
String 타입으로 암묵적 형변환을 할 때 2가지 조건이 만족해야 한다.
+
연산자(문자열 연결 연산자) 사용문자열 타입으로 암묵적 형변환을 할 때 사용하는 연산자는 +
이다.
이 때, +
연산자는 수식에서 덧셈 연산자가 아닌 문자열 연결 연산자로 취급한다.
1 + '2' // '12'
위 코드처럼 피연산자 중 하나 이상이 문자열 타입이라면, +
연산자는 다른 피연산자를 문자열 타입으로 암묵적 형변환을 진행하여 문자열인 12
을 출력한다.
아래 코드는 어떻게 형변환이 될까?
5 + 5 + 'tree'
정답은 '10tree'
이다.
자바스크립트에서는 왼쪽에서 오른쪽으로 순차적으로 연산을 진행하기에 먼저 5 + 5
를 처리한 후 암묵적 형변환이 발생하여 '10' + 'tree'
를 처리하게 된다.
숫자 타입을 포함하여 다른 데이터 타입과의 암묵적 형변환 결과를 살펴보자.
// 숫자 타입
0 + '' // "0"
-0 + '' // "0"
1 + '' // "1"
-1 + '' // "-1"
NaN + '' // "NaN"
Infinity + '' // "Infinity"
-Infinity + '' // "-Infinity"
// 불리언 타입
true + '' // "true"
false + '' // "false"
// null 타입
null + '' // "null"
// undefined 타입
undefined + '' // "undefined"
// 심볼 타입
(Symbol()) + '' // TypeError: Cannot convert a Symbol value to a string
// 객체 타입
({}) + '' // "[object Object]"
Math + '' // "[object Math]"
[] + '' // ""
[10, 20] + '' // "10,20"ㄴ
(function(){}) + '' // "function(){}"
Array + '' // "function Array() { [native code] }"
위 코드에서 눈여겨 봐야될 부분은 심볼 타입과 객체 타입과의 암묵적 형변환 과정이다.
먼저, 심볼 타입의 경우, 암묵적으로 Boolean 타입으로만 형변환이 가능하다.
숫자나 문자열 타입으로는 형변환이 불가능하다. 2 장에서 심볼 타입 설명할 때 간단하게 언급한 적이 있으니 참고하자.
다음으로, 객체 타입이다.
객체는 형변환을 할 때 2가지 단계를 거친다.
valueOf()
함수를 실행해 그 결과가 원시 값인 경우 해당 값을 사용한다.valueOf()
함수가 빈 객체를 반환한다면, toString()
함수를 실행해 문자열로 반환한다.이 때, 유념할 점은 기본적으로 valueOf()
를 재정의하지 않는 이상 valueOf()
는 빈 객체를 반환한다.
그렇기에 대개 toString()
함수의 실행값인 [object Object]
를 문자열로 반환한다.
아래 코드를 보면서 정리해보자.
const wow = {
toString: () => "3"
}
console.log(1 + wow) // 13
const lol = {
valueOf: () => "7",
toString: () => "1"
}
console.log(1 + lol) // 17
숫자 타입으로 암묵적 형변환을 할 때는 크게 3가지 연산자를 사용한다.
산술 연산자
-, *, /, %
등의 산술 연산자를 활용하면 암묵적 형변환을 얻을 수 있다.+
연산자는 산술 연산자가 아닌 문자열 연결 연산자로 처리되기에 항상 주의해야한다!NaN
의 결과를 얻는다.'3' - 1 // 2
'3' * 1 // 3
'3' / 2 // 1.5
'3' % 3 // 0
'3' + 1 // 31
'three' - 1 // NaN
비교 연산자
'3' > 2 // true
+
단항 연산자
// 문자열 타입
+'' // 0
+'0' // 0
+'1' // 1
+'string' // NaN
// 불리언 타입
+true // 1
+false // 0
// null 타입
+null // 0
// undefined 타입
+undefined // NaN
// 심볼 타입
+Symbol() // TypeError: Cannot convert a Symbol value to a number
// 객체 타입
+{} // NaN
+[] // 0
+[10, 20] // NaN
+(function(){}) // NaN
''
), 빈 배열([]
), null
, false
는 0으로, true
는 1로 반환된다.undefined
는 반환되지 않아 NaN
을 뱉어낸다.불리언 타입으로의 암묵적인 형변환은 조건식의 평가 결과를 도출할 때 사용된다.
그렇기에 자바스크립트 엔진은 Truthy
한 값(참으로 평가되는 값)과 Falsy
한 값(거짓으로 평가되는 값)으로 구분한다.
Falsy 한 값은 아래와 같이 총 6개이다.
''
(빈 문자열)Truthy 한 값은 위 6개의 Falsy 한 값을 제외한 모든 값이 해당된다.
명시적 형변환은 해당 데이터 타입이 갖고 있는 빌트인 메서드나 데이터 타입 생성자 함수, 빌트인 함수를 활용해서 형변환하는 것을 명확히 보여주는 방법이다.
암묵적 형변환과 반대로 개발자가 형변환을 하겠다는 의도를 코드를 통해 보여주는 것을 의미한다.
앞서 암묵적 형변환에서 설명한 것처럼 String
, Number
, Boolean
이 3가지의 명시적 형변환에 대해 알아보자.
들어가기에 앞서, 앞에 설명한 암묵적 형변환을 의도적으로 활용하여 형변환을 발생시키는 방법도 어떻게 보면? 명시적 형변환으로 판단할 수 있다.
하지만, 혼동될 여지를 막기 위해, 연산자를 활용한 암묵적 형변환 방식에 대해서는 고려하지 않겠다.
문자열 타입으로 형변환하는 방법은 크게 2가지다.
// 1. String 생성자 함수 사용
console.log(String(1)); // "1"
console.log(String(NaN)); // "NaN"
console.log(String(Infinity)); // "Infinity"
console.log(String(true)); // "true"
console.log(String(false)); // "false"
// 2. Object.prototype.toString 메소드 사용
console.log((1).toString()); // "1"
console.log((NaN).toString()); // "NaN"
console.log((Infinity).toString()); // "Infinity"
console.log((true).toString()); // "true"
console.log((false).toString()); // "false"
숫자 타입으로 형변환하는 방법은 크게 2가지다.
parseInt()
, parseFloat()
함수 사용// 1. Number 생성자 함수 사용
console.log(Number('0')); // 0
console.log(Number('-1')); // -1
console.log(Number('10.53')); // 10.53
console.log(Number(true)); // 1
console.log(Number(false)); // 0
// 2. parseInt, parseFloat 함수 사용
console.log(parseInt('0')); // 0
console.log(parseInt('-1')); // -1
console.log(parseFloat('10.53')); // 10.53
// 주의할 점
console.log(parseInt('one')) // NaN
console.log(parseInt('1one')) // 1
console.log(parseInt('one1')) // NaN
parseInt
나 parseFloat
함수를 사용할 때, 내부에 숫자가 아닌 문자열만 있다면, NaN 를 반환한다.
단, 왼쪽부터 순차적으로 볼 때, 숫자로 변환이 가능한 문자열이 있다면 해당 부분만 숫자로 변환한다.
이 때, 주의할 것이 왼쪽부터 순차적으로 보기에 오른쪽에 숫자로 변환이 가능한 문자열이 있다고하더라고 왼쪽에 숫자로 변환이 불가능한 문자열이 있다면 NaN 을 반환한다.
불린 타입으로 형변환하는 방법은 크게 2가지다.
!
부정 논리 연산자를 2번 사용// 1. Boolean 생성자 함수 사용
// 문자열 타입
console.log(Boolean('x')); // true
console.log(Boolean('')); // false
console.log(Boolean('false')); // true
// 숫자 타입
console.log(Boolean(0)); // false
console.log(Boolean(1)); // true
console.log(Boolean(NaN)); // false
console.log(Boolean(Infinity)); // true
// null 타입
console.log(Boolean(null)); // false
// undefined 타입
console.log(Boolean(undefined)); // false
// 객체 타입
console.log(Boolean({})); // true
console.log(Boolean([])); // true
// 2. ! 부정 논리 연산자를 2번 사용
// 문자열 타입
console.log(!!'x'); // true
console.log(!!''); // false
console.log(!!'false'); // true
// 숫자 타입
console.log(!!0); // false
console.log(!!1); // true
console.log(!!NaN); // false
console.log(!!Infinity); // true
// null 타입
console.log(!!null); // false
// undefined 타입
console.log(!!undefined); // false
// 객체 타입
console.log(!!{}); // true
console.log(!![]); // true
여기서 주의할 점은 !
논리 연산자를 2번 사용하는 방법이 명시적 형변환이다.
근데 코드를 작성하면 !
논리 연산자를 한번만 사용해도 형변환이 발생한다.
console.log(!'false') // false
console.log(!!'false') // true
위 코드를 보면, 두 코드 형변환이 발생되어 모두 Boolean 값을 출력하는 것을 볼 수 있다.
왜 그럴까?
!
를 하나만 사용했을 때 'false'
는 암묵적 형변환이 발생하는 것이다.
!
은 NOT 을 의미하는 논리연산자이기에 Truthy 한 값인 false
의 반대인 false 를 반환한 것이다.!
를 2 번 사용했을 때는 명시적 형변환이 발생하기에 true ( 'false'
는 Truthy 하다 )를 반환한 것이다.
암묵적 형변환은 충분히 껄끄러운 자바스크립트의 특징이다. 하지만, 개인적으로 이를 지양할 필요는 없다고 생각한다. 암묵적 형변환을 활용해 코드들을 간결하게 작성하고 형변환을 보다 편리하게 사용할 수 있기 때문이다.
하지만 조심해야 하는 것은 맞다! 표현식을 작성할 때 의도치 않게 암묵적 형변환이 발생하지 않도록 주의가 필요하며, 여러 사람들과 코드를 공유하는 상황이 온다면, 명시적 형변환을 활용하여 확실하게 보여주는 것이 더 좋을 때도 있을 것이다.
즉, 암묵적 형변환은 필요에 따라 조심히 사용하자. 다만 항상 유념하자!
자바스크립트에서 발생하는 형변환(Type Coercion)은 크게 암묵적 형변환과 명시적 형변환으로 나눌 수 있다.
암묵적 형변환은 개발자의 의도와 다르게 다른 연산의 과정을 통해 데이터 타입이 다른 타입으로 변환된 것을 의미한다.
명시적 형변환은 개발자가 의도를 담아 명시적으로 코드를 작성하여 데이터 타입의 변환된 것을 의미한다.
모던 자바스크립트 Deep Dive
MDN 문서 - Object.prototype.toString()