본문 바로가기

Language/JavaScript

<JS> 암시적 변환

1. 암시적변환

 - 암시적 강제변환(이하 암시적 변환)은 부수 효과가 명확하지 않게 숨겨진 형태로 일어나는 타입변환이다. 대부분 암시적 변환은 위험하고 나쁜 코드라고 생각하기 쉽다. 필자 또한 그랬고 수많은 개발자들도 그렇게 생각하고 있다. 하지만 암시적 변환의 목적은 불필요한 상세 구현을 줄이는 것이다. 이미 나도 모르는 사이에 읽기 좋은 코드를 위해 쓰고 있었을 수도 있다.

 

 - 먼저 JS코드를 보기 전에 엄격한 타입 언어에서 말하는 이론적인 의사코드를 보자.

 

SomeType x = SomeType(AnotherType(y))

 

 - 어떤 y 값을 SomeType인 x에 할당하였다. 이 언어에서는 바로 SomeType으로 변환할 수 없어서 중간단계를 거쳤다고 가정하였다. 이를 만약 다음과 같이 작성할 수 있다면 어떨까.

 

SomeType x = SomeType(y)

 

 - 위처럼 코드를 쓸 수 있다면 중간 변환 단계를 줄여 타입변환을 단순화할 수 있다. 즉 y 값이 AnotherType을 거쳐 SomeType이 된다는 상세한 변환 과정을 다 알 필요가 없다. 단순화 시킨 타입변환 코드가 코드 가독성을 높이고 세세한 구현부를 추상화하거나 감추는 데 도움이 될 수 있는 것이다.

 

 - 위의 예시가 완벽한 비유는 아닐 수 있다. 심지어 잠재적인 문제가 있을 수도 있다. 하지만 개발자라면 잠재적인 문제가 뭔지 인식하고 피하는 방법 또한 확실히 알아야 한다. 안전하게 가기 위해 조그마한 문제 때문에 모든것을 포기하고 돌아가는 것이 맞는 것일까. 우리는 객관적으로 암시적 변환을 바라보고 수용해야할 필요가 있다.

 

2. 문자열과 숫자

* 숫자 => 문자열

 - '+' 연산자는 숫자의 덧셈 또는 문자열 접합 이라는 2 가지 목적으로 오버로드된다. 보통 피연산자가 하나라도 문자열인 경우 문자열 붙이기를 실행한다고 잘못 알고 있는 경우가 많다. 하지만 실제로는 더 복잡한 방식으로 이루어진다.

 

const x = [1, 2];
const y = [3, 4];

x + y; // "1,23,4"

 

 - 명세에 따르면 + 알고리즘은 한쪽 피연산자가 문자열이거나 다음 과정을 통해 문자열 표현형으로 나타낼 수 있으면 문자열 붙이기를 한다. 따라서 피연산자 중 하나가 객체라면, 먼저 이 값에 ToPrimitive 추상 연산을 수행한다. 당연히 위는 valueOf()에서 단순 원시값이 아니므로 바로 toString()으로 넘어간다. 참고로 ToPrimitive연산은 valueOf()에서 단순 원시값이 나올경우 해당 원시값을 사용하고, 그렇지 않으면 toString()을 확인한다. 결과적으로 위는 "1, 2"와 "3, 4"가 되어 문자열이 이어지는 것이다.

 

 - 어쨌든 문자열로 어떻게 암시적 변환이 일어나는지는 알았다. 그렇다면 이게 무슨 의미가 있을까. 우리는 흔히 ""(빈 문자열)을 숫자에 더하여 문자열로 바꾸곤 한다. 이는 암시적 변환의 좋은 사례 중 하나이다. 

 

 - 참고로 명시적 강제 변환인 String(x)와 달리 x + "" 에서 주의해야할 사항이 있다. 위에서 설명했듯 x는 ToPrimitive 연산 과정에서 valueOf()를 호출하고, 그 결괏값은 toString 추상 연산을 하게 된다. 하지만 String()은 toString()을 직접 호출하기 때문에 만약 x가 커스텀 객체이고, valueOf나 toString을 직접 작성했다면 두 결괏값이 달라질 수 있다.

 

* 문자열 => 숫자

 - 이번에는 빼기 연산자를 알아보자. '-' 연산자는 숫자 뺄셈 기능이 전부이다. (곱하기와 나누기 역시 마찬가지이다.) 따라서 x - 0 은 x를 숫자로 강제변환한다.

 

const x = [8];
const y = [5];

x - y; // 3

 

 - 먼저 각 배열은 문자열로 강제변환되고, 이후 숫자로 강제 변환된다. 마침내 - 연산이 수행된다.

 

3. 불리언

* 불리언 => 숫자

 - 불리언을 숫자로 단순화하는 것은 종종 더 나은 코드를 제공한다.

 

function onlyOne(a, b, c) {
 return !!((a && !b && !c) || (!a && b && !c) || (!a && !b && c));
}

 

 - 위 코드는 세 인자 중 하나만 truthy 인지 아닌지를 확인하는 함수이다. truthy 체크 시 암시적 강제변환을 하고 최종 반환 값은 명시적 강제변환(!!)을 하였다. 그러나 이러한 코드는 인자가 늘어날 때마다 비교 로직을 조합하고 복잡한 코드를 구현해야한다. 하지만 불리언 값을 숫자로 바꾼다면 어떨까

 

function onlyOne() {
 let sum = 0;
 for (let i = 0; i < arguments.length; i++) {
   if (arguments[i]) {
     sum += arguments[i];
   }
 }
 return sum == 1;
}

 

 - truthy를 숫자로 강제변환하면 1이므로 이를 활용한 코드이다. sum += arguments[i] 에서 암시적 강제변환이 일어난다.

 

* ? => 불리언

 - 불리언으로의 암시적 변환은 가장 흔하지만 가장 골칫거리이다. 불리언으로의 암시적 강제 변환이 일어나는 표현식을 다음과 같다.

  • if ( ) 문의 조건 표현식
  • for (  ;  ;  ) 에서 두 번째 조건 표현식
  • while ( ) 및 do ... while( ) 루프의 조건 표현식
  • ? : 삼항 연산 시 첫 번째 조건 표현식
  • || 및 && 의 좌측 피연산자

 - 위와 같은 상황에서 우리는 알게 모르게 불리언 값이 아닌 조건 표현식을 평가하기 위해 강제 변환을 사용해 온 것이다.

 

 - 여기서 의문인 부분이 있을 것이다. 위에 굵게 표시해둔 부분이다. 이들을 '논리 __ 연산자'라고 부르는데 실제 기능은 '선택 연산자' 와 같은 이름으로 부르는 것이 더 적절할 수 있다. 그 이유는 자바스크립트에서 이 연산자들은 다른 언어와 달리 실제로 결괏값이 논리 값이 아니기 때문이다. 결괏값은 두 피연산자 중 오직 '한쪽의 값'이다. 실제 명세에서는 다음과 같이 정의되어 있다.

 

&& 또는 || 연산자의 결괏값이 반드시 불리언 타입이어야 하는 것은 아니며 항상 두 피연산자 표현식 중 어느 한쪽 값으로 귀결된다.

 

 - 필자는 여기서 깜짝 놀랐다. || 와 &&을 통해 특정 값을 도출하는 것을 종종 사용해왔지만 특별한 스킬인 줄로만 알았지 이것이 원래 의도인줄은 몰랐기 때문이다.

 

 - 알다시피 || 연산자는 앞의 값이 truthy 하면 해당 값을 반환하고 그렇지 않으면 falsy 값을 찾거나 마지막 값을 반환한다. &&는 첫 falsy값을 찾아 반환하고 모두 truthy 하다면 마지막 값을 반환한다. 그래서 우리는 아래와 같은 코드를 흔히 작성해왔다.

 

function foo(text, name) {
  text = text || "hello";
  name = name || "harry";
  console.log(text + " " + name);
}
foo(); // hello harry
foo("bye", "jarry") // bye jarry

 

 - 어쨌든 결론은 || 와 &&의 연산 결과는 실제로 true/false 가 아니라는 것이다. 그렇다면 지금까지 작성해온 논리 표현식은 어떻게 동작 했을까? 혹시 지금까지 잘못된 코드를 작성하고 있지는 않았을까? 다행히 절묘하게도 잘 작성해왔고, 잘 동작해왔으니 걱정할 필요가 없다.

 

const x = 10;
const y = null;
const z = "harry"

if (x && (y || z)) {
  console.log("x는 true, y또는 z가 true")
}

 

 - 위 코드에서 x && (y || z) 표현식의 실제 결과는 이제 짐작하듯이 true가 아닌 "harry" 이다. 그리고 이 문자열을 if문이 암시적 강제변환을 하여 true로 만들게 된다.

 

 - 아직도 암시적 변환코드가 잘못된 코드냐고 묻는다면 아니라고 당당히 얘기할 수 있을 것이다. 만일 아직도 암시적 변환이 틀렸다고 주장한다면 위 코드를 아래와 같이 쓰도록 하자.

const x = 10;
const y = null;
const z = "harry"

if (!!x && (!!y || !!z)) {
  console.log("x는 true, y또는 z가 true")
}

 


참고

 

해당 포스팅은 You Don't Know JS 를 읽고 개인적으로 필요한 내용을 추가 및 정리한 글입니다.

 

 

 

GitHub - getify/You-Dont-Know-JS: A book series on JavaScript. @YDKJS on twitter.

A book series on JavaScript. @YDKJS on twitter. Contribute to getify/You-Dont-Know-JS development by creating an account on GitHub.

github.com

 

You Don’t Know JS - YES24

모호하고 애매한 여덟 가지 자바스크립트 개념 길라잡이_You Don’t Know JS 시리즈웹 초창기 시절부터 자바스크립트는 사람들이 대화하듯 웹 콘텐츠를 소비할 수 있게 해준 기반 기술이었다. 20년

www.yes24.com

 

'Language > JavaScript' 카테고리의 다른 글

<ES2022> ES2022 Features  (0) 2022.04.03
<JS> 스코프란  (0) 2021.12.29
<JS> 특수 값과 레퍼런스  (0) 2021.12.23
<ES2021> ES2021 Features  (0) 2021.10.25
<ES2020> ES2020 Features  (0) 2021.10.24