부동소수점
실수를 부호·지수·가수로 나눠 이진수로 저장하는 방식으로, 유한한 비트로 무한한 실수를 표현하기 때문에 필연적으로 오차가 생깁니다.
자바스크립트의 숫자 표현
자바스크립트에는 정수 타입이 따로 없고, Number 타입 하나가 IEEE 754 표준의 64비트 배정밀도 부동소수점을 사용합니다.
| 구성 | 비트 수 | 역할 |
|---|---|---|
| 부호(sign) | 1 | 양수/음수 |
| 지수(exponent) | 11 | 소수점 위치 |
| 가수(mantissa) | 52 | 유효 숫자 |
가수가 52비트로 고정되어 있으므로, 이진수로 딱 떨어지지 않는 소수는 근사값으로 잘려서 저장됩니다.
왜 0.1 + 0.2 !== 0.3 인가
10진수 0.1과 0.2를 2진수로 바꾸면 0.0001100110011...처럼 순환소수가 됩니다. 2진수로 유한하게 표현할 수 있는 소수는 분모가 2의 거듭제곱인 값(0.5, 0.25, 0.125 …)뿐이기 때문입니다.
저장 시점에 이미 근사값이 되고, 두 근사값을 더하면 오차가 누적됩니다.
console.log(0.1 + 0.2); // 0.30000000000000004
console.log(0.1 + 0.2 === 0.3); // false
console.log((0.1).toFixed(20)); // "0.10000000000000000555"
===는 두 값을 있는 그대로 비교하므로 0.30000000000000004와 0.3은 다른 값으로 판정됩니다.
오차를 다루는 방법
1. 자릿수를 정해 반올림
(0.1 + 0.2).toFixed(2); // "0.30" ← 문자열로 변환됨
+(0.1 + 0.2).toFixed(2); // 0.3
가장 단순하지만 결과가 문자열이 되므로 다시 숫자로 변환해야 합니다.
2. 허용 오차(엡실론)로 비교
Number.EPSILON은 1과, 1보다 큰 수 중 표현 가능한 가장 작은 수의 차이(약 2.22e-16)입니다. 두 값의 차이가 이보다 작으면 같다고 간주합니다.
function isEqual(a, b) {
return Math.abs(a - b) < Number.EPSILON;
}
console.log(isEqual(0.1 + 0.2, 0.3)); // true
다만 값이 커질수록 표현 간격도 커지므로, 큰 수를 비교할 때는 상대 오차를 써야 합니다.
function isEqual(a, b) {
return Math.abs(a - b) <= Number.EPSILON * Math.max(Math.abs(a), Math.abs(b), 1);
}
3. 정수로 바꿔 계산
금액처럼 오차가 허용되지 않는 값은 최소 단위(원, 센트)의 정수로 바꿔 다루거나, BigInt 혹은 decimal.js 같은 십진 연산 라이브러리를 사용합니다.
(0.1 * 100 + 0.2 * 100) / 100; // 0.30000000000000004 → 곱셈에서도 오차가 남는다
Math.round(0.1 * 100 + 0.2 * 100) / 100; // 0.3
주의사항
- 정수도 안전하지 않습니다.
Number.MAX_SAFE_INTEGER(2^53 - 1)를 넘으면 정수조차 정확히 표현되지 않습니다.
Number.MAX_SAFE_INTEGER + 1 === Number.MAX_SAFE_INTEGER + 2; // true
- 서버에서 받은 큰 ID 값(Long)을 그대로 숫자로 파싱하면 정밀도가 깨질 수 있어 문자열이나
BigInt로 다루는 편이 안전합니다. - 부동소수점은 자바스크립트만의 문제가 아니라 IEEE 754를 쓰는 대부분의 언어(Java의
double, C의double등)에 동일하게 존재합니다.
핵심 정리
- 자바스크립트의 모든 숫자는 IEEE 754 64비트 부동소수점이며, 2진수로 떨어지지 않는 소수는 근사값으로 저장됩니다.
0.1 + 0.2는0.30000000000000004가 되므로===비교는false입니다.- 비교는
Number.EPSILON기반 허용 오차로, 정확한 계산은 정수 변환이나 십진 라이브러리로 처리합니다.