Rust에서의 Base64 인코딩: 완전한 가이드
당신은 어떤 바이트를 텍스트 전용 문으로 보낼 참이고, 입장료는 시작하던 것보다 약 3분의 1쯤 더 긴 글자, 숫자, 더표, 슬래시의 문자열입니다.환영합니다, Base64 - 인터넷의 통행료 부스. 이 사이트의 홈 페이지가 해당 형식을 끝까지 깊이 있게 설명하므로, 여기서는 모양만 짚어 두겠습니다: Base64는 입력 바이트 3개를 64심볼 알파벳에서 고른 문자 4개로 쓰고, 뒤에 붙은 = 문자 한두 개가 읽는 쪽에게 진짜 데이터가 어디서 끝났는지 알려 줍니다. 4대 3이라는 이 교환이 바로 이 형식의 경제 전체이며, 이 가이드는 그것을 Rust에서 잘 해내는 방법에 관한 것입니다.
먼저 알아 둘 첫 번째 사실은, Rust 표준 라이브러리가 이것을 대신해 주지 않는다는 것입니다. std에 숨어 있는 base64_encode()도 없고, 당신의 마음을 바꿔 놓을 use std::...도 없습니다. 생태계는 이름이 그저 base64인 크레이트 하나에 정착했고, 그 크레이트는 이제 구조를 떠받치는 기둥이 되었습니다: 0.23.1 버전은 2026년 8월 4일에 출시되었고, 이 크레이트는 2015년 12월 이후 45개 버전을 공개했으며, 다운로드 카운터는 15억에 근접해 있습니다. 아래 모든 인코딩 예제는 그 한 개의 크레이트를 쓰고, 줄 감기와 PEM 어머를 위한 두 조력자도 함께 듭니다.
툴체인과 크레이트
먼저 툴체인입니다, 세상마다 명령어 하나씩:
# Debian / Ubuntu
sudo apt install rustc cargo
# 또는 rustup과 cargo를 설정해 주는 공식 설치 프로그램
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
그다음은 크레이트로, 어떤 cargo 프로젝트 안에서든:
cargo new my-app
cd my-app
cargo add base64
그 한 줄이 곧 설치 전체이며, 끌어오는 의존성은 정확히 0개입니다. 크레이트에는 알아 둘 선택 피처가 세 개 동봉되어 있습니다: std(기본 켜짐; std::io 스트리밍, 표준 Error 구현, 힙 할당을 줍니다), alloc(임베디드 no_std 빌드를 위한 할당 API), simd-unsafe(기본 켜짐; 몇 섹션 뒤에 등장하는 SIMD 엔진). 최소 지원 Rust 버전은 1.71.0이므로, 최신이면 무엇이든 그것을 실행할 수 있습니다. 그 주위에는 핵심이 의도적으로 하지 않는 일들을 위한 조력자들이 앉아 있습니다:
- line-wrap(0.2 버전)은 MIME과 PEM이 요구하는 76자 또는 64자 줄 바꿈을 삽입합니다;
base64크레이트 자체는 의도적으로 줄 감기를 거부하는데, 곧 보게 될 것입니다. - pem(4 버전)은 인증서와 키의
-----BEGIN ...-----블록을 만들고 파싱합니다; 내부적으로base64에 의존하고, 어머와 줄 감기를 더합니다. - base64ct(1.8 버전)은 RustCrypto 프로젝트의 상수 시간 디코더로, 라운드 트립의 읽는 쪽이 민감한 반일 때를 위해서입니다.
- base64-turbo(0.3 버전)은 더 새로운 고처리량 코덱으로, 최신 하드웨어에서 정점이 100 GiB/s를 넘습니다.
인코딩 한 번, 문자 네 개
가장 작을 수 있는 의식은 이렇습니다. 그리고 이미 전체 라운드 트립을 증명합니다:
use base64::prelude::*;
fn main() {
let packed = BASE64_STANDARD.encode("Hello, world!");
println!("{packed}");
// SGVsbG8sIHdvcmxkIQ==
let back = BASE64_STANDARD.decode(packed).unwrap();
println!("{}", String::from_utf8(back).unwrap());
// Hello, world!
}
눈여겨볼 것이 두 가지 있습니다. prelude 모듈은 조용히 두 가지를 한꺼번에 건네줍니다: BASE64_STANDARD 엔진과 당신이 그 메서드를 호출하는 Engine 트레잇. 그래서 그냥 use base64::prelude::*; 하나면 이 예제에 필요한 전부입니다. 그리고 encode()는 AsRef<[u8]> 경계 덕분에 바이트로 읽힐 수 있는 무엇이든 받습니다: &str, &[u8] 리터럴, Vec<u8>, 당신이 이름을 말하세요. 예제의 디코딩 부분은 인코더를 주시해 두기 위해 거기 있는 것뿐입니다. 반대 방향은 자매 사이트에서 자기만의 완전한 가이드를 받으니까요. 크레이트 자체의 스모크 테스트가 궁금하다면, 그 문서가 asdf를 인코딩해 YXNkZg==를 받아 냅니다; 같은 알파벳, 같은 수학.
각 바이트의 정확한 가격
존재하는 모든 Base64 인코더는 같은 세금을 부과하며, 수학이 보이기 시작하면 예산을 짤 수 있습니다. 각 출력 문자는 6비트를 실고, 각 입력 바이트는 8비트를 실으며, 둘 다에 해당하는 가장 작은 한 묶음은 24비트입니다: 정확히 3바이트 들어가고, 정확히 4문자 나옵니다. 그 비율이 바로 전체 쇼이며, 그래서 킬로바이트 3 파일은 4킬로바이트가 되고, 메가바이트 10 업로드는 13.3가 됩니다. 패딩은 눈에 보이는 반올림 오차입니다: 입력이 3바이트의 배수가 아니면, 마지막 그룹에 유휴 용량이 남고, 인코더는 출력 길이가 4의 배수로 남도록 =로 그것을 채웁니다. RFC 4648의 진리 표입니다. 표준 엔진이 정확히 재현하는 바로 그것:
| 입력 | 길이를 3으로 나눈 나머지 | 인코딩 결과 | 출력 길이 |
|---|---|---|---|
"" (빈 문자열) |
0 | "" (빈 문자열) |
0 |
f |
1 | Zg== |
4 |
fo |
2 | Zm8= |
4 |
foo |
0 | Zm9v |
4 |
foobar |
0 | Zm9vYmFy |
8 |
use base64::prelude::*;
let words: [&[u8]; 4] = [b"", b"f", b"fo", b"foo"];
for input in words {
println!("{:?} -> {:?}", String::from_utf8_lossy(input), BASE64_STANDARD.encode(input));
}
// "" -> ""
// "f" -> "Zg=="
// "fo" -> "Zm8="
// "foo" -> "Zm9v"
그 첫 줄을 두 번 읽어 보세요. 모두가 머릿속에서 그리는 것을 틀리는 바로 그 줄이니까요: 빈 입력은 AA==가 아니라 빈 문자열로 인코딩됩니다. AA==라는 문자열은 정확히 1바이트, NUL의 인코딩이며, 그것은 진짜로 다른 페이로드입니다. 그리고 인코딩 전에 버퍼 크기를 정해야 할 때, 크레이트는 그 수학을 const fn으로 건네 주므로, 컴파일 타임에 배열 크기까지 정할 수 있습니다:
let padded = base64::encoded_len(15, true).unwrap();
let slim = base64::encoded_len(15, false).unwrap();
println!("{padded} / {slim}"); // 20 / 20
println!("{:?}", base64::encoded_len(13, true)); // Some(20)
println!("{:?}", base64::encoded_len(13, false)); // Some(18)
println!("{:?}", base64::encoded_len(14, false)); // Some(19)
println!("{:?}", base64::encoded_len(100, false)); // Some(134)
13바이트와 14바이트 줄을 지켜 보세요. 대충 추산하는 수학을 꼬이는 것이 바로 그 줄이니까요: 13바이트는 패딩 없으면 18문자, 패딩 있으면 20문자가 필요하고, 14바이트는 19와 20이 필요합니다. 이 함수는 Option을 돌려주며, 길이 계산이 오버플로우될 때만 None이므로, 메모리에 실제로 존재할 수 있는 어떤 입력에 대해든 unwrap()은 안전합니다. 메일 모양의 세계에서는 세금에 할증이 붙습니다: MIME은 76자에서 라인을 감고, 옛 준칙으로는 줄이 감긴 Base64가 원래 크기의 약 1.37배에 헤더 오버헤드가 더 드는 셈입니다. 크레이트 자신의 FAQ는 패딩 자체에 대해 정중하지 못한 의견을 갖고 있습니다: = 바이트는 '그 패딩은 잘못되었다'고 말하는 기회를 제공한다는 것을 제외하면 디코딩에는 영향을 주지 않는다고, 그리고 "의문 없이 엑사바이트 규모의 저장과 전송이 쓸모없는 = 바이트에 허비되었을 것이다"라고요.
패딩: 읽는 쪽에 대한 결정
base64 0.23에서 맨인코딩 함수들은 비추천 처리되었습니다 - 지금의 방식은 Engine의 메서드를 호출하는 것이며, 엔진은 하나의 정책입니다: 어떤 알파벳으로 쓸지, 그리고 어떤 패딩을 추가할지. 프리셋은 base64::engine::general_purpose에 살며, 인기 있는 네 개는 prelude로 다시 내보내져 있습니다:
| 엔진 | 알파벳 | 패딩 추가 | 가장 좋은 대상 |
|---|---|---|---|
STANDARD / BASE64_STANDARD |
+ / |
네 | 모든 것, 기본값 |
STANDARD_NO_PAD / BASE64_STANDARD_NO_PAD |
+ / |
아니요 | 당신도 소비하는 슬림한 페이로드 |
URL_SAFE / BASE64_URL_SAFE |
- _ |
네 | 그래도 패딩을 원하는 URL 콘텐츠 |
URL_SAFE_NO_PAD / BASE64_URL_SAFE_NO_PAD |
- _ |
아니요 | JWT, URL, 객체 ID |
패딩 없는 인코딩은 크레이트가 참아 주는 해킹이 아닙니다. 일급 입장입니다: 미리 구성된 NO_PAD와 PAD 설정 상수와 함께, 0.23.0에서 추가된 *_INDIFFERENT 형제들이 곁에 있습니다. 프리셋이 맞지 않으면, Alphabet와 다이얼 하나인 GeneralPurposeConfig로 자기만의 엔진을 만들 수 있고, 엔진은 만들기가 싸므로 요청마다 다시 만들기보다 그 결과를 const에 저장하세요:
use base64::engine::general_purpose::{GeneralPurpose, GeneralPurposeConfig};
use base64::prelude::*;
const SLIM: GeneralPurpose = GeneralPurpose::new(
&base64::alphabet::STANDARD,
GeneralPurposeConfig::new().with_encode_padding(false),
);
fn main() {
println!("{}", SLIM.encode("fo")); // Zm8
println!("{}", BASE64_STANDARD.encode("fo")); // Zm8=
}
이제 이 결정은 다른 사람들의 디코더에 대한 결정이 됩니다. 그것은 결코 순전히 미학적일 수 없습니다. 디코딩 쪽의 엄격함 규칙은 DecodePaddingMode에서 오고, 아래 표가 "상대는 내가 쓴 것을 읽을 수 있는가?"에 답합니다:
| 당신이 인코딩하는 엔진 | 엄격한 STANDARD 디코더 |
STANDARD_NO_PAD 디코더 |
INDIFFERENT 디코더 |
|---|---|---|---|
STANDARD (패딩 있음) |
읽을 수 있음 | =를 거부 |
읽을 수 있음 |
STANDARD_NO_PAD |
거부: 패딩 부족 | 읽을 수 있음 | 읽을 수 있음 |
URL_SAFE_NO_PAD |
거부: 알파벳이 틀림 | 거부: 알파벳이 틀림 | URL 알파벳으로만 읽을 수 있음 |
실용 규칙은 그 표에서 흘러나옵니다. 양 끝을 모두 통제한다면, 엔진을 하나로 정하고 어디에서나 쓰세요. 그리고 바이트를 아끼려면 패딩 없음을 선호하세요. 바깥세상에서 데이터를 소비한다면, 당신의 디코더가 어떤 엔진으로 내보내야 할지에 대해 한 표를 갖습니다: 그대로 쓰이는 STANDARD 디코더는 당신의 패딩을 필요로 하고, STANDARD_PAD_INDIFFERENT 디코더는 둘 다 받아들입니다. 그리고 이 선택에는 보안의 맛도 있습니다. 같은 페이로드의 패딩 있는 표기와 패딩 없는 표기를 모두 허용하면 Base64는 변형 가능해집니다; 크레이트 자신의 문서가 링크하는 2022년 논문 "실전에서의 Base64 가소성"(Chatzigiannis and Chalkias, ePrint 2022/361)이 왜 그런지 보여 줍니다. 같은 데이터를 두 가지 다른 방식으로 쓸 수 있는 프로토콜은, 인코딩된 문자열을 정체성으로 취급하는 코드를 놀라게 만드는 습관이 있습니다. 그래서 당신의 형식이 하나의 정준 표기를 정의한다면, 경계에서 그것을 강제하세요.
토큰과 링크를 위한 Base64url
표준 Base64의 알파벳 마지막 두 글자는 +와 /이고, URL에서 그것들은 그 언어에서 가장 비싼 문자 두 개입니다: 더표는 %2B가 되고, 슬래시는 %2F가 되며, 패딩은 %3D가 됩니다. RFC 4648 제5절은 URL과 파일명에 안전한 알파벳으로 이것을 고치는데, 두 트러블메이커를 -와 _로 바꾸고 보통 패딩도 건너뜁니다. 엔진들이 그 구분을 놓칠 수 없게 만듭니다:
use base64::engine::general_purpose::URL_SAFE_NO_PAD;
use base64::Engine;
let packed = URL_SAFE_NO_PAD.encode(b"\xfb\xef\xbe");
println!("{packed}"); // ----
let back = URL_SAFE_NO_PAD.decode(packed).unwrap();
println!("{back:02x?}"); // [fb, ef, be]
가장 악질적인 입력의 3바이트가, 퍼센트 이스케이프 하나 없이 URL, 파일명, 쿠키, 데이터베이스 키 어디에나 붙여 넣을 수 있는 4문자 문자열이 됩니다. 이것이 바로 JSON Web Token이 사는 알파벳입니다: JWT는 점으로 이어진 base64url 세 부분이며, jsonwebtoken 크레이트(2026년 기준 11 버전)로 하나를 주면 이렇게 보입니다:
use serde::Serialize;
use jsonwebtoken::{EncodingKey, Header, encode};
#[derive(Debug, Serialize)]
struct Claims {
sub: String,
company: String,
exp: u64,
}
let key = b"secret";
let my_claims = Claims {
sub: "b@b.com".to_owned(),
company: "ACME".to_owned(),
exp: 19_000_000_000, // 확실히 먼 미래
};
let token = encode(&Header::default(), &my_claims, &EncodingKey::from_secret(key)).unwrap();
println!("{token}");
// eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJiQGIuY29t...
11 버전에는 신참을 물리는 설정 요구가 하나 있습니다: 이 크레이트는 Cargo.toml에서 rust_crypto 또는 aws_lc_rs 피처 중 정확히 하나만 켜져 있어야 하고, 둘 다 꺼져 있으면 토큰에 서명하거나 검증하는 첫 번째 시도에서 panic합니다. struct 안의 exp 클레임에 주목하세요: 크레이트의 검증은 기본적으로 그것을 필수로 여깁니다. 그래서 진짜 토큰은 어쨌든 하나를 싣고 있고, 토큰 안의 base64url 알파벳은 전부 라이브러리의 일이죠. 토큰을 주기는커녕 검사만 한다면, 자매 글이 5줄짜리 엿보기를 보여 줍니다. 그리고 황금 규칙의 재확인, 토큰에는 특별히 힘 있게 적용되는 규칙: JWT의 세 부분은 모두 키 없이 읽을 수 있습니다. Base64는 창가 좌석이지 자물쇠가 아닙니다.
텍스트 들어가고, 바이트 나와요
인코더는 마음을 읽지 않으므로, Rust에서 "이 문자열을 인코딩해"라는 것은 항상 "이 문자열의 UTF-8 바이트를 인코딩해"를 뜻합니다. str::as_bytes()가 건네 주는 것이 그것이니까요. 좋은 소식은 현대 웹이 거의 전부 UTF-8이라는 것이라, 정직한 길은 짧고 행복합니다:
use base64::prelude::*;
let text = "café";
let packed = BASE64_STANDARD.encode(text.as_bytes());
println!("{packed}"); // Y2Fmw6k=
멀티바이트의 경우들은 모두 성실합니다:
| 원문 텍스트 | Base64 | 라운드 트립 |
|---|---|---|
café |
Y2Fmw6k= |
깨끗 |
日本語 |
5pel5pys6Kqe |
깨끗 |
😀 |
8J+YgA== |
깨끗 |
π ≈ 3.14159 |
z4Ag4omIIDMuMTQxNTk= |
깨끗 |
진짜 결정은 하나뿐이며, 그것은 어떤 바이트에서 출발하느냐입니다. 데이터가 텍스트가 아니라 바이트로 도착했다면 - 디스크에서 읽은 파일이거나 네트워크 호출의 버퍼이거나 - 문자열을 통째로 건너뛰고 Vec<u8>를 바로 인코딩하세요. 그것은 PNG나 protobuf 같은 UTF-8이 아닌 페이로드에 대한 유일한 정답이기도 합니다. 아무 이미지나 코드 옆에 두고, 읽기를 그것으로 향하게 하세요:
use base64::prelude::*;
let file_bytes = std::fs::read("sprite.png").unwrap();
let size = file_bytes.len();
let packed = BASE64_STANDARD.encode(file_bytes);
println!("{size} bytes -> {} base64 chars", packed.len());
// 모든 인코딩된 PNG는 iVBORw0K로 시작한다
assert!(packed.starts_with("iVBORw0K"));
그 마지막 assert는 공짜의 정상성 점검이며, 인터넷에서 가장 알아볼 수 있는 접두사 중 하나입니다. 그리고 만약 같은 논리 텍스트를 두 가지 다른 문자 집합을 통해 인코딩하거나, 다른 문자 집합으로 오독한 바이트를 인코딩한다면, 라운드 트립은 무표정으로 문자 깨짐으로 돌아옵니다. 인코더는 절대 거짓말을 하지 않습니다. 당신이 주는 바이트를 그저 인코딩할 뿐인데, 그것이 바로 그 가장 큰 강점이자 유일한 함정입니다.
형식이 줄을 원할 때
base64 크레이트는 의도적으로 줄 바꿈을 넣지 않으며, 그것이 그렇게 결정한 처음은 아닙니다. 0.5.0 버전은 설정 가능한 줄 끝을 갖춘 내장 MIME 줄 감기를 실었고, 0.10.0 버전은 그것을 제거했는데, 라이브러리가 일반적인 크레이트로서 줄 감기는 의견이 너무 강하고 no_std 이야기를 복잡하게 만든다고 판단했기 때문이죠. 형식이 줄을 요구한다면, line-wrap 크레이트가 정확히 그것을 위해 존재합니다. 그 단 하나의 함수 line_wrap()는 당신의 미리 할당된 버퍼, 입력 길이, 열 한계, 줄 끝을 받아, 자신이 삽입한 줄 끝 바이트의 수를 돌려줍니다:
use base64::prelude::*;
let data = BASE64_STANDARD.encode(vec![b'a'; 300]); // 400자
let mut buf = vec![0u8; data.len() + 16];
buf[..data.len()].copy_from_slice(data.as_bytes());
let endings = line_wrap::line_wrap(&mut buf, data.len(), 76, &line_wrap::crlf());
buf.truncate(data.len() + endings);
let wrapped = String::from_utf8(buf).unwrap();
println!("{} chars in, {} bytes out, {} line endings", data.len(), wrapped.len(), endings);
// 들어간 문자 400, 나온 바이트 410, 줄 끝 10개 (CRLF 쌍 5개)
버퍼를 줄 끝을 위한 여분까지 미리 크기를 정하고, 함수를 호출한 다음 보고된 합으로 잘라 내세요; CRLF 쌍 다섯 개가 바로 MIME의 76열 규칙의 가격입니다. PEM의 경우 한계와 줄 끝을 64열과 line_wrap::lf()로 바꾸면, 어머의 본문을 갖게 됩니다. 그러면 pem 크레이트가 한 번의 호출로 배너를 더합니다:
let pem_block = pem::encode(&pem::Pem::new("CERTIFICATE", b"0123456789abcdef"));
println!("{pem_block}");
// -----BEGIN CERTIFICATE-----
// MDEyMzQ1Njc4OWFiY2RlZg==
// -----END CERTIFICATE-----
let back = pem::parse(pem_block).unwrap();
println!("{}: {} bytes", back.tag(), back.contents().len());
// CERTIFICATE: 16 bytes
// 소비자가 까다로우면 Unix 방식의 줄 끝
let lf_block = pem::encode_config(
&pem::Pem::new("KEY", b"0123456789abcdef"),
pem::EncodeConfig::new().set_line_ending(pem::LineEnding::LF),
);
기본적으로 pem::encode는 CRLF, 역사적인 PEM 관례를 씁니다; set_line_ending 빌더는 그것을 기대하는 도구들을 위해 LF로 바꿉니다. pem 크레이트가 하지 않는 것에 주목하세요: 당신이 볼 수 있는 base64 함수를 한 번도 호출하지 않습니다. 인코딩은 그 내부의 일이기 때문이죠. 형식이 줄을 원할 때, 아키텍처는 일마다 하나의 크레이트입니다.
상수 공간에서의 스트리밍
하나의 변수에 담아 둘 만큼 큰 데이터를 넘어선 경우, 크레이트는 Rust 나머지 io와 같은 스트리밍 철학으로 답합니다: write::EncoderWriter는 어떤 대상이든 감싸고, 거기에 쓰는 모든 것을 상수 공간으로 base64로 인코딩합니다. 버퍼를 위한 완전한 의식은 이렇며, 이 섹션의 주인공은 finish() 호출입니다:
use std::io::Write;
use base64::prelude::*;
use base64::write::EncoderWriter;
fn main() {
let mut encoder = EncoderWriter::new(Vec::new(), &BASE64_STANDARD);
encoder.write_all(b"the quick brown fox jumps over the lazy dog").unwrap();
let packed = encoder.finish().unwrap();
println!("{}", String::from_utf8(packed).unwrap());
// dGhlIHF1aWNrIGJyb3duIGZveCBqdW1wcyBvdmVyIHRoZSBsYXp5IGRvZw==
}
왜 finish()가 주인공일까요? 마지막 부분 그룹을 비워 내고 패딩을 추가하는 호출이 바로 그것이기 때문이며, 인코더에는 그것을 하지 않는 형제 메서드가 있습니다. 크레이트 자신의 문서는 명쾌하게 말합니다: finish()는 "남은 입력 바이트를 인코딩하고, 적절한 경우 패딩을 추가합니다. 해체될 때 자동으로 호출됩니다(Drop 구현을 보세요)만, 기저 쓰기 대상을 호출할 때 발생하는 어떤 오류도 억제됩니다. 그런 오류를 처리하고 싶다면 스스로 finish()를 호출하세요." Drop 구현은 BufWriter처럼 행동합니다: 비워 내지만, Drop 중의 오류는 무시합니다. 그래서 마지막 부분 그룹은 잃지 않지만, "아마 작동했을 것이다"는 출시 전략이 될 수 없습니다. 그것이 알려 주었을 쓰기의 오류는 사라졌으니까요.
전체 파이프라인을 한 번의 호출로 갖고 싶다면, 같은 스트림은 io::copy를 통해 한 단계 떨어뜨려서도 작동하며, "이걸 그냥 포맷 문자열 안에 넣기만 하면 되는데" 순간을 위한 보너스 래퍼도 있습니다:
use std::io;
use base64::prelude::*;
use base64::write::EncoderWriter;
let file = b"the quick brown fox jumps over the lazy dog".to_vec();
let mut cursor = io::Cursor::new(file);
let mut encoder = EncoderWriter::new(Vec::new(), &BASE64_STANDARD);
io::copy(&mut cursor, &mut encoder).unwrap();
let packed = encoder.finish().unwrap();
println!("{}", String::from_utf8(packed).unwrap());
use base64::display::Base64Display;
use base64::prelude::*;
let value = Base64Display::new(b"\0\x01\x02\x03", &BASE64_STANDARD);
println!("base64: {value}"); // base64: AAECAw==
그 Base64Display 래퍼는 작은 보석입니다: 힙 할당을 하나도 하지 않은 채 어떤 포맷 문자열이든 안에서 바이트를 Base64로 서식화하므로, 로그 줄과 디버그 출력이 갑자기 즐거워집니다.
할당, 그리고 그 부재
편리한 메서드는 할당합니다. 그리고 삶의 대부분에서 그것이 올바른 거래입니다. 하지만 Engine 트레잇은 인코딩의 세 가지 맛을 드러내며, 아래 표가 바로 결정 행렬 전체입니다:
| 메서드 | 출력 | 할당 |
|---|---|---|
encode() |
새로운 String |
항상 |
encode_string() |
당신의 String 뒤에 덧붙임 |
커져야 할 때만 |
encode_slice() |
당신의 &[u8] 안에 작성 |
절대 안 함 |
use base64::prelude::*;
let input = b"Hello, world!";
let mut buf = vec![0u8; base64::encoded_len(input.len(), true).unwrap()];
let written = BASE64_STANDARD.encode_slice(input, &mut buf).unwrap();
buf.truncate(written);
println!("{}", std::str::from_utf8(&buf).unwrap()); // SGVsbG8sIHdvcmxkIQ==
// 또는 버퍼를 전부 스택 위에 유지
let mut stack = [0u8; 24];
let n = BASE64_STANDARD.encode_slice(b"abc 123", &mut stack).unwrap();
println!("{}", String::from_utf8(stack[..n].to_vec()).unwrap()); // YWJjIDEyMw==
// 그리고 크기를 잘못 정했다면 버퍼 오버플로가 아니라 오류를 얻습니다
let mut tiny = [0u8; 5];
println!("{:?}", BASE64_STANDARD.encode_slice(input, &mut tiny));
// Err(OutputSliceTooSmall)
버퍼는 encoded_len()으로 크기를 정하고, encode_slice()로 쓰세요. 크기를 잘못 정하면 미정의 동작 대신 깔끔한 EncodeSliceError::OutputSliceTooSmall를 얻는데, 시스템 언어에서 그것은 지루한 오후와 긴 오후의 차이입니다. 임베디드 작업에서는 같은 함수들이 alloc 피처 뒤에 존재하므로, API는 유지한 채 힙만 내릴 수 있습니다.
속도: SIMD 엔진들
2026년 7월에 출시된 0.23.0 버전이 헤드라인 기능을 가져왔습니다: 표준과 URL-안전한 알파벳을 위한 SIMD 가속 엔진. 그것들이 세 개이며, 당신의 하드웨어를 얼마나 힘주어 신뢰하느냐로 나뉩니다:
| 엔진 | 실행 시 검출 | no_std에서 작동 |
|---|---|---|
Simd |
네, AVX2 또는 NEON을 고르고, 스칼라 엔진으로 폴백 | 아니요, 검출에 std 필요 |
Avx2 |
아니요, CPU가 AVX2를 갖는다고 가정 | 네, x86_64 타깃에서 |
Neon |
아니요, CPU가 NEON을 갖는다고 가정 | 네, aarch64 타깃에서 |
use base64::engine::general_purpose::GeneralPurposeConfig;
use base64::engine::{Avx2, Simd};
use base64::Engine;
let turbo = Simd::standard(GeneralPurposeConfig::new());
println!("{}", turbo.encode("simd works!"));
// c2ltZCB3b3JrcyE=
if let Some(fixed) = Avx2::standard(GeneralPurposeConfig::new()) {
println!("{}", fixed.encode("hello avx2")); // aGVsbG8gYXZ4Mg==
}
Simd 생성자는 CPU 검출을 한 번 수행하고, 찾은 가장 좋은 커널을 돌려주며, 아무것도 해당하지 않으면 스칼라 엔진을 돌려줍니다. 그래서 const 안에서든 시작 시에든 한 번만 만들고 재사용하세요. 능력 있는 하드웨어에서는 인코딩과 디코딩 모두 스칼라 경로보다 몇 배 빠릅니다. 솔직한 각주 하나: SIMD 경로가 이 크레이트에서 unsafe에 닿는 유일한 곳이며, 그래서 피처 이름이 simd-unsafe입니다. 피처를 끄면 크레이트 전체가 다시 #![forbid(unsafe_code)]가 되고, 스칼라 엔진은 여전히 정직한 일을 합니다. 원시 처리량이 전부인 경우, base64-turbo 크레이트가 한계를 더 밀어붙입니다. 실행 시 검출 뒤에 AVX512, AVX2, NEON 커널을 두고 정점은 100 GiB/s를 넘으며, 모든 나머지는 100% 안전한 스칼라 폴백입니다. base64 크레이트는 MIT/Apache-2.0 이중 라이선스이므로, 속도 포함 이것들 전부 무료입니다.
네 가지 알파벳 더
RFC 알파벳이 기본이지만, base64 크레이트는 네 가지를 더 싣고 있습니다. 각각이 자기만의 트위스를 필요로 했던 어떤 실제 프로토콜을 위한 작은 기념비입니다:
| 알파벳 | 트위스트 | 누가 쓰는지 | abc 123의 인코딩 결과 |
|---|---|---|---|
alphabet::CRYPT |
./가 먼저, 그다음 숫자와 글자, 패딩 없음 |
고전 유닉스 crypt(3) 비밀번호 해시 | MK7X612mAk |
alphabet::BCRYPT |
./가 먼저, 그다음 글자, 그다음 숫자 |
bcrypt 비밀번호 해시 | WUHhGBCwKu |
alphabet::IMAP_MUTF7 |
쉼표가 슬래시 대신 들어옴, 패딩 없음 | IMAP의 modified UTF-7 메일박스 이름 | YWJjIDEyMw |
alphabet::BIN_HEX |
기호(구두점)가 무성하고 혼동하기 쉬운 글자를 건너뛰는 알파벳 | BinHex 4, 옛 Macintosh 파일 래퍼 | B@*M)$%b-` |
use base64::engine::general_purpose::{GeneralPurpose, NO_PAD};
use base64::Engine;
let crypt = GeneralPurpose::new(&base64::alphabet::CRYPT, NO_PAD);
println!("{}", crypt.encode(b"abc 123")); // MK7X612mAk
let bcrypt = GeneralPurpose::new(&base64::alphabet::BCRYPT, NO_PAD);
println!("{}", bcrypt.encode(b"abc 123")); // WUHhGBCwKu
let imap = GeneralPurpose::new(&base64::alphabet::IMAP_MUTF7, NO_PAD);
println!("{}", imap.encode(b"abc 123")); // YWJjIDEyMw
같은 입력, 세 가지 다른 출력, 모두 자기 방언 안에서 유효한 Base64입니다. crypt 알파벳이 진짜 초능력을 가진 쪽입니다: 심볼들이 비트 패턴과 일치하도록 정렬되어 있어서, 인코딩된 문자열을 정렬하면 원래 바이트를 정렬한 것과 같은 순서가 나옵니다. 그래서 GEDCOM 5.5(1996)가 멀티미디어 필드에 그것을 썼고 - 5.5.1 개정판은 그 기능을 버렸지만 - 크레이트는 여전히 당신을 위해 그 알파벳을 싣고 있습니다. 그리고 당신이 필요한 방언이 크레이트에 없다면, 64문자 문자열로 정의할 수 있습니다. Alphabet::new()가 인코딩과 디코딩 표를 대신 만들어 주니까요:
use base64::alphabet::Alphabet;
use base64::engine::general_purpose::{GeneralPurpose, PAD};
use base64::Engine;
// 기괴한 세계의 base64: +/가 끝이 아니라 앞에
let alphabet = Alphabet::new(
"+/ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789",
).expect("a valid 64 char alphabet");
let bizarro = GeneralPurpose::new(&alphabet, PAD);
println!("{}", bizarro.encode(b"hello 99")); // YETqZE6eMRi=
// 표준 엔진은 이렇게 말합니다:
println!("{}", base64::prelude::BASE64_STANDARD.encode(b"hello 99"));
// aGVsbG8gOTk=
커스텀 알파벳 길에 대한 경고 하나: 방언을 발명하는 순간, 당신의 데이터를 읽을 수 있는 사람은 지구에서 당신 하나로 줄어듭니다. 그래서 프로토콜이 요구할 때만 하세요. 그리고 어느 프로토콜인지라고 주석을 남겨 두세요.
인코더가 일하는 곳
Base64 인코딩은 Rust 프로젝트에서 예측 가능한 등장인물 무대로 모습을 드러냅니다:
- JSON API의 파일 업로드: 파일이 텍스트 의상을 입은 바이트 필드인 곳, 압도적 격차로 가장 흔한 용도.
- HTML과 CSS의 Data URI:
data:image/png;base64,...종류, 작은 아이콘에는 훌륭하고 히어로 이미지에는 의심스럽습니다. - JWT와 OAuth: base64url이 방언이고
jsonwebtoken크레이트가 도구인 곳. - 인증서와 키의 PEM 블록: Base64를 라인당 64자로 줄 감는
-----BEGIN CERTIFICATE-----섹션. - XML과 설정 파일의 이진 데이터: 내보낸 북마크와 설정 덤프에서 여전히 발견되는
<data encoding="base64">패턴. - LDAP와 LDIF 파일: 이진 속성 값을 한 줄에 담아 두기 위해 Base64를 쓰는 곳.
- QR 코드 페이로드와 클립보드 인계: 텍스트는 여행을 견디지만 이진 데이터는 견디지 못하는 곳.
- HTTP Basic 인증 헤더:
Basic TWFuOnBhc3M=가 자격 증명 쌍인 곳, 그리고 이것이 숨기는 문제가 아니라 포장 문제라는 재확인.
그리고 모든 것을 지배하는 황금 규칙: Base64는 포장용 테이프이지 자물쇠가 아닙니다. 그것은 암호화가 아니고 압축도 아닙니다 - 압축의 반대이며, 이 글을 가진 누구든 한 줄로 그것이 한 모든 일을 거꾸로 돌릴 수 있습니다. 자유롭게 인코딩하세요. 하지만 비밀번호, API 키, 또는 비밀을 인코딩해 놓고 그것에 보호되었다고 부르지 마세요. 숨겨야 한다면 진짜 암호화를 쓰세요. 그리고 크다면, 멀티파트 업로드가 그 세금보다 그저 더 쌌을 가능성을 생각해 보세요.
작은 걸음의 10년
이 형식은 웹보다 오래되었습니다. 1987년, Privacy-Enhanced Mail 프로토콜(RFC 989)은 7비트 메일 채널로 이진 데이터를 실어야 했으며, 정확히 64자 라인의 인코딩으로 이것을 표준화했습니다. 인터넷의 모든 -----BEGIN CERTIFICATE----- 블록은 그 결정의 후손이며, 그래서 PEM 파일은 오늘도 64에서 줄을 갑니다. 1996년 MIME 사양(RFC 2045)은 그 방식을 받아들여, 64자 알파벳에서 따서 "base64"라고 이름 붙이고, 줄 감기를 76자로 옮겼습니다. 그것들보다 앞서, 유닉스 박스에는 uuencode가, 맥에는 BinHex가 동봉되어 있었으며, 각각 자기만의 알파벳을 갖고 있었고, 둘 다 오늘도 파일 헤더를 단 화석처럼 옛 시스템에서 모습을 드러냅니다. 2006년, RFC 4648은 모두가 인용하는 표준이 되었습니다: 알파벳 표, base64url 변종, 그리고 이 글의 모든 엔진이 구현하는 정준 인코딩 규칙과 함께요. 그 제3.5절은 인코더가 쓰이지 않는 트레일링 비트를 0으로 설정할 것을 요구하며, 크레이트는 그렇게 합니다. 그래서 만약 당신의 페이로드가 나중에 엄격한 디코더의 InvalidLastSymbol 검사에 걸리면, 부패는 상류에서 일어난 것입니다.
크레이트의 역사 자체도 운율을 맞춥니다. 2015년 12월에 crates.io에 등장했고, 0.5.0 버전은 자부심 어린 MIME 줄 감기를 더했습니다. 설정 가능한 줄 끝과 함께요. 그리고 2018년의 0.10.0 버전은 줄 감기와 공백 처리를 제거했는데, 범용 크레이트는 인코딩을 하고 시는 애플리케이션 계층에 맡겨야 한다는 판단에서였습니다; 같은 릴리스는 스트리밍 EncoderWriter를 더했습니다. 2022년의 0.20.0 버전은 엔진 추상화를 도입하고 정준 패딩을 기본으로 만들었고, 0.21.0 버전은 옛 자유 함수를 비추천 처리하고 엔진 메서드를 선호했는데, 컴파일러 노트는 "Engine::encode를 사용하세요"였습니다 (여전히 작동합니다. 그래서 많은 레거시 코드가 쾌활하게 컴파일되는 겁니다). 2024년, 0.22.0 버전은 오류 의미를 또렷이 하고 디코딩을 5에서 10퍼센트 빠르게 했습니다. 그리고 2026년 7월, 0.23.0 버전은 SIMD 엔진, 커스텀 패딩 심볼, 더 선명한 오류 메시지, MSRV의 1.71 상향과 함께 도착했고, 8월 4일의 0.23.1 패치는 non-SIMD 아키텍처의 테스트 스위트를 고쳤습니다.
미소 지을 만한 것들
완전한 가이드는 미소로 끝맺어야 하니까요:
- 단어 "base64"는
YmFzZTY0로 인코딩됩니다. 자기 자신을 설명하는 형식은, 기술적으로 모스 부호로 말하는 거울과 같은 것입니다. - 빈 문자열은 빈 문자열로 인코딩됩니다. 아무것도 아닌 것은 비용이 하나도 들지 않는 유일한 입력이며, 그것은 일종의 면세입니다.
AA==는 아무것도 아닌 것의 인코딩이 아닙니다. NUL 바이트 하나인 인코딩입니다. Base64에서 "아무것도 아닌 것"과 "영"은 다른 존재이며, 디코더는 그것들을 구별합니다.- 모든 Base64 인코딩 PNG는
iVBORw0K로 시작합니다. 그것은 포장용 테이프에 갇힌 PNG 매직 넘버이며, 인터넷에서 가장 알아볼 수 있는 접두사 중 하나입니다. - URL에서, 표준 Base64 문자들은 이스케이프 의상이 필요합니다: 더표는
%2B가 되고, 슬래시는%2F가 되며, 패딩은%3D가 됩니다. Base64url은 그 문자들이 자기 자신의 얼굴을 쓸 수 있게 존재합니다. - YouTube 영상 ID는 패딩 없는 base64url입니다: 8바이트의 ID가 어디에나 붙여 넣을 수 있는 11문자 문자열이 됩니다. 인터넷 전체에서 패딩 없음 모드의 가장 눈에 띄는 용도 중 하나입니다.
- 옛 crypt(3) 비밀번호 알파벳은 올바르게 정렬됩니다: 정렬된 인코딩 문자열은 정렬된 평문과 같은 순서로 섭니다. GEDCOM 5.5(1996)는 멀티미디어 필드에 그 알파벳을 썼고, 5.5.1 개정판은 그 기능을 버렸으며, 크레이트는 여전히 그것을 당신을 위해 싣고 있습니다.
- BinHex, 옛 Macintosh 래퍼는,
7,O,g,o같은 시각적으로 혼동하기 쉬운 문자를 제외하도록 알파벳을 지었습니다. 철자 검사 이전의 세계에서, 사람의 눈을 위해 설계된 인코더입니다. - 크레이트 자신의 FAQ는 패딩에 대해 노골적입니다: 엑사바이트 규모의 저장과 전송이 의문 없이 쓸모없는
=바이트에 허비되었을 것입니다. 통행료 부스는 1987년부터 징수해 왔습니다. - Base64는 암호화가 아닙니다. 그랬다면 이 글의 어떤 예제 출력도 읽을 수 없었을 겁니다. 그것은 창가 좌석이지 금고가 아닙니다.
짧은 버전
엔진을 데이터가 여행할 길 기준으로 고르세요: 당신이 직접 디코딩도 하는 모든 것에는 BASE64_STANDARD, 양 끝을 통제하고 바이트를 되받고 싶을 때는 _NO_PAD 엔진, 토큰과 URL에는 URL_SAFE_NO_PAD, 프로토콜이 고집할 때만 커스텀 Alphabet을 쓰세요. 버퍼는 encoded_len()으로 크기를 정하고, 큰 것들은 EncoderWriter로 스트리밍해 늘 finish()로 닫으세요. 줄 감기는 형식이 요구할 때만 line-wrap과 pem으로 하세요. 가능하면 SIMD 엔진에게 무거운 일을 맡기세요. 그리고 4대 3 교환이 바로 텍스트 전용 문을 통과하는 가격이라는 것을 잊지 마세요. 모든 것을 인코딩하되, 진짜 자물쇠가 필요한 것만 보호하세요. 그리고 반대 방향으로 가야 할 때, 문자열을 그 여정을 시작한 바이트로 되돌려 풀어야 할 때, 자매 글이 Rust에서의 디코딩을 다루며, 정확한 오류 메시지의 완전한 점수표까지 갖추고 있습니다.
마지막 업데이트: 2026-09-08
관련 문서: Rust에서의 Base64 디코딩: 완전한 가이드