본문으로 건너뛰기
ZEKILO Dev
포맷

JSON 큰 숫자가 바뀌는 이유 — 2⁵³과 BigInt

2026.10.03 업데이트 · 5분 읽기

JSON의 큰 정수 ID가 JavaScript에서 끝자리가 바뀌는 이유를 IEEE 754 배정밀도와 2⁵³ 한계로 설명하고, ID를 문자열로 주고받거나 BigInt로 읽어 값을 지키는 방법을 정리했습니다.

JSON 포맷터·검증 바로 사용하기

서버가 보낸 12345678901234567890이 화면에서 12345678901234567000으로 보인다면 JSON이 잘못된 것이 아닙니다. JavaScript가 숫자를 64비트 부동소수점(IEEE 754 배정밀도)으로 읽기 때문이며, 정수는 2⁵³−1(9,007,199,254,740,991)까지만 정확합니다. 가장 확실한 해결책은 큰 ID를 문자열로 주고받는 것입니다. 지금 가진 JSON에 이런 숫자가 있는지는 JSON 포맷터·검증에서 확인할 수 있습니다. 숫자를 적힌 그대로 두고, JavaScript에서 읽으면 달라지는 숫자가 몇 개인지 알려 줍니다.

어떤 일이 일어나는가

JSON.stringify(JSON.parse('{"n":12345678901234567890}'));
// '{"n":12345678901234567000}'

JSON.parse("9007199254740993");
// 9007199254740992

오류도 경고도 없이 값만 달라집니다. 주문 번호나 게시물 ID처럼 64비트 정수로 만든 값이 이렇게 바뀌면, 조회가 되지 않거나 다른 레코드를 가리키게 됩니다.

왜 2⁵³인가

JavaScript의 Number는 IEEE 754 배정밀도(binary64) 형식입니다. 64비트 가운데 부호가 1비트, 지수가 11비트, 가수가 52비트이고, 가수 앞의 숨은 1비트를 더해 정밀도는 53비트입니다. 그래서 2⁵³까지의 정수는 모두 정확하게 담지만, 2⁵³과 2⁵⁴ 사이에서는 짝수만 나타낼 수 있고 수가 커질수록 간격이 더 벌어집니다. 9007199254740993이 9007199254740992가 되는 이유입니다.

값 의미
9,007,199,254,740,991 2⁵³−1. Number.MAX_SAFE_INTEGER
9,007,199,254,740,992 2⁵³. 여기부터 이웃한 정수를 구분하지 못함
9,223,372,036,854,775,807 2⁶³−1. 부호 있는 64비트 정수의 최댓값(19자리)

2⁵³은 16자리 수입니다. 정수가 17자리 이상이면 항상 이 범위를 넘고, 16자리면 9,007,199,254,740,991 이하인지 확인해야 합니다.

표준은 무엇을 말하나

RFC 8259 6절의 숫자 문법에는 자릿수 제한이 없습니다. 그래서 12345678901234567890은 올바른 JSON입니다. 다만 같은 절은 구현이 숫자의 범위와 정밀도에 한계를 둘 수 있다고 하고, IEEE 754 binary64보다 큰 정밀도를 기대하지 않아야 상호 운용성이 좋다고 적습니다. 정수는 −(2⁵³)+1부터 2⁵³−1까지가 구현들이 값에 정확히 동의하는 범위입니다.

즉 문제는 읽는 쪽에서 생깁니다. 64비트 정수형이나 임의 정밀도 정수로 읽는 언어에서는 값이 그대로지만(위 예시 값은 20자리라 부호 없는 64비트 범위입니다), JavaScript의 JSON.parse는 Number로 읽습니다. 같은 절이 상호 운용성 문제의 예로 드는 1E400은 JSON.parse에서 Infinity가 되고, 다시 JSON.stringify하면 null이 됩니다.

소수와 표기도 바뀐다

  • 1.0은 JSON.parse를 거쳐 다시 쓰면 1이 됩니다. 값은 같지만 표기가 달라져, 원문을 비교하거나 서명할 때 문제가 됩니다.
  • 자릿수가 많은 소수는 가장 가까운 배정밀도 값으로 바뀝니다. 0.1234567890123456789는 0.12345678901234568이 됩니다.
  • 금액처럼 정확해야 하는 값은 문자열이나 최소 단위 정수(원, 센트)로 보내는 편이 좋습니다.

값을 지키는 방법

ID는 문자열로 보냅니다. {"id":"12345678901234567890"}처럼 따옴표로 감싸면 어떤 파서에서도 바뀌지 않습니다. ID는 계산하지 않으므로 숫자일 이유가 없습니다. 서버의 직렬화 설정에서 64비트 정수를 문자열로 내보내도록 바꾸세요.

계산이 필요하면 BigInt를 씁니다. BigInt("12345678901234567890")은 정밀도를 잃지 않습니다. 단, JSON.stringify는 BigInt를 만나면 TypeError를 던지므로 내보낼 때는 toString()으로 문자열을 만듭니다.

숫자로 온 값을 그대로 읽어야 하면 원문에 접근합니다. TC39의 「JSON.parse source text access」 제안(4단계)은 reviver의 세 번째 인수로 숫자의 원문을 줍니다. 2026년 10월 기준 Node.js 24.14.0에서 아래 코드가 동작하는 것을 확인했습니다. 지원하지 않는 실행 환경에서는 context가 전달되지 않으니 먼저 확인하세요.

const data = JSON.parse('{"n":12345678901234567890}', (key, value, context) =>
  typeof value === "number" && !Number.isSafeInteger(value) && /^-?\d+$/.test(context.source)
    ? BigInt(context.source)
    : value,
);
// data.n === 12345678901234567890n

확인하거나 정리할 때도 조심합니다. JSON.parse로 읽었다가 다시 쓰는 포맷터는 정렬만 해도 값을 바꿉니다. ZEKILO Dev의 JSON 포맷터·검증은 숫자를 계산하지 않고 글자 그대로 옮기므로 {"n":12345678901234567890}을 정렬해도 값이 그대로이고, 「JavaScript에서 읽으면 정밀도가 바뀌는 숫자 1개가 있습니다. 여기서는 적힌 그대로 유지합니다.」라고 알려 줍니다. 1.0도 1.0으로 남습니다.

정리

  • JSON 표준에는 숫자 크기 제한이 없지만, 2⁵³−1보다 큰 정수는 JavaScript에서 정확히 담긴다는 보장이 없습니다.
  • 17자리 이상의 정수 ID는 문자열로 주고받습니다.
  • JavaScript에서 큰 정수를 다뤄야 하면 BigInt를 쓰고, 내보낼 때는 문자열로 바꿉니다.
  • JSON을 눈으로 확인할 때는 숫자를 바꾸지 않는 JSON 포맷터·검증을 쓰세요.

이 글과 관련된 도구

출처와 기준

다른 가이드