자바스크립트의 btoa와 atob로 Base64 다루기

자바스크립트의 btoa와 atob로 Base64 다루기

자바스크립트로 Base64 인코딩을 다뤄야 할 일이 생기면 가장 먼저 마주치게 되는 함수가 btoa()atob()인데요. 이름이 워낙 짧고 비대칭적이라 어느 쪽이 인코딩이고 어느 쪽이 디코딩인지 헷갈리기 십상입니다.

이번 포스팅에서는 두 함수가 무엇이고 어디에 자주 쓰이는지부터 짚어보겠습니다. 그리고 한글이 들어갈 때의 함정과 현대적인 대안도 함께 살펴보겠습니다.

btoa와 atob는 무엇일까요?

btoa()atob()는 자바스크립트에 내장된 Base64 인코딩과 디코딩 함수입니다. 이름의 의미를 풀어서 외워두면 헷갈리지 않는데요.

  • btoa = “binary to ascii” — 바이트 문자열을 Base64 문자열로 변환 (인코딩)
  • atob = “ascii to binary” — Base64 문자열을 원래 바이트 문자열로 변환 (디코딩)

여기서 두 가지 헷갈리기 쉬운 부분을 짚어두면 이름의 binaryascii가 둘 다 정확한 표현은 아닙니다.

binary는 진짜 이진 데이터(바이트 배열)가 아니라 각 글자가 0~255 범위에 들어가는 문자열(바이트 문자열)을 가리킵니다. 함수가 설계된 1990년대에는 자바스크립트에 바이트 배열(Uint8Array)이 없어서 1바이트짜리 문자 하나로 바이트를 흉내 내는 우회 표현이 필요했던 거죠.

ascii 역시 임의의 ASCII 텍스트가 아니라 Base64 알파벳으로 인코딩된 64가지 ASCII 문자만 쓰는 문자열(Base64 문자열)을 가리킵니다. 즉 풀어 쓰면 “byte string to Base64”가 더 정확한데 Base64 문자가 모두 ASCII 안에 있다 보니 약자에서는 ascii로 묶인 거죠.

btoa("Hello"); // "SGVsbG8="
atob("SGVsbG8="); // "Hello"

여기서 Base64는 이진 데이터를 64가지 안전한 문자(A-Z, a-z, 0-9, +, /)로 표현하는 인코딩 방식인데요. 이메일이나 HTTP 헤더처럼 텍스트만 안전하게 다룰 수 있는 환경에서 이미지나 바이너리 데이터를 주고받기 위해 만들어졌습니다. ASCII 영역을 벗어나지 않는 64자만 사용하기 때문에 어떤 시스템에서도 깨질 걱정 없이 전달할 수 있습니다.

왜 4문자당 3바이트가 될까요?

Base64라는 이름이 말해주듯 64가지 문자를 사용하는데, 이는 2의 6제곱입니다. 즉 한 글자당 6비트의 정보를 담을 수 있는데요.

그런데 컴퓨터의 기본 단위는 8비트(1바이트)이기 때문에 Base64 문자 4개(24비트)가 정확히 3바이트(24비트)에 대응합니다.

원본 3바이트 (24비트)  ↔  Base64 4문자 (24비트)

따라서 Base64로 인코딩하면 데이터 크기가 약 1.33배(4 / 3) 늘어납니다. 원본 길이가 3의 배수가 아니면 끝에 =가 한두 개 붙어 패딩 역할을 합니다.

btoa("A"); // "QQ==" (2개 패딩)
btoa("AB"); // "QUI=" (1개 패딩)
btoa("ABC"); // "QUJD" (패딩 없음)

어디에 자주 쓰일까요?

btoa()atob()는 일상적인 텍스트 인코딩에서는 잘 안 쓰이지만 몇 가지 정해진 자리에서는 여전히 활약합니다.

가장 흔히 보이는 자리는 HTTP Basic 인증 헤더입니다. 사용자명과 비밀번호를 :로 이어서 Base64로 인코딩하는 것이 표준입니다.

const credentials = btoa("admin:password");
fetch("/api/data", {
  headers: { Authorization: `Basic ${credentials}` },
});
// Authorization: Basic YWRtaW46cGFzc3dvcmQ=

또 다른 단골 자리는 Data URI인데요. 작은 이미지나 SVG를 외부 파일 없이 HTML이나 CSS에 직접 박아 넣을 때 Base64로 인코딩합니다.

const svg = '<svg xmlns="http://www.w3.org/2000/svg">...</svg>';
const dataUri = `data:image/svg+xml;base64,${btoa(svg)}`;
// <img src="data:image/svg+xml;base64,..." />

디버깅 용도로는 JWT 토큰의 페이로드를 빠르게 들여다볼 때도 쓸 수 있습니다. JWT는 점(.)으로 구분된 세 부분이 모두 Base64URL로 인코딩되어 있는데요.

const token = "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjMifQ.signature";
const [header, payload] = token.split(".");
console.log(JSON.parse(atob(payload))); // { sub: "123" }

물론 실제 검증에는 라이브러리를 쓰는 것이 안전하지만 디버깅용으로 잠깐 들여다볼 때는 충분합니다.

한글이 들어가면 깨지는 이유

btoa()는 1바이트 문자(Latin-1 범위)만 처리할 수 있습니다. 그래서 한글이나 이모지를 그대로 넣으면 에러가 발생하는데요.

btoa("한글"); // ❌ InvalidCharacterError
btoa("🚀"); // ❌ InvalidCharacterError

원인은 btoa()가 만들어진 시점이 유니코드 이전이기 때문입니다. 당시에는 한 글자가 1바이트 안에 들어간다고 가정하고 만들었기 때문에 다중 바이트 문자가 들어오면 어떻게 처리해야 할지 모릅니다.

해결 방법은 한글을 UTF-8 바이트로 먼저 변환한 뒤 그 바이트들을 Latin-1 문자열로 표현해서 btoa()에 넘기는 것인데요.

function utf8ToBase64(str) {
  const bytes = new TextEncoder().encode(str);
  return btoa(String.fromCharCode(...bytes));
}

function base64ToUtf8(base64) {
  const binary = atob(base64);
  const bytes = Uint8Array.from(binary, (c) => c.charCodeAt(0));
  return new TextDecoder().decode(bytes);
}

const encoded = utf8ToBase64("한글"); // "7ZWc6riA"
const decoded = base64ToUtf8(encoded); // "한글"

이렇게 한 단계를 거치면 한글뿐 아니라 이모지나 어떤 유니코드 문자든 안전하게 다룰 수 있습니다.

더 현대적인 대안

매번 TextEncoder로 우회하는 패턴이 번거롭다 보니 더 편리한 API가 등장했는데요.

Node.js 환경에서는 Buffer가 한글까지 자연스럽게 처리합니다.

Buffer.from("한글").toString("base64"); // "7ZWc6riA"
Buffer.from("7ZWc6riA", "base64").toString(); // "한글"

브라우저에서도 비교적 최근에 표준화된 Uint8Array.prototype.toBase64()Uint8Array.fromBase64()를 쓸 수 있습니다.

const bytes = new TextEncoder().encode("한글");
const base64 = bytes.toBase64(); // "7ZWc6riA"

const decoded = Uint8Array.fromBase64("7ZWc6riA");
new TextDecoder().decode(decoded); // "한글"

이 메서드들은 한글을 직접 입력으로 받지는 않지만 TextEncoderTextDecoder와 짝지어 쓰면 btoaatob보다 훨씬 깔끔하게 Base64를 다룰 수 있습니다.

마치며

지금까지 btoa()atob()가 무엇이고 어디에 쓰이는지부터 한글이 들어갔을 때 우회하는 방법과 현대적인 대안까지 살펴보았습니다.

btoa()atob()는 1990년대에 설계된 함수라 한계가 분명하지만 Basic 인증이나 Data URI처럼 ASCII 범위 안에서 가볍게 쓰는 자리에서는 여전히 가장 직관적인 선택지인데요. 한글이나 이모지가 섞이는 작업이라면 Buffer.from()이나 새로 표준화된 Uint8Array.toBase64()를 우선 고려해보시면 좋겠습니다.

더 자세한 명세는 HTML Living Standard를 참고하세요.

This work is licensed under CC BY 4.0 CC BY

개발자를 위한 뉴스레터

달레가 정리한 AI 개발 트렌드와 직접 만든 콘텐츠를 전해드립니다.

Discord