[JS] 자바스크립트의 숫자

자바스크립트는 다른 언어와 다른 점이 많다. 그 중 숫자도 한 자리를 차지하고 있다.
이번에 팀 서비스 예상 금액을 계산하는 알고리즘을 구현하게 됐는데, 이때 자바스크립트의 숫자가 피곤하게 했다.
뜬금없이 특정 연산에서 100000000.0과 같은 소수점이 붙게 되었고, 이를 계기로 자바스크립트의 숫자에 대해 다시 한번 공부하고 정리해본다.

Number

하나의 숫자 타입

자바스크립트의 숫자 타입은 number가 유일하다. 그리고 number 타입은 정수와 소수 모두를 포함한다.

1
2
3
4
typeof(1);
// "number"
typeof(1.1);
// "number"

IEEE 754

자바스크립트는 IEEE 754 표준을 따른다. 그리고 배 정도(Double precision) 표준 포맷을 사용한다. 이는 C 기준으로 double과 대응된다.


숫자 구문

지수형

아주 크거나 아주 작은 숫자는 지수형으로 표시된다.

1
2
3
4
5
6
7
8
9
const a = 1E10
a;
// 10000000000

1 / a;
// 1e-10

a.toExponential();
// "1e+10"

toExponential() 메서드로 지수형을 나타낼 수 있다. 결과 타입은 string인 걸 주의하자.

1
2
a.toExponential();
// "1e+10"

숫자 리터럴 bin, oct, hex

숫자 리터럴은 다른 진법으로도 나타낼 수 있다.

1
2
3
0xf3;           // 243의 16진수
0o363;           // 8 진수
0b11110011;     // 243의 이진수

소수 값

이진 부동 소수점의 부작용

자바스크립트의 부동소수점 연산을 할 때 주의할 점이 있다.

1
2
0.1 + 0.2 === 0.3;
// false

일반적으로 알고 있는 산수 상식과 다른 결과를 보여준다. (이 문제는 파이썬에서도 있다.)
왜 이런 결과가 나올까? 0.1 + 0.2 연산의 결과는 0.30000000000000004이기 때문이다.
(이유는 ‘모든 자바스크립트 개발자가 알아야하는 부동소수점’ 글의 설명이 매우 잘되어 있다)

1
2
0.1 + 0.2
// 0.30000000000000004

그럼 이 문제를 어떻게 해결할까?

Machine Epsilon

컴퓨터에서 실수는 연속적으로 표현할 수 없다는 한계가 있다. 따라서 수와 수 사이에는 어떤 미세한 간격이 있는데, 이를 머신 입실론이라 한다.
예를 들어 무한히 긴 0.33333333333….과 같은 실수의 마지막은 값은 일반적으로 2나 4가 된다. 이처럼 컴퓨터가 다룰 수 있는 가장 작은 수를 나타낸다.

그리고 자바스크립트에서 머신 입실론을 통해 미세한 반올림 오차를 허용 공차로 처리할 수 있다.

자바스크립트에서 Number.EPSILONE을 통해 머신 입실론 값을 확인할 수 있다.

1
2
Number.EPSILON;
// 2.220446049250313e-16

이 머신 입실론 값을 사용하여 반올림 허용 오차 이내의 ‘같음’을 비교할 수 있다.

1
2
3
4
5
6
function equal(n1, n2) {
    return Math.abs(n1 - n2) < Number.EPSILON;
}

equal(0.1 + 0.2, 0.3);
// true

특수 숫자

NaN

NaN(Not A Number)은 그대로 해석하면 알 수 있듯이 숫자 아님이다. 하지만 이 표현은 올바르지 않다.

1
2
3
4
5
6
const a = 1 / 'a';
a;
// NaN

typeof(a);
// "number"

위 예제에서 보면 NaN의 타입은 number라는 걸 알 수 있다. 이 상황을 그대로 해석하면 ‘숫자가 아닌 숫자 타입’이다.

문제는 NaN을 체크할 때도 발생한다.

1
2
3
4
5
6
7
const a = 1 / 'a';
a === NaN;
// false
a == NaN;
// false
NaN === NaN;
// false

NaN은 비교연산자를 통해 비교할 수 없다. 자기 자신과도 다른걸 보면 알 수 있다. 이를 해결하기 위해 isNaN() 메소드를 지원한다.

1
2
3
4
5
const a = 1 / 'a';
isNaN(a);
// true
isNaN('foo');
// true

근데 이 isNaN은 의미 그대로 ‘숫자가 아닌지’를 확인한다. 이런 버그(?)는 예상하지 못한 문제를 일으킬 수 있다. 그래서 ES6부터는 Number.isNaN()이 해결해준다.

1
2
3
4
5
const a = 1 / 'a';
Number.isNaN(a);
// true
Number.isNaN('foo');
// false

Infinity

전통적인 컴파일 언어에서는 0으로 나누기할 경우 컴파일/런타임 에러를 발생시킨다. 하지만 자바스크립트는 특별하다.

1
2
1 / 0;
// Infinity

위 예제 코드에서 Infinity(Number.POSITIVE_INFINITY)를 출력하는 걸 확인할 수 있다. 만약 분자가 음수일 경우 -Infinity(Number.NEGATIVE_INFINITY)가 출력된다.

0, -0

자바스크립트에서는 -0이 존재한다. (특정 브라우저에서는 0으로 출력된다)

1
2
0 / -3;
// -0

근데 이를 문자열화하면 항상 “0”을 출력한다.

1
2
3
4
5
6
(-0).toString();
// "0"
String(-0);
// "0"
JSON.stringify(-0);
// "0"

근데 반대로 숫자로 변경할 경우 -0을 출력한다.

1
2
3
4
5
6
+"-0";
// -0
Number("-0");
// -0
JSON.parse("-0");
// -0

근데 웃긴건 두 값은 같다.

1
2
-0 === 0;
// true

이런 값을 만든 이유는 값의 크기로 어떤 정보와 그 값의 부호로 또 다른 정보를 동시에 나타내야 하는 애플리케이션이 있기 때문이라고 한다.

만약 +0, -0 개념이 없다면 어떤 변숫값이 0에 도달하여 부호가 바뀌는 순간, 그 직전까지 이 변수의 이동 방향을 알 수 없다. 즉, 정보 소실을 방지하기 위해 0의 부호를 보존한 것이다.

Reference