BigInt란?
| 타입 | 크기 | 범위 | 대략적 범위 | 비고 |
|---|---|---|---|---|
| number | 4 bytes | $-2^{31}$ ~ $2^{31}-1$ | 약 21억 | |
| BigInt | 8 bytes | $-2^{63}$ ~ $2^{63}-1$ | 약 922경 | ES2020 신규 도입 |
BigInt 는 2020년 새로 도입된 타입으로 사실상 실생활에 쓰이는 모든 수 범위를 표현할 수 있다. 그러나 BigInt값이 포함된 객체에 대해 JSON.stringify() 함수를 실행하면 아래와 같은 에러가 발생하게 된다.
TypeError: Do not know how to serialize a BigInt원인
백엔드 서버의 시점에서, 주로 Client와 통신 과정에서 이러한 오류가 나타나게 된다.
1. DB ORM에서 `BigInt`로 값을 받음 (주로 id 값)
2. 반환할 객체에 값이 `BigInt` 타입으로 추가됨
3. Client에게 API 응답을 반환하기 전 `JSON.stringify()` 사용하는 과정에서 에러 발생JavaScript의 내장 함수인 JSON.stringify()는 JSON 표준에 존재하는 6개 타입 number, string, object, array, boolean, null을 제외하면 타입 객체에 prototype으로 구현된 toJSON() 함수를 이용해 serialization을 수행한다.
하지만 ES2020에서 추가된 BigInt에 대해서는 BigInt.prototype.toJSON()이 정의되어있지 않기 때문에 serialization 수행이 불가능해 발생한 문제이다.
Date 타입도 이와 동일한 경우이지만, BigInt와 비교하면 훨씬 이전인 JavaScript 초창기부터 존재하여 2009년 ES5 도입시 Date.prototype.toJSON()이 추가되어 BigInt와는 달리 에러가 발생하지 않는다.
해결방안
조금 찾아보니, Mozilla mdn web docs에서 TypeError를 피할 3가지 방법을 제시하였다.
처음부터
BigInt데이터 사용을 피하고,number로 사용Number(value);JSON 형식의 텍스트로 변환할 때, 직접 만든 serialization 로직을
JSON.stringify()함수에 전달하여,BigInt를string이나number타입으로 변환JSON.stringify(data, (key, value) => (typeof value === "bigint" ? String(value) : value));BigInt.prototype.toJSON()함수를 직접 선언declare global { interface BigInt { toJSON: () => string; } } BigInt.prototype.toJSON = function () { return this.toString(); };
첫 번째 경우에는 BigInt 사용의 의미가 없고, 세 번째의 경우에는 수많은 라이브러리 코드에서 쓰였을JSON.stringify()가 예상치 못한 동작을 할 수 있으므로 부작용 발생 가능성이 존재했다. 따라서 두번째 방식이 가장 적절하다고 판단하였다.
BigInt를 string으로 변환하여 온전하게 큰 수를 문자열 형식으로 두는 것이 일반적으로 쓰이는 방법으로 알려져있다. 따라서 JSON.stringify()함수가 필요할 때마다 객체에 존재하는 BigInt 값을 모두 string으로 바꿀 필요가 있다.
NestJS에서의 적용
NestJS 프레임워크의 경우, 응답 반환 전 데이터를 전처리할 수 있는 Interceptor가 존재하여 이를 편리하게 전역적으로 적용할 수 있다.
단계
DTO에서
BigInt대신String으로 사용 (요청 body에string타입의 값으로 존재)ORM과의 통신 직전에
string을BigInt로 변환하여 DB 쿼리에 활용반환 값 생성 직전
Interceptor가BigInt를string으로 변환하도록 설정
// src/common/interceptors/bigint-to-string.interceptor.ts
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from "@nestjs/common";
import { Observable } from "rxjs";
import { map } from "rxjs/operators";
@Injectable()
export class BigIntToStringInterceptor<T = unknown> implements NestInterceptor<T, unknown> {
intercept(context: ExecutionContext, next: CallHandler<T>): Observable<unknown> {
return next.handle().pipe(map((data) => this.transform(data)));
}
// 객체 내부의 BigInt 타입 값만 찾아내어 String으로 변환하는 함수
// Array에 대해 map함수 사용
// Object에 대해서는 함수 재귀
// 그 외 값은 그대로 두고 JSON.stringify()에서 처리
private transform(data: unknown): unknown {
if (data === null || data === undefined) {
return data;
}
if (typeof data === "bigint") {
return data.toString();
}
if (Array.isArray(data)) {
return data.map((item) => this.transform(item));
}
if (typeof data === "object" && data !== null) {
const transformedObject: Record<string, unknown> = {};
for (const [key, value] of Object.entries(data)) {
transformedObject[key] = this.transform(value);
}
return transformedObject;
}
return data;
}
}// src/main.ts
import { NestFactory } from "@nestjs/core";
import { AppModule } from "./app.module";
import { BigIntToStringInterceptor } from "./common/interceptors/bigint-to-string.interceptor";
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// 글로벌 인터셉터 설정
app.useGlobalInterceptors(new BigIntToStringInterceptor());
await app.listen(3000);
}
bootstrap();레퍼런스
Mozilla - TypeError: BigInt value can't be serialized in JSON