파일 업로드
  1. base64hub
  2. 코드로 다루는 Base64
  3. JavaScript
코드

JavaScript에서 Base64 사용하기

브라우저에는 Latin-1만 처리하는 btoa와 atob, 그리고 바이트를 제대로 처리하는 최신 Uint8Array.toBase64() / fromBase64()가 있습니다. 아래 출력은 헤드리스 Chromium 기준입니다.

btoa와 atob: Latin-1 전용

btoa(“binary to ASCII”)는 모든 문자를 1바이트로 취급하는 문자열을 인코딩합니다. atob는 그 반대입니다. ASCII에서는 잘 동작합니다.

btoa("hello");        // "aGVsbG8="
atob("aGVsbG8=");     // "hello"

코드 포인트 0–255 밖의 문자가 있으면 실패합니다.

btoa("héllo ✓");
// InvalidCharacterError: Failed to execute 'btoa' on 'Window':
// The string to be encoded contains characters outside of the Latin1 range.

더 나쁜 경우는 Latin-1에 들어가는 문자입니다. 예외는 나지 않지만 엉뚱한 바이트를 인코딩합니다. btoa("héllo")는 aOlsbG8=(Latin-1의 é = 0xE9)를 반환하는 반면, 다른 모든 언어와 Base64 인코더는 UTF-8 버전인 aMOpbGxv를 만듭니다.

btoa와 atob는 Node.js 16 이상에도 레거시 전역 함수로 있지만, 서버에서는 Buffer API가 더 빠르고 base64url 인코딩도 내장하고 있습니다. Node.js에서 Base64 사용하기를 참고하세요.

UTF-8 문자열 인코딩과 디코딩

이식성 있는 해결법은 TextEncoder로 문자열을 UTF-8 바이트로 바꾼 다음, 그 바이트를 “바이너리 문자열"로 btoa에 넘기는 것입니다.

function encodeUtf8(str) {
  const bytes = new TextEncoder().encode(str);
  let bin = "";
  for (const b of bytes) bin += String.fromCharCode(b);
  return btoa(bin);
}

function decodeUtf8(b64) {
  const bin = atob(b64);
  const bytes = Uint8Array.from(bin, c => c.charCodeAt(0));
  return new TextDecoder().decode(bytes);
}

encodeUtf8("héllo wörld");            // "aMOpbGxvIHfDtnJsZA=="
decodeUtf8("aMOpbGxvIHfDtnJsZA==");   // "héllo wörld"
atob("aMOpbGxvIHfDtnJsZA==");         // "héllo wörld"  (no TextDecoder)

예전의 btoa(unescape(encodeURIComponent(str))) 기법도 같은 결과를 내지만, 지원 중단된 unescape에 의존합니다.

Uint8Array.toBase64와 fromBase64

새 API는 바이너리 문자열 단계 없이 바이트를 직접 다룹니다. Chrome과 Edge 140 이상, Firefox 133 이상, Safari 18.2 이상에서 지원합니다.

const bytes = new TextEncoder().encode("héllo wörld");
bytes.toBase64();                                      // "aMOpbGxvIHfDtnJsZA=="

new TextDecoder().decode(Uint8Array.fromBase64("aMOpbGxvIHfDtnJsZA=="));
// "héllo wörld"

기존 버퍼에 디코딩하고 얼마나 사용했는지 알려 주는 setFromBase64도 있습니다.

const target = new Uint8Array(8);
target.setFromBase64("aGVsbG8K");      // { read: 8, written: 6 }

이 API에 의존하기 전에 typeof Uint8Array.prototype.toBase64 === "function"으로 기능을 감지하세요. 모든 옵션은 Uint8Array.toBase64 가이드에서 다룹니다.

패딩 있는 URL-safe Base64와 패딩 없는 URL-safe Base64

새 API에서는 옵션을 넘기면 됩니다.

const d = new TextEncoder().encode("<<???>>");
d.toBase64({ alphabet: "base64url" });                     // "PDw_Pz8-Pg=="
d.toBase64({ alphabet: "base64url", omitPadding: true });  // "PDw_Pz8-Pg"
Uint8Array.fromBase64("PDw_Pz8-Pg", { alphabet: "base64url" }); // bytes of "<<???>>"

btoa/atob에서는 알파벳을 직접 변환하세요.

const toB64Url = b64 =>
  b64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");

const fromB64Url = s => {
  s = s.replace(/-/g, "+").replace(/_/g, "/");
  return s + "=".repeat((4 - s.length % 4) % 4);
};

toB64Url(btoa("<<???>>"));        // "PDw_Pz8-Pg"
atob(fromB64Url("PDw_Pz8-Pg"));   // "<<???>>"

JWT가 바로 이 방식을 씁니다. Base64와 Base64URL 비교를 참고하거나 JWT 디코더에 토큰을 붙여넣어 보세요.

파일, Blob, 큰 데이터

사용자가 고른 파일을 읽을 때 FileReader.readAsDataURL은 Data URI를 반환합니다. 일반 Base64가 필요하면 쉼표까지 잘라 내세요.

const reader = new FileReader();
reader.onload = () => {
  const dataUrl = reader.result;           // "data:text/plain;base64,aGVsbG8K"
  const b64 = dataUrl.slice(dataUrl.indexOf(",") + 1);
};
reader.readAsDataURL(file);

새 API로는 더 짧습니다. new Uint8Array(await file.arrayBuffer()).toBase64().

Base64를 다시 다운로드할 수 있는 파일로 바꾸려면 바이트로 디코딩한 뒤 Blob으로 감싸세요.

const bytes = Uint8Array.fromBase64(b64);
const url = URL.createObjectURL(new Blob([bytes], { type: "application/pdf" }));

큰 버퍼를 String.fromCharCode(...bytes)에 스프레드로 넘기면 절대 안 됩니다. 500,000바이트에서 Chromium은 RangeError: Maximum call stack size exceeded를 던집니다. 대신 조각 단위로 바이너리 문자열을 만드세요.

function bytesToBase64(bytes) {
  let bin = "";
  for (let i = 0; i < bytes.length; i += 0x8000) {
    bin += String.fromCharCode.apply(null, bytes.subarray(i, i + 0x8000));
  }
  return btoa(bin);
}
// 5,000,000 bytes -> 6,666,668 characters

출력은 입력보다 약 3분의 1 큽니다. 정확한 공식은 크기 증가 가이드에 있습니다.

검증과 오류 메시지

atob는 ASCII 공백을 무시하고 패딩 누락을 허용하므로 atob(" aG k= ")와 atob("aGk")는 둘 다 "hi"를 반환합니다. 표준 알파벳 밖의 문자는 거부합니다.

atob("PDw_Pz8-Pg");
// InvalidCharacterError: Failed to execute 'atob' on 'Window':
// The string to be decoded is not correctly encoded.

fromBase64는 SyntaxError를 던지며, lastChunkHandling으로 더 엄격하게 만들 수 있습니다.

Uint8Array.fromBase64("PDw_Pz8-Pg");
// SyntaxError: Found a character that cannot be part of a valid base64 string.

Uint8Array.fromBase64("aGl=");                                   // 2 bytes, accepted
Uint8Array.fromBase64("aGl=", { lastChunkHandling: "strict" });
// SyntaxError: The base64 input terminates with non-zero padding bits.

기본값인 "loose"는 패딩 누락을 허용합니다. "strict"는 올바른 패딩과 0인 패딩 비트를 요구하고, "stop-before-partial"은 스트리밍 디코더를 위해 완성되지 않은 마지막 조각을 남겨 둡니다. 메시지 문구는 브라우저마다 다르지만 오류 이름은 표준입니다. 눈으로 확인하려면 Base64 검사기가 정확한 문제 위치를 강조해 주고, 흔한 Base64 오류에 원인이 정리되어 있습니다.

자주 묻는 질문

btoa가 InvalidCharacterError를 던지는 이유는 무엇인가요?

btoa는 Latin-1 범위(코드 포인트 0–255)의 문자만 받습니다. 이모지, 키릴 문자, 한글을 포함한 CJK 문자가 하나라도 있으면 예외가 납니다. 먼저 TextEncoder로 문자열을 UTF-8 바이트로 바꾸거나 Uint8Array.prototype.toBase64()를 사용하세요.

Uint8Array.toBase64를 프로덕션에서 써도 되나요?

최신 Chrome, Edge, Firefox, Safari에는 들어 있지만 오래된 브라우저와 Node.js 24에는 없습니다. typeof Uint8Array.prototype.toBase64 === "function"으로 기능을 감지하고, 없으면 btoa 도우미 함수로 대체하세요. 자세한 내용은 Uint8Array.toBase64 해설에 있습니다.

atob 결과가 é 대신 é 같은 이상한 문자로 깨지는 이유는 무엇인가요?

atob는 바이트 하나당 문자 하나를 반환하므로, 2바이트 UTF-8 시퀀스가 Latin-1 문자 두 개가 됩니다. 결과를 Uint8Array.from(bin, c => c.charCodeAt(0))과 TextDecoder에 통과시키면 제대로 된 텍스트를 얻을 수 있습니다.

atob로 Base64URL을 디코딩하려면 어떻게 하나요?

-를 +로, _를 /로 바꾼 다음 길이가 4의 배수가 되도록 =를 채우세요. 새 API에서는 Uint8Array.fromBase64(s, { alphabet: "base64url" })을 호출하면 두 단계를 모두 건너뛸 수 있습니다.

계속 읽기