큰 정수가 정밀도를 잃는 이유
핵심 요약: 2^53-1을 넘는 정수는 JSON.parse에서 정밀도를 잃습니다. 스노우플레이크 ID, 주문 번호, 데이터베이스 bigint 모두 여기 부딪히므로 문자열로 끝까지 유지하세요.

핵심부터 말하면, 스노우플레이크 ID, 주문 번호, bigint 기본 키를 직렬화, 전송, 파싱 전 과정에서 문자열로 취급하세요. 필드가 2^53-1 안에 머묾이 증명될 때만 숫자 타입을 유지하고, 그때는 Schema의 maximum 제약으로 강제하세요.
손상이 파싱 중에 조용히 일어나기 때문입니다.
JSON.parse('{"id": 9007199254740993}');
// { id: 9007199254740992 }
빠진 자릿수는 사라졌고 복구할 것이 없습니다.
2^53은 어디서 오나
JavaScript에는 숫자 타입이 하나입니다. IEEE 754 배정밀도, 유효 비트 53개(암묵 1, 명시 52). 따라서 정확한 정수는 ±(2^53-1)에서 멈춥니다:
-9007199254740991 .. 9007199254740991
그 선을 넘으면 연속 표현 가능 값 간격이 2, 4, 8입니다. 9007199254740993은 홀수이고 그 크기에서 모든 표현 가능 값이 짝수라 9007199254740992로 반올림됩니다.
피해를 받는 필드
| 필드 | 자릿수 | 위험 |
|---|---|---|
| 스노우플레이크 ID | 18~19 | 높음, 마지막 자릿수 변경 |
| 주문 / 거래 번호 | 16~20 | 높음 |
| 데이터베이스 bigint 기본 키 | 19 | 높음 |
| 최소 단위 돈 | — | 낮음 |
| 밀리초 타임스탬프 | 13 | 안전 |
공통점: 식별자이지 수량이 아닙니다. 식별자는 전송될 뿐 더하거나 곱하지 않으므로 문자열이 올바른 타입입니다.
네 가지 수정
문자열로 직렬화. 백엔드가 응답을 쓰기 전에 bigint 필드를 변환합니다. 간단하고 지속적이며, 이 필드가 문자열임을 문서화하세요.
직렬화기에 맡기기. Jackson의 Long용 ToStringSerializer, Go의 json:",string" 태그. 같은 발상으로 값을 숫자 경로 밖에 둡니다.
BigInt 지원으로 파싱. json-bigint 같은 라이브러리. BigInt는 Number 산술과 섞을 수 없고 JSON.stringify가 거부한다는 점에 주의하세요.
범위 제약. 필드가 진짜로 2^53을 넘지 못한다면 Schema에 maximum을 더해 범위를 벗어난 데이터가 브라우저에서 조용히 실패하는 대신 검증에서 실패하게 하세요.
이 버그가 있는지 판별하는 방법
알려진 ID를 왕복시키세요. 직렬화, 전송, 파싱, 문자별 비교. 또는 원본 응답 본문을 검사하세요. 따옴표 없는 19자리 숫자가 마지막 자릿수를 잘못 달고 도착하면 거의 확실히 파싱 시 손실입니다.
FAQ
JSON.parse가 왜 오류 없이 마지막 자릿수를 바꾸나?
JavaScript에는 숫자 타입이 하나, IEEE 754 배정밀도라 2^53-1을 넘는 정수는 가장 가까운 표현 가능 값에만 떨어질 수 있습니다. JSON.parse는 이를 정상 숫자 변환으로 취급해 던지지도 경고하지도 않습니다.
프런트엔드가 큰 정수를 온전히 유지하려면?
json-bigint 같은 라이브러리를 쓰세요. JSON.parse는 못 합니다. 비용은 BigInt가 Number 산술과 섞이지 않고 JSON.stringify가 거부한다는 것이며, 주변 코드도 바뀌어야 합니다.
API 문서에 이 필드가 문자열이라고 명시해야 하나?
예. 문자열 직렬화는 가장 간단하고 지속적인 수정이지만 소비자가 값을 텍스트로 취급해야만 동작하므로 필드 타입을 API 계약에 적어야 합니다.