문자열 인코딩과 디코딩
Buffer.from(string, "utf8")이 텍스트를 UTF-8 바이트로 바꾸고, .toString("base64")가 이를 인코딩합니다. 디코딩은 같은 두 호출을 반대로 하면 됩니다.
const enc = Buffer.from("héllo wörld", "utf8").toString("base64");
console.log(enc); // aMOpbGxvIHfDtnJsZA==
console.log(Buffer.from(enc, "base64").toString("utf8")); // héllo wörld
두 호출 모두 "utf8"이 기본값이지만, 명시하면 의도가 분명해집니다. Buffer는 Uint8Array의 하위 클래스이므로 일반 Uint8Array는 감싸야 합니다. u8.toString("base64")는 인수를 무시하고 "104,105"를 반환하지만, Buffer.from(u8).toString("base64")는 aGk=를 반환합니다.
URL-safe Base64 (base64url)
Node에는 URL-safe 알파벳 전용 인코딩이 있습니다. 패딩은 절대 붙이지 않습니다.
Buffer.from("<<???>>").toString("base64"); // PDw/Pz8+Pg==
Buffer.from("<<???>>").toString("base64url"); // PDw_Pz8-Pg
Buffer.from("PDw_Pz8-Pg", "base64url").toString(); // <<???>>
받는 쪽이 패딩 있는 URL-safe Base64를 원한다면 직접 붙이세요. s + "=".repeat((4 - s.length % 4) % 4).
디코딩 쪽에서는 Node가 관대합니다. "base64"와 "base64url" 모두 어느 알파벳이든 받고 패딩도 신경 쓰지 않습니다.
Buffer.from("PDw_Pz8-Pg==", "base64").toString(); // <<???>>
Buffer.from("PDw/Pz8+Pg==", "base64url").toString(); // <<???>>
JWT와 토큰에는 편리하지만(JWT 디코더에서 해 보세요), 아래의 검증 섹션도 꼭 보세요. 두 알파벳의 배경은 Base64와 Base64URL 비교에 있습니다.
파일 인코딩과 디코딩
fs는 "base64"를 인코딩으로 받으므로 양방향 모두 한 단계를 줄일 수 있습니다.
import { readFileSync, writeFileSync } from "node:fs";
readFileSync("hello.txt").toString("base64"); // aGVsbG8K
readFileSync("hello.txt", "base64"); // aGVsbG8K (same thing)
writeFileSync("copy.txt", Buffer.from("aGVsbG8K", "base64"));
writeFileSync("copy2.txt", "aGVsbG8K", "base64"); // decodes while writing
파일을 HTML이나 CSS에 넣으려면 결과 앞에 접두사를 붙이세요. `data:image/png;base64,${b64}`. Data URI 생성기는 브라우저에서 같은 문자열을 만들어 줍니다.
큰 파일 스트리밍
2 GB 파일을 인코딩하려고 메모리에 통째로 읽는 것은 좋지 않습니다. Transform 스트림은 3바이트 단위의 온전한 그룹만 인코딩하고 나머지를 다음 조각으로 넘긴다면 조각 단위로 인코딩할 수 있습니다.
import { createReadStream, createWriteStream } from "node:fs";
import { pipeline } from "node:stream/promises";
import { Transform } from "node:stream";
class Base64Encode extends Transform {
constructor() { super(); this.rest = Buffer.alloc(0); }
_transform(chunk, _enc, cb) {
const buf = Buffer.concat([this.rest, chunk]);
const cut = buf.length - (buf.length % 3);
this.rest = buf.subarray(cut);
cb(null, buf.subarray(0, cut).toString("base64"));
}
_flush(cb) { cb(null, this.rest.toString("base64")); }
}
await pipeline(createReadStream("big.bin"), new Base64Encode(),
createWriteStream("big.b64"));
1 MB 무작위 파일에서 출력은 readFileSync("big.bin").toString("base64")와 정확히 일치했습니다. 나머지를 넘기지 않으면 3의 배수가 아닌 모든 조각 경계마다 출력 중간에 = 패딩이 끼어듭니다. 그 이유는 Base64의 작동 원리에서 다룹니다.
디코딩하지 않고 디코딩 후 크기를 알려면 Buffer.byteLength("aGVsbG8K", "base64")를 쓰세요. 6을 반환합니다.
입력 검증
Buffer.from(s, "base64")는 잘못된 내용에도 절대 예외를 던지지 않습니다. 두 알파벳 어디에도 없는 문자는 버리고 남은 것을 반환합니다.
Buffer.from("aGk=!!", "base64"); // <Buffer 68 69>
Buffer.from("not base64 at all", "base64"); // <Buffer 9e 8b 5b 6a c7 ba e1 ab 5a 96>
Buffer.from("aGk", "base64").toString(); // hi (missing padding is fine)
신뢰할 수 없는 입력이라면 알파벳과 길이를 확인한 뒤, 다시 인코딩해서 비교하세요. 왕복 비교를 하면 aGl=처럼 0이 아닌 패딩 비트를 잡아낼 수 있습니다. aGl=도 hi로 디코딩되지만 정규 형식이 아닙니다.
function isBase64(s) {
return /^[A-Za-z0-9+/]*={0,2}$/.test(s) && s.length % 4 === 0 &&
Buffer.from(s, "base64").toString("base64") === s;
}
isBase64("aGk="); // true
isBase64("aGk"); // false (no padding)
isBase64("aGl="); // false (non-canonical)
isBase64("not base64"); // false
base64url이라면 정규식을 [A-Za-z0-9_-]로 바꾸고 길이 검사를 빼세요. Base64 검사기는 같은 검사를 대화식으로 보여 줍니다.
함정과 오류 메시지
- 잘못된 인수 타입.
Buffer.from(123, "base64")는TypeError [ERR_INVALID_ARG_TYPE]: The first argument must be of type string or an instance of Buffer, ArrayBuffer, or Array or an Array-like Object. Received type number (123)을 던집니다. 대개 JSON에서undefined나 숫자가 섞여 들어온 경우입니다. new Buffer(). 이 생성자는 보안상의 이유로 지원 중단되었습니다(DEP0005).Buffer.from이나Buffer.alloc을 사용하세요.- 바이너리를 텍스트로 디코딩하기. 이미지 바이트에
.toString("utf8")을 쓰면 오류 없이 대체 문자(U+FFFD)가 생기고, 다시 쓰면 파일이 손상됩니다. 바이너리 데이터는Buffer로 유지하세요. - Data URI 접두사.
Buffer.from("data:image/png;base64,iVBOR...", "base64")는 실패하지 않습니다. 접두사를 쓰레기 데이터로 디코딩할 뿐입니다. 먼저 쉼표에서 나누세요. - 줄 바꿈. Node는 디코딩할 때
\n과\r을 무시하므로 MIME 형식으로 줄 바꿈된 입력도 동작하지만, 인코더는 줄을 바꾸지 않습니다. MIME 줄이 필요하면b64.match(/.{1,76}/g).join("\r\n")을 사용하세요.
버전별 참고 사항
base64url인코딩: Node.js 15.7.0과 14.18.0.btoa와atob전역 함수: Node.js 16.0, 레거시로 표시됨.Uint8Array.prototype.toBase64와Uint8Array.fromBase64는 Node.js 24에서 쓸 수 없습니다(typeof가"undefined"반환). 더 새로운 V8 버전과 함께 들어올 예정이며, 그때까지는Buffer를 쓰면 됩니다.Buffer.from(string, "base64")는 모든 버전에서 공백과 잘못된 문자를 무시해 왔으므로 검증에 의존하지 마세요.
자주 묻는 질문
Buffer.from(str, 'base64')는 잘못된 입력에 예외를 던지나요?
아니요. Node는 알파벳 밖의 문자를 건너뛰고 첫 번째 =에서 멈추므로, 쓰레기 입력도 아무 오류 없이 어떤 바이트로든 디코딩됩니다. 신뢰할 수 없는 입력이라면 문자열을 직접 검증하세요.
base64url은 Node.js 어느 버전에서 추가되었나요?
base64url 인코딩은 Node.js 15.7.0에서 추가되었고 14.18.0으로 백포트되었습니다. 현재 버전이라면 어디서든 믿고 쓸 수 있습니다.
Node.js에서 btoa를 써도 되나요?
Node는 btoa와 atob를 레거시로 표시하고 Buffer를 권장합니다. 이 함수들은 브라우저 호환성을 위해 존재하며, JavaScript에서 Base64 사용하기에서 설명한 Latin-1 제한도 그대로 가지고 있습니다.