Base64는 공부할 때보다 실무에서 마주칠 때 "아 이게 그거였구나" 하게 되는 기술이에요. 결론부터 말하면 Base64는 텍스트만 다닐 수 있는 통로(HTML·CSS, 이메일, HTTP 헤더, JSON, 설정 파일)에 바이너리를 실어야 할 때 쓰고, 그 대가로 크기가 약 33%(정확히는 4/3배) 늘어나요.
이번 글에서는 원리를 비트 단위로 짧게 보고, 실제로 만나는 다섯 장소를 코드와 함께 짚어 볼게요. 크기 증가는 입력 길이별로, 그리고 gzip·brotli 압축 후에는 어떻게 되는지 직접 재 봤어요. "data URI가 좋냐 나쁘냐" 같은 판단도 그 숫자로 하면 훨씬 명확해져요.
핵심 요약
Base64는 3바이트(24비트)를 6비트씩 4조각으로 나눠 64개 문자로 바꿔요. 그래서 크기가 4/3배, 약 33% 늘어나요.
짧은 입력은 패딩 때문에 증가율이 더 커서 1바이트는 4글자(400%), 10바이트는 16글자(160%)가 돼요.
12.4KB PNG를 직접 재 보니 Base64는 133%였지만 gzip 압축 후엔 차이가 5% 남짓으로 줄었어요.
대표 사용처는 data URI, 이메일 첨부(MIME), HTTP Basic 인증, JSON 속 바이너리, PEM 인증서·키 파일이에요.
원리: 3바이트를 4글자로 바꾸는 과정
Base64는 입력을 3바이트(24비트)씩 끊고, 이걸 6비트씩 네 조각으로 나눠요. 6비트는 0~63을 표현하니 64개 문자 중 하나로 대응시킬 수 있죠. 문자표는 A-Z(0~25), a-z(26~51), 0-9(52~61), 그리고 +, / 예요.
입력 길이가 3의 배수가 아니면 마지막 묶음을 = 로 채워 4글자를 맞추는데, 이게 패딩이에요.
import base64
base64.b64encode(b'Man') # b'TWFu'
base64.b64encode(b'Ma') # b'TWE='
base64.b64encode(b'M') # b'TQ=='
base64.b64encode('한'.encode()) # b'7ZWc' (UTF-8 3바이트 → 4글자)
공식으로 쓰면 출력 길이는 4 × ceil(n / 3) 이에요. 그래서 큰 데이터는 133%에 수렴하지만, 짧은 입력은 패딩 때문에 증가율이 훨씬 커요.
토큰이나 ID처럼 짧은 값을 Base64로 만들 때 "생각보다 길어진다"고 느끼는 이유가 이거예요.
1. data URI — 작은 이미지를 문자열로 박아 넣기
CSS나 HTML에 작은 이미지를 파일 대신 문자열로 직접 넣는 방식이에요. data:image/png;base64,iVBORw0KGgo... 같은 형태죠. 아이콘 몇 개 때문에 HTTP 요청을 늘리지 않으려고 쓰던 기법이에요.
// Node: 파일을 data URI로
const fs = require('node:fs');
const uri = 'data:image/png;base64,' + fs.readFileSync('icon.png').toString('base64');
// 브라우저: 사용자가 고른 파일을 data URI로
const reader = new FileReader();
reader.onload = () => console.log(reader.result); // data:image/png;base64,...
reader.readAsDataURL(fileInput.files[0]);
그럼 33% 커지는 게 실제 전송에선 얼마나 손해일까요? 이 사이트의 12.4KB짜리 PNG 하나로 직접 재 봤어요.
결과가 흥미로워요. Base64는 문자당 6비트 정보만 담으니 압축기가 그 낭비를 대부분 되찾아요. 서버가 HTML·CSS를 gzip이나 brotli로 내려준다면 전송량 손해는 5% 안팎이에요. 그런데도 data URI를 남발하면 안 되는 이유는 따로 있어요.
- 캐시를 못 함: 이미지가 CSS 안에 박히면, 이미지 하나만 바뀌어도 CSS 전체를 다시 받아야 해요.
- 렌더링 차단: CSS 파일이 커지면 첫 화면이 그만큼 늦게 그려져요.
- 디코딩 비용: 브라우저가 문자열을 다시 바이너리로 풀어야 해요.
HTTP/2 이후로는 요청 수 부담이 많이 줄었으니, 1~2KB 이하 아이콘이나 외부 파일을 쓸 수 없는 이메일 템플릿이 아니라면 별도 파일이 낫다는 게 제 기준이에요. SVG 아이콘이라면 Base64 대신 data:image/svg+xml, 뒤에 URL 인코딩한 SVG를 넣는 게 더 작아요.
2. 이메일 첨부 (MIME) — 첨부 용량이 빡빡한 이유
메일 원문 보기를 해 보면 첨부 파일 부분에 Content-Transfer-Encoding: base64 헤더와 함께 알 수 없는 문자열이 길게 이어져요. 그게 첨부 파일이에요. SMTP는 원래 7비트 텍스트용으로 설계돼서 바이너리를 그대로 못 실어요.
MIME 규격(RFC 2045)은 Base64 출력을 76자마다 줄바꿈(CRLF) 하도록 정해요. 그래서 실제 증가율은 133%가 아니라 위 측정처럼 약 137%예요. 10MB 파일을 첨부하면 메일 본문은 13.7MB 가까이 돼요. 메일 서비스의 "첨부 25MB 제한"이 실제로는 18MB 남짓의 파일만 허용하는 것처럼 느껴지는 이유예요.
파이썬 base64.encodebytes() 가 이 76자 줄바꿈 형식을 만들어 주고, b64encode() 는 줄바꿈 없이 한 줄로 만들어요. 둘을 섞어 쓰면 디코딩 쪽에서 줄바꿈 처리 때문에 문제가 생기니, 어떤 형식을 기대하는지 확인하세요.
3. HTTP Basic 인증 — 아이디:비밀번호를 그대로 담는다
Authorization: Basic c21pdGg6cGFzc3dvcmQ= 같은 헤더를 본 적 있을 거예요. 아이디:비밀번호 를 Base64로 인코딩한 값이에요.
const header = 'Basic ' + Buffer.from('smith:password').toString('base64');
// Basic c21pdGg6cGFzc3dvcmQ=
Buffer.from('c21pdGg6cGFzc3dvcmQ=', 'base64').toString(); // 'smith:password'
헤더 값에는 콜론이나 특수문자, 한글이 들어가면 곤란해서 Base64로 감싼 거지, 숨기려는 목적이 아니에요. 그래서 Basic 인증은 반드시 HTTPS 위에서만 써야 해요. 로그에 이 헤더를 그대로 찍는 실수도 조심하세요. 로그 파일이 곧 비밀번호 목록이 돼요. 이 부분은 Base64는 암호화가 아니다 글에서 더 자세히 다뤘어요.
4. JSON 안의 바이너리와 btoa 한글 에러
JSON은 텍스트 포맷이라 바이너리 타입이 없어요. 이미지 썸네일, 서명 값, 파일 조각을 API로 주고받을 때 Base64 문자열로 넣는 게 흔한 해법이에요.
여기서 프런트엔드 개발자가 자주 만나는 에러가 있어요. 브라우저 btoa() 에 한글을 넣으면 터져요.
Uncaught InvalidCharacterError: Failed to execute 'btoa' on 'Window':
The string to be encoded contains characters outside of the Latin1 range.
btoa 는 "문자 하나 = 1바이트"인 Latin1 문자열만 받아요. 한글처럼 1바이트를 넘는 문자는 먼저 UTF-8 바이트로 바꾼 다음 인코딩해야 해요.
// 문자열 → UTF-8 바이트 → Base64
const b64 = btoa(String.fromCharCode(...new TextEncoder().encode('한글')));
// '7ZWc6riA'
// Base64 → 바이트 → 문자열
const text = new TextDecoder().decode(Uint8Array.from(atob(b64), c => c.charCodeAt(0)));
// '한글'
최신 브라우저(2025년 이후 주요 브라우저)는 Uint8Array.prototype.toBase64() 와 Uint8Array.fromBase64() 를 지원하기 시작해서 이 우회가 필요 없어지는 중이에요. 다만 지원 범위를 확인하고 쓰세요. Node.js라면 처음부터 Buffer.from(str).toString('base64') 가 UTF-8로 처리해 줘요.
몇 MB짜리 파일을 Base64로 JSON에 넣는 건 피하세요. 크기가 33% 늘고, 서버와 클라이언트 모두 전체를 메모리에 올려 문자열로 다뤄야 해요. 큰 파일은 multipart 업로드나 미리 서명된 URL(presigned URL) 방식이 맞아요.
5. 인증서와 키 파일 (PEM) — 환경 변수 줄바꿈 함정
-----BEGIN CERTIFICATE----- 로 시작하는 파일, 서버 설정하면서 한 번쯤 봤을 거예요. PEM 형식은 바이너리 인증서(DER)를 Base64로 인코딩하고 64자마다 줄을 바꾼 뒤 앞뒤에 표지를 붙인 거예요. 그래서 텍스트 편집기로 열 수 있어요.
문제는 이걸 환경 변수에 넣을 때예요. 줄바꿈이 \n 이라는 두 글자로 저장되면 Node.js에서 이런 에러가 나요.
Error: error:1E08010C:DECODER routines::unsupported
code: 'ERR_OSSL_UNSUPPORTED'
키가 "지원하지 않는 형식"이라는 메시지라 키 종류 문제로 오해하기 쉬운데, 대부분 줄바꿈이 깨진 거예요. 이렇게 복원하면 돼요.
const pem = process.env.PRIVATE_KEY.replace(/\\n/g, '\n');
crypto.createPrivateKey(pem); // 정상
아예 PEM 전체를 한 번 더 Base64로 감싸서 한 줄로 환경 변수에 넣고, 코드에서 디코딩하는 방법도 많이 써요. 줄바꿈 걱정이 사라지거든요.
다섯 곳 정리 표와 선택 기준
| 쓰이는 곳 | 왜 Base64인가 | 주의할 점 |
|---|---|---|
| data URI | 파일 없이 문자열로 삽입 | 캐시 불가, 1~2KB 이하만 |
| 이메일 첨부 | 7비트 텍스트 전송 구간 통과 | 76자 줄바꿈 포함 약 137% |
| Basic 인증 | 헤더에 특수문자 안전하게 담기 | HTTPS 필수, 로그 노출 |
| JSON 바이너리 | JSON에 바이너리 타입 없음 | btoa 한글 에러, 큰 파일 부적합 |
| PEM 인증서·키 | 텍스트로 다루기 쉽게 | 64자 줄바꿈, 환경 변수 \n |
이 밖에도 JWT, 쿠버네티스 Secret, 웹푸시 키, 소스맵 등 Base64는 정말 많은 곳에 있어요. JWT처럼 URL에 들어가는 곳은 +, / 를 바꾼 Base64URL 변형을 쓰는데, 이건 URL-safe Base64와 패딩 글에서 따로 다뤘어요.
Base64 말고 다른 선택지: hex, Base32와 비교
바이너리를 텍스트로 바꾸는 방법이 Base64만 있는 건 아니에요. 상황에 따라 다른 인코딩이 더 나을 때도 있어요.
| 인코딩 | 문자 수 | 크기 비율 | 장점 | 주 사용처 |
|---|---|---|---|---|
| hex (16진수) | 16 | 200% | 바이트 경계가 눈에 보임, 대소문자 무관 | 해시값, 디버깅 덤프, 색상 코드 |
| Base32 | 32 | 160% | 대소문자 구분 없음, 헷갈리는 글자 제외 | OTP 시크릿, 사람이 받아 적는 코드 |
| Base64 | 64 | 133% | 가장 짧음 | 첨부, data URI, 키·인증서 |
| Base64URL | 64 | 133% (패딩 생략 시 더 짧음) | URL·파일명에 안전 | JWT, URL 토큰 |
SHA-256 해시를 hex로 쓰면 64자, Base64로 쓰면 44자예요. 사람이 눈으로 비교하거나 로그에서 grep할 값이면 hex가 편하고, 저장 공간이나 URL 길이가 중요하면 Base64가 유리해요. 구글 OTP 같은 2단계 인증 앱의 시크릿 키가 Base32인 건, 사용자가 손으로 받아 적을 때 0 과 O, 1 과 l 을 헷갈리지 않게 하려는 배려예요.
어떤 문자열이 Base64인지 의심될 때는 세 가지를 보면 돼요. 길이가 4의 배수인지, 끝에 = 가 0~2개 붙어 있는지, 문자가 A-Z a-z 0-9 + / 범위 안인지. 셋 다 맞으면 거의 확실히 Base64고, - 나 _ 가 섞여 있으면 Base64URL이에요.
자주 묻는 질문
Base64와 hex 중 무엇을 써야 하나요?
크기가 중요하면 Base64(133%), 사람이 읽고 비교하기 쉬워야 하면 hex(200%)가 좋아요. 해시값을 로그나 문서에 남길 땐 hex가 관례이고, 바이너리 파일이나 키를 JSON·설정에 넣을 땐 Base64가 일반적이에요.
Base64로 인코딩하면 용량이 왜 늘어나나요?
3바이트(24비트)를 6비트씩 4글자로 나타내기 때문이에요. 한 글자가 1바이트를 차지하는데 정보는 6비트만 담으니 4/3배, 약 33% 커져요. 짧은 입력은 패딩 때문에 증가율이 더 크고, MIME 형식은 줄바꿈까지 더해져 약 37% 늘어나요.
data URI로 이미지를 넣으면 페이지가 빨라지나요?
아주 작은 아이콘 몇 개라면 요청 수를 줄여 도움이 될 수 있지만, HTTP/2 이후엔 효과가 작아요. gzip 압축 후 전송량 손해는 5% 안팎이지만 캐시를 못 하고 CSS가 커져 첫 렌더링이 늦어질 수 있어요. 1~2KB를 넘는 이미지는 별도 파일이 대체로 나아요.
btoa에서 한글을 넣으면 에러가 나는 이유는 무엇인가요?
btoa 는 각 문자가 1바이트인 Latin1 문자열만 받기 때문이에요. 한글은 먼저 TextEncoder 로 UTF-8 바이트를 얻은 뒤 인코딩해야 해요. Node.js에서는 Buffer.from(문자열).toString('base64') 를 쓰면 바로 돼요.
PEM 파일의 Base64를 디코딩하면 무엇이 나오나요?
DER이라는 바이너리 형식의 인증서나 키가 나와요. 사람이 읽을 수 있는 텍스트가 아니라 ASN.1 구조의 바이트라서, 내용을 보려면 openssl x509 -in cert.pem -text -noout 같은 명령을 써야 해요.
정리
Base64는 텍스트 통로에 바이너리를 싣기 위한 포장이에요. 3바이트가 4글자가 되니 약 33% 커지지만, gzip·brotli를 거치면 전송량 차이는 5% 안팎으로 줄어요. 진짜 비용은 캐시 불가, 메모리, 디코딩 쪽에 있어요. data URI는 작은 아이콘에만, 큰 파일은 JSON 대신 multipart로, PEM은 줄바꿈을 조심하면 대부분의 함정을 피할 수 있어요.
어떤 Base64 값이 무엇을 담고 있는지 궁금하면 Base64 인코더/디코더에 붙여 보세요. 한글 바이트 수가 어떻게 계산되는지는 UTF-8과 EUC-KR 한글 바이트 글이 도움이 될 거예요.