9-1. 타입 변환이란
변수를 선언할 때 일일이 타입을 선언하진 않지만, 자바스크립트의 모든 값은 타입을 가지고 있다.(이에 타입을 엄격하게 부여해주는 타입스크립트를 사용하고 있다). 개발자는 의도에 따라 타입을 변환시킬 수 있는데, 이를 명시적 타입변환(explicit coercion) 혹은 타입 캐스팅(type casting) 이라고 한다.
var x = 10;
var str = x.toString();
console.log(typeOf str, str); // string 10
// 원본 타입이 변환된것은 아니다.
console.log(typeOf x, x) // number 10
위 메서드 toString 처럼 의도적으로 숫자타입을 문자타입으로 개발자가 변환시켰다. 이는 다른 이가 보더라도 개발자의 의도를 파악할 수 있는 부분이다.
한편 이와 달리 컴파일러가 표현식을 평가하면서 암묵적으로 타입이 자동 변환되기도 하는데, 이를 암묵적 타입 변환(implicit coercion) 혹은 타입 강제 변환(type coercion) 이라고 한다.
var x = 10;
var str = x + " " ;
console.log(typeOf str, str); // string 10
두가지 변환 모두 원시 값을 직접 변환하는 것은 아니다. 새로운 타입의 원시값을 생성하는것이다.
위 예제처럼 기존 숫자 x 는 10이고, 이를 표현식에서 문자 "" 와 마주하게 되니, 자바스크립트 엔진은 표현식을 수행시키기 위해, 숫자 x 를 문자로 변경하여 표현식을 수행한다. 이 때 변환은 원시값의 변화가 아니라 한번 쓰이고 버려지는 값이다. 즉, 표현식을 실행 시키기 위한 엔진의 암죽적 타입 변환과정이다.
명시적 타입변환이던 암묵적 타입 변환이던 중요한 것은 개발자 본인 및 다른 사용자가 개발자의 의도를 파악할 수 있어야 하는 점이다. 암묵적으로 의도치않게 변하게 된다면, 이는 개발자 자신의 의도가 아니고, 코드를 해석하는 입장에서는 결코 바람직하지 않다.
암묵적 타입 변환에 대해 좀 더 파고 들어보자
9-2. 암묵적 타입 변환
엔진 자체가 문맥을 고려해서 자체적으로 타입을 변경하는 과정이라고 앞에서 설명하였다. 여기서 문맥이라고 표현했는데, 문맥은 상황에 따라 다양하게 나타날 수 있다. 어떤 상황에서는 숫자 타입이어야 하고, 어떤 상황에서는 문자 타입이어야 하는 것을 엔진은 해석하게 된다.
암묵적 타입 변환이 이러난다는 것은 결국 자바스크립트 엔진이 오류가 발생하는것을 최소화 하는 방향으로 해석할 수 있다. 암묵적 타입 변환이 발생하면 크게 문자열, 숫자, 불리언과 같은 원시 타입중 하나로 자동 변환한다.
다음 예제를 통해 각각의 타입이 문자열 타입으로 암묵적 타입 변환을 하는 결과를 살펴보자.
// 숫자 타입
0 + '' // '0'
-0 + '' // '0'
1 + '' // '1'
-1 + '' // '-1'
NaN + '' // 'NaN'
Infinity + '' // 'Infinity'
-Infinity + '' // '-Infinity'
// 불리언 타입
true + '' // 'true'
false + '' // 'false'
// null 타입
null + '' // 'null'
//undefined 타입
undefined + '' // 'undifined'
//심벌 타입
(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 ] }"
+ 연산자를 통해 모두 문자열으로 변환되는것을 확인할 수 있다.
이와 대조적으로 나머지 산술연산자 -, *, / 등은 산술연산자가 작동하는 표현식은 숫자이기에 그 문맥에 맞게 암묵적으로 모두 숫자타입으로 변경시킨다. 또한 비교 연산자 역시 그러한데, 비교 연산자의 역할은 불리언 값을 만드는 것이다. 비교 연산자는 피연산자의 크기를 비교해야하기에 문맥상 숫자 타입이어야 한다(피연산자가) . 따라서 비교연산자 역시 암묵적으로 숫자 타입으로 변환시킨다.
다시 예제를 살펴보자
// 문자열 타입
+'' // 0
+'0' // 0
+'1' // 1
+'string' // NaN
// 불리언 타입
+true. // 1
+false // 0
// null 타입
+null. // 0
// undefined 타입
+undefined // NaN
// symbol 타입
+Symbol() // TypeError: Cannot convert a Symbol value to a number
// 객체 타입
+{} // NaN
+[] // 0
+[10, 20] // NaN
+(function(){}) // NaN
다 기억하긴 힘들 수 있지만, NaN 이 되는 부분들은 한번 더 기억을 해놓는 편이 좋다.
이제 불리언 타입으로의 변환을 살펴보자. 앞서 제어문에서의 조건문 내 평가되는 타입은 불리언이라고 했다. 자바스크립트 엔진은 조건식의 평가 결과를 불리언 타입으로 암묵적으로 변환한다. 우리가 그냥 익숙하게 사용했던 경우들도 다 암묵적으로 타입이 변환된 것이다.
if('') console.log(1);
if(true) console.log(2);
if(0) console.log(3);
if('str') console.log(4);
if(null) console.log(5);
// 2 4
조건문에서 true 라 평가받는것은 위에서 분명히 true 로 적은 부분과, 문자열 'str' 이다. 문자열은 불리언으로 평가되는 값이 아니다. 허나 자바스크립트 엔진은 불리언 타입이 아닌 값을 Truthy 값(참으로 평가되는 값) 또는 Falsy 값(거짓으로 평가되는 값)으로 구분한다. 즉, 제어문의 조건식과 같이 불리언 값으로 평가되어야 할 문맥에서 Truthy 값은 true 로, Falsy 값은 false 로 암묵적 타입 변환을 한다.
아래 값들은 false로 평가되는 Falsy 값이다. 암기를 하는것을 추천한다.
- false
- null
- undefined
- 0, -0
- NaN
- ''
이 값들을 제외하고는 모두 Truthy 값이다.
9-3. 명시적 타입 변환
위에서 설명했듯이 개발자의 의도에 따라 명시적으로 타입을 변경하느 것을 의미한다.
문자열 타입으로의 변환 방법은 다음과 같다
- String 생성자 함수를 new 연산자 없이 호출하는 방법
- Object.prototype.toString 메서드를 사용하는 방법
- 문자열 연결 연산자를 사용하는 방법
// 1번 방법
String(1); // '1'
String(NaN); // 'NaN'
String(true); // 'true'
String(false); // 'false'
// 2번 방법
(1).toString(); // '1'
(NaN).toString(); // 'NaN'
(true).toString(); //'true'
// 3번 방법
1 + ''; // '1';
...
문자열 연산자를 활용하는 방법은 사실 암묵적 타입 변환이라 할 수 있다. 허나, 개발자의 의도를 알 수 있다면 이 역시 명시적으로 볼 수도 있다.
숫자 타입으로의 변환과정은 다음과 같다
- Number 생성자 함수를 new 연산자 없이 호출하는 방법
- parseInt, parseFloat 함수를 사용하는 방법 (문자열만 숫자 타입으로 변환 가능)
- + 단항 산술 연산자를 이용하는 방법
- * 산술 연산자를 이용하는 방법
// 1번 방법
Number('0') // 0
Number('-1') // -1
Number('10.53') // 10.53
Number(true) // 1
Number(false) // 0
// 2번 방법
parseInt('0') // 0
parseInt('-1') // -1
parseFloat('10.53') // 10.53
// 3번 방법
+'0' // 0
+'-1' // -1
+'10.53' // 10.53
+true // 1
+false // 0
// 4번 방법
'0' * 1 // 0
'-1' * 1 // -1
'10.53' * 1 // 10.53
'true' * 1 // 1
'false' * 1 //0
불리언 타입으로 변환 시키는 방법은 다음과 같다
- Boolean 생성자 함수를 new 연산자 없이 호출하는 방법
- ! 부정 논리 연산자를 두번 사용하는 방법
// 1번 방법
Boolean('x'); // true
Boolean(''); // false
Boolean('false'); // true
Boolean(0); // false
Boolean(1); // true
Boolean(NaN); // false
Boolean(Infinity); // true
Boolean(null); // false
Boolean(undefined); // false
Boolean([]); // true
Boolean({}); // true
// 2번 방법
!!'x'; // true
!!''; // false
!!'false'; // true
!!0; // false
!!1; // true
!!NaN; // false
!!Infinity; // true
!!null; // false
!!undefined; // false
!![]; // true
!!{}; // true
9-4. 단축 평가
예시를 통해 살펴보는게 빠르겠다.
'Cat' && 'Dog' // "Dog"
논리곱(&&) 연산자는 두 개의 피연산자가 모두 true 로 평가될 때 true 를 반환한다. 이전 글에서 설명했듯이 좌항에서 우항으로 평가가 진행되는데, 주목해야 할 점은 반환값이 우항이라는 점이다. 또한 평가 결과가 불리언이 아니라는 점! 2가지를 잘 살펴보아야 한다.
이 전까지 Truthy 값으로서 true 로 평가받게 되는데 ("Cat"), 논리곱 연산자에서는 좌항의 평가 결과만으로 표현식을 평가할 순 없다. 즉 우항 역시 평가를 해야한다. 이 때 논리곱 연산자는 두번째 우항의 평가를 생략하고 그대로 값을 반환하게 된다. 그래서 'Dog' 을 반환한 것이다.
'Cat' || 'Dog // 'Cat'
논리합(||) 연산자의 경우 두 개의 피연산자 중 하나만 true로 평가되어도 true 를 반환한다. 역시나 좌항에서 우항으로 평가가 진행된다.
이때도 마찬가지로 좌항이 true 로 평가되면, 우항의 평가 과정을 진행하지 않는다. 논리합 연산자는 논리 연산의 결과를 결정한 첫 번째 피연산자, 즉 문자열 'Cat' 을 그대로 반환한다.
어려운 표현들이 나와 햇갈릴만 하다. 단순하게 생각하여 논리곱과 논리합 연산자는 논리 평가 결과에 따른 불리언값을 반환하는 것이 아니라 피연산자를 그대로 반환하게 된다. 일단 이렇게 생각하고 있자. 이러한 현상이 발생하는 것을 단축 평가라 한다.
단축평가는 표현식을 평가하는 도중에 평가 결과가 확정된 경우 나머지 평가 과정을 생략하는 것을 말한다.
// 논리합(||) 연산자
'Cat' || 'Dog' // 'Cat'
false || 'Dog' // false
'Cat' || false // 'Cat'
// 논리곱(&&) 연산자
'Cat' && 'Dog' // 'Dog'
false && 'Dog' // false
'Cat' && false // false
위 예시를 통해 좀 더 설명을 하자면, 논리곱 연산자 좌항에 false 로서 이미 평가 결과가 결정이 나였다면, 그리고 그 평가가 false 이기에 그대로 false 를 반환한다. 애초에 우항으로 넘어갈 필요가 없으니깐.
허나 다음 좌항이 'Cat' 이고 우항이 false 일 경우 우항 역시 평가를 통해 불리언값으로서 false 가 나온게 아니라, 그냥 우항의 피연산자 false 를 그대로 반환한 것이다.
이러한 특성으로 인해 논리합과 논리곱 연산자는 조건문에서 잘 활용할 수 있다.
var done = true;
var message = '';
// 주어진 조건이 true 일때
if(done) message = '완료';
// if 문은 단축 평가로 대체 가능하다.
// done 이 true 라면 message 에 '완료'를 할당
message = done && '완료';
console.log(message); // 완료
논리곱 연산자의 단축 평가를 활용하면, 좌항에서 평가가 완료되면 (즉, done 이 Truthy 로서 true 로 평가되었다면) 우항은 평가 과정을 생략하고 우항의 피연산자 값 '완료' 를 그대로 반환하여 message 에 선언한다. 이러한 과정은 위 if 문에서의 조건문과 같다.
var done = false;
var message = '';
// 주어진 조건이 false일 때,
if(!done) message = '미완료';
// if 문은 단축 평가로 대체 가능하다.
// done이 false 라면 message에 '미완료' 를 할당
message = done || '미완료';
console.log(message); // '미완료'
논리합 연산자 역시 조건문을 대체할 수 있다. 논리합의 경우 좌항이 true 라면 그 좌항의 피연산자 값을 그대로 반환한다. 그렇기에 좌항이 false 가 나와야 우항으로 평가가 이어지게 된다. 여기에 논리합 연산자 역시 단축 평가로서 좌항의 평가가 완료되었다면, 우항은 평가가 진행되지 않는다. 따라서 우항의 피연산자 값 그대로 반환하여 message 에 선언된다.
이전 글에서 논리곱 연산자의 경우 리엑트에서 조건부 렌더링을 할 때 많이 사용된다고 언급했었다. 좌항이 Truthy 로 평가되어 true가 될 때 우항은 피 연산자 그대로 반환하기에, 우항에 랜더링할 부분을 넣어서 그대로 반환하여 랜더링 하게 하는 것이었다. 이런식으로 주로 조건을 걸어줄 때 많이 응용되곤 한다.
이번에는 옵셔널 체이닝 연산자에 대해 살펴보자
ES11(ECMAScript2020)에서 도입된 옵셔널 체이닝(optional chaining)연산자 ?. 는 좌항의 피연산자가 null 또는 undefined인 경우 undefined 를 반환하고, 그렇지 않으면 우항의 프로퍼티 참조를 이어간다 (프로퍼티에 대해선 다음에)
var elem = null;
//elem이 null 이거나 undefined 이면 undefined 를 반환하고, 그렇지 않으면 우항의 프로퍼티 참조를 이어간다.
var value = elem?.value;
console.log(value); // undefined
옵셔널 체이닝이 존재하지 않을때는 주로 && 이 사용되었다. 논리곱 연산자의 특징은 좌항의 피연산자가 Falsy 값이라면 그 값을 그대로 반환하게 된다. null, undefined 뿐 아니라 '', 0 또한 그대로 반환하게 된다. 개발자의 의도에 따라 '' 부분을 객체로 판단하고자 할 때가 있다. 예를 들자
var str = '';
var length = str && str.length;
console.log(length); // ''
문자열이 없을 때를 개발자가 조건으로 문자열의 길이를 뽑아내려 할 때가 있다. 여기서 알 수 있듯이 '' 은 Falsy 로 평가되는 값으로서 논리곱 연산자에 의해 좌항 그대로 반환되어 length 변수에 할당된다. 따라서 length 를 불러오면 그대로 '' 을 반환하게 된다.
반면 옵티컬 체이닝의 경우 평가하려하는 객체가 null 이나 undefined 가 아니라면 우항의 프로퍼티 참조를 그대로 이어가게 된다.
var str = "";
// 문자열의 길이를 참조한다. 객체가 null 이나 undefined 가 아니라면 참조를 이어간다.
var length = str?.length;
console.log(length); // 0
이처럼 Falsy 로서 평가되었다 한들 null 이나 undefined 가 아니라면 그대로 우항의 프로퍼티를 참조하기에, 좀 더 유연하게 참조를 이어갈 수 있다.
마지막으로 null 병합 연산자 역시 알아보자
ES11에서 마찬가지로 도입되었는데, ?? 로 표현한다. 좌항의 피연산자가 null 이거나 undefined 일 경우 우항의 피연산자를 반환하고, 그렇지 않으면 좌항의 피연산자를 반환한다.
이러한 연산자는 어째서 만들어 졌을까?
var foo = " " || "default string";
console.log(foo); // "default string"
병합 연산자가 생기기 전 논리합 연산자의 경우 좌항의 Falsy 로 평가된다면 우항의 피연산자를 그대로 반환하게 된다. 앞에서 Falsy 로 평가되는 피연산자는 6가지가 있었고, 개발자는 이러한 연산자 중 특정 연산자만 false 로 인식하게 하고 싶을 수가 있다. 즉, 빈 문자열은 개발자가 의도로 변수에 할당하고 싶을 수도 있다는 의미다.
이렇기 때문에 옵셔널 체이닝과 같이 null 과 undefined 만을 특정하여 2가지가 할당되려 한다면 기본값을 할당하라 라고 의도하게 만들 수 있다. 즉, 변수의 기본값을 설정하는데 빈 문자열이나 0 을 포함시키기 싫은 것이다.
var foo = " " ?? "default string";
console.log(foo); // " "
빈 문자열이나 0 또한 초기값으로 두지 않기 위해 병합 연산자를 사용했고, 실제로 빈 문자열은 초기값으로 인정되지 않았다. null 이나 undefined 가 아니기 때문이다. 이렇게 기본값을 설정할 때 유용하게 사용될 수 있다.
이전부터 옵셔널 체이닝과 병합 연산자에 대해 기존 논리합과 논리곱과의 관계에서 차이점을 알아보고 싶었는데, 이번에 알게되어서 좀 더 자바스크립트의 이해에 다가간 느낌이다.
그리고 아무런 생각없이 당연하게 사용하던 조건문에서 알게 모르게 암묵적 타입 변화를 이용하고 있었다는 점을 알게 되었다. 물론 편리하게 계속 사용하겠지만, 그 원리에 대해 이해하고 나니 다시금 보이는 느낌이다.
리엑트에서 조건부 랜더링을 진행할 때 삼항조건식과 더불어 논리곱 연산자는 많이 사용된다. 왜 우항 부분을 그대로 랜더링하는지 궁금했었는데, 이제 알게 된것 같다.
다음에는 자바스크립트의 핵심이라 할 수 있는 객체 리터럴에 대해 공부해보자.
'Programing > Javascript' 카테고리의 다른 글
[Deep Dive] 원시값과 객체 (0) | 2022.10.25 |
---|---|
[Deep Dive] 객체 리터럴 (1) | 2022.10.23 |
[Deep Dive] 제어문 (0) | 2022.10.21 |
[Deep Dive] 연산자 (0) | 2022.10.19 |
[Deep Dive] 데이터 타입 (2) | 2022.10.07 |