Menu

자바스크립트 Number와 BigInt: 부동소수점과 큰 정수 다루기

자바스크립트 Number 타입의 실체와 부동소수점 오차, MAX_SAFE_INTEGER 한계, 그리고 BigInt를 써야 할 타이밍까지 정리했습니다.

숫자 타입은 하나로 통일 (거의)

대부분의 언어는 정수와 실수를 별도 타입으로 나눠서 제공합니다. 반면 자바스크립트는 역사적으로 Number 하나로 모든 걸 처리해 왔습니다. 423.14-0.001이든, 결국 같은 원시 타입 — 64비트 IEEE 754 배정밀도 부동소수점 — 으로 취급됩니다.

index.js
Output
Click Run to see the output here.

형 변환도 필요 없고, 2^31에서 오버플로가 나지도 않으니 편리하죠. 하지만 부동소수점 표현 방식에는 대가가 따르고, 초보자들이 여기에 매번 발목을 잡힙니다. 그래서 2020년에 Number만으로는 부족한 경우를 위해 두 번째 숫자 타입인 BigInt가 추가됐습니다.

0.1 + 0.2가 0.3이 아닌 이유

이 코드를 실행해 보세요:

index.js
Output
Click Run to see the output here.

첫 번째 줄은 0.30000000000000004를, 두 번째 줄은 false를 출력합니다. 이건 자바스크립트만의 이상한 동작이 아닙니다. 파이썬, 자바, C처럼 IEEE 754 부동소수점을 사용하는 모든 언어에서 똑같이 일어납니다.

이유는 이렇습니다. 0.10.2는 이진수로 정확하게 표현할 수 없어요. 마치 1/3을 10진수로 정확히 적을 수 없는 것과 같은 원리죠. 가장 가까운 이진 근삿값이 저장되고, 그 과정에서 생긴 미세한 오차가 계속 쌓입니다. 그러니 소수점이 들어간 Number 값은 적어둔 값과 아주 비슷한 근삿값 이라고 생각하는 편이 안전합니다.

돈을 다룰 때는 $19.9919.99로 저장하지 마세요. 센트 단위 정수인 1999로 저장하고, 화면에 보여줄 때만 포매팅하세요. 부동소수점 오차 버그를 피하는 가장 확실한 습관입니다.

부동소수점 값 안전하게 비교하기

등호 비교(===)는 믿을 수 없으니, 필요할 때는 허용 오차(tolerance)를 두고 비교합니다:

index.js
Output
Click Run to see the output here.

Number.EPSILON은 1과 그 다음으로 표현 가능한 숫자 사이의 가장 작은 차이를 뜻합니다. 값이 1 근처일 때 기본 오차 허용치로 쓰기에 적당하죠. 반면 값이 아주 크거나 아주 작을 때는, 입력값의 크기에 맞춰 스케일이 조정되는 허용치가 필요합니다.

안전한 정수 범위 (MAX_SAFE_INTEGER)

일정 크기까지의 정수는 64비트 부동소수점에서 정확히 표현됩니다. 하지만 그 범위를 넘어가는 순간부터 한 비트씩 정밀도가 깎여 나가기 시작합니다:

index.js
Output
Click Run to see the output here.

2^53 - 1은 그 아래의 모든 정수가 빠짐없이 표현되는 마지막 정수입니다. 그 이상으로 넘어가면 일부 정수는 Number 타입에 아예 존재하지 않고, 가장 가까운 이웃 값으로 반올림돼 버리죠. 데이터베이스에서 가져온 64비트 ID를 JSON 숫자로 파싱하는 경우라면, 이게 바로 소리 없이 데이터를 오염시키는 버그의 씨앗이 됩니다.

BigInt 등장

BigInt는 임의 정밀도 정수를 위한 별도의 원시 타입입니다. 정수 리터럴 뒤에 n을 붙이거나, BigInt(...)를 호출해서 만들 수 있습니다:

index.js
Output
Click Run to see the output here.

BigInt은 사용 가능한 메모리 외에는 상한이 없습니다. 다음과 같은 상황에 적합한 도구죠:

  • 2^53을 넘어가는 데이터베이스 ID나 트위터/X의 스노우플레이크 ID
  • 암호학 관련 연산
  • 속도보다 정확한 결과가 더 중요한 모든 정수 연산

반대로 일상적인 카운터, 배열 인덱스, 돈을 센트 단위로 다루는 경우에는 적합하지 않습니다. 이런 용도에는 일반 Number가 훨씬 빠르고, 언어의 모든 API와 자연스럽게 호환됩니다.

BigInt 연산

양쪽 피연산자가 모두 BigInt라면 익숙한 연산자들이 그대로 동작합니다:

index.js
Output
Click Run to see the output here.

나눗셈은 0 방향으로 버림 처리됩니다. BigInt는 소수점을 지원하지 않기 때문이죠. 소수가 필요하다면 다시 Number로 돌아가거나, 별도의 decimal 라이브러리를 써야 합니다.

타입을 섞어 쓰지 마세요

많이들 걸려 넘어지는 규칙이 하나 있습니다. 바로 NumberBigInt를 같은 식 안에서 섞어 쓸 수 없다는 점입니다.

index.js
Output
Click Run to see the output here.

비교 연산은 예외입니다. <, >, ==는 두 타입 간 자동 변환을 허용합니다:

index.js
Output
Click Run to see the output here.

그래서 ==는 같다고 보지만, ===는 다르다고 판단합니다. 이미 어디서나 ===를 쓰고 있다면(당연히 그래야 하죠), 서로 다른 숫자 타입끼리 비교하는 코드는 설계상 문제 신호로 봐야 합니다. 한쪽으로 타입을 통일하고 변환해서 쓰세요.

서로 변환하기

변환 방법은 두 가지인데, 각각 함정이 하나씩 있습니다:

index.js
Output
Click Run to see the output here.

Number → BigInt 변환은 꽤 깐깐합니다. 소수점이 있거나 NaN이면 바로 에러가 납니다. 반대로 BigInt → Number는 관대하지만 값이 손실됩니다. MAX_SAFE_INTEGER를 넘어가는 값은 반올림되어 버리죠. 서버에서 받아온 BigInt를 변환할 일이 생긴다면, 정말로 변환이 필요한지 한 번 더 따져 보세요.

특수한 숫자 값들

이야기가 나온 김에, Number 타입에는 수학적으로는 '숫자'라고 부르기 애매한 세 가지 값이 있습니다.

index.js
Output
Click Run to see the output here.

Infinity-Infinity는 0으로 나누거나 부동소수점 범위를 초과할 때 등장합니다. 반면 NaN("not a number")은 산술 연산의 결과가 의미 있는 값으로 나오지 않을 때 생기죠.

NaN은 자기 자신과도 같지 않다는 걸로 유명한데, 이건 JS 버그가 아니라 IEEE 754 표준에 정의된 동작입니다. 값을 검사할 때는 Number.isNaN(x)를 쓰세요. 전역 함수인 예전 isNaN은 인자를 먼저 숫자로 강제 변환하기 때문에 엉뚱한 결과를 냅니다(isNaN("hello")true를 반환합니다). 항상 Number.isNaN 쪽을 쓰는 게 안전합니다.

문자열을 숫자로 변환하는 방법

사용자 입력이나 JSON 데이터에 담긴 숫자는 문자열 형태로 들어오는 경우가 많습니다. 변환하는 방법은 크게 세 가지가 있습니다:

index.js
Output
Click Run to see the output here.

Number()는 엄격한 편입니다. 숫자가 아닌 값은 모두 NaN이 되는데, 빈 문자열이나 공백만 있는 문자열은 예외적으로 0을 반환합니다. 반면 parseIntparseFloat는 관대합니다. 읽을 수 있는 데까지 읽고 나머지는 그냥 무시해 버리죠. 의도에 맞는 쪽을 골라 쓰되, 결과를 사용하기 전에 반드시 NaN인지 확인하세요.

문자열을 BigInt로 변환할 때는 BigInt("123")를 쓰면 됩니다. 엄격한 방식이라 잘못된 입력이 들어오면 예외를 던집니다.

한눈에 보는 규칙

  • 카운터, 연산, 좌표처럼 일상적으로 쓰는 숫자: Number를 쓰세요.
  • 돈 계산: 센트 단위 정수로 바꿔서 Number로 다루거나, decimal 전용 라이브러리를 쓰세요.
  • 2^53보다 큰 정수(데이터베이스 ID, 암호학, 조합론 등): n 접미사를 붙여 BigInt로 처리하세요.
  • 부동소수점 비교는 ===가 아니라 허용 오차(tolerance)를 두고 비교하세요.
  • 잘못된 결과인지 확인할 때는 전역 함수 대신 Number.isNaNNumber.isFinite를 사용하세요.
  • NumberBigInt를 같은 식에서 섞어 쓰지 마세요. 필요하면 명시적으로 변환하세요.

다음 주제: null vs undefined

자바스크립트에는 "값이 없다"를 표현하는 방법이 두 가지 있습니다. 바로 nullundefined인데, 서로 바꿔 쓸 수 있는 게 아닙니다. 다음 글에서는 각각이 무슨 뜻인지, 어떻게 다른지, 그리고 언제 어느 쪽을 써야 하는지 살펴보겠습니다.

자주 묻는 질문

자바스크립트에서 0.1 + 0.2는 왜 0.3이 안 나오나요?

자바스크립트의 Number는 64비트 IEEE 754 부동소수점이기 때문인데요, 0.1과 0.2는 2진수로 정확히 표현이 안 됩니다. 그래서 결과가 0.30000000000000004로 찍히는 거죠. 자바스크립트 버그가 아니라 파이썬, 자바 등 같은 포맷을 쓰는 언어라면 전부 겪는 현상입니다. 돈 계산처럼 정밀도가 중요하면 정수(원 단위 대신 전 단위)로 환산해서 쓰거나 decimal 라이브러리를 쓰세요.

BigInt는 뭐고 언제 써야 하나요?

BigIntNumber.MAX_SAFE_INTEGER(2^53 - 1)를 넘어가는 정수를 다루기 위한 별도의 원시 타입입니다. 숫자 뒤에 n을 붙여서 9007199254740993n처럼 쓰거나 BigInt(value)로 만들면 돼요. 64비트 DB ID, 암호화 연산처럼 속도보다 정밀도가 중요한 정수 연산에 쓰면 됩니다.

Number랑 BigInt를 섞어서 쓸 수 있나요?

안 됩니다. 1n + 1을 실행하면 TypeError: Cannot mix BigInt and other types 에러가 나요. BigInt(n) 또는 Number(b)로 명시적으로 변환해야 합니다. <== 같은 비교 연산자는 두 타입 사이에서도 동작하지만, ===는 타입이 다르기 때문에 항상 false를 반환합니다.

Coddy로 코딩 배우기

시작하기