한 줄로 인코딩하고 디코딩하기
이 API는 Base64용 메서드 세 개와 hex용 메서드 세 개를 추가합니다.
| 메서드 | 하는 일 |
|---|---|
bytes.toBase64(options) | Uint8Array를 Base64 문자열로 |
Uint8Array.fromBase64(string, options) | Base64 문자열을 새 Uint8Array로 |
bytes.setFromBase64(string, options) | 기존 Uint8Array에 디코딩 |
bytes.toHex() | Uint8Array를 소문자 hex 문자열로 |
Uint8Array.fromHex(string) | hex 문자열을 새 Uint8Array로 |
bytes.setFromHex(string) | 기존 Uint8Array에 hex 디코딩 |
텍스트는 먼저 바이트가 되어야 합니다. TextEncoder는 항상 UTF-8을 만들므로 유니코드도 꼼수 없이 동작합니다.
const bytes = new TextEncoder().encode("Grüße, 世界");
const b64 = bytes.toBase64();
console.log(b64); // "R3LDvMOfZSwg5LiW55WM"
const back = new TextDecoder().decode(Uint8Array.fromBase64(b64));
console.log(back); // "Grüße, 世界"
fetch().arrayBuffer()나 crypto.subtle.digest()에서 얻은 ArrayBuffer는 뷰가 필요합니다: new Uint8Array(buffer).toBase64(). 다른 타입 배열에는 이 메서드가 아예 없습니다. Uint16Array의 원시 메모리를 인코딩하려면 감싸세요: new Uint8Array(u16.buffer, u16.byteOffset, u16.byteLength).
옵션: alphabet, omitPadding, lastChunkHandling
alphabet
"base64"(기본값)는 +와 /를 씁니다. "base64url"은 -와 _를 씁니다. 이 옵션은 인코딩과 디코딩 모두에 적용되며, 한 알파벳으로 설정한 디코더는 다른 알파벳의 문자를 거부합니다.
const data = new Uint8Array([251, 255]);
data.toBase64(); // "+/8="
data.toBase64({ alphabet: "base64url" }); // "-_8="
data.toBase64({ alphabet: "base64url", omitPadding: true }); // "-_8"
Uint8Array.fromBase64("-_8", { alphabet: "base64url" }); // Uint8Array [251, 255]
Uint8Array.fromBase64("-_8"); // SyntaxError
omitPadding
toBase64는 Base64URL 알파벳에서도 기본적으로 = 패딩을 붙입니다. JWT 방식의 출력이 필요하면 omitPadding: true를 넘기세요. 디코딩할 때의 패딩은 lastChunkHandling이 정합니다.
lastChunkHandling
이 옵션은 fromBase64와 setFromBase64가 마지막 문자 묶음을 어떻게 처리할지 정합니다. Hi?>의 바이트로 디코딩되는 입력으로 세 모드를 테스트했습니다.
| 입력 | "loose" (기본값) | "strict" | "stop-before-partial" |
|---|---|---|---|
SGk/Pg== | Hi?> | Hi?> | Hi?> |
SGk/Pg (패딩 없음) | Hi?> | SyntaxError | Hi? (Pg 앞에서 멈춤) |
SGk/Ph== (0이 아닌 채움 비트) | Hi?> | SyntaxError | Hi?> |
SGk/P (문자 하나가 남음) | SyntaxError | SyntaxError | Hi? |
- loose는
atob처럼 동작합니다. 패딩은 선택 사항이고, 마지막 문자의 남는 비트는 무시됩니다. - strict는 RFC 4648이 설명하는 대로 정규적이고 패딩된 입력만 받습니다. Base64 문자열을 비교하거나 서명하는 경우처럼 인코딩된 형태가 유일해야 할 때 쓰세요.
- stop-before-partial은 완전한 4글자 묶음을 모두 디코딩하고, 불완전한 끝부분은 그대로 둡니다. 묶음의 나머지가 다음 조각에 도착하는 스트리밍을 위한 모드입니다.
모든 모드에서 ASCII 공백(스페이스, 탭, 줄 바꿈)은 건너뛰므로 MIME 방식으로 줄을 나눈 입력도 바로 디코딩됩니다. 알파벳 밖의 다른 문자는 SyntaxError를 던집니다.
기존 버퍼에 쓰기
setFromBase64와 setFromHex는 이미 있는 Uint8Array에 디코딩하고, 얼마나 사용했는지 돌려줍니다.
const target = new Uint8Array(8);
const result = target.setFromBase64("SGVsbG8gd29ybGQ=");
console.log(result); // { read: 8, written: 6 }
Hello world는 11바이트이고 대상은 8바이트를 담습니다. 디코더는 들어가는 완전한 3바이트 묶음만 쓰므로 6바이트에서 멈추고 8글자를 읽었다고 알려 줍니다. 계속하려면 문자열의 나머지와 대상의 subarray로 다시 호출하세요. stop-before-partial과 함께 쓰면 스트림을 미리 할당한 버퍼에 조각 단위로 디코딩할 수 있습니다.
const out = new Uint8Array(1024);
const { read, written } = out.setFromBase64("SGk/Pg", {
lastChunkHandling: "stop-before-partial",
});
// read: 4, written: 3. Keep "Pg" and prepend it to the next chunk.
hex 메서드도 같은 패턴을 따릅니다. toHex()는 항상 소문자로 쓰고, fromHex()는 대소문자를 모두 받으며, 자릿수가 홀수면 SyntaxError입니다.
new Uint8Array([0xde, 0xad, 0xbe, 0xef]).toHex(); // "deadbeef"
Uint8Array.fromHex("CAFE"); // Uint8Array [202, 254]
btoa와 atob가 불편한 이유
btoa와 atob는 브라우저 초창기부터 있던 함수로, “바이너리 문자열”, 즉 모든 문자 코드가 0부터 255까지의 바이트 값인 JavaScript 문자열에 대해 동작합니다. 여기서 세 가지 문제가 생깁니다.
유니코드는 예외를 던지거나 조용히 틀립니다. U+00FF보다 큰 문자는 btoa가 InvalidCharacterError를 던지게 만듭니다(Chrome: “The string to be encoded contains characters outside of the Latin1 range”). 한글도 여기에 해당합니다. U+0080부터 U+00FF 사이의 문자는 더 나쁩니다. 오류 없이 Latin-1로 인코딩됩니다. btoa("Grüße")는 R3L832U=를 돌려주는데, 다른 모든 시스템이 기대하는 UTF-8 인코딩은 R3LDvMOfZQ==입니다.
디코딩 결과가 바이트가 아니라 문자열입니다. atob("Q2Fmw6k=")는 "Café"를 돌려줍니다. 제대로 된 텍스트를 얻으려면 문자열을 바이트로 바꾼 뒤 UTF-8로 디코딩해야 하며, 반복문이나 Uint8Array.from(atob(s), c => c.charCodeAt(0))이 필요합니다.
큰 입력은 조심해야 합니다. 흔한 한 줄 코드 btoa(String.fromCharCode(...bytes))는 모든 바이트를 함수 인자로 넘기므로, 1 MB 배열에서는 RangeError: Maximum call stack size exceeded를 던집니다. 문자열을 조각 단위로 만들어야 합니다.
Base64URL 지원도 없고, 패딩이나 엄격함을 조절할 수도 없습니다. 예전 우회책 btoa(unescape(encodeURIComponent(text)))는 UTF-8 Base64를 만들어 주지만, 지원 중단된 unescape에 의존합니다. 새 API에서는 이 모든 것이 new TextEncoder().encode(text).toBase64()가 됩니다.
브라우저와 Node.js 지원
이 메서드들은 TC39 제안 “Uint8Array to/from Base64"에서 나왔으며, 2025년 7월에 Stage 4에 도달해 ECMAScript 2026에 포함되었습니다.
| 런타임 | 여섯 메서드를 모두 갖춘 첫 버전 |
|---|---|
| Chrome, Edge | 140 |
| Firefox | 133 |
| Safari (macOS, iOS) | 18.2 |
| Node.js | 25.0.0 |
Chrome 140과 함께 이 기능은 2025년 9월에 Baseline이 되었으므로 현재의 모든 주요 브라우저에 있습니다. Node.js에서는 메서드를 직접 테스트했습니다. Node 25.0.0과 26.12.1에는 여섯 개가 모두 있습니다. LTS 릴리스인 Node 24.22.1에는 기본적으로 하나도 없습니다. V8이 실험적 플래그 --js-base-64를 받지만, 저희 테스트에서 이 플래그는 toBase64, fromBase64, toHex, fromHex만 노출하고 setFromBase64와 setFromHex는 없었으므로 Node 24는 미지원으로 취급하세요. Node 24 이하에서는 "base64url"도 받는 Buffer.from(b64, "base64")와 buf.toString("base64")를 쓰세요. MDN의 호환성 데이터에 따르면 Deno 2.5와 Bun 1.1.22도 이 API를 지원합니다.
오래된 런타임을 위한 대체 코드
기능을 감지하고, 없으면 조각 단위로 처리하는 btoa/atob로 대체하세요. 이 버전은 Node 24(대체 경로)와 Node 26(네이티브 경로)에서 UTF-8 텍스트와 1 MB 배열로 테스트했습니다.
function bytesToBase64(bytes) {
if (typeof bytes.toBase64 === "function") return bytes.toBase64();
let bin = "";
for (let i = 0; i < bytes.length; i += 0x8000) {
bin += String.fromCharCode(...bytes.subarray(i, i + 0x8000));
}
return btoa(bin);
}
function base64ToBytes(b64) {
if (typeof Uint8Array.fromBase64 === "function") return Uint8Array.fromBase64(b64);
return Uint8Array.from(atob(b64), (c) => c.charCodeAt(0));
}
이 대체 코드는 기본 옵션만 다룹니다. 어디서나 base64url, strict, setFromBase64가 필요하다면 완전한 폴리필을 쓰세요. core-js가 es.uint8-array.to-base64, es.uint8-array.from-base64 등의 모듈로 제공합니다.
코드를 쓰지 않고 인코딩을 시험해 보려면 Base64 인코더와 디코더를, URL-safe 알파벳에는 Base64URL을 쓰세요. Node의 Buffer를 포함한 더 많은 JavaScript 예제는 JavaScript에서 Base64와 Node.js에서 Base64에 있습니다.
자주 묻는 질문
새 Base64 API를 지금 배포해도 안전한가요?
브라우저에서는 그렇습니다. Chrome과 Edge 140, Firefox 133, Safari 18.2에 들어 있습니다. Node.js에서는 버전 25 이상이 필요합니다. 오래된 브라우저나 Node 24 이하를 지원한다면 대체 코드를 유지하세요.
Uint8Array.fromBase64는 Base64URL을 받나요?
네, { alphabet: "base64url" }을 주면 받습니다. 이 옵션이 없으면 -와 _가 SyntaxError를 일으킵니다. 기본 loose 모드에서는 두 알파벳 모두 패딩이 선택 사항입니다.
toBase64로 문자열을 인코딩하려면 어떻게 하나요?
먼저 UTF-8 바이트로 바꾸세요: new TextEncoder().encode(text).toBase64(). 디코딩은 new TextDecoder().decode(Uint8Array.fromBase64(b64))로 거꾸로 하면 됩니다.
왜 제 Buffer나 Uint16Array에서는 toBase64가 함수가 아니라고 나오나요?
이 메서드는 Uint8Array와 그 하위 클래스에만 있습니다. Node의 Buffer는 Node 25부터 갖게 됩니다. 다른 타입 배열은 메모리를 감싸세요: new Uint8Array(arr.buffer, arr.byteOffset, arr.byteLength).