워로디스

TypeScript. Uint8Array 해싱과 문자열 ID 표현 본문

개발/TypeScript

TypeScript. Uint8Array 해싱과 문자열 ID 표현

워로디스 2026. 9. 16. 22:07

파일이나 바이너리 데이터를 내용 기반 식별자(Content ID) 로 만들고 싶다면 다음 구조가 가장 단순하고 실용적이다.

Uint8Array
  ↓
SHA-256
  ↓
앞 16바이트 사용 (128bit)
  ↓
hex 또는 base64url 문자열

1. 해시 알고리즘: SHA-256

브라우저나 Web Crypto API를 사용할 수 있는 환경이라면 SHA-256이 무난하다.

const digest = new Uint8Array(
  await crypto.subtle.digest("SHA-256", data)
);

digest는 32바이트, 즉 256비트의 Uint8Array다.

SHA-1도 단순 파일 식별 용도로는 우연 충돌 가능성이 충분히 낮을 수 있지만, 이미 암호학적 충돌 저항성이 깨진 알고리즘이다. 새 시스템이라면 특별한 이유가 없는 한 SHA-256을 사용하는 편이 낫다.

2. 256비트 전체가 필요한가?

일반적인 파일 식별키라면 SHA-256의 32바이트 전체를 사용할 필요까지는 없다.

앞 16바이트만 사용하면 128비트 ID가 된다.

const idBytes = digest.subarray(0, 16);

128비트의 우연 충돌 확률은 일반적인 규모에서는 사실상 무시할 수 있다.

대략적인 충돌 확률은 birthday bound에 의해:

p ≈ n² / 2^(b + 1)

이며 b = 128일 경우:

서로 다른 파일 수 대략적인 충돌 확률
100만 1.5 × 10⁻²⁷
10억 1.5 × 10⁻²¹
1조 1.5 × 10⁻¹⁵

따라서 일반적인 파일 저장소, 캐시, DB 식별키 용도로는 128비트면 충분하다.

SHA-256의 앞 16바이트만 사용한다고 해서 앞부분이 특별히 취약해지는 것도 아니다. 결과적으로 128비트 해시를 사용하는 것으로 보면 된다.

3. subarray()slice()

둘 다 앞 16바이트를 얻을 수 있다.

digest.subarray(0, 16);
digest.slice(0, 16);

차이는:

  • subarray() → 원래 버퍼를 바라보는 view
  • slice() → 데이터를 복사해서 새로운 Uint8Array 생성

단순히 ID를 문자열로 변환할 목적이라면 복사가 필요 없으므로 subarray()가 적절하다.

const idBytes = digest.subarray(0, 16);

4. 바이트를 문자열로 표현하기

128비트 ID를 문자열로 표현하는 대표적인 방법은 hexbase64url이다.

Hex

16바이트가 32글자가 된다.

431def63c17668af6635cecba744e35a

장점:

  • 단순함
  • 사람이 읽고 디버깅하기 편함
  • URL, 파일명, DB key 등에 안전하게 사용 가능
  • 거의 모든 환경에서 쉽게 구현 가능

단점은 base64url보다 문자열이 길다는 것이다.

Base64url

16바이트가 padding 없이 22글자가 된다.

Qx3vY8F2aK9mNc7Lp0TjWg

같은 128비트 정보를 더 짧게 표현할 수 있다.

표현 128비트 ID 길이
hex 32자
base64url 22자

두 표현 방식의 충돌 확률은 완전히 동일하다. 바이트를 문자열로 표현하는 방법만 다르기 때문이다.

5. TypeScript에서 Hex 변환

호환성을 중요하게 생각한다면 직접 작은 helper를 두는 것이 가장 안정적이다.

function toHex(bytes: Uint8Array): string {
  return Array.from(
    bytes,
    byte => byte.toString(16).padStart(2, "0")
  ).join("");
}

그러면 파일 ID는 다음처럼 만들 수 있다.

async function fileId(data: Uint8Array): Promise<string> {
  const digest = new Uint8Array(
    await crypto.subtle.digest("SHA-256", data)
  );

  return toHex(digest.subarray(0, 16));
}

결과는 128비트, 32글자 hex ID다.

6. Uint8Array.prototype.toHex()에 대한 주의점

최신 JavaScript 표준에는 다음과 같은 Uint8Array 변환 API가 추가되어 있다.

bytes.toHex();
bytes.toBase64(...);

하지만 다음 세 가지는 별개의 문제다.

  1. JavaScript 표준에 API가 포함되어 있는가
  2. 실제 브라우저나 Node 런타임이 구현하고 있는가
  3. 현재 설치된 TypeScript의 lib.d.ts가 그 API를 알고 있는가

따라서:

bytes.toHex();

가 특정 최신 런타임에서 동작하더라도 TypeScript에서는:

Property 'toHex' does not exist on type 'Uint8Array'

같은 오류가 발생할 수 있다.

tsconfig.json에:

{
  "compilerOptions": {
    "lib": ["ESNext", "DOM"]
  }
}

를 지정했다고 해서 반드시 해결되는 것도 아니다.

ESNext현재 설치된 TypeScript가 알고 있는 최신 API를 의미하기 때문이다.

따라서 여러 환경을 지원해야 한다면 현재로서는 작은 toHex() helper를 직접 두는 방법이 가장 예측 가능하다.

7. Node.js에서는 Buffer 사용

Node.js 전용 코드라면 훨씬 간단하다.

Hex

const id = Buffer
  .from(digest.subarray(0, 16))
  .toString("hex");

Base64url

const id = Buffer
  .from(digest.subarray(0, 16))
  .toString("base64url");

Node에서는 특히 base64url을 직접 변환할 필요가 없어서 편하다.

8. 브라우저에서 Base64url

기존 브라우저 API만 사용하면 흔히 다음처럼 구현한다.

const id = btoa(String.fromCharCode(...bytes))
  .replace(/\+/g, "-")
  .replace(/\//g, "_")
  .replace(/=+$/, "");

이 방식은 잘못된 방식은 아니고, 일반 Base64를 Base64url 형식으로 변환하는 정상적인 구현이다.

다만 코드가 지저분하고 의도가 잘 드러나지 않는다.

그래서 선택지는 보통 다음과 같다.

  • 최신 런타임만 지원 → 네이티브 Base64 API 사용
  • Node.js → Buffer.toString("base64url")
  • 폭넓은 브라우저 지원 → 작은 helper 또는 검증된 라이브러리 사용
  • 문자열 길이가 크게 중요하지 않음 → 그냥 hex 사용

9. 권장 구현

범용 TypeScript 코드에서 단순성과 호환성을 우선한다면 다음 정도가 적당하다.

function toHex(bytes: Uint8Array): string {
  return Array.from(
    bytes,
    byte => byte.toString(16).padStart(2, "0")
  ).join("");
}

export async function fileId(
  data: Uint8Array
): Promise<string> {
  const digest = new Uint8Array(
    await crypto.subtle.digest("SHA-256", data)
  );

  return toHex(digest.subarray(0, 16));
}

특성은 다음과 같다.

입력        Uint8Array
해시        SHA-256
ID 크기     128bit / 16byte
문자 표현   hex
문자열 길이 32자

10. 결론

일반적인 파일 내용 기반 식별자라면 다음 구성을 기본값으로 두기 좋다.

Uint8Array
    ↓
SHA-256
    ↓
128bit로 truncate
    ↓
hex

즉:

SHA-256(data)
  → subarray(0, 16)
  → hex

hex 32자가 부담스럽지 않다면 이 방법이 가장 단순하고 호환성이 좋다.

문자열 길이가 중요하다면 동일한 16바이트를 base64url로 표현해서 22자로 줄일 수 있다.

SHA-256 → 128bit → hex       // 32자, 단순하고 호환성 좋음
SHA-256 → 128bit → base64url // 22자, 더 압축된 표현

해시 자체의 충돌 안전성은 둘 사이에 차이가 없다.

반응형

'개발 > TypeScript' 카테고리의 다른 글

Typescript 라이브러리 프로젝트 구성  (0) 2026.03.22