0.1 + 0.2가 0.3이 아니어서 결제 금액이 1원씩 어긋난 이유
문제 발생
장바구니 합계가 가끔 1원씩 어긋났고, 검증 코드가 통과하지 못했습니다.
0.1 + 0.2; // 0.30000000000000004
0.1 + 0.2 === 0.3; // false원인 분석
JavaScript의 숫자는 배정밀도 이진 부동소수점입니다. 가수부가 52비트뿐이라 십진 소수를 이진 분수로 정확히 표현할 수 없는 경우가 생깁니다. 0.1은 그 대표적인 예로, 애초에 저장된 값이 정확히 0.1이 아닙니다.
toFixed(2) 같은 표시용 반올림이 이 오차를 가려 주기 때문에 화면에서는 잘 드러나지 않습니다. 문제는 비교와 누적에서 나타납니다 — 항목이 많아질수록 오차가 쌓입니다.
Number.EPSILON은 1과 그보다 큰 가장 작은 부동소수점 수의 차이(2⁻⁵² ≈ 2.22 × 10⁻¹⁶)입니다. 비교의 허용 오차로 쓸 수 있지만, MDN이 주의를 덧붙입니다 — EPSILON은 크기가 1 근처인 수에 적합하고, 큰 수를 비교할 때는 크기에 비례해 허용 오차를 키워야 합니다.
해결 방안
- 돈은 정수 단위로 다룹니다. 가장 확실한 해결입니다. 원 단위 통화라면 정수 그대로, 소수점이 있는 통화라면 최소 단위(센트)로 계산하고 표시할 때만 나눕니다.
const totalCents = items.reduce((sum, item) => sum + item.priceCents, 0);
const display = new Intl.NumberFormat("ko-KR", { style: "currency", currency: "KRW" })
.format(totalCents / 100);- 비교가 필요하면 허용 오차를 씁니다. 크기에 맞춰 조정합니다.
const equal = (x, y, tolerance = Number.EPSILON) => Math.abs(x - y) < tolerance;
equal(0.1 + 0.2, 0.3); // true
equal(1000.1 + 1000.2, 2000.3, 2000 * Number.EPSILON); // true- 안전한 정수 범위를 넘어가면
BigInt를 씁니다.Number.MAX_SAFE_INTEGER(2⁵³ − 1)를 넘는 정수는 더 이상 정확하지 않습니다. 큰 id를 다룰 때 주의할 부분입니다. - DB 쪽도 함께 맞춥니다. 금액 컬럼을
FLOAT/DOUBLE로 두면 같은 문제가 저장 계층에서 생깁니다.DECIMAL이나 정수 컬럼을 씁니다. - 반올림 시점을 한 곳으로 모읍니다. 중간 계산마다 반올림하면 오차가 다른 방식으로 쌓입니다. 계산은 최소 단위로 끝까지 하고, 표시 직전에 한 번만 포맷합니다.
댓글0
댓글을 남기려면 로그인이 필요해요. 로그인
아직 댓글이 없어요. 첫 의견을 편하게 남겨 보세요.