[자바스크립트] 숫자로 강제변환할 때 일어나는 일들

자바스크립트에서 숫자가 아닌 값에서 숫자로 강제 변환을 할때 내부적으로 어떤 일이 있어나는지 깊게 살펴본다. 🧐

강제변환

우선 자바스크립트의 강제변환(type coercion)이라는 개념을 잠시 보자. 강제변환은 명시적 또는 암시적 규칙에 따라 한 데이터 유형에서 다른 데이터 유형으로 변경하는 것이다.

강제변환은 명시적인것과 암시적인 것, 두가지 유형이 있는데 어떤 형태로든 숫자로 강제변환을 하려고 할때 일어나는 일을 보려고 하는 것이 목적이므로 단순하고 명시적인 강제변환을 다뤄보기로 한다.

명시적 강제변환

Number 함수를 통해 명시적으로 숫자로 강제변환하는 예를 보자.

Number("3");

결과적으로 문자열 “3”이 숫자 3으로 변환된다. 매우 간한 것 같다. 하지만 실제로 내부적으로 처리하는 로직은 그리 간단치는 않다. 그 자세한 과정을 살펴보고자 한다. 그 전에 자바스크립트 명세의 추상연산자에 대해서 잠깐 보고 넘어가야 한다.

추상연산자

추상연산자(Abstract operation)은 ECMAScript 명세에서 정의한 함수 들이다. 언어의 일부는 아니지만 (엔진 내부에서 구현할 필요가 없으며 자바스크립트에서 직접 호출할 수 없다) 명세를 간결하게 작성하기 위해 정의되었다.

숫자로 강제변환하는 과정에 관여하는 추상연산자는 ToPrimitive, ToNumber, OrdinaryToPrimitive 이다. Number("3") 은 추상연산 ToNumber 로직이 작동된다고 보면 되는데 자세히 보면,

ToNumber(argument)

ToNumber추상연산은 argument의 타입이

  1. Undefined 일때: NaN을 리턴한다.
  2. Null 일때: 0을 리턴한다.
  3. Boolean 일때: true는 1, false는 0을 리턴한다.
  4. Number 일때: argument를 리턴한다.
  5. String 일때: 문법이 String을 StringNumericLiteral의 확장으로 해석 할 수없는 경우 NaN을 리턴하고 가능하다면 별도의 복잡한 문법을 통해 숫자로 변환하여 리턴한다.
  6. Symbol 일때: TypeError를 발생시킨다.
  7. BigInt 일때: TypeError를 발생시킨다.
  8. Object 일때: ToPrimitive 추상연산으로 원시값으로 변환한 후 ToNumber 추상연산을 다시 시도한다.

참고로 ToNumber 로직을 JavaScript 언어로 보기 쉽게 구현한 것을 여기에서 볼 수 있다.

ToPrimitive(input, hint)

어떤 형태든지 원시값(Number, String, Boolean, Null, Undefined 중 하나. 반대 되는 개념은 참조값.)으로 변환한다. 로직을 정리해보면,

  1. input 값의 타입이 object 이고, input값에 Symbol.toPrimitive 메소드가 있으면 : 해당메소드를 실행하여 결과가 object이면 TypeError를 발생시키고 아니면 그 결과를 리턴한다.
  2. input 값의 타입이 object이고 input 값에 Symbol.toPrimitive 메소드가 없으면: OrdinaryToPrimitive(input, hint)를 실행한다. 이때 hint 값은 별도의 세팅이 없는경우 number로 세팅된다.
  3. input 값의 타입이 object가 아니면 input을 리턴한다.

참고로 ToPrimitive 로직을 JavaScript 언어로 보기 쉽게 구현한 것을 여기에서 볼 수 있다.

OrdinaryToPrimitive(O, hint)

O에 내장된 toString 혹은 valueOf 메소드를 통해 타입이 Object인 O을 원시값으로 변환한다. 두 메소드를 통해 원시값으로 변환이 되지 않으면 TypeError 를 발생한다. 로직을 정리해보면,

  • hint가 string인 경우 : O.toString 이 호출가능하고 O.toString.call(O) 결과의 타입이 Obejct가 아니면(원시값이면) 그 결과를 리턴한다.
  • O.toString이 호출가능하지 않고 O.toString.call(O) 결과의 타입이 Object이면 그 다음으로 O.valueOf가 호출가능한지 확인하고 가능하면 O.valueOf.call(O)을 실행하여 결과의 타입이 Object가 아니면(원시값이면) 그 결과를 리턴한다.
  • O.valueOf 가 호출가능하지 않고 O.valueOf.call(O)의 결과역시 Object 타입이면(원시값이 아니므로) TypeError를 발생시킨다.
  • hint가 number인 경우: 위와 동일하고 순서만 반대(valueOf ->toString)이다.

참고로 OrdinaryToPrimitive로직을 JavaScript 언어로 보기 쉽게 구현한 것을 여기에서 볼 수 있다.

예제로 보기

다시 아래 예제로 돌아와보자. 결과는 무엇일까?

Number("3");

모두가 알고있듯이 결과는 숫자 3이다. 자세히 들여다 보자.

자바스크립트 내장 함수인 Number는 ToNumber(argument)추상연산의 로직이 작동된다고 보면 되므로 아래처럼 설명될 수 있다.

ToNumber(argument) 의 로직에서 argument의 타입이 string인 케이스에 해당되고 문자열 "3"은 명세의 StringNumericLiteral로서 별도의 문법을 통해 숫자 3으로 변환이 일어난다. (예컨대 특정 코드 범위(0xD800 to 0xDBFF)의 숫자 값들을 특별하게 처리한다. 이와 관련된 프로세스는 제가 자세히 다루기는 어려우므로 생략합니다. 명세를 참고 바랍니다.)

예제로 보기2

다른 예제를 한 번 보자.

Number([3]);

결과는 무엇일까?

정답은 숫자 3 이다. 결과는 같지만 사정은 좀 더 복잡하다. 자세히 들여다보자.

우선 ToNumber(argument)추상연산 로직에서 argument 타입이 object 인 케이스에 해당된다. 위에서 이렇게 설명했다.

ToPrimitive 추상연산으로 원시값으로 변환한 후 ToNumber 추상연산을 다시 시도한다.

  1. 따라서 ToPrimitive([3])가 실행된다. input 값은 [3] 이 되고 hint는 주어지지 않으면 default가 된다. [3]의 타입이 Object이므로
  2. OrdinaryToPrimitive 추상연산으로 넘겨진다. 이때 hint가 default이면 OrdinaryToPrimitive를 호출할때 hint는 number로 자동 세팅된다. 따라서 OrdinaryToPrimitive([3] , ‘number’) 이렇게 호출될 것이다.
  3. hint 가 number 이므로 배열 [3] 의 valueOf 메소드가 있는지 체크할 것이다. 배열의 자체 valueOf메소드는 없고 Object.prototype.valueOf()를 상속받는다. 이 메소드는 “원시 값을 가지고 있지 않다면, valueOf는 객체 스스로를 반환” 한다. 따라서 [3] 의 valueOf() 결과는 자기 자신([3])이다.
  4. valueOf 결과가 [3] 이고, 배열은 타입이 Object이다. 그러므로 다음 스텝은
  5. [3] 의 toString 메소드가 있는지 체크한다. 배열의 toString메소드가 웬말이냐고하겠지만 존재한다.( 자세한 것은 여기 참고 Array.prototype.toString()) 결과는 문자열 "3" 이 된다. "3" 이 Object 타입이 아니므로 더 이상 진행하지 않고 "3" 을 리턴한다.
  6. 아직 끝나지 않았다. 다시 ToNumber의 로직으로 넘어온다. ToPrimitive의 결과("3")를 다시 ToNumber('3') 추상연산을 실행한다. 이 과정은 Number("3") 의 설명과 동일하다. 즉 결과적으로 숫자 3 이 리턴된다.

예제로 보기3

예제를 하나만 더 보자.

Number([123, 234]);

결과는 무엇일까. 자세히 들여다보자.

예제2와 마찬가지로 ToNumber(argument)추상연산 로직에서 argument 타입이 object 인 케이스에 해당된다.

  1. 따라서 ToPrimitive([123, 234])가 실행된다. input 값은 [123, 234] 이 된고 hint는 주어지지 않으면 default가 된다. [123, 234]의 타입이 Object이므로
  2. OrdinaryToPrimitive 추상연산으로 넘겨진다. 이때 hint가 default이면 OrdinaryToPrimitive를 호출할때 hint는 number로 자동 세팅된다. 따라서 OrdinaryToPrimitive([123, 234] , ‘number’) 이렇게 호출될 것이다.
  3. hint 가 number 이므로 배열 [123, 234] 의 valueOf 메소드가 있는지 체크할 것이다. 배열의 자체 valueOf메소드는 없고 Object.prototype.valueOf()를 상속받는다. 이 메소드는 “원시 값을 가지고 있지 않다면, valueOf는 객체 스스로를 반환” 한다. 따라서 [123, 234] 의 valueOf() 결과는 자기 자신([123, 234])이다.
  4. valueOf 결과가 [123, 234] 이고, 배열은 타입이 Object이다. 그러므로 다음 스텝은
  5. [123, 234] 의 toString 메소드가 있는지 체크한다. 배열의 toString메소드가 웬말이냐고하겠지만 존재한다.( 자세한 것은 여기 참고 Array.prototype.toString()) 결과는 문자열 "123,234" 가 된다. "123,234"가 Object 타입이 아니므로 더 이상 진행하지 않고 "123,234" 을 리턴한다.
  6. 아직 끝나지 않았다. 다시 ToNumber의 로직으로 넘어온다. ToPrimitive의 결과("123,234")를 다시 ToNumber("123,234") 추상연산을 실행한다. ToNumber의 “String 일때” 케이스이다. "123,234"는 숫자로 변환가능한 문자열이 아니기 때문에 결과는 NaN이 리턴된다.

이 예제의 최종 결과는 NaN (Not A Number)이다. 이번 결과는 예상하기가 쉽지 않았다. 왜냐면 예제 2 처럼 뭔가 그럴듯한 결과가 나올 수 도 있을거라고 생각되기 때문이다. 자바스크립트의 강제변환은 이처럼 알아서 “그럴듯"하게 처리해서 코드가 간결해지거나 타입에 신경을 덜쓰고 핵심 로직에 집중할 수 있게 하는 장점이 분명이 존재한다. 하지만 동시에 예상할 수 없는 오류로 고생을 할 수도 있다.

"3" * 3; // 결과는 숫자 9

특히 이런 암시적 강제변환의 경우에 잘 알고 쓰면 약일 수 있지만 잘 모르고 쓰면 독이 될 수도 있다. 만약 자신이 없다면 오늘 들었던 예제들처럼 좀 더 명시적인 변환을 위주로 사용하길 권한다.

다음에 기회가 되면 암시적 강제변환의 예시들도 살펴보겠다.

이 글이 마음에 드셨다면 👏🏽👏🏽와 커피한잔 후원하기 (카카오페이),

이메일로 소식을 받아보고 싶으시다면 이메일 구독하기

Vue.js 관련 이야기들 | working at habitfactory.co | 커피한잔 후원하기 https://bit.ly/355PDlu | 이메일 구독하기 https://bit.ly/3ax32Fn

Vue.js 관련 이야기들 | working at habitfactory.co | 커피한잔 후원하기 https://bit.ly/355PDlu | 이메일 구독하기 https://bit.ly/3ax32Fn