너굴 개발 일지

[JavaScript] 타입 변환을 알아야 하는 이유 본문

JavaScript

[JavaScript] 타입 변환을 알아야 하는 이유

너굴냥 2023. 12. 24. 20:04

JavaScript 사용시 타입 변환이 왜 일어나는지 어떤 규칙에 의해 타입이 바뀌는지 공부한 글입니다.

목차

서론

본문

결론

 


서론

JavaScript를 사용하다보면 종종 의도대로 코드가 동작하지 않는 경우가 있다.

const sumNumber = (a, b) => {
  return a + b;
};
console.log(sumNumber(1, 2)); // 3
console.log(sumNumber(100)); // NaN

 

위 예시에서 함수명과 내부 코드를 보면 숫자 타입인 두 개의 인자를 더한 결과값을 반환하는 함수라고 유추되는데 의도대로 동작하지 않는다. 

원인은 타입 변환으로 인해 의도대로 동작하지 않는 문제가 발생한 것이다. 그러면 타입 변환이 무엇이며 왜 일어나는지 알아보자

 

타입 변환이란? 

자바스크립트의 모든 값에는 타입이 있으며 값의 타입은 변환할 수 있다. 이때 개발자가 의도적으로 값의 타입을 변환하는 명시적 타입 변환(또는 Type Casting), 표현식을 평가하는 도중에 JavaScript 엔진에 의해 암묵적으로 타입이 자동으로 변환되는 암묵적 타입 변환(또는 Type Coercion)이 존재한다.

명시적 타입 변환

const num = 10;

// 명시적 타입 변환
// 숫자를 문자열로 타입 캐스팅함
const str = num.toString();

// num 변수의 값이 변경되진 않음
console.log(typeof num, num); // number 10
console.log(typeof str, str); // string 10

암묵적 타입 변환

const num = 10;

// 암묵적 타입 변환
// 문자열 연결 연산자는 숫자 타입 num의 값을 바탕으로 새로운 문자열을 생성
const str = num + "";

// num 변수의 값이 변경되진 않음
console.log(typeof num, num); // number 10
console.log(typeof str, str); // string 10

명시적 타입 변환은 개발자가 의도한 것이지만 암묵적 타입 변환은 개발자 의도가 아닌 JavaScript 엔진에 의해 실행되기에 왜 굳이..? 라는 생각이 들었다.

 

암묵적 타입 변환이 왜 일어날까?

JavaScript는 동적 타입 언어로 런타임에 변수의 값이 할당될 때 해당 값의 타입에 따라 변수 타입이 결정된다.
따라서 다양한 유형의 값을 함께 사용하기 위해 암묵적 타입 변환을 수행해 에러 없이 평가한다.

그러나 유연한 계산을 할 수 있다는 장점보다는 단점이 많은 것 같다는 생각이 든다. 그러면 명시적 타입 변환만 사용하고 암묵적 타입 변환은 발생하지 않도록 코드를 작성하면 되지 않을까?

 

명시적 타입 변환만 사용하는 것이 좋은 방법 아닐까?

때로는 명시적 타입 변환보다 암묵적 타입 변환이 가독성 측면에서 더 좋을 수 있기에 100% 맞는 논리는 아니다.

// 10이라는 숫자 타입의 값을 문자열로 타입 변환하는 코드
(10).toString()
10 + '' // 더욱 간결하고 이해하기 쉬움

중요한 건 타입 변환이 어떻게 일어나며 어떤 결과가 나올 지 코드를 예측할 수 있어야 한다. 따라서 타입 변환이 어떻게 동작하는지 정확히 이해하고 사용해야 한다.

 

타입 변환은 어떻게 진행될까?

기본적으로 원시값은 변경 불가능한 값이므로 변경할 수 없다. 따라서 타입 변환은 기존 원시값을 사용해 다른 타입의 새로운 원시값을 생성한다.

위 예제의 경우, 암묵적 타입 변환에서는 x + “” 표현식 평가를 위해 num 변수의 숫자 값을 바탕으로 새로운 문자열 값 “10”을 생성하고 이것으로 표현식 “10” + “”를 평가한다. 이때 암묵적으로 생성된 문자열 "10"은 num 변수에 재할당되지 않는다.

이를 통해, JavaScript 엔진은 표현식을 에러 없이 평가하기 위해 피연산자의 값을 암묵적 타입 변환해 새로운 타입의 값을 만들어 단 한 번 사용하고 버리는 것을 알 수 있다.

 

타입 변환 규칙

숫자 표현식에서 숫자가 아닌 값

문자열

-, *, /, % 중 하나를 포함하는 숫자 표현식의 피연산자로 문자열을 전달할 때, 숫자의 암묵적 타입 변환 프로세스는 자바스크립트 내부에 내장된 Number 함수를 호출하는 것과 비슷하다. 숫자 문자만을 포함하는 문자열(Numeric Characters)을 가졌다면 어떤 문자열이라도 동등한 숫자로 바뀐다. 하지만 문자열에 숫자가 아닌 것(Non-Numeric Characters)이 포함되있다면 NaN을 반환한다.

3 * "3" // 3 * 3
3 * Number("3") // 3 * 3
Number("5") // 5

Number("1.") // 1
Number("1.34") // 1.34
Number("0") // 0
Number("012") // 12

Number("1,") // NaN
Number("1+1") // NaN
Number("1a") // NaN
Number("one") // NaN
Number("text") // NaN

+연산자

+연산자는 다른 수학 연산자와는 다르게 두 가지 기능을 수행한다.

1. 수학적 덧셈

2. 문자열 합치기

문자열이 +연산자의 피연산자로 주어졌을 때, JavaScript는 문자열을 숫자로 바꾸려하지 않고 숫자를 문자로 바꾸려 한다.

// concatenation
1 + "2" // "12"
1 + "js" // "1js"

// addition
1 + 2 // 3
1 + 2 + 1 // 4

//addition, then concatenation
1 + 2 + "1" // "31"
(1 + 2) + "1" // "31"

//concatenation all through
1 + "2" + 1 // "121"
(1 + "2") + 1 // "121"

객체 (Objects)

JavaScript에서 객체 암묵적 형변환은 대부분 [object Object]를 반환한다.

"name" + {} // "name[object Object]"

모든 JavaScript 객체는 toString 메소드를 상속받는다. 상속받은 toString 메소드는 객체가 문자열 타입으로 변환해야 할 때마다 쓰인다. toString 메소드의 반환 값은 문자열 합치기(string concatenation) 혹은 수학적 표현식(mathematical expressions)과 같은 연산에서 사용된다.

const foo = {}
foo.toString() // [object Object]

const baz = {
	toString: () => "I'm object baz"
}

baz + "!" // "I'm object baz!"

객체가 수학적 표현식에서 사용되는 경우, JavaScript는 반환 값이 숫자가 아니면 반환 값을 숫자로 변환하려 할 것이다.

const foo = {
	toString: () => 4
}

2 * foo // 8
2 / foo // 0.5
2 + foo // 6
"four" + foo // "four4"

const baz = {
	toString: () => "four"
}

2 * baz // NaN
2 + baz // 2four

const bar = {
	toString: () => "2"
}

2 + bar // "22"
2 * bar // 4

배열 객체

배열에서 상속된 toString 메소드는 약간 다르게 동작하며 이는 아무런 인자 없이 배열의 join 메소드를 호출한 것과 유사하다.

[1,2,3].toString() // "1,2,3"
[1,2,3].join() // "1,2,3"
[].toString() // ""
[].join() // ""

"me" + [1,2,3] // "me1,2,3"
4 + [1,2,3] // "41,2,3"
4 * [1,2,3] // NaN

따라서 배열을 어딘가로 넘길 때는 toString 메소드를 거치면 어떻게 되는지 생각해보자. 숫자로 변할지 문자열로 변할지 말이다.

4 * [] // 0
4 / [2] // 2

// similar to 
4 * Number([].toString())
4 * Number("")
4 * 0

4 / Number([2].toString())
4 / Number("2")
4 / 2

True, False 그리고 ""

Number(true) // 1
Number(false) // 0
Number("") // 0

4 + true // 5
3 * false // 0
3 * "" // 0
3 + "" // "3"

valeof method

문자열이나 숫자가 올 곳에 객체를 넘길 때마다 JavaScript 엔진에 의해 사용될 valueOf 메소드를 정의하는 것도 가능하다.

const foo = {
  valueOf: () => 3
}

3 + foo // 6
3 * foo // 9

객체에 toString과 valueOf 메소드가 전부 정의되어 있을 때 JavaScript 엔진은 valueOf 메소드를 사용한다.

const bar = {
  toString: () => 2,
  valueOf: () => 5
}

"sa" + bar // "sa5"
3 * bar // 15
2 + bar // 7

참고로 valueOf 메소드는 객체를 원시 값으로 변환할 때 사용하기 위해 만들어졌다.

const two = new Number(2)
two.valueOf() // 2

Falsy and Truthy

모든 JavaScript 값은 true나 false로 변환될 수 있다. 이때 true로 형변환하는 것을 truthy, false로 형변환하는 것을 falsy라고 부른다.

다음은 JavaScript에서 반환시에 falsy로 취급되는 값들이다. 

false
0
null
undefined
""
NaN
-0

이외에는 전부 trythy로 취급된다.

if (-1) // truthy
if ("0") // truthy
if ({}) // truthy

위 코드처럼 truthy를 이용해도 괜찮지만 값이 참임을 명시적으로 표현해주는 것이 더 좋은 작성법이다. 명심해야 할 것은 만일 JavaScript의 묵시적 형변환을 완벽히 이해하더라도, JavaScript의 묵시적 형변환에 의존하지 말아야 한다.

아래 코드 대신에

const counter = 2

if (counter)

다음 코드가 훨씬 좋은 코드이다.

if (counter === 2)

//or

if (typeof counter === "number")

이유는 아래 예시를 참고하면 된다.

아래 함수는 number 타입의 변수를 받아 number 타입이 아니라면 에러를 발생하는 함수다.

const add = (number) => {
  if (!number) new Error("Only accepts arguments of type: number")
  //your code
}

하지만 인자 값으로 0을 전달하면 의도치 않은 에러가 발생한다.

add(0) // Error: Only accepts arguments of type: number

//better check

const add = (number) => {
  if (typeof number !== "number") new Error("Only accepts arguments of type: number")
  //your code
}

add(0) // no error

NaN

NaN은 자기 자신과도 같지 않은 특별한 숫자 값이다.

NaN === NaN // false

const notANumber = 3 * "a" // NaN

notANumber == notANumber // false

NaN은 JavaScript에서 유일하게 자기 자신과 같지 않은 값이다. 따라서 NaN을 확인하려면 자신과 비교하면 된다.

if (notANumber !== notANumber) // true

ECMAScript 6는 NaN을 체크하기 위한 메소드(Number.isNaN)을 만들었다.

Number.isNaN(NaN) // true
Number.isNaN("name") // false

전역 isNaN 함수에 대해서도 알아둬야 한다. 이 함수는 인자가 실제로 NaN인지 체크하기 전에 인자로 들어온 값을 강제로 형변환한다.

isNaN("name") // true
isNaN("1") // false

따라서 전역 isNaN 함수는 사용하지 않는 것이 좋다. 이 함수의 동작은 아래 코드와 비슷하다고 생각하면 된다.

const coerceThenCheckNaN = (val) => {
	const coercedVal = Number(val)
    return coercedVal !== coercedVal ? true : false
}

coerceThenCheckNaN("1a") // true
coerceThenCheckNaN("1") // false
coerceThenCheckNaN("as") // true
coerceThenCheckNaN(NaN) // true
coerceThenCheckNaN(10) // false

 

결론

타입 변환이란 값의 타입을 변환하는 것을 의미하며 명시적 타입 변환과 암묵적 타입 변환으로 나뉜다.

JavaScript 엔진에 의해 암묵적으로 타입이 자동으로 변환되는 암묵적 타입 변환으로 인해 다양한 유형의 값과 함께 에러 없이 표현식을 평가할 수 있다. 

암묵적 타입 변환은 피할 없기에 타입 변환이 어떻게 동작하며 어떤 결과가 나올지 예측할 있어야 한다.


참고 자료

https://ko.javascript.info/type-conversions

 

형 변환

 

ko.javascript.info

https://dev.to/promisetochi/what-you-need-to-know-about-javascripts-implicit-coercion-e23

 

What you need to know about Javascript's Implicit Coercion

Common Javascript implicit coercion gotchas, how it works, what should be avoided and why

dev.to