SOFTMOA TECHNOLOGY

JavaScript JSON.parse에서 큰 정수 ID 끝자리가 바뀔 때 해결 방법

소프트모아가 정리한 기술 기록입니다.

Contents
작성일 2026. 10. 31.

API에서 받은 주문 ID의 끝자리가 바뀐다면 화면 표시보다 JSON을 읽는 단계를 먼저 확인해야 합니다. JavaScript의 Number는 큰 정수를 모두 구별하지 못합니다.[1] 계산하지 않는 식별자는 서버가 JSON 문자열로 보내고, 클라이언트도 문자열로 유지하는 계약으로 바꾸는 것이 이 글의 해결 방향입니다. 이미 반올림된 숫자를 나중에 문자열이나 BigInt로 바꾸는 것만으로 원래 ID가 돌아오지는 않습니다.[3]

오류 없이 두 ID가 하나가 되는 재현

2026년 10월 31일 Node.js v26.10.0에서 확인했습니다. 예제는 실제 주문이 아닌 가상 데이터 두 건이며, 파일과 DB를 변경하거나 외부 API를 호출하지 않습니다. 코드를 id-check.mjs로 저장한 뒤 node --version과 node id-check.mjs를 실행하면 됩니다.

const raw = '{"orders":[{"id":9007199254741003},{"id":9007199254741004}]}';
const ids = JSON.parse(raw).orders.map(order => order.id);
console.log('원문:', raw);
console.log('파싱:', ids.join(', '));
console.log('고유 ID 수:', new Set(ids).size);
console.log('안전한 정수:', ids.map(Number.isSafeInteger).join(', '));
console.log('사후 BigInt:', BigInt(ids[0]).toString());

실행 결과:

원문: {"orders":[{"id":9007199254741003},{"id":9007199254741004}]}
파싱: 9007199254741004, 9007199254741004
고유 ID 수: 1
안전한 정수: false, false
사후 BigInt: 9007199254741004

원문에는 서로 다른 ID가 두 개 있지만 파싱한 값은 같고, Set의 고유 ID 수는 하나입니다. 이는 주문 한 건이 삭제됐다는 뜻이 아니라 메모리의 숫자 표현에서 식별이 무너졌다는 뜻입니다. 두 번째 ID처럼 이 범위에서도 정확히 표현되는 수는 있습니다. 안전 범위를 넘는 모든 수가 반드시 바뀌는 것은 아니지만, 이웃 정수와 구별된다는 보장이 없습니다.[1]

Number.MAX_SAFE_INTEGER는 9007199254740991입니다. Number.isSafeInteger는 정수인지뿐 아니라 절댓값이 이 한도 이내인지 확인합니다.[1] 따라서 false는 수신값을 다시 확인할 신호이지, 원래 숫자나 손실 자릿수를 알려주는 복구 함수가 아닙니다. JSON.parse는 문법에 맞는 JSON 숫자를 Number로 읽으므로 이 사례에서 예외가 나지 않습니다.[2]

따옴표는 파싱한 뒤가 아니라 보내기 전에 붙입니다

수정 대상은 화면의 콤마나 자리수 옵션이 아니라 응답 필드의 자료형입니다. 서버가 원래 ID를 보유한 상태에서 문자열로 직렬화해야 합니다. 브라우저에서 이미 파싱한 뒤 String(id)를 호출하는 것은 늦습니다.[3] 다음 코드를 앞 예제 아래에 이어 붙이면 수정된 계약을 비교할 수 있습니다.

const fixed = '{"orders":[{"id":"9007199254741003"},{"id":"9007199254741004"}]}';
const orders = JSON.parse(fixed).orders;
console.log('문자열:', orders.map(order => order.id).join(', '));
console.log('고유 ID 수:', new Set(orders.map(order => order.id)).size);
console.log('재전송:', JSON.stringify({ orders }));
console.log('정수 차이:', (BigInt(orders[1].id) - BigInt(orders[0].id)).toString());
try {
  JSON.stringify({ id: BigInt(orders[0].id) });
} catch (error) {
  console.log('BigInt 직접 직렬화:', error.name);
}

실행 결과:

문자열: 9007199254741003, 9007199254741004
고유 ID 수: 2
재전송: {"orders":[{"id":"9007199254741003"},{"id":"9007199254741004"}]}
정수 차이: 1
BigInt 직접 직렬화: TypeError

고유 ID는 두 개이고, 재전송해도 원래 숫자열이 유지됩니다. 정수 차이는 진단용이며 주문 처리 순서를 뜻하지 않습니다. 조회만 한다면 계산용 타입으로 바꾸지 않아도 됩니다. 선행 0에 의미가 있는 ID도 문자열 원형을 유지합니다.

BigInt가 꼭 필요한 계산이라면 손실 전 문자열에서 생성합니다.[3] 위 출력의 TypeError는 별도의 toJSON·replacer 처리가 없는 기본 JSON.stringify가 BigInt를 그대로 직렬화하지 못하기 때문에 발생합니다.[2] 전송 필드는 명시적으로 문자열로 돌리고, 수신 측도 같은 타입을 기대하게 해야 합니다. 모든 숫자 필드를 일괄 문자열로 바꾸면 수량 등 다른 필드의 계약까지 달라지므로 ID 필드의 변경 범위를 합의합니다.

배포 때 확인할 것은 원문과 왕복 결과입니다

  • 서버 응답 원문에서 ID가 큰따옴표로 감싸졌는지 확인합니다. 운영 자료 대신 합성 ID를 쓰고, 고객 ID 전체를 로그에 남기지 않습니다.
  • API 스키마, 응답 직렬화, 클라이언트 검증과 캐시 키에서 ID를 문자열로 맞춥니다. 숫자 ID가 들어오면 자동 변환으로 숨기지 않고 계약 위반으로 식별합니다.
  • 두 ID가 다른 값으로 유지되는지와 저장·재조회 후 같은 문자열로 돌아오는지를 각각 테스트합니다. 이 예제는 JSON 왕복만 확인하므로 DB 저장 검증을 대신하지 않습니다.

변환된 value만 BigInt로 바꾸는 reviver도 이미 잃은 자릿수를 복원하지 못합니다.[3] 원문 토큰을 제공하는 context.source를 이용하는 방식은 별도 선택지이지만, 실행 환경 지원과 필드별 변환 규칙을 따로 확인해야 합니다.[2] 이 글의 수정은 발신 측을 함께 바꿀 수 있는 경우에 해당합니다. 숫자형 ID를 고집하는 외부 API라면 원문을 보존한 파싱 경로를 별도로 검토해야 합니다.

새 계약을 적용해도 과거에 잘못 저장된 ID는 자동 복구되지 않습니다. 원본 시스템과 대조할 근거가 있는 건만 정정하고, 근거가 없는 값은 임의로 끝자리를 고치지 않습니다. 서로 다른 원본이 같은 Number가 된 뒤에는 그 값 하나로 어느 원본이었는지 판별할 수 없기 때문입니다.[1]

참고 문서

소프트모아의 서비스는 홈페이지에서 확인할 수 있습니다.

소프트모아는 해당 시스템을 구축합니다. 문의하기

Share this

FrontEnd

Have a questions?

견적 및 기술문의

mobile : 010-7931-4813

Contact Form