이번 포스팅은 강제변환의 마지막, 암시적 강제변환의 차례입니다. 말 그대로 변환 과정이 명확하게 보이지 않는 타입 변환들이 이 암시적 변환에 속합니다.
대부분의 코드에서 이러한 암시적인 타입 변환은 읽는 사람에게 혼란을 줍니다. 따라서 이해하기 쉬운 코드를 작성하기 위해서라면 암시적 강제변환만큼은 피해야 한다는 말도 적지 않습니다.
분명 틀린 말은 아니지만, 그렇다고 암시적 변환이 정말 나쁜 자바스크립트의 언어적 결함이라는 의견에는 100% 동의하기 어렵습니다. "버그가 아니라 기능입니다." 라는 말도 있듯이, 이번 시간에는 암시적 강제변환과 그를 어떻게 활용할 수 있는지 알아보겠습니다.
문자열 <-> 숫자
+ 연산자는 숫자끼리의 덧셈이라는 목적과 문자열끼리의 접합이라는 두 가지 기능을 오버로드한 연산자입니다. 문자열과 기타 다른 타입들이 섞여있는 경우엔 (적어도 한 쪽의 피연산자가 문자열인 경우) 문자열끼리의 접합이라고 가정하고 접합시킵니다.
이 과정에서 문자열이 아닌 타입이 문자열로 바뀌는 과정을 살펴보면, 먼저 ToPrimitive 추상 연산을 수행하고, 단순 원시 값을 반환하기 위해 valueOf() 나 toString()등의 메소드를 통해 문자열로 변환됩니다.
단순히 어떤 값을 문자열로 바꾸기에는 String(var) 과 같은 방법도 있습니다. 그런데 놀랍게도, + 연산자와 String() 연산자를 통한 강제변환은 다른 결과를 반환하기도 합니다. 아래의 예시를 보겠습니다.
물론 일반적인 숫자 값을 변환할때는 별 문제가 없지만, valueOf(), toString() 메소드를 직접 구현한 객체라면 이야기가 달라집니다.
valueOf()와 toString() 메소드를 각각 다른 값으로 구현한 객체가 실제로 얼마나 있을지는 모르겠습니다. 하지만 String(var)을 사용하는 강제변환 방식이 어떻게 동작하는지, +를 사용한 강제변환과 어떤 차이를 가지고 있는지 알 수 있는 좋은 예시입니다.
불리언 -> 숫자
다음은 불리언 타입을 숫자 타입으로 변환하는 경우입니다. 불리언과 숫자 사이의 변환은 다양한 곳에서 유용합니다. 예를 들어 여러개의 인자를 받아 그 중 하나만 true인 경우를 찾는 로직을 구현한다고 했을때, 이 변환을 사용하면 간단히 구현할 수 있습니다.
위 코드는 true나 truthy한 값을 숫자로 강제변환했을때 1이 되는 특성을 이용한 방법입니다. 반대로 false나 falsy한 값은 0으로 바뀌게 되어 조건문의 내용이 실행되지 않습니다. (실행이 되더라도 0이 더해지기 때문에 로직에는 문제가 없습니다.)
변수 sum에 arguments[i]를 더해주는 부분을 명시적으로 나타내면 아래와 같습니다.
위의 두 코드를 비교해보면 개인적으로 보기에는 암시적인 변환이 좀 더 깔끔해 보이기는 합니다. 상황에 맞게 사용해주시면 되겠습니다.
-> 불리언
이번엔 반대로 불리언으로 강제변환되는 경우입니다. 불리언 값으로의 암시적 강제변환은 이러한 표현식에서 일어납니다.
- if ( 불리언 값 )
- for ( ; 불리언 값 ; )
- while ( 불리언 값 ) / do ~ while ( 불리언 값 )
- ( 불리언 값 ) ? ( 식1 ) : ( 식2 )
- OR 연산자 ( || ), AND 연산자 ( && ) 의 좌측 피연산자
불리언 값이 들어가야할 자리에 불리언이 아닌 값이 들어가게되면 불리언 값으로 암시적 강제변환이 이루어집니다.
다른건 어려울게 없는데, 마지막 논리 연산자 부분은 알아야 할 내용이 있어 살펴보겠습니다.
다른 언어와는 다르게, 자바스크립트에서의 논리 연산자는 불리언 값을 반환하지 않습니다.
논리 연산자인데 불리언 값을 반환하지 않는다? 뭔가 조금 이상합니다. 그럼 대체 무엇을 반환하는건지 확인해보면 바로 피연산자들 중 하나를 반환합니다.
실제로 논리 연산자를 사용한 코드를 실행시켜보면 다음과 같은 결과를 얻을 수 있습니다.
단 하나의 예시도 불리언 값이 아닌 피연산자의 값들을 반환하는 것을 알 수 있습니다. &&과 || 모두 첫 번째 피연산자의 참과 거짓에 따라 반환할 피연산자를 선택합니다.
삼항연산자로 나타내면 이해가 쉬울 것 같습니다.
AND 연산자는 왼쪽 피연산자가 참이면 오른쪽 피연산자를 반환하고, OR 연산자는 왼쪽 피연산자가 참이면 왼쪽 피연산자를 반환합니다. 물론 깊게 들어가면 의미상의 차이는 있긴 하지만 이렇게 기억하면 쉽게 떠올릴 수 있습니다.
심볼
심볼도 강제변환을 가지고 있습니다. 다만 숫자로는 변환이 불가능하고, 문자열은 명시적 변환에 한해서만 가능합니다. 재미있게도 불리언 값으로는 명시적/암시적 구분없이 가능합니다. (모두 true를 반환합니다.) 별로 사용되지는 않지만, 알아두고 넘어갑시다.
동등 비교 == / ===
자바스크립트 개발자라면 한 번쯤 들어보았을 질문입니다. ==와 ===의 차이가 무엇일까요?
저는 이 질문을 처음 들었을때, ==은 값을 비교하는 것이고, ===는 타입까지 비교한다라고 대답했었던 기억이 납니다.
물론 이것도 틀린 대답은 아니었습니다. 하지만 100점짜리 대답은 아니었죠. 둘의 정확한 동작을 살펴보면 동등 비교 == 의 경우 두 피연산자의 타입을 변환해서 비교하고, 일치 비교 === 의 경우 별다른 타입 변환 없이 그대로 비교합니다.
따라서 오히려 타입을 비교하는게 기본 동작이고, 값만 비교하기 위해 동등 비교가 타입 변환을 추가로 시행하는 형태였던 것입니다. 성능을 따져보면 === 가 더 빠른 동작입니다. (실제로 둘 사이의 유의미한 차이는 없을지라도요.)
비교시 강제변환이 어떻게 일어나는지 예시를 통해 확인해보겠습니다.
- 숫자 == 문자열
숫자와 문자열을 비교했을때 일치 비교(===)의 경우 타입이 다르기 때문에 false가 출력되고, 동등 비교(==)의 경우 값이 같기 때문에 true를 반환하는 모습입니다.
이때 동등 비교의 명세에 의하면 왼쪽의 피연산자가 숫자이고 오른쪽의 피연산자가 문자열인 경우, 문자열을 숫자로 강제 변환한 뒤 비교하게 됩니다. 반대로 왼쪽의 피연산자가 문자열이고 오른쪽의 피연산자가 숫자인 경우 왼쪽의 문자열을 숫자로 강제 변환한 뒤 비교합니다. 어느쪽이던, 문자열을 숫자로 변환하여 비교하는 것을 알 수 있습니다.
- 불리언 == *
이번엔 불리언 타입의 비교입니다. 불리언 타입과의 비교는 항상 조심해야합니다. truthy 한 값이라고 해서 불리언 값 true와 동등 비교했을때 참이 나오지 않습니다. 코드로 확인해보겠습니다.
둘의 타입이 다르기 때문에 당연히 일치 비교시에는 false가 출력됩니다. 그런데 동등 비교시에도 false가 나오는건 어째서일까요? 분명 변수 str에 들어있는 비어있지 않은 문자열은 truthy한 값임을 우리는 알고 있는데도요.
동등 비교에서 왼쪽 피연산자가 불리언이라면 왼쪽 피연산자를 숫자로 변환한 뒤 오른쪽 피연산자와 비교한 결과를 반환합니다. 반대로 오른쪽 피연산자가 불리언이라면 오른쪽 피연산자를 숫자로 변환하고 비교한 결과를 출력해줍니다.
따라서 불리언 타입의 동등 비교에서는 true와 false가 강제 변환된 숫자 값인 1과 0으로 취급되는것과 마찬가지입니다.
만약 여기서 비교하는 문자열이 1이었다면, 위 코드처럼 true가 출력되게 됩니다.
- null == undefined
자바스크립트의 특이한 타입들인 null과 undefined도 빼놓을 수 없죠.
이 둘은 정말 이상하게도 서로를 비교하면 true를 반환하도록 짜여있습니다. 서로가 서로에게 타입을 맞추기 때문에 이 둘의 비교는 항상 참일 수 밖에 없습니다. 적어도 비교 구문에서는 구분되지 않는 값으로 처리한다고 생각하면 됩니다.
이 둘을 비교할때 명시적인 변환을 사용한답시고 가독성을 떨어뜨리는 코드를 작성할 필요가 전혀 없다는 의미와도 같습니다.
- 객체 == 비객체
객체와 원시값의 비교는 항상 객체를 원시값으로 변환하여 비교합니다. 예를 들어 비교대상 중 한쪽이 배열이라고 한다면 다음과 같은 코드가 성립합니다.
objArr과 num과의 비교가 일어나면 배열(객체) 안쪽의 값을 추출(변환)하여 1이라는 원시 값을 확보하고, 이를 1의 값을 가지고 있는 변수 num과 비교하여 true를 반환하게됩니다.
단, 여기서도 null과 undefined는 예외입니다. null 값을 갖는 변수를 선언하고, 이 변수를 Object()를 통해서 래핑해준 값과 비교해보면 false를 반환합니다.
이는 null이 객체 래퍼를 갖고 있지 않아 생기는 문제로 Object()를 통해 박싱되지 않기 때문에 원시값 null과 비교하면 false를 얻게 됩니다. 이는 객체 래퍼를 갖고 있지 않은 undefined도 마찬가지입니다.
번외
범용적인 예시들은 많이 살펴보았고, 이번엔 정말 특이한 몇 가지 강제 변환 케이스를 소개하려합니다.
먼저 [] == ![] 를 보면 분명 왼쪽은 truthy한 값이고 오른쪽은 falsy한 값인 것 같은데, 어째서 true가 나오는지 의문스럽습니다. 단순히 보기에도 true == false 처럼 보이는데 어떻게 이런 결과가 나오는걸까요.
오른쪽 값을 보시면 ! 를 통해 Boolean형으로 변환되고 있는것을 확인할 수 있습니다. 따라서 [] == false와 같은 모양이 됩니다. 그런데 우리는 위에서 Boolean형과 비교를 할때면 숫자로 변환한다고 공부했습니다. 그럼 양쪽 값을 숫자로 변환해가다보면 결국 0 == 0 이 되어 평가식의 값은 true가 됩니다.
명세에 나온 과정을 따라가보니 언뜻 보기엔 이상한 결과처럼 보였지만, 사실 자바스크립트 엔진은 정상적으로 일을 처리하고 있었습니다.
두 번째도 마찬가지로 명세를 따라가보겠습니다. 객체와 원시 값(문자열)의 비교이므로 객체의 원시 값을 추출해냅니다. [null]의 경우 원시 값을 추출하게 되면 "" 이 되므로 결국 "" == "", true가 반환됩니다.
세 번째는 개행 문자와의 비교입니다. \n은 개행 문자(공백 문자)로 숫자로 변환했을때 0이 되는 특수한 문자열입니다. 이 외에도 \f, \r, \t등이 숫자로 변환하면 0이 됩니다. \u, \x 를 숫자로 변환하려하면 유효하지 않은 개행문자라는 에러메시지가 출력되며 변환되지 않습니다. 다른 문자들은 NaN으로 변환됩니다.
정리
세 포스트에 걸쳐 강제 변환에 대한 내용을 정리해보았습니다. 그동안 막연하게만 알고있던 자바스크립트의 변환에 대해 어떻게 작동하는지, 어떤 규칙을 갖고 있는지 살펴볼 수 있는 보람찬 시간이었습니다.
몇 번 예시 코드들을 적으며 실행해보니 이러한 변환을 안전하게 사용하기 위해서는 가급적 == 보다는 === 를 사용하고, 피연산자들이 불리언 타입이거나, [], "", 0 등 특수한 값들이 된다면 (이제는 해결할 수 있는) 오작동을 할 염려가 있으니 일치 비교를 통해 확실하게 코드를 작성하는 것이 프로그래머의 정신건강에 이로울 것 같다는 점도 확실히 알게 되었습니다.
다음 포스팅에서는 문법에 대해 알아보겠습니다.
'이론 > Frontend' 카테고리의 다른 글
Jest 기본 사용법 (0) | 2021.04.03 |
---|---|
JavaScript의 문법 (Grammar of JavaScript) (0) | 2021.03.30 |
JavaScript의 강제변환 - 2 (Coercive Type Conversion of JavaScript) (0) | 2021.03.05 |
JavaScript의 강제변환 - 1 (Coercive Type Conversion of JavaScript) (0) | 2021.03.02 |
JavaScript의 네이티브 (Natives of JavaScript) (0) | 2021.02.21 |
댓글