Javascript 형변환

김혜진·2020년 11월 22일
9

javascript

목록 보기
9/9
post-thumbnail

Javascript에 관한 여러가지 meme들을 보면서 키득거리다가도 간혹 헷갈리는 부분들이 있어 한번 짚고 넘어가고자(제대로 알고 웃고자) 강제 형변환에 대해 정리해봅니다.

잘못된 정보는 정정 바랍니다. 감사합니다.

Type coercion 강제 형변환

Type coercion is the automatic or implicit conversion of values from one data type to another (such as strings to numbers). Type conversion is similar to type coercion because they both convert values from one data type to another with one key difference — type coercion is implicit whereas type conversion can be either implicit or explicit.
MDN

Type coercion이란 하나의 데이터 타입이 다른 데이터 타입으로 자동적으로 혹은 암시적으로 변환되는 것을 의미한다. 즉, 의도치 않게 발생한 형변환이므로 강제 형변환, 강제 타입 변환.. 등 이라 부른다.

이 글에서는 문맥에 따라 강제 형변환과 암시적 형변환 두 가지로 부를 예정이다.

Type conversion(coercion아님 주의) 은 명시적이거나 암시적인 형변환 모두를 뜻하는 좀 더 일반적인 의미이다.

명시적 형변환

Boolean, Number, String 등의 객체를 사용해 타입 변환을 할 때 명시적 형변환이라 부른다.

const array = [];

Boolean(array); // Type conversion (explicit)


const number = 2;

String(string); // Type conversion (explicit)

강제 or 암시적 형변환

강제(암시적) 형변환은 각기 다른 타입의 값들을 !!, ==, +, -, && .. 등의 비교, 산술, 문자열, 논리 연산자로 연산 시 발생한다.

연산 불가한 값이라고 에러를 뿜는 대신 강제 변환을 시키더라도 js 내에서 연산 가능한 값으로 변환 후 결과를 반환하려 하기 때문이다.

이때 초기 데이터 타입과 관계없이 강제 형변환을 통해 3개의 원시 타입 boolean, number, string 중 하나로 변경된다.

const array = [];

!!array // Type coercion (implicit)


const number = 2;

number + '' // Type coercion (implicit)

그 때문인지 개인적으로 대부분의 혼란은 원시 타입끼리의 변환보다 객체 타입에서 원시 타입으로 강제 형변환이 될 때 야기되는 것 같다.

0 == '0' 가 true 인 것 보다 [] == 0가 true인 것이 더 받아들이기 힘든 건 나만 느낌이 아니라 생각한다..

변환 규칙

String

+연산자 사용 시 피연산자 중 문자열이 하나라도 있으면 문자열 타입으로 강제 형변환이 발생한다.

+가 String Operator 라고도 불린다는 것을 나만 몰랐나..?

이처럼 피연산자의 위치나 연산 순서와 관계없이 + 연산에 문자열이 하나라도 포함된다면 prototype.toString 메소드를 호출해 문자열 타입으로 강제 변환이 일어난다.
생성자가 없는 undefinednull도 문자열로 변환된다.

undefined + 'undefined';   // "undefinedundefined"
null + 'null';             // "nullnull"

Object.prototype 에도 toString 메소드가 존재하므로(String.prototype이 동일한 메소드 명으로 오버라이딩 하여 사용) 객체, 배열 등의 object 타입이 피연산자에 포함되어있을 때도 동일하게 적용된다.

[] + '';                // ""
['hello', 'ciao'] + ''; // "hello,ciao"
({}) + '';              // "[object Object]"
(function() {}) + '';   // "function() {}"

위의 예시들의 객체에 해당하는 왼쪽 피연산자에서 .toString() 을 호출해보면 내부 값이 문자열로 변환된다는 것을 확인할 수 있다.

그러나 Object.prototype.valueOf 메소드 오버라이딩하여 사용을 하는 경우가 있다면 반환 값이 달라진다는 점을 유의해야 한다. 이는 후술할 예정

Number

+를 제외한 산술 연산자(-, /, *, >, < 등) 사용시 숫자 타입으로 강제 형변환이 발생한다.

문자열이 포함 되어있을 때 ++를 제외한 나머지 연산자들이 다르게 동작하기 때문에

"2" + "2" - "2"

위의 연산은

"2" + "2"   // "22", 문자열로 강제 변환 후
"22" - "2" // 20, 숫자 타입으로 강제 변환

문자열 변환 > 숫자 변환의 순서로 20이 반환되는 것이다.

Number 타입 변환 불가한 경우

그러나 객체, 문자열 값이 있는 배열, 문자열, undefiend, NaN은 숫자 타입으로 변환이 불가하며 이러한 경우 NaN(Not a Number)를 반환한다.

['hello', 'ciao'] + 0; // "hello,ciao0"

['hello', 'ciao'] - 0; // NaN
undefined - 0;         // NaN
null - 0;              // NaN

배열 내 숫자, 숫자 문자열이 하나만 있는 경우는 연산 가능

하지만 배열의 값이 문자열이 아닌 숫자 혹은 숫자 문자열 값인 경우에는 형변환이 발생하여 연산이 가능하게 된다.

[10] / 5;   // 2
[10] * 5  // 50
['10'] < 5  // false
['10'] > 5  // true

이는 위의 문자열 변환에서와 동일하게 객체의 toString 메소드를 호출하여 내부 값을 먼저 문자열로 변환한 후 숫자 타입 변환이 이루어지기 때문이다.

그러나 앞서 언급한 바와 동일하게 Object.prototype.valueOf 메소드 오버라이딩하여 사용을 하는 경우가 있다면 반환 값이 달라진다는 점을 유의해야 한다.

문득 ES6의 template literal이 그때 당시에도 있었다면 +연산 시에도 문자열 형변환을 막지 않았을까 생각해본다.. 근데 이게 왜 Number 변환 쓰면서 떠오르지..?

Boolean

비교 연산자(==, !=, &&, || 등) 사용시 발생한다.

object의 boolean값은 대부분의 경우 true 다.

자바스크립트의 falsy 값은 false로, truthy 값은 true로 반환된다.
이에 따라 truthy로 분류되는 객체는 내부 값의 유무와 관계없이 모두 true를 반환한다.

/* 명시적 형변환 */
Boolean([]); // true
Boolean({}); // true
Boolean([1, 2, 3]); // true
Boolean({ hi: 'hi', hello: 'hello' }); // true

/* 강제 형변환 */
[] || ''     // []
{} && 0      // 0
[] ?? 123    // []
{ hi: 'hi' } && '' // { hi: 'hi' }

object를 == 연산자와 사용하는 경우에는 원시 값을 사용한다.

객체 자체는 falsy 값이 아니기 때문에 위의 설명과 같이 boolean에서 true 값으로 취급되지만, 객체가 원시 값을 필요로 하는 경우에는 해당 객체에 valueOftoString 메소드를 호출해 반환받은 값을 원시 값으로 사용한다. 여기서 혼란이 생길 여지가 아주 크다. 자바스크립트 이자식...

따라서 == 연산자와 함께 쓰여 객체의 원시 값을 사용해야하는 경우에는 객체는 true 값을 반환하지 않는다.

앞서 언급한 바와 동일하게 Object.prototype.valueOf 메소드 오버라이딩하여 사용을 하는 경우가 있다면 반환 값이 달라지지만 그렇지 않는 경우를 기본이라 생각하고 toString 을 호출한다고 가정하겠다.

[] == 0;   // true
           // 이는 곧 [].toString() == 0 연산과 동일하다.

{} == 1;   // false
           // ({}).toString() == 1 연산과 동일하다.

좀 더 구체적인 예시를 위해 아래의 밈을 보자.

0 == [] === true
0 == '0' === true

라면

'0' == []

이 당연히true 값이어야 할 것 같지만, false가 나온다 (!!)

toString()을 호출하여 각 원시 값을 통해 간단히 그 이유를 알아보자면

[].toString();    // ''

빈 배열의 원시 값은 빈 문자열 즉, falsy 값이다.

0 == '';         // true
0 == [];         // true

그러므로 같은 falsy 값인 0 과 비교할 때 true를 반환한다.

또한, 빈 배열의 원시 값이'' 빈 문자열 이기 때문에 문자열 '0'와 비교하면 false가 반환되게 된다.

0 == [];     // true
[] == '0';   // false
'' == '0';   // false 

마지막으로 숫자 0 과 문자열 '0'은 둘 중 하나가 타입을 변환하여 연산되는데 무엇이 무슨 타입으로 바뀌는지는 기준을 잘 모르겠다. 아시는 분은 댓글 바랍니다..

0 == '0'  // true

여기까지 이해한다면 위에 첨부한 밈에서 별가의 입장이 될 수 있다.. 하핫

깜짝 퀴즈

({}).toString();  // "[object Object]"
{} == '[object Object]'  // true? false? 답을 유추해 보자

toString(), valueOf()

강제 형변환과 관련해 반드시 알고가야 하는 것은 앞서 언급했던 Object.prototype.toStringObject.prototype.valueOf 메소드들이다.

우선 자바스크립트에서 Object.prototype을 상속받은 객체라면 모두 아래와 같이 toString, valueOf 메소드를 사용할 수 있다.

const array = [];
array.toString();
array.valueOf();

const obj = {};
obj.toString();
obj.valueOf();

const date = new Date();
date.toString();
date.valueOf();
...

Object.prototype.toString()

toString 메서드는 문자열을 반환하는 object의 대표적인 방법이다.
MDN

위에서도 계속해서 사용했던 메소드로 문자열 즉, 원시 값을 반환해주는 메소드이다.

Object.prototype.valueOf()

JavaScript는 객체를 원시 값으로 변환할 때 valueOf 메서드를 호출합니다. 보통 원시 값을 필요로 할 때 알아서 사용하므로 직접 호출해야 하는 경우는 매우 드뭅니다.
기본적으로 Object의 모든 후손 객체는 valueOf를 상속받습니다. 내장된 핵심 객체는 모두 valueOf를 재정의override해 적합한 값을 반환합니다. 어떤 객체가 원시 값을 가지고 있지 않다면, valueOf는 객체 스스로를 반환합니다.
여러분의 코드에서 valueOf를 호출해 내장 객체를 원시 값으로 변환할 수 있습니다. 사용자 정의 객체를 만들 땐 valueOf를 재정의해 Object의 메서드 대신 다른 행동을 부여할 수도 있습니다.
MDN

실제로 사용해본 적도 없고 사용된 소스코드를 본 적도 없어서 조사하다가 도대체 뭐하는 친구지? 뺄까? 싶기도 했던 메소드였는데, 무슨 유레카도 아니고 샤워하다가 갑자기 그 필요성을 깨달아 버렸다. 이제와 MDN 다시 읽어보니 저기 다 쓰여져있다. 역시 아는 만큼 보이는 거고 MDN 만세다.

valueOf를 override하지 않고 기본 동작으로 사용한다면 단순히 객체 스스로를 반환한다.

해당 객체를 그대로 반환하는 것을 확인할 수 있다. 이는 원시 값이 아니다.

허나, 실제로 이렇게 사용할 일은 없고 사용해서도 안되지만 예시를 위해..Object.prototype.valueOf을 아래와 같이 재정의한다고 해보자.

Object.prototype.valueOf = () => 100;

당연히 반환 값도 변경된다.

아무튼 중요한 것은 valueOf override 코드에 따라 반환 값이 원시 값일 수도, 아닐 수도 있다는 것이다.

Object.prototype.valueOf = () => {
  // do something ...
};

호출 순서

문자열로 우선 변환되는 경우에는 toString > valueOf으로,
그 외의 경우에는 valueOf > toString의 순으로 호출된다.

여러가지를 테스트 해본 결과 alert을 제외하고는 문자열 우선 변환은 없었고 실제로 ECMAScript에서도 alert 외의 예시가 없었다. 테스트는 valueOf를 오버라이드 해둔 후 강제 변환시 어떤 값이 반환되는지로 확인했다. 문자열 우선 변환의 경우를 잘 알고 계시는 분은 댓글 남겨주세요..

따라서 valueOf 메소드가 오버라이드 되어있었다면 대부분의 경우 valueOf > toString순으로 호출되기 때문에 앞서 예시들의 반환 값이 완전히 달라질 수도 있다.

실제로 valueOf 오버라이드해 사용하는 경우는 많지 않겠지만 toString 이외에 다른 원시 값을 반환받아 사용해야하는 경우에 사용할 수 있다는 것을 알아두면 좋을 것 같다.

마무리

강제(암시적) 형변환은 각기 다른 타입의 값들을 비교, 산술, 문자열, 논리 연산자로 연산 시 발생하며, 이때 초기 데이터 타입과 관계없이 강제 형변환을 통해 3개의 원시 타입 boolean, number, string 중 하나로 변경시킨다.

각 연산자마다 변환되는 타입과 사용하는 값들이 다르고, 각 타입 우선순위에 따라 원시 값을 반환할 수 있는 두 메소드 valueOftoString의 호출 순서가 다르다.

참고


ECMAScript The Abstract Equality Comparison Algorithm
Type coercion
Type coercion in JavaScript
JavaScript type coercion explained
Javascript vs memes
MDN Object.prototype.valueOf()
MDN Object.prototype.toString()
코어 자바스크립트 - 형변환

profile
꿈꿀 수 있는 개발자가 되고 싶습니다

3개의 댓글

comment-user-thumbnail
2020년 12월 7일

스펀지밥 밈 너무 귀엽네요 ㅋㅋㅋㅋ 그리고 중간에 0 == '0' 이 비교 될때 형변환에 대해서 궁금해하신것 같은데 Number 와 string 타입이 비교될시 String 이 Number로 변환되고 비교되어서 true가 나오는걸로 알고있습니다 ! you don't know JS 타입과 문법 참조해보시면 좋을것 같아요 글 잘 보고 갑니다 :)

1개의 답글
comment-user-thumbnail
2022년 3월 24일

배열 내 숫자가 있는경우 (*)산술연산자의 결과떄문에 보게되었는데 이것 외에도 알고가는게 정말 많았습니다.
다른 부분을 읽던도중에
스크린샷 첨부가 안되어 글로만 남긴다면
{ hi: 'hi' } && '' 결과가 { hi: 'hi' }로 나온다고 작성되어있는데 "'로 나오더라고요! 저도 헷갈려서 찾아보니 &&연산자가 첫번쨰 falsy값을 찾는다고 하군요.
https://ko.javascript.info/logical-operators 여기서 읽어보고확인했습니다! 혹시 제가 잘못 이해한 내용이 있는지 알려주시면 감사하겠습니다.

답글 달기