BigInt

BigInt (빅인트)

BigInt 값은 number 기본형으로 표현하기에는 너무 크거나 너무 작은 정수 값을 나타냅니다. 원문은 MDN의 JavaScript 내장 객체 참고 문서입니다.

출처: BigInt - JavaScript | MDN

본문

BigInt 값(때로는 그냥 BigInt라고도 함)은 bigint 기본형으로, 정수 리터럴 끝에 n을 붙이거나 BigInt() 함수를 호출해(연산자 new 없이) 정수 값이나 문자열 값을 전달해 만듭니다.

const previouslyMaxSafeInteger = 9007199254740991n;
const alsoHuge = BigInt(9007199254740991); // 9007199254740991n
const hugeString = BigInt("9007199254740991"); // 9007199254740991n
const hugeHex = BigInt("0x1fffffffffffff"); // 9007199254740991n
const hugeOctal = BigInt("0o377777777777777777"); // 9007199254740991n
const hugeBin = BigInt("0b11111111111111111111111111111111111111111111111111111"); // 9007199254740991n

BigInt 값은 어떤 면에서는 Number 값과 비슷하지만 몇 가지 핵심 사항에서 다릅니다. BigInt 값은 내장 Math 객체의 메서드와 함께 사용할 수 없고, 연산에서 Number 값과 섞을 수 없습니다 — 같은 타입으로 강제 변환해야 합니다. 단, 값을 앞뒤로 강제 변환할 때 주의해야 합니다. BigInt 값이 Number 값으로 강제 변환될 때 정밀도가 손실될 수 있기 때문입니다.

타입 정보 (Type information)

BigInt 값(bigint 기본형)은 typeof로 검사하면 "bigint"를 반환합니다. typeof 1n === "bigint"; // true, typeof BigInt("1") === "bigint"; // true. BigInt 값은 Object로 감쌀 수도 있습니다: typeof Object(1n) === "object"; // true.

연산자 (Operators)

대부분의 연산자는 BigInt를 지원하지만, 대부분은 피연산자의 혼합 타입을 허용하지 않습니다 — 두 피연산자 모두 BigInt이거나 둘 다 아니어야 합니다.

  • 산술 연산자: +, -, *, /, %, **
  • 비트 연산자: >>, <<, &, |, ^, ~
  • 단항 부정(-)
  • 증가/감소: ++, --

불리언을 반환하는 연산자는 숫자와 BigInt를 피연산자로 섞는 것을 허용합니다.

  • 관계 연산자와 동등 연산자: >, <, >=, <=, ==, !=, ===, !==
  • 논리 연산자는 피연산자의 truthiness에만 의존합니다.

BigInt를 전혀 지원하지 않는 연산자도 몇 가지 있습니다.

  • 단항 더하기(+) — asm.js에서의 상충되는 용법 때문에 지원할 수 없어, asm.js를 깨지 않기 위해 제외됐습니다.
  • 부호 없는 오른쪽 시프트(>>>) — 모든 BigInt 값이 부호 있는 값이므로 지원되지 않는 유일한 비트 연산자입니다.

특수한 경우:

  • 문자열과 BigInt가 관련된 더하기(+)는 문자열을 반환합니다.
  • 나눗셈(/)은 BigInt가 분수 수량을 표현할 수 없으므로 분수 구성 요소를 0 방향으로 잘라냅니다.
const previousMaxSafe = BigInt(Number.MAX_SAFE_INTEGER); // 9007199254740991n
const maxPlusOne = previousMaxSafe + 1n; // 9007199254740992n
const theFuture = previousMaxSafe + 2n;  // 9007199254740993n
const prod = previousMaxSafe * 2n;       // 18014398509481982n
const diff = prod - 10n;                 // 18014398509481972n
const mod = prod % 10n;                  // 2n
const bigN = 2n ** 54n;                  // 18014398509481984n
const expected = 4n / 2n;                // 2n
const truncated = 5n / 2n;               // 2n, not 2.5n

비교 (Comparisons)

BigInt 값은 Number 값과 엄격히 같지는 않지만 느슨하게는 같습니다.

0n === 0; // false
0n == 0;  // true
1n < 2;   // true
2n > 1;   // true
2n >= 2;  // true

BigInt 값과 Number 값은 배열에서 섞일 수 있고 정렬될 수도 있습니다.

const mixed = [4n, 6, -12n, 10, 4, 0, 0n];
mixed.sort(); // 기본 정렬 동작
// [ -12n, 0, 0n, 10, 4n, 4, 6 ]
mixed.sort((a, b) => a - b);
// 혼합 타입에서는 뺄셈이 동작하지 않으므로 실패
// TypeError: can't convert BigInt value to Number value
mixed.sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
// [ -12n, 0, 0n, 4n, 4, 6, 10 ]

Object로 감싼 BigInt 값과의 비교는 다른 객체처럼 동작하며, 같은 객체 인스턴스를 비교할 때만 동등함을 나타냅니다. Object(0n) === 0n; // false, Object(0n) === Object(0n); // false, const o = Object(0n); o === o; // true.

Number 값과 BigInt 값 사이의 강제 변환은 정밀도 손실을 초래할 수 있으므로 다음이 권장됩니다.

  • 2^53보다 큰 값이 합리적으로 예상될 때만 BigInt 값을 사용하세요.
  • BigInt 값과 Number 값 사이를 강제 변환하지 마세요.

조건부 (Conditionals)

BigInt 값은 Boolean 함수로 변환되거나, ||, &&, ! 논리 연산자와 함께 사용되거나, if 문 같은 조건부 테스트 안에서 사용될 때 Number와 같은 변환 규칙을 따릅니다. 즉 오직 0n만 falsy이고 나머지는 모두 truthy입니다.

if (0n) {
  console.log("Hello from the if!");
} else {
  console.log("Hello from the else!");
}
// "Hello from the else!"
0n || 12n; // 12n
0n && 12n; // 0n
Boolean(0n); // false
Boolean(12n); // true
!12n; // false
!0n;  // true

암호화 (Cryptography)

BigInt 값에서 지원되는 연산은 일정 시간(constant-time)이 아니므로 타이밍 공격(timing attack)에 노출됩니다. 따라서 완화 요소 없이 JavaScript BigInt를 암호화에 사용하는 것은 위험할 수 있습니다. 매우 일반적인 예로, 공격자는 101n ** 65537n17n ** 9999n 사이의 시간 차이를 측정하고, 경과 시간을 기반으로 개인 키 같은 비밀의 크기를 추론할 수 있습니다. BigInt를 꼭 사용해야 한다면 타이밍 공격 FAQ에서 일반적인 조언을 확인하세요.

JSON 내에서의 사용 (Use within JSON)

JSON.stringify()를 어떤 BigInt 값과 사용하면 TypeError가 발생합니다. BigInt 값은 기본적으로 JSON에서 직렬화되지 않기 때문입니다. 그러나 JSON.stringify()는 BigInt 값에 대해 특별히 백도어를 남깁니다 — BigInt의 toJSON() 메서드를 호출하려 시도합니다. (다른 어떤 기본형 값에 대해서는 그렇게 하지 않습니다.) 따라서 내장 객체를 패치하는 것이 명시적으로 권장되지 않는 몇 안 되는 경우 중 하나로, 자신만의 toJSON() 메서드를 구현할 수 있습니다.

BigInt.prototype.toJSON = function () {
  return { $bigint: this.toString() };
};
console.log(JSON.stringify({ a: 1n }));
// {"a":{"$bigint":"1"}}

BigInt.prototype을 패치하고 싶지 않다면, JSON.stringify의 replacer 매개변수를 사용해 BigInt 값을 직렬화할 수 있습니다.

const replacer = (key, value) =>
  typeof value === "bigint" ? { $bigint: value.toString() } : value;
const data = { number: 1, big: 18014398509481982n };
const stringified = JSON.stringify(data, replacer);
console.log(stringified);
// {"number":1,"big":{"$bigint":"18014398509481982"}}

그런 다음 JSON.parse의 reviver 매개변수로 처리할 수 있습니다.

const reviver = (key, value) =>
  value !== null &&
  typeof value === "object" &&
  "$bigint" in value &&
  typeof value.$bigint === "string"
    ? BigInt(value.$bigint)
    : value;
const payload = '{"number":1,"big":{"$bigint":"18014398509481982"}}';
const parsed = JSON.parse(payload, reviver);
console.log(parsed); // { number: 1, big: 18014398509481982n }

참고: JSON.stringify()의 replacer를 일반화해 모든 객체의 BigInt 값을 제대로 직렬화하는 것은 가능하지만, JSON.parse()의 reviver는 주의해서 사용해야 합니다. 직렬화는 되돌릴 수 없기 때문입니다 — 우연히 $bigint라는 이름의 속성을 가진 객체와 실제 BigInt를 구분할 수 없습니다. 또한 위 예제는 대체(replacing)와 복원(reviving) 중에 전체 객체를 만들므로, 많은 BigInt를 담은 큰 객체에서는 성능이나 저장 공간에 영향을 줄 수 있습니다. 페이로드의 형태를 알고 있다면, 그냥 문자열로 직렬화하고 속성 키 이름을 기반으로 복원하는 편이 나을 수 있습니다.

사실 JSON은 임의로 긴 숫자 리터럴을 허용합니다. 다만 JavaScript에서는 전체 정밀도로 파싱하지 못할 뿐입니다. 64비트 정수처럼 더 긴 정수를 지원하는 언어로 다른 프로그램과 통신하면서 BigInt를 JSON 숫자로 전송하고 싶다면 Lossless number serialization 문서를 참고하세요.

BigInt 강제 변환 (BigInt coercion)

BigInt를 기대하는 많은 내장 연산은 먼저 인자를 BigInt로 강제 변환합니다. 그 과정은 다음과 같이 요약됩니다.

  • BigInt는 그대로 반환됩니다.
  • undefinednullTypeError를 던집니다.
  • true1n, false0n이 됩니다.
  • 문자열은 정수 리터럴을 담고 있는 것처럼 파싱해 변환됩니다. 파싱 실패는 SyntaxError를 발생시킵니다. 문법은 문자열 숫자 리터럴의 부분집합으로, 소수점이나 지수 표시는 허용되지 않습니다.
  • 숫자는 의도치 않은 암묵적 변환으로 인한 정밀도 손실을 막기 위해 TypeError를 던집니다.
  • Symbol은 TypeError를 던집니다.
  • 객체는 먼저 [Symbol.toPrimitive]()("number" 힌트로), valueOf(), toString() 메서드를 이 순서대로 호출해 기본형으로 변환한 뒤, 그 결과 기본형을 BigInt로 변환합니다.

JavaScript에서 거의 같은 효과를 얻는 최선의 방법은 BigInt() 함수입니다. BigInt(x)x를 변환하는 데 같은 알고리즘을 사용하되, 숫자가 TypeError를 던지지 않고 정수라면 BigInt로 변환합니다. 참고: BigInt를 기대하는 내장 연산은 강제 변환 후 BigInt를 고정 너비로 자르는 경우가 많습니다. 여기에는 BigInt.asIntN(), BigInt.asUintN(), 그리고 BigInt64ArrayBigUint64Array의 메서드가 포함됩니다.

생성자, 정적 메서드, 인스턴스 멤버

  • BigInt() — BigInt 타입의 기본형 값을 반환합니다. new와 함께 호출하면 오류를 던집니다.
  • BigInt.asIntN() — BigInt 값을 주어진 최하위 비트 수로 자르고 부호 있는 정수로 반환합니다.
  • BigInt.asUintN() — BigInt 값을 주어진 최하위 비트 수로 자르고 부호 없는 정수로 반환합니다.
  • 인스턴스 속성: constructor, BigInt.prototype[Symbol.toStringTag](값 "BigInt", Object.prototype.toString()에서 사용).
  • 인스턴스 메서드: toLocaleString()(언어별 표현의 문자열), toString()(지정 진법으로 이 BigInt 값을 나타내는 문자열).

사양 및 호환성

BigInt는 ECMAScript 사양에서 정의되며, Baseline "Widely available"로 분류되어 2020년 9월부터 브라우저 전반에서 사용 가능했습니다.

더 알아보기