Base64 형식을 다루어야 하나요? 그러면 여러분에게 이 웹사이트가 딱 맞네요! 저희 웹사이트의 아주 편리한 온라인 도구를 사용하여 데이터를 인코딩하거나 디코딩해보세요.

Python에서의 Base64 인코딩: 완전한 가이드

이것이 동전의 다른 면입니다. 당신 손에는 데이터가 있습니다. 파일, 비밀번호 쌍, 바이너리 덩어리, 유니코드 한 단락. 그리고 어딘가 앞 단계에서, 그것은 글자밖에 받아주지 않는 채널을 지나가야 합니다. 이것이 Base64의 온전한 일입니다. 데이터 3바이트를 64자 알파벳의 문자 4개로 다시 쓰고, 마지막 그룹을 =로 채워 올려 모든 것이 4의 배수로 나오게 한 뒤, 그 글자들을 건네는 것. 이 사이트의 홈 페이지는 포맷을, 알파벳 포함해 온전히 설명하므로, 그 부분은 숨을 한 번 쉬는 정도로만 하고, 나머지 시간은 Python이 실제로 무엇을 하는 데 씁니다.

시작 전에 경제성에 대해 정직한 한 문장을 남깁니다. 이 주제에 대한 모든 대화에 그 숫자가 나오기 때문이죠. 텍스트 안전성의 값은 크기입니다. Base64는 데이터를 대략 3분의 1 불립니다. 입력 바이트 3개당 문자 4개이므로, 1메가바이트의 바이너리는 글자로 1메가바이트와 3분의 1이 됩니다. 토큰이나 설정 값에는 아무 문제가 없지만, 비디오 파일이라면 당신의 선택지를 생각해야 하는 이유입니다.

그리고 좋은 소식: Python의 답은 임포트 한 줄, 함수 하나입니다. base64.b64encode는 수십 년째 표준 라이브러리에 있고, 설치할 것도 없으며, 밑에서는 C 속도로 돌아갑니다. 이 가이드의 나머지는, 이 원라이너를 현실에서 유용하게 만드는 긴 꼬리입니다. 모든 TypeError 버그의 절반을 막아 주는 bytes 전용 규칙, URL-safe 알파벳, MIME 줄바꿈 도구, 그리고 Base64가 조용히 자기 일을 하고 있는 프로토콜들. JWT, HTTP 헤더, WebSocket 핸드셰이크, 이메일, PEM, data URL.

b64encode를 만나자

계약은 네 문장에 들어갑니다. 하나: 입력은 바이트 계열 객체, bytes, bytearray, memoryview이며, 평범한 문자열은 거부됩니다. 둘: 출력은 bytes 객체이며, 절대 str가 아닙니다. 셋: 출력은 항상 4문자의 배수로 패딩되므로, 입력 바이트 하나조차 Zg==를 만듭니다. 넷: 출력은 딱 한 줄이며, 입력이 아무리 커도 줄바꿈되지 않습니다. 이 글의 나머지는 이 네 문장에 대한 주석입니다:

import base64
encoded = base64.b64encode(b"foobar")
print(encoded)
# b'Zm9vYmFy'
print(len(encoded))
# 8

URL이나 헤더, JSON 필드를 위해 진짜 문자열을 얻으려면, 결과를 ASCII로 디코딩하세요. 알파벳이 다른 것이 들어 있을 수 없다는 것을 보장하므로, 이 단계는 안전하고 값싸습니다:

import base64
text = base64.b64encode(b"foobar").decode("ascii")
print(text)
# Zm9vYmFy

시그니처에는 인자가 하나 더 있습니다. altchars인데, 표준 알파벳의 +과 /를 다른 글자 쌍으로 바꿉니다. 정확히 URL-safe 변형 뒤의 그 손잡이이므로, 그 생각을 붙잡아 두세요. 토큰과 쿼리 문자열을 이야기하는 몇 섹션 뒤에 다시 만날 테니깐요.

타입의 벽: str은 bytes가 아니다

이 글에서 첫 번째 Python 특유의 벽은 타입 시스템이며, 그것을 감으로 익힐 가치가 있습니다. b64encode는 이 언어에서 가장 관대하지 않은 에러 메시지 중 하나로 문자열을 거부합니다:

import base64
try:
  base64.b64encode("hello")
except TypeError as caught:
  print(caught)
# a bytes-like object is required, not 'str'

해결책은 이 가이드 전체에서 가장 중요한 습관 하나입니다. 텍스트를 먼저 바이트로 바꾸고, 기대하기 대신 인코딩을 의도적으로 고르세요:

import base64
print(base64.b64encode("été".encode("utf-8")))
# b'w6l0w6k='
print(base64.b64encode("été".encode("utf-16")))
# b'//7pAHQA6QA='

같은 문자, 서로 다른 바이트 문자열 두 개, 서로 다른 Base64 출력 두 개. 인코딩 선택은 디테일이 아니라 결정입니다. 와이어, 데이터베이스, API를 건너는 모든 것의 기본값은 UTF-8입니다. UTF-16은 Windows API와 대화할 때 나타납니다. 이때 앞에 바이트 순서 마크(BOM)를 데려오는데, 이것까지 인코딩하고 싶지 않을 수 있습니다. utf-16-le를 쓰거나 lstrip("\ufeff")로 잘라 내면 됩니다. Latin-1은 여전히 오래된 유럽 파일 속에 숨어 있고, 거기서는 한 문자가 정확히 한 바이트이므로 이 질문 자체는 아예 생기지 않습니다. 유지할 정신적 모델: 인코더는 당신의 텍스트를 본 적이 없습니다. 보는 것은 항상 비트뿐입니다. 바이트가 벽을 건넜던 순간, 인코딩의 질문은 닫힙니다. 그리고 바로 이것이, 디코딩 쪽이 나중에 그 인코딩이 누구의 것인지 다시 물어봐야 하는 이유이기도 합니다.

base64url: 두 글자 바꾸고, 패딩 뺄 것

표준 알파벳에는 URL과 파일 시스템이 싫어하는 두 문자가 숨어 있습니다. + 기호는 어떤 폼 디코더든 조용히 공백으로 읽습니다. / 기호는 경로 구분자입니다. 그래서 쿼리 문자열이나 파일명에 있는 표준 알파벳 페이로드는 틱틱 소리가 나는 폭탄입니다. RFC 4648의 5절은 그 해결책을 정의합니다. +가 -가 되고 /가 _가 되는 변형. 데이터 길이를 컨텍스트에서 알 수 있을 때는 패딩을 빼는 변형. RFC는 이것을 "base64"가 아니라 base64url이라고 부를 것을 못 박습니다. JSON Web Token, OAuth 토큰, API 커서 파라미터에서 만나게 되는데, 달리 말하면 현대 웹의 대부분에서입니다.

Python은 전용 함수와 첫 섹션의 altchars 손잡이, 둘 다 들어 줍니다. 둘은 동일한 출력을 만듭니다:

import base64
data = b"\xfb\xff\xfe"
print(base64.b64encode(data))
# b'+//+'
print(base64.urlsafe_b64encode(data))
# b'-__-'
print(base64.b64encode(data, altchars=b"-_"))
# b'-__-'

토큰과 쿼리 문자열에서는 패딩도 보통 같이 가 버립니다. 끝에 붙은 =는 퍼센트 인코딩이 필요하고, 어떤 미들박스에서는 어쨌든 그것을 깨뜨리니까요:

import base64
padded = base64.urlsafe_b64encode(b"fooba")
print(padded)
# b'Zm9vYmE='
print(padded.rstrip(b"="))
# b'Zm9vYmE'

제거하고, 보내세요. 수신 쪽은 나머지 연산 트릭, "=" * (-len(s) % 4)로 패딩을 다시 붙여 줍니다. 길이가 요구하는 만큼 정확히. 실전 규칙: 데이터가 URL, 파일명, JWT 안에 앉을 것이라면 urlsafe 변형을 쓰고 패딩을 빼세요. 이메일 본문이나 텍스트 파일 안에 앉을 것이라면, 패딩을 단 표준 알파벳이 정석입니다.

독자가 줄을 원할 때: MIME과 76자 규칙

b64encode의 끝없는 한 줄은 JSON 필드, 헤더, URL에 완벽합니다. 하지만 이메일에는 의견이 있습니다. MIME 표준인 RFC 2045는 Base64 출력을 76자 이하의 줄로 끊어야 한다고 요구하며, Python의 레거시 도구는 정확히 그것을 만들도록 만들어졌습니다. Python 3.1에 추가된 encodebytes는 바이트 객체의 줄바꿈을 해 줍니다:

import base64
wrapped = base64.encodebytes(b"x" * 100)
for line in wrapped.splitlines():
  print(len(line), line[:12])
# 76 eHh4eHh4eHh4
# 60 eHh4eHh4eHh4

그 작동 방식은 꽤 기발합니다. 이 모듈은 57바이트 청크, 즉 상수 MAXBINSIZE로 인코딩합니다. 57바이트가 정확히 76자가 되니까요. 그리고 현대 CPython에서는 각 줄바꿈 줄이 단순한 라인 피드로 끝납니다. RFC 2045는 CRLF를 요구했지만, Python의 LF 출력은 Python 자신의 것을 포함해 생태계의 모든 디코더에게 받아들여집니다. 레거시 파일-대-파일 함수 encode는 같은 줄바꿈을 한 파일 핸들에서 다른 파일 핸들로 직접 해 줍니다. 메모리에 두 번 담고 싶지 않은 큰 파일에게는 깔끔한 도구가 되는 것이죠.

어떤 도구를 언제 쓸지, 요약하면: JSON 필드, URL, 헤더, 데이터베이스 컬럼에 들어가는 것은 b64encode; 이메일 본문과 PEM 스타일 갑옷은 encodebytes; 큰 파일을 스트리밍하면서 줄바꿈을 공짜로 얻고 싶다면 레거시 encode. 그릇된 것을 고르는 것은 클래식한 버그입니다. JSON 필드 안의 줄바꿈 한 줄이면, 반대편의 엄격한 디코더가 예외를 던지기에 충분하니까요.

확장된 가문

base64 모듈은 사실 base-N 모듈입니다. RFC 4648 가문 전체를 싣고, 컴퓨터 세계의 다른 구석들에서 온 사촌들도 두 명 태우고 있습니다. 대부분은 같은 바이트-입력-바이트-출력 계약의 한 줄짜리 교체품입니다:

함수 알파벳 어디서 만나나
b16encode / b16decode 0-9A-F "Base16"은 그저 16진수일 뿐. 모듈에서 왕복이 가장 빠른 것, 해시와 UUID에 훌륭하다
b32encode / b32decode A-Z2-7 라이센스 키와 활성화 코드. 0, O, 1, I가 없으니 소리 내어 읽혀도 살아남는다
b32hexencode / b32hexdecode 0-9A-V 16진수 알파벳을 쓴 Base32, Python 3.10에 추가. 인코딩된 데이터를 사전순으로 정렬할 수 있게 해 준다
a85encode / a85decode 표시 가능한 문자 85개 PostScript와 PDF의 ASCII85, Unix btoa 유틸리티의 후예. Python 3.4부터 모듈에
b85encode / b85decode 표시 가능한 문자 85개 git과 Mercurial 바이너리 diff가 쓰는 Base85 포맷. 이것도 Python 3.4부터
z85encode / z85decode 표시 가능한 문자 85개 ZeroMQ의 Z85, Python 3.13에 추가. 데이터를 4바이트 그룹으로 프레임한다

그들은 당신이 이미 배운 규칙을 바꾸지 않습니다. 바이트가 들어가 바이트가 나가고, 고를 알파벳, 그리고 반대편에서 기다리는 짝 디코딩 함수. 실제로는 사람이 값을 읽을 수 있어야 할 때 b16을, 값을 손으로 치거나 말해야 할 때 b32를, 85자 사촌들은 스펙이 그렇게 말할 때에만 꺼냅니다. 그 밖의 모든 것에는, 이 글의 시작인 Base64 쌍이 옳은 도구입니다. 그리고 그 나머지 모든 것이 그 위에 세워져 있는 도구이기도 하죠.

페이지 속 이미지: Data URL

웹에서 가장 눈에 띄는 Base64는 data: URI입니다. 브라우저가 두 번째 요청을 보내지 않도록, 미디어를 HTML이나 CSS 안에 직접 넣는 것이죠. 포맷은 data:, 미디어 타입, base64라는 단어, 콤마, 그리고 인코딩된 바이트. 디스크의 파일로 하나를 만드는 것은 3줄 작업입니다:

import base64
with open("logo.png", "rb") as handle:
  encoded = base64.b64encode(handle.read()).decode("ascii")
uri = "data:image/png;base64," + encoded
print(uri[:40])
# data:image/png;base64,iVBORw0KGgoAAAAN...

주의할 것 두 가지, 둘 다 지키기 쉽습니다. 첫째, 브라우저는 data URI를 기꺼이 렌더링하고, 그중 메가바이트 단위도 문서에 기꺼이 담습니다. 몇 KB를 넘는 것이라면, 적당한 캐시 헤더를 단 평범한 이미지 요청이 중요한 모든 지표에서 이깁니다. 둘째, 콜론 뒤의 미디어 타입은 약속입니다. 바이트가 JPEG라면, URI는 image/jpeg라고 말합니다. 어떤 툴링은 그 쌍을 검증하고, 어떤 렌더러는 추측하는 것 자체를 거부하니까요. .decode("ascii") 단계도 장식은 아닙니다. 없으면 bytes 객체를 문자열에 이어 붙이는 셈이고, 그 대가로 TypeError를 거두게 되죠. 타입의 벽이 순회를 나온 것입니다.

발급할 수 있는 토큰: JWT

JSON Web Token은 마침표로 연결된 base64url 세 조각입니다. 헤더, 페이로드, 서명. 진짜 토큰을 발행한다면, 조각을 손으로 구부리지 마세요. PyJWT(pip install pyjwt)를 설치하고, base64url 부분, 패딩, 서명을 한 번의 호출로 만들게 하세요:

import jwt
# 32바이트 미만의 키는 PyJWT의 InsecureKeyLengthWarning을 받습니다, 데모 키에게는 합당한 불평이죠.
token = jwt.encode(
  {"sub": "1234567890", "name": "John Doe"},
  "super-secret-key",
  algorithm="HS256"
)
print(token)
# eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIi...
print(type(token))
# <class 'str'>

밑에서 PyJWT는 정확히 이 글이 설명하는 일을 합니다. JSON으로 직렬화하고, urlsafe 인코더를 통과시키고, 패딩을 제거하는 것. RFC 7515의 JWS 정의에 따르는 것이죠. 테스트 픽스처나 디버깅 세션을 위해 조각을 손으로 조립해야 하는 순간이 온다면, 레시피는 어디든 같은 산술입니다:

import base64
import json
payload = json.dumps({"sub": "1234567890"}).encode("ascii")
part = base64.urlsafe_b64encode(payload).rstrip(b"=")
print(part)
# eyJzdWIiOiAiMTIzNDU2Nzg5MCJ9

신뢰의 방향에 대해 주석 하나: 토큰을 만드는 것은 쉬운 절반입니다. 수신자는 하나의 클레임도 신뢰하기 전에 서명을 검증해야 하며, PyJWT 2.x는 명시적인 algorithms 목록이 없으면 토큰을 디코딩하지 않습니다. 이것은 기능이죠. "어떤 알고리즘이든" 실수란, 지금까지 쓰인 인증 코드에서 가장 비싼 줄 중 하나이니까요.

HTTP: Basic 인증과 WebSocket 핸드셰이크

HTTP에서 Base64에 목숨을 거는 순간이 두 개 있습니다. 첫 번째는 이 프로토콜에서 가장 오래된 인증 방식입니다. Basic 인증(RFC 7617)으로, 클라이언트가 user:pass를 base64 인코딩해 Basic이라는 단어 뒤에 싣어 보내죠:

import base64
credentials = base64.b64encode(b"jane:pa:ss").decode("ascii")
header = "Basic " + credentials
print(header)
# Basic amFuZTpwYTpzcw==

requests가 이미 스택에 들어 있다면, auth=("jane", "pa:ss")로 이 헤더를 대신 만들어 줍니다. 인코딩 디테일을 코드 밖으로 꺼내 주는 것이니, 쓰느 값이 있죠. 그리고 지금 무슨 일이 일어나고 있는지 정직하게 직시하세요. RFC 7617은 불퉁하게 말합니다. 이 방식은 "사용자 인증의 안전한 방법이 아니며, 평문으로 전송되는 엔티티를 어떤 방식으로라도 보호하지도 않는다"고요. 자격 증명은 트래픽을 보는 누구나 코드 한 줄로 되찾을 수 있으므로, 이것은 보안 경계가 아니라 TLS로 보호된 연결을 위한 편의 기능입니다.

두 번째 순간은 WebSocket 핸드셰이크(RFC 6455)입니다. 서버는 매직 GUID에 붙인 키의 SHA-1 해시의 Base64로 답하면서, 클라이언트의 랜덤 키를 읽었다고 증명합니다:

import base64
import hashlib
key = "dGhlIHNhbXBsZSBub25jZQ=="
magic = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
accept = base64.b64encode(
  hashlib.sha1((key + magic).encode("ascii")).digest()
).decode("ascii")
print(accept)
# s3pPLMBiTxaQ9kYGzzhZRbK+xOo=

출력은 RFC 자체의 풀이 예제에서 온 정확한 값입니다. 처음부터 만든 구현을 확인하는 멋진 방법이죠. 프로덕션에서는 websockets 라이브러리가 이 단계를 양쪽 끝에서 대신 해 줍니다. 당신이 손으로 만드는 순간은, 당신의 이해를 증명하는 작은 테스트 서버를 쓸 때뿐입니다.

이메일: 원조 고객

Base64가 1993년에 표준화된 이유는 한 가지 일 때문이었습니다. 그리고 그 일은 이메일이었습니다. RFC 2045의 Content-Transfer-Encoding: base64에 따라, 바이너리를 텍스트만의 SMTP 세계에서 살아남게 만드는 것. Python의 email 패키지는 당신이 Base64를 한 줄도 쓰지 않아도, 메시지를 만들고, 인코딩을 고르고, 본문을 표준 줄 길이에서 줄바꿈해 줍니다:

import email.mime.multipart
import email.mime.application
msg = email.mime.multipart.MIMEMultipart()
msg["Subject"] = "binary payload"
part = email.mime.application.MIMEApplication(
  b"\x00\x01\x02", _subtype="octet-stream"
)
msg.attach(part)
text = msg.as_string()
print(text)
# ...
# Content-Transfer-Encoding: base64
#
# AAEC
# --...

흥미로운 것은 MIMEApplication 파트입니다. 바이트를 정확한 76자 줄로 줄바꿈하고, 전송 인코딩 헤더를 찍어 주는데, 이는 앞서 본 encodebytes의 동작을 프레임워크가 적용한 것입니다. 완전한 메시지가 아니라 맨 단편을 조립 중이라면, email.encoders.encode_base64(obj)가 메시지 객체에서 인코딩과 줄바꿈을 한 번에 해 줍니다. 그리고 메시지가 반대편에 도착하면, 이야기의 디코딩 쪽, 즉 get_payload(decode=True)부터 헤더 디코딩까지는, 관련 디코딩 글에서 다룹니다.

키와 인증서의 PEM 갑옷

PEM 파일, -----BEGIN ...-----로 시작하는 인증서와 개인 키, CRL은 Base64 주위의 갑옷일 뿐입니다. 라벨 줄, 줄바꿈된 Base64, 닫는 라벨. 갑옷은 보기 쉬운 편입니다. 본체는 이미 만난 줄바꿈 출력이 그대로이니까요:

import base64
der = b"\x30\x03\x02\x01\x05"   # 설명용 작은 DER 덩어리
body = base64.encodebytes(der).decode("ascii")
armor = ("-----BEGIN CERTIFICATE-----\n"
         + body
         + "-----END CERTIFICATE-----")
print(armor)
# -----BEGIN CERTIFICATE-----
# MAMCAQU=
# -----END CERTIFICATE-----

라벨 줄 두 개를 걷어 내고, 나머지를 이어 붙이면, b64decode가 DER 바이트를 돌려 줍니다. 프로덕션에서는 거의 손으로 하지 않습니다. cryptography 패키지(pip install cryptography)가 public_bytes로 갑옷을 생성하고, load_pem_x509_certificate 일행으로 그것을 파싱하며, Base64 단계를 밑에서 대신 해 주니까요. 수동 경로의 가치가 빛나는 순간은, 원시 DER 바이트가 이미 당신의 손에 있을 때입니다. 데이터베이스 컬럼, 설정 파일, 프로토콜에서 온 버퍼. 그리고 당신의 앞에 놓인 스펙이 "PEM으로 주세요"라고 말할 때.

파일 보내기: 업로드, 다운로드, 그리고 .b64 습관

인터넷에서 가장 오래된 사용 사례는, 텍스트만 실어 나르는 채널을 건너야 하는 바이너리 파일입니다. 줄 끝을 깨뜨리는 FTP, 업로드를 거부하는 폼, 바이너리를 삼키는 채팅 창. 레시피는 읽기, 인코딩, 보내기, 그리고 반대편에서 디코딩하게 하는 것인데, 흥미로운 절반은 그 안의 두 단계입니다:

import base64
with open("photo.png", "rb") as src:
  data = src.read()
wrapped = base64.encodebytes(data)
with open("photo.b64", "wb") as dst:
  dst.write(wrapped)
print(len(wrapped), "bytes on disk for", len(data), "in the photo")
# 원본보다 약 3분의 1 크다

메모 두 가지. .b64 확장자는 표준이 아니라 커뮤니티 관례입니다. 그래서 수신 쪽도 그 관례를 알아야 합니다. JSON API가 보통 페이로드를 "image_base64" 같은 이름 붙은 필드로 감싸고 문서에 그렇게 명시하는 이유가 바로 그것입니다. 그리고 encodebytes의 줄바꿈된 76자 줄은 디스크 상 파일의 선택 포맷입니다. 메일 클라이언트에서 PDF 리더까지, 인간이 가진 모든 텍스트 도구에서 깨끗하게 복사-붙여넣기되니까요. 반대 방향, 그런 파일을 다시 바이트로 읽는 것은, 관련 디코딩 글의 한 호출입니다. 여기서는 당신은 오직 송신자이고, 송신자의 일은 일관성을 지키는 것입니다.

저장 문제: 설정 파일, 환경 변수, 데이터베이스

개발자들은 Base64를 텍스트만 받아 주는 곳에 넣는 것을 좋아합니다. .env 파일, .ini 설정, TEXT 컬럼. 인코딩 단계는 자명하고, 가장 흔한 모양은 Base64 속 JSON입니다:

import base64
import json
config = {"api_user": "svc-bot", "api_pass": "hunter2-not-really"}
packed = base64.b64encode(
  json.dumps(config).encode("utf-8")
).decode("ascii")
print(packed)
# eyJhcGlfdXNlciI6ICJzdmMtYm90IiwgImFwaV9wYXNzIjogImh1bnRlcjItbm90LXJlYWxseSJ9

그리고 경고입니다. 이 글 전체에서 가장 비싼 오해가 여기에 살고 있으니까요. Base64는 버티는 흐려놓기도 아니고, 암호화도 아닙니다. RFC 4648의 12절은 RFC가 할 수 있는 한 명백하게 말합니다. base 인코딩은 "비밀번호처럼 쉽게 알아볼 수 있는 정보를 시각적으로 가리지만, 계산적인 기밀성은 어떤 것도 제공하지 않는다"고요. Base64 시크릿을 담은 .env 파일은 그것을 흘겨보는 사람으로부터 당신을 보호하지만, 읽는 사람으로부터는 보호하지 않습니다. base64 -d 명령 한 번 뒤에 "시크릿"은 이미 평문으로 그의 터미널에 앉아 있습니다. 데이터가 진짜로 민감하다면, 먼저 암호화하세요. cryptography 패키지는 정확히 이를 위해 Fernet을 함께 주므로. 그리고 나서야, 저장소가 텍스트를 요구한다면 암호문을 Base64로 만드세요.

100만 바이트 이후: 빅데이터와 청킹

b64encode는 C 속도의 함수입니다. 보통 랩톱에서 메가바이트 하나를 밀리초 하나쯤에 처리하죠. 하지만 이것은 스트리밍 함수가 아닙니다. 표준 라이브러리 어디에도 update()를 하고 finish()로 끝내는 짝이 없으니, 메모리에 담고 싶은 것보다 큰 데이터를 인코딩한다는 것은 경계 산술을 직접 한다는 뜻입니다. 입력 바이트 3개가 출력 문자 4개이므로, 어떤 청크 경계도 3바이트 이음매 위에 올라가야 합니다:

import base64
def encode_chunks(chunks):
  out = []
  leftover = b""
  for chunk in chunks:
    buffer = leftover + chunk
    whole = len(buffer) // 3 * 3
    if whole:
      out.append(base64.b64encode(buffer[:whole]))
    leftover = buffer[whole:]
  if leftover:
    out.append(base64.b64encode(leftover))
  return b"".join(out)
with open("video.mp4", "rb") as handle:
  encoded = encode_chunks(iter(lambda: handle.read(65536), b""))

출력은 파일 전체를 한 번의 호출로 인코딩한 것과 바이트 단위로 동일합니다. 그룹이 끊어질 수 있는 곳은 3바이트 이음매뿐이니까요. 패딩은 정확히 한 번, 마지막 청크에 등장합니다. 반대편의 엄격한 디코더가 기대하는 것이죠. iter(lambda: handle.read(65536), b"") 줄은 파일을 고정 크기 조각으로 읽는 표준 관용 표현이며, leftover 변수가 알고리즘 전체입니다. 디코딩 쪽은 3바이트 대신 4문자 이음매를 지키므로, 두 글은 산술을 나누어 담고 반복하지 않습니다.

인코더가 헷갈리는 곳

인코딩 쪽은 디코딩 쪽보다 함정이 적습니다. 글자를 만드는 쪽이 당신이니까, 잘못될 것이 적으니까요. 그래도 이것들은 매주 나타납니다. 그리고 일찍 알아채기만 하면, 전부 2분이면 고치는 법이 있습니다:

  • 인코더에 문자열을 넣었다. 타입의 벽 섹션의 TypeError. 소스에서 .encode("utf-8")로 고치세요. 그리고 치기 전에, 당신이 진짜로 어떤 인코딩을 의미하는지 생각해 보세요.
  • 출력이 바이트인 것을 잊었다. b64encode는 바이트를 반환합니다. URL이나 JSON 필드에 들어가는 str는 그것에서 오는 것이므로, .decode("ascii") 단계는 레시피의 일부이지, 사후 생각이 아닙니다.
  • URL-safe 교체를 손으로 했다. str.replace("+", "-").replace("/", "_")는 동작합니다. 하지만 urlsafe_b64encode가 한 호출인 곳에, 두 글자의 유지보수 부채를 지는 셈입니다. 더 나쁜 것은, 더하기는 고치고 슬래시는 잊은 반쯤 된 교체가, 어떤 스펙과도 일치하지 않는 알파벳을 만든다는 것입니다.
  • URL에 패딩을 남겨 두었다. 쿼리 문자열 안의 끝에 붙은 =는 어떤 도구에는 퍼센트 인코딩되고, 어떤 도구에는 제거됩니다. 그리고 수신 쪽의 패딩 산술은 가장 혼란스러운 방식으로 깨집니다. 제거하세요. 길이가 디코더에게 필요한 모든 것을 말합니다.
  • 안 맞는 곳에 줄바꿈을 했다. encodebytes의 줄바꿈은 이메일과 PEM에는 맞고, JSON 필드나 URL에는 독입니다. 새어 들어온 줄바꿈 한 줄이면, 반대편의 엄격한 디코더가 당신의 포맷이 아니라 데이터에 대해 예외를 던지기에 충분합니다.
  • 두 번 인코딩했다. 데이터는 이미 앞 단계에서 Base64였습니다. 다른 API에서 미리 인코딩되어 온 필드, .b64 처리를 두 번 받은 파일. 두 번째 인코딩은 디코딩하면 첫 인코딩으로 돌아가는 문자열을 만듭니다. 왕복은 한 번, 매직 바이트를 확인하고, 멈추세요.
  • 시크릿을 Base64에게 맡겼다. 저장 섹션의 경고, 실제로 돈을 쓰니까 반복합니다. 위협 모델에 파일을 읽는 누군가가 들어 있다면, 당신에게 필요한 것은 알파벳이 아니라 암호입니다.

실제로 읽을 수 있는 변경 로그

모듈의 나이는 혁명이 아니라, 조용하고 날짜가 찍힌 개선을 통해 드러납니다. 인코더의 관점에서, 조각들이 도착한 순서의 짧은 버전:

버전 일어난 일
Python 2.4 (2004) Barry Warsaw의 완전한 RFC 3548 지원이 출하: b16, b32, b64 가문, 그리고 오늘 쓰는 standard_*과 urlsafe_* 변형
Python 3.1 (2009) encodebytes가 도착하고 encodestring는 비추천. 오래된 튜토리얼이 여전히 발을 걸리는 이름 바꾸기
Python 3.4 (2014) 모든 인코더가 어떤 바이트 계열 객체든 받고, a85encode와 b85encode가 모듈에 합류
Python 3.6 (2016) binascii.b2a_base64가 newline 스위치를 배움. 이것이 b64encode를 끝없는 한 줄로 있게 하는 것
Python 3.9 (2020) 레거시 이름 encodestring와 decodestring가 마침내 삭제
Python 3.10 (2021) b32hexencode와 b32hexdecode, 정렬 가능한 16진수 알파벳의 사촌
Python 3.13 (2024) ZeroMQ의 알파벳, z85encode와 z85decode가 가문에 합류
Python 3.14 (2025) 표준 라이브러리 전반, base64 포함 더 빠른 임포트, 그리고 최대 6배 빠른 b16decode. 검증을 이제 정규식이 아닌 bytes.translate 위에서 돌리므로

원한다면, 관통하는 실: 이 모듈은 1995년에 작업을 C 레벨의 binascii 모듈에 위임하도록 다시 쓰였고, 그 위임은 오늘도 사실입니다. 첫 번째 바이트 시대 변경, 모든 것이 어디서나 바이트를 쓰게 한 Python 3 개발 도중의 2007년 커밋, 이 글의 타입 벽이 온 곳이 바로 거기로, 현대 인코더가 바이트를 받아 바이트를 돌려 주는 이유이기도 합니다. 나머지는 전부 그 단일 계약을 둘러싼 래퍼이니까요.

모듈이 알려주지 않는 것들

진지한 일은 끝났으니, 장부의 인코딩 쪽에 있는 작은 즐거움들을 꺼내 볼까요:

  • 문서 자체의 예시는 10년 넘게 같은 데모를 보여 왔습니다: b'data to be encoded'가 들어가 b'ZGF0YSB0byBiZSBlbmNvZGVk'가 나옵니다. 알고 있든 몰랐든, 당신은 이 쌍을 이미 만난 적 있습니다.
  • b64encode 밑의 C 함수는 "예의를 갖춰 줄바꿈을 하나 붙인다"라고 적힌 주석과 함께 끝 줄바꿈을 더합니다. 한 줄의 소스에, 한 문화 전체가 들어 있죠.
  • password라는 단어는 cGFzc3dvcmQ=로 인코딩됩니다. 그래서 로그 파일의 Base64는 스캐너에게는 시크릿처럼 보이지만, 읽는 사람에게는 명령 한 번을 사이에 두고 시크릿이 되는 것입니다.
  • b64encode는 줄바꿈을 하지 않습니다. 영원히. 1기가바이트 입력은 1.3기가바이트짜리 한 줄을 만들고, 함수는 깜짝도 하지 않습니다. 줄을 원했다면, encodebytes를 요청했어야 했습니다.
  • 모듈의 문서 문자열은 여전히 RFC 3548, 즉 2003년판 스펙을 이름으로 부릅니다. RFC 4648은 2006년에 이어 받았고, 문서 문자열은 그저 눈치채지 못한 채로 있습니다.
  • Python 2에는 타입의 벽이 아예 없었습니다: b64encode는 기꺼이 str을 받아 하나를 돌려 주었습니다. 2007년 바이트 대정비가 그것을 끝냈고, "왜 내 인코딩이 크래시되지" 스레드의 대부분은 여전히 오래된 Python 2 튜토리얼을 가리킵니다.
  • 가문에서 가장 새로운 멤버인 z85encode(Python 3.13)는 가장 까다롭습니다. ZeroMQ는 데이터를 4바이트 그룹으로 프레임하므로, 스펙은 인코딩된 출력이 5문자의 배수일 것을 요구합니다. 그리고 문서는 패딩은 당신의 몫으로 둡니다. 입력은 4바이트의 배수로 도착해야 합니다. 인코더가 당신을 위해 패딩해 주지 않으니까요. 3바이트 입력은 4문자 프레임을 만들지만, 어떤 ZeroMQ 동료도 그것을 받아 주지 않습니다.

그러면 인코더의 철학을 세 가지 규칙으로 남깁니다. 먼저 바이트를, 그다음 인코딩을 결정하세요. 타입의 벽은 대부분의 Python Base64 버그가 태어나는 곳이니까요. 알파벳은 데이터가 아니라 채널을 위해 고르세요. 이메일과 파일에는 패딩을 단 표준을, URL과 토큰에는 패딩 없는 base64url을. 그리고 키보드에서 제3의 변형을 즉흥적으로 만들어 내지 마세요. 그리고 출력을, 그 독자가 기대하는 모양으로 지켜 두세요. JSON과 헤더에는 한 줄, MIME과 PEM에는 76자 줄. 반대편의 디코더가 그것으로 당신을 쏘일 테니깐요.

그 글자들이 반대편에 도착하면, 재미가 진짜로 시작됩니다. 빠진 패딩, 조용한 폐기, Base64가 아닌 것 같은 페이로드, 그리고 두 가지 성향을 헤쳐 나가야 하는 디코더. 모든 것이 이 페이지 아래 관련 Base64 디코딩 글에서 상세히 다뤄지며, 두 가이드는 짝으로 읽을 때 좋습니다. 즐거운 인코딩을.

마지막 업데이트: 2026-09-08

관련 문서: Python에서의 Base64 디코딩: 완전한 가이드