한번 살펴보기 시작하면 Base64는 어디서나 나타납니다 — JWT 토큰, 데이터 URI, 이메일
첨부 파일, 이진 파일을 담은 API 페이로드. 인코딩 자체는
RFC 4648에
정의되어 있으며 개념적으로는 매우 간단합니다: 임의의 바이트를 취해서 64개의 출력 가능한
ASCII 문자만을 사용하여 표현합니다. 사람들을 힘들게 하는 것은 JavaScript의 구현입니다 —
브라우저와 Node.js에서 다른 API, btoa()를 던지게 만드는 유니코드 함정, 그리고
JWT가 의존하는 URL 안전 변형. 이 가이드는 작동하는 코드와 함께 이 모든 것을 다룹니다.
브라우저에서 btoa()와 atob()
브라우저에는 오래전부터
btoa()와
atob()가
있었습니다. 이름이 헷갈리지만(binary to ASCII와 그 반대), 간단한 문자열에 대한 사용법은
간단합니다:
// Encode a plain ASCII string
const encoded = btoa('hello world');
console.log(encoded); // "aGVsbG8gd29ybGQ="
// Decode it back
const decoded = atob('aGVsbG8gd29ybGQ=');
console.log(decoded); // "hello world"
// A more realistic example — encoding a simple auth token
const credentials = 'apiuser:s3cr3tkey';
const basicAuth = 'Basic ' + btoa(credentials);
// "Basic YXBpdXNlcjpzM2NyM3RrZXk="
// This is exactly what HTTP Basic Authentication usesbtoa()는 모든 문자의 코드 포인트가
≤ 255(Latin-1 범위)인 문자열만 처리합니다. 이모지나 비 Latin 문자를 포함한 문자열을 전달하면
즉시 InvalidCharacterError를 던집니다.
이것은 브라우저 코드에서 가장 일반적인 Base64 버그 중 하나입니다.// ❌ This throws — emoji is outside Latin-1
btoa('Hello 🌍');
// Uncaught DOMException: Failed to execute 'btoa' on 'Window':
// The string to be encoded contains characters outside of the Latin1 range.
// ❌ This also throws — any non-ASCII character will do it
btoa('café');
// Uncaught DOMException: ...브라우저에서 유니코드 안전하게 처리하기
해결책은 먼저 문자열을 UTF-8 바이트로 인코딩한 다음 해당 바이트를 Base64 인코딩하는 것입니다.
클래식 접근법은 encodeURIComponent와 퍼센트 디코딩 트릭을 사용합니다. 현대적인
접근법은 TextEncoder를 사용하며,
모든 현대 브라우저와
Node.js 11+에서 사용 가능합니다:
// ✅ Unicode-safe encode using TextEncoder
function encodeBase64(str) {
const bytes = new TextEncoder().encode(str); // UTF-8 byte array
const binString = Array.from(bytes, byte =>
String.fromCodePoint(byte)
).join('');
return btoa(binString);
}
// ✅ Unicode-safe decode using TextDecoder
function decodeBase64(base64Str) {
const binString = atob(base64Str);
const bytes = Uint8Array.from(binString, char =>
char.codePointAt(0)
);
return new TextDecoder().decode(bytes);
}
// Now emojis and international text work fine
console.log(encodeBase64('Hello 🌍')); // "SGVsbG8g8J+MjQ=="
console.log(decodeBase64('SGVsbG8g8J+MjQ==')); // "Hello 🌍"
console.log(encodeBase64('Héllo café')); // "SMOpbGxvIGNhZsOp"
console.log(decodeBase64('SMOpbGxvIGNhZsOp')); // "Héllo café"이 두 유틸리티 함수를 코드베이스 어딘가에 두고 기본 btoa()가 존재한다는 것을
잊어버리세요. TextEncoder/TextDecoder 쌍이 순수 ASCII를 넘어서는
모든 것에 적합한 도구입니다. 지금 바로
Base64 인코더 도구로 시도해 볼 수 있습니다.
Node.js에서 Buffer.from()
Node.js는 Buffer 클래스를 통해 이에 대한 자체 API를 가지고 있으며, 더 깔끔하게 인코딩/디코딩을 처리합니다. 입력 인코딩을 명시적으로 지정하기 때문에 유니코드 문제가 없습니다:
// Encode string → Base64
const encoded = Buffer.from('Hello 🌍', 'utf8').toString('base64');
console.log(encoded); // "SGVsbG8g8J+MjQ=="
// Decode Base64 → string
const decoded = Buffer.from('SGVsbG8g8J+MjQ==', 'base64').toString('utf8');
console.log(decoded); // "Hello 🌍"
// Practical example — encoding a JSON payload to embed in a config file
const config = {
apiKey: 'sk-prod-abc123',
projectId: 'proj_x9f2k',
region: 'us-east-1'
};
const encodedConfig = Buffer.from(JSON.stringify(config), 'utf8').toString('base64');
// eyJhcGlLZXkiOiJzay1wcm9kLWFiYzEyMyIsInByb2plY3RJZCI6InByb2pfeDlmMmsiLCJyZWdpb24iOiJ1cy1lYXN0LTEifQ==
// Decode and parse it back
const decodedConfig = JSON.parse(
Buffer.from(encodedConfig, 'base64').toString('utf8')
);
console.log(decodedConfig.region); // "us-east-1"btoa()와 atob()는 브라우저 호환성을 위해 Node.js 16+에서도
전역으로 사용할 수 있지만, Buffer API가 Node.js에서 더 관용적이며
Node.js v0.1부터
있었습니다. JSON 특정 인코딩에는 JSON을 Base64로 도구가 빠른 수동 변환에 유용합니다.
URL 안전 Base64 — JWT가 실제로 사용하는 것
표준 Base64는 알파벳에 +와 /를 사용합니다. 이 두 문자는
URL에서 특수 문자입니다 — +는 쿼리 문자열에서 공백을 의미하고, /는
경로 구분자입니다. URL이나 JWT 세그먼트에 Base64가 필요할 때는 URL 안전 변형을 사용합니다:
+를 -로, /를 _로 교체하고
= 패딩을 제거합니다. 이는
RFC 4648 §5에
표준화되어 있으며 모든 JWT 라이브러리가 내부적으로 사용하는 것입니다:
// Convert standard Base64 to URL-safe Base64
function toBase64Url(base64Str) {
return base64Str
.replace(/+/g, '-')
.replace(///g, '_')
.replace(/=+$/, ''); // strip padding
}
// Convert URL-safe Base64 back to standard Base64
function fromBase64Url(base64UrlStr) {
// Restore padding — length must be a multiple of 4
const padded = base64UrlStr + '==='.slice((base64UrlStr.length + 3) % 4);
return padded
.replace(/-/g, '+')
.replace(/_/g, '/');
}
// Encode a string to URL-safe Base64
function encodeBase64Url(str) {
return toBase64Url(btoa(str));
}
// Decode URL-safe Base64 to a string
function decodeBase64Url(str) {
return atob(fromBase64Url(str));
}
// Example: manually inspect a JWT payload
const jwt = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjQyLCJyb2xlIjoiYWRtaW4iLCJpYXQiOjE3MTM0MDAwMDB9.signature';
const [header, payload] = jwt.split('.');
console.log(decodeBase64Url(header));
// {"alg":"HS256","typ":"JWT"}
console.log(decodeBase64Url(payload));
// {"userId":42,"role":"admin","iat":1713400000}그래서 JWT에서 eyJhbGciOiJIUzI1NiJ9 같은 Base64 문자열을 보게 됩니다 —
패딩 없음, 플러스 기호 대신 대시. URL 쿼리 파라미터로 인코딩된 데이터를 보낼 때는 항상
URL 안전 변형을 사용하여 URL이 깨지는 것을 방지하세요.
Base64 디코더 도구는 표준 및 URL 안전 Base64 모두 자동으로 처리합니다.
FileReader API로 파일 인코딩
흔한 브라우저 작업: 사용자가 이미지나 문서를 선택하고, API에 Base64로 전송해야 합니다.
FileReader API는
정확히 이를 위한 readAsDataURL()을 가지고 있습니다 — MIME 유형이 포함된 완전한
데이터 URI를 제공합니다:
// Wrap FileReader in a Promise for easier async usage
function fileToBase64(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => {
// result is "data:image/png;base64,iVBORw0KGgo..."
// Strip the data URI prefix to get just the Base64 string
const base64 = reader.result.split(',')[1];
resolve(base64);
};
reader.onerror = () => reject(new Error('Failed to read file'));
reader.readAsDataURL(file);
});
}
// Hook it up to a file input
const fileInput = document.getElementById('avatarUpload');
fileInput.addEventListener('change', async (event) => {
const file = event.target.files[0];
if (!file) return;
try {
const base64 = await fileToBase64(file);
console.log(`File size: ${file.size} bytes`);
console.log(`Base64 length: ${base64.length} chars`);
// Send to your API
await fetch('/api/users/42/avatar', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ image: base64, mimeType: file.type })
});
} catch (err) {
console.error('Upload failed:', err.message);
}
});원시 Base64만이 아닌 전체 데이터 URI(MIME 유형 접두사 포함)가 필요하다면
.split(',')[1]을 건너뛰고 reader.result를 직접 사용하세요.
대량 파일 변환에는 이미지를 Base64로 도구가
코드 없이 이미지를 처리합니다.
이진 데이터와 Uint8Array 인코딩
때로는 문자열이나 File에서 시작하지 않고 WebCrypto 작업, 캔버스 내보내기, 또는
WebAssembly 모듈의 원시 바이트를 가지고 있을 수 있습니다. 두 환경에서
Uint8Array에서 Base64로, 그리고 다시 돌아오는 방법:
// --- Browser ---
// Uint8Array → Base64 (browser)
function uint8ToBase64(bytes) {
const binString = Array.from(bytes, byte =>
String.fromCodePoint(byte)
).join('');
return btoa(binString);
}
// Base64 → Uint8Array (browser)
function base64ToUint8(base64Str) {
const binString = atob(base64Str);
return Uint8Array.from(binString, char => char.codePointAt(0));
}
// Example: export a canvas as raw PNG bytes → Base64
const canvas = document.getElementById('myCanvas');
canvas.toBlob(blob => {
blob.arrayBuffer().then(buffer => {
const bytes = new Uint8Array(buffer);
const encoded = uint8ToBase64(bytes);
console.log('PNG as Base64:', encoded.slice(0, 40) + '...');
});
}, 'image/png');
// --- Node.js ---
// Uint8Array / Buffer → Base64 (Node.js)
function uint8ToBase64Node(bytes) {
return Buffer.from(bytes).toString('base64');
}
// Base64 → Buffer (Node.js)
function base64ToBufferNode(base64Str) {
return Buffer.from(base64Str, 'base64');
}
// Example: hash a password and encode the result
const crypto = require('crypto');
const hash = crypto.createHash('sha256').update('mySecretPassword').digest();
// hash is a Buffer (which extends Uint8Array)
console.log(hash.toString('base64'));
// "XohImNooBHFR0OVvjcYpJ3NgxxxxxxxxxxxxxA=="데이터 URI로 이미지 임베딩
웹 개발에서 Base64의 가장 실용적인 사용 중 하나는 이미지를 HTML이나 CSS에 직접 임베딩하여 HTTP 요청을 없애는 것입니다. 인라인 SVG나 이메일 템플릿에서 데이터 URI를 보셨을 겁니다. 패턴은 다음과 같습니다:
<!-- Inline image in HTML — no separate network request -->
<img
src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=="
alt="1x1 transparent pixel"
width="1"
height="1"
/>/* Inline background image in CSS — commonly used for small icons and loading spinners */
.spinner {
width: 32px;
height: 32px;
background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+PHBhdGggZD0iTTEyIDJhMTAgMTAgMCAxIDAgMCAyMCAxMCAxMCAwIDAgMCAwLTIweiIvPjwvc3ZnPg==");
background-repeat: no-repeat;
background-position: center;
background-size: contain;
}// Generate a data URI from a fetched image (Node.js)
const fs = require('fs');
const path = require('path');
function imageFileToDataUri(filePath) {
const ext = path.extname(filePath).slice(1).toLowerCase();
const mimeMap = { png: 'image/png', jpg: 'image/jpeg', jpeg: 'image/jpeg',
gif: 'image/gif', svg: 'image/svg+xml', webp: 'image/webp' };
const mimeType = mimeMap[ext] ?? 'application/octet-stream';
const fileData = fs.readFileSync(filePath);
const base64 = fileData.toString('base64');
return `data:${mimeType};base64,${base64}`;
}
const dataUri = imageFileToDataUri('./logo.png');
// "data:image/png;base64,iVBORw0KGgo..."
// Drop this into an <img src> or CSS background-image두 환경에서 모두 작동하는 컴팩트 유틸리티 모듈
코드베이스에 btoa() 호출을 흩뿌리는 것보다, 유니코드와 URL 안전 변형을 처리하고
브라우저와 Node.js 모두에서 작동하는 단일 유틸리티 모듈을 갖는 것이 좋습니다. 다음은 그 모두를 수행하는 것입니다:
// base64.js — drop into any project
const isNode = typeof process !== 'undefined' && process.versions?.node;
export function encode(str) {
if (isNode) {
return Buffer.from(str, 'utf8').toString('base64');
}
// Browser: encode to UTF-8 bytes first, then Base64
const bytes = new TextEncoder().encode(str);
const binString = Array.from(bytes, b => String.fromCodePoint(b)).join('');
return btoa(binString);
}
export function decode(base64Str) {
if (isNode) {
return Buffer.from(base64Str, 'base64').toString('utf8');
}
// Browser: Base64 → bytes → UTF-8 string
const binString = atob(base64Str);
const bytes = Uint8Array.from(binString, c => c.codePointAt(0));
return new TextDecoder().decode(bytes);
}
export function encodeUrlSafe(str) {
return encode(str)
.replace(/+/g, '-')
.replace(///g, '_')
.replace(/=+$/, '');
}
export function decodeUrlSafe(str) {
const padded = str + '==='.slice((str.length + 3) % 4);
return decode(padded.replace(/-/g, '+').replace(/_/g, '/'));
}
export function encodeBytes(bytes) {
if (isNode) return Buffer.from(bytes).toString('base64');
const binString = Array.from(bytes, b => String.fromCodePoint(b)).join('');
return btoa(binString);
}
export function decodeToBytes(base64Str) {
if (isNode) return Buffer.from(base64Str, 'base64');
const binString = atob(base64Str);
return Uint8Array.from(binString, c => c.codePointAt(0));
}// Usage examples
import { encode, decode, encodeUrlSafe, decodeUrlSafe } from './base64.js';
encode('Hello 🌍'); // "SGVsbG8g8J+MjQ=="
decode('SGVsbG8g8J+MjQ=='); // "Hello 🌍"
encodeUrlSafe('[email protected]'); // "dXNlckBleGFtcGxlLmNvbQ" (no +, /, or =)
decodeUrlSafe('dXNlckBleGFtcGxlLmNvbQ'); // "[email protected]"주의해야 할 일반적인 함정
- btoa()는 비 Latin 문자에서 던집니다 — 코드 포인트 255 이상의 문자는
InvalidCharacterError를 발생시킵니다. 항상TextEncoder접근법이나 Node.js에서Buffer.from(str, 'utf8')를 사용하세요. - 패딩은 디코딩에 중요합니다 — Base64 문자열의 길이는 4의 배수여야 합니다. 누락된
=패딩은 브라우저에 따라atob()가 조용히 잘못된 결과를 반환하거나 던지게 합니다. URL 안전 문자열을 디코딩하기 전에 항상 패딩을 복원하세요. - Node.js에서 Buffer와 문자열 인코딩 —
Buffer.from(str)는 기본적으로 UTF-8이지만,Buffer.from(str, 'binary')는 문자열을 Latin-1 바이트로 처리합니다. 디코딩 시 잘못된 인코딩을 사용하면 디버그하기 어려운 깨진 출력이 생성됩니다. - 데이터 URI MIME 유형 —
data:;base64,...(MIME 유형 없음)는 일부 브라우저에서는 작동하지만 다른 브라우저에서는 작동하지 않습니다. 항상 MIME 유형을 포함하세요:data:image/png;base64,.... - MIME Base64의 줄바꿈 — RFC 4648은 구현체가 76자마다 줄바꿈을 삽입할 수 있습니다(이메일 인코더가 하듯).
atob()와Buffer.from()모두 이를 처리하지만, Base64를 직접 생성한다면 대상 시스템이 이를 예상하지 않는 한 줄바꿈을 추가하지 마세요.
마무리
JavaScript에서 Base64는 물릴 때까지 사소해 보이는 주제 중 하나입니다.
짧게 요약하면: 사용자 생성 콘텐츠에 기본 btoa()를 절대 사용하지 마세요 —
유니코드를 올바르게 처리하려면 TextEncoder로 감싸세요. Node.js에서는
Buffer.from(str, 'utf8').toString('base64')가 올바른 관용구입니다.
인코딩된 문자열이 URL이나 JWT에 들어갈 때는 URL 안전 변형으로 전환하세요. 빠른 실험이나
일회성 변환에는
Base64 인코더, Base64 디코더,
JSON을 Base64로 도구가 시간을 절약해줍니다.
MDN Base64 용어집 페이지도
이 중 어느 것이든 두 번째 의견이 필요할 때 좋은 브라우저 중심 참고 자료입니다.