PHP에서의 Base64 인코딩: 완전한 가이드
채널이 좋아하지 않는 데이터를 살아남게 해야 하는 상황, 누구나 마주합니다. JSON 필드에 있어야 하는 바이너리 블롭. HTML 태그 안에 살아야 하는 이미지. 설정 파일에 있어야 할 인증서. URL, 헤더, 쿠키를 여행할 토큰. 이것이 바로 Base64 인코딩의 일상입니다. 원본 데이터의 바이트 3개를 64글자 알파벳의 문자 4개로 다시 쓰고, = 기호 하나둘이 꼬리를 마무리하죠. 그래서 결과는 무엇이든 실어 나를 수 있는 일반 텍스트가 됩니다. 이 사이트의 홈 페이지가 이 포맷을 온전히 설명해 두었으니, 이 글은 PHP가 당신에게 무엇을 주는지, 조용히 무엇을 대신 결정해 주는지, 그리고 함정이 어디 있는지에 초점을 맞춥니다.
PHP 쪽에서는, 이야기가 좋은 소식으로 시작됩니다. base64_encode()는 PHP 4부터 코어에 있었고, 인자는 하나, 언제나 문자열을 돌려 주며, 실패하지 않습니다. strict 모드도, 오류 경로도, 설정도 없습니다. 이 인코딩은 결정론적입니다. 같은 바이트는 언제나 같은 문자를 낳습니다. 개발자인 당신의 일은 함수를 작동시키는 것이 아니라, 주변 세계를 제대로 동작하게 만드는 것입니다. 목적지에 맞는 알파벳을 고르고, 맞는 줄바꿈을 붙이고, 맞는 문자 인코딩으로 변환한 뒤, 33퍼센트 크기 청구서를 똑바로 바라보며 감당하는 것. (Base64는 보통 데이터를 약 3분의 1로 늘립니다. 입력 바이트 3개당 문자 4개니까요. 머릿속에 넣어 두세요. 계속 다시 찾아올 테니까요.)
이 글을 끝까지 읽으면, PHP 개발자가 실제로 만나는 Base64의 모든 맛을 만드는 방법을 알게 될 것입니다. 한 줄 출력, MIME 래핑된 이메일, PEM 래핑된 키, URL-safe 토큰, data URI, 그리고 데이터가 메모리에 담기 너무 클 때를 위한 스트리밍 트릭까지.
함수 하나, 옵션 제로
API 전체입니다. 최신 PHP가 보고하는 그대로:
base64_encode(string $string): string
다시 읽어 보세요. 매개변수 하나, 반환 값 하나, 플래그 없음. 매뉴얼은 이것을 MIME base64라고 설명합니다. "8비트 클린이 아닌 전송 계층, 예를 들어 메일 본문 같은 것을 통해 바이너리 데이터가 전송을 살아남도록 설계된" 인코딩. 그 표현이 약속하지 않는 것에 주목하세요. 줄바꿈도, 래핑도, 출력이 어디에 살 것인지에 대한 의견도 없습니다. 함수는 긴 줄 하나를 내뿜고, 목적지가 원하는 래핑은 두 번째 호출로 당신이 할 일입니다. PHP 8.0부터 서명에는 네이티브 타입이 붙었고, PHP 8.1부터는 null을 넘기면 비권장 알림이 발생합니다. null이 될 수 있는 값은 먼저 ''으로 병합 처리하세요.
출력 크기는 호출 전에 예측할 수 있는 고정 패턴을 따릅니다:
| 입력 바이트 | 출력 문자 | 패딩 |
|---|---|---|
| 0 | 0 | 없음 |
| 1 | 4 | 두 개 = |
| 2 | 4 | 하나 = |
| 3 | 4 | 없음 |
| 3,000,000 | 4,000,000 | 없음 |
| 100,000 | 133,336 | = 두 개 |
패턴은 바이트 3개의 완결 그룹마다 문자 4개, 그리고 마지막 불완전 그룹에는 = 기호 하나둘로 패딩을 하는 것입니다. 알아둘 가치가 있는 귀결 하나: 바이트 1개와 3개는 모두 문자 4개를 낳습니다. 그래서 인코딩된 길이가 정확한 입력 크기를 숨깁니다. 추정은 할 수 있습니다(4로 나눈 뒤 3을 곱하고, 패딩을 빼면 됩니다)만, 정확하게 읽을 수는 없죠.
줄바꿈이 가는 곳
base64_encode()는 스스로 래핑하지 않으므로, 래핑 결정은 목적지의 문제입니다. 실제로는 세 가지 답이 있습니다.
줄바꿈 없음. 함수의 원시 출력, 정확히 한 줄. URL, JSON 페이로드, 헤더, 데이터베이스 값, 그리고 줄바꿈이 버그가 되는 모든 곳에서 이것이 원하는 것입니다. 많은 사람들이 "Base64만 줘"라고 말할 때 뜻하는 것도 바로 이것입니다.
MIME 래핑: 76자리에 CRLF. RFC 2045 6.8절의 이메일 관례입니다. 인코딩된 줄은 76자리를 넘을 수 없고, 디코더는 줄바꿈을 무시해야 합니다. 전통적인 짝 호출은 chunk_split()이며, 매뉴얼은 바로 이 이유 때문에 "See Also" 목록에서 base64_encode()와 함께 싣고 있습니다:
$binary = file_get_contents('/var/www/uploads/report.pdf');
$wrapped = chunk_split(base64_encode($binary), 76, "\r\n");
PEM 래핑: 64자리에 LF. 키와 인증서는 더 오래된 Privacy-Enhanced Mail 관례(RFC 1421)를 사용합니다. 더 짧은 64자 줄. 같은 도구, 다른 숫자:
$der = file_get_contents('/etc/ssl/raw-key.der');
$armor = "-----BEGIN PRIVATE KEY-----\n"
. chunk_split(base64_encode($der), 64, "\n")
. "-----END PRIVATE KEY-----\n";
두 래핑 방식 모두에 적용되는 chunk_split() 함정이 하나 있습니다. 입력 길이가 줄 길이의 정확한 배수여도, 함수는 결과 끝에 구분자를 붙입니다. 다운스트림 소비자가 끝의 빈 줄에 걸려든다면, 이것이 이유입니다. 구분자에 대한 rtrim() 호출이 고쳐 줍니다. 언젠가 당신을 구해 줄 비대칭도 알아 두세요. 디코더는 줄바꿈을 완전히 무시하므로, MIME 래핑된 페이로드와 래핑되지 않은 페이로드는 같은 바이트로 디코딩됩니다. 래핑은 줄 기반 도구와 사람에게 대한 배려이지, 의미의 차이는 아닙니다.
출력을 URL-safe하게 만들기
표준 알파벳에는 +와 /가 포함되며, 둘 다 텍스트 파일 바깥에서는 골칫덩이입니다. 폼 인코딩된 쿼리 문자열의 +는 당신의 애플리케이션이 보기 전에 공백이 되고, /는 URL에서 경로 구분자입니다. 파일명과 토큰도 각각 불만이 있습니다. RFC 4648 5절은 URL과 파일명 안전 알파벳으로 이것을 해결합니다. +가 -로, /가 _로 바뀌고, 끝의 = 패딩은 보통 생략됩니다. RFC는 이것을 "base64 인코딩과 같은 것으로 간주해서는 안 된다"고 단언하므로, 보통 base64url이라고 불리는 별개의 포맷으로 취급하세요.
만드는 것은 문자열 연산 두 개면 됩니다:
function base64url_encode(string $data): string
{
return rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
}
var_dump(base64url_encode("hi>?there\x00bin")); // string(18) "aGk-P3RoZXJlAGJpbg"
사용할 때: JSON Web Token의 부분, OAuth의 state와 nonce 파라미터, URL 경로에 넣는 API ID, 그리고 주소창이나 파일명에 복사될 모든 것. 사용하지 말 때: 이메일 본문, PEM 아머, 그리고 반대편에 표준 알파벳 소비자가 있는 모든 곳. -와 _는 그들의 어휘에 없으니까요. 그리고 두 알파벳을 조용히 섞지 마세요. URL-safe로 인코딩한 토큰은 어디에서나, 영원히 URL-safe로 디코딩되어야 합니다. 이것이 base64url의 상호운용 규칙 전부가 됩니다.
Unicode와 당신이 의도한 바이트
PHP 문자열은 바이트 시퀀스이며, base64_encode()는 받은 바이트가 무슨 의미인지 묻지 않고 그대로 인코딩합니다. 이것은 당신의 "텍스트"가 실제로 당신이 생각하는 인코딩이 아닌 날까지 장단입니다. 전형적인 실패: 에디터에서는 UTF-8처럼 보이지만, 구식 소스에서 Windows-1252로 도착한 문자열. 그 바이트를 그대로 인코딩하면, 디코딩한 뒤 UTF-8을 가정하는 수신인은 당신의 발음 기호가 달린 문자 대신 깨진 텍스트를 받게 됩니다.
수정법은, 인코딩하기 전에 정규화하는 것입니다. mbstring 확장을 쓰는 것이지요(PHP 소스에 함께 들어오지만, 기본적으로는 비활성화되어 있음):
$fromLegacy = "caf\xE9 au lait"; // Windows-1252 바이트: é는 0xE9
$utf8 = mb_convert_encoding($fromLegacy, 'UTF-8', 'Windows-1252');
$encoded = base64_encode($utf8);
var_dump($utf8); // string(13) "café au lait": 이제 é는 UTF-8 바이트 2개
소스가 이미 UTF-8이라면, 변환을 건너뛸 수 있고, 값싼 건전성 검사는 mb_check_encoding($utf8, 'UTF-8')입니다. 조언 한마디: 이미 인코딩된 Base64 문자열을 텍스트로 다시 인코딩해서 "수정"하려 하지 마세요. 바로 아래 함정 절의 이중 인코딩 함정이요, PHP 코드베이스에서 가장 흔한 단일 Base64 버그입니다.
파일, 블롭, 그리고 .b64 관례
가장 직관적인 인코딩 작업: 파일이 텍스트가 되는 것. PHP 문자열은 바이트이므로, 걱정할 "바이너리 모드"라는 것은 없습니다. file_get_contents()가 정확한 바이트를 건네고, base64_encode()가 정확한 텍스트를 건넵니다:
$binary = file_get_contents('/var/www/uploads/photo.png');
$encoded = base64_encode($binary);
file_put_contents('/var/www/uploads/photo.png.b64', $encoded);
var_dump(strlen($binary), strlen($encoded)); // 매번, 그 33% 청구서
이것을 안전하게 유지하는 습관이 두 개 있습니다. 첫째, 무엇을 인코딩하는지 알기. finfo 클래스(fileinfo 확장, 표준 PHP 빌드에 번들됨)가 파일명이 아니라 바이트에서 진짜 타입을 알려 줍니다:
$mime = (new finfo(FILEINFO_MIME_TYPE))->file('/var/www/uploads/photo.png');
var_dump($mime); // string(9) "image/png"
둘째, 저장을 계획할 때 크기 청구서를 기억하기: 500 KB 이미지는 670 KB 텍스트 파일이 되고, 1 GB 영상은 1.33 GB 텍스트 파일이 됩니다. 바로 그래서 아래 큰 데이터 절이 존재합니다.
data URI: 페이지에 이미지를 담기
data URI는 페이로드를 URL 안에 직접 내장하므로, 그것을 가져오기 위한 두 번째 요청이 필요 없습니다. RFC 2397은 모양을 정의합니다. data:, 옵션 미디어 타입, 옵션 ;base64 플래그, 쉼표, 그리고 데이터. 이미지가 있는 바이너리 미디어의 경우 플래그가 존재하므로, 페이로드는 정확히 base64_encode()가 만든 것입니다:
function data_uri_for(string $path): string
{
$mime = (new finfo(FILEINFO_MIME_TYPE))->file($path);
$payload = base64_encode(file_get_contents($path));
return 'data:' . $mime . ';base64,' . $payload;
}
$uri = data_uri_for('/var/www/uploads/avatar.png');
echo '<img src="' . $uri . '" alt="avatar" />' . "\n";
여기서 왜 Base64인가? URI는 원시 바이트나 쉼표를 안전하게 포함할 수 없는데, Base64 알파벳은 이스케이프가 전혀 필요 없으니까요. 다만 트레이드오프는 실재합니다. 인코딩된 페이로드는 파일보다 약 33퍼센트 크므로, HTML 문서 자체가 커집니다. 브라우저는 파일 URL을 캐시하는 방식으로 data URI를 캐시하지 않으므로, 매 페이지 뷰마다 바이트가 다시 내려받습니다. 그리고 RFC 자체가 data URI는 짧은 값에만 유용하다고 말합니다. 오래된 HTML 파서는 속성 길이에 대해 하드 제한이 있었고, 최신 브라우저는 훨씬 관대하지만, 그래도 태그 안의 메가바이트는 좋아하지 않습니다. 아바타, 아이콘, 작은 인라인 그래픽에는 이것을 쓰시고, 나머지는 전부 진짜 파일을 쓰세요.
JWT와 API 토큰
JSON Web Token은 모던 API에서 Base64의 플래그십 소비자이며, 위에서 본 URL-safe, 패딩 없는 방언을 사용합니다. RFC 7519에 따르면, 축약형 JWT는 점으로 구분된 base64url 세 부분입니다. 헤더, 페이로드, 서명. 헤더와 페이로드는 그냥 JSON이고, 서명은 원시 바이트입니다. 손으로 하나 만들어 보는 것은 모든 움직이는 부분을 눈으로 보는 즐거운 방법입니다:
function base64url_encode(string $data): string
{
return rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
}
$header = base64url_encode(json_encode(['alg' => 'HS256', 'typ' => 'JWT']));
$payload = base64url_encode(json_encode([
'sub' => '1234567890',
'name' => 'John Doe',
'iat' => 1516239022,
]));
$signingInput = $header . '.' . $payload;
$signature = base64url_encode(hash_hmac('sha256', $signingInput, $secret, true));
$token = $signingInput . '.' . $signature;
주의할 것이 두 개 있습니다. 서명은 원시 HMAC 바이트의 base64url 인코딩이며, 그래서 hash_hmac()는 원시 출력을 위해 true로 호출됩니다. 그리고 헤더와 페이로드는 누구나 읽을 수 있는데, 이것은 설계대로입니다. JWT는 서명된 표이지, 비밀이 아니니까요. 프로덕션에서는 서명이나 검증을 손으로 만들지 마세요. 커뮤니티 패키지는 firebase/php-jwt(v7, PHP 8.0 이상 필요)이며, Composer로 설치합니다:
composer require firebase/php-jwt
use Firebase\JWT\JWT;
$secret = 'correct-horse-battery-staple-long-enough-secret';
$token = JWT::encode([
'sub' => '1234567890',
'name' => 'John Doe',
'exp' => time() + 3600,
], $secret, 'HS256');
var_dump(substr_count($token, '.')); // int(2): 헤더, 페이로드, 서명
라이브러리 v7 라인의 버전 메모: HMAC 알고리즘은 최소 키 길이를 강제하므로, 32바이트보다 짧은 HS256 시크릿은 인코딩이 일어나기도 전에 거부됩니다. 긴 시크릿이 어차피 표준이고, 이것은 라이브러리가 그것을 대충 다루기를 거부하게 만드는 것에 불과합니다.
라이브러리가 base64url 변환, 서명, 만료 검사를 당신을 대신해 처리하고, 반쯤 믿을 수 있는 데이터를 돌려주는 대신 타입된 예외를 던집니다. 이것으로 토큰을 쓰면, base64_encode()를 직접 건드릴 일이 전혀 없는데, 그것이 정확히 마땅한 방식입니다.
HTTP: Basic 인증과 WebSocket 핸드셰이크
헤더를 짓는 작업이 두 개 있습니다. 둘 다 PHP가 Base64를 하고, 프로토콜이 나머지를 합니다.
HTTP Basic 인증(RFC 7617): 클라이언트가 Authorization: Basic 뒤에 username:password의 Base64를 보냅니다. 만드는 것은 문자열 연결 하나:
function basic_authorization_header(string $username, string $password): string
{
return 'Basic ' . base64_encode($username . ':' . $password);
}
$headers[] = 'Authorization: ' . basic_authorization_header('alice', 'secret123');
RFC가 당신을 그렇게 하도록 만들므로, 소리 내어 한 번 말하세요. 이것은 인코딩이지, 보호가 아닙니다. 패킷 캡처를 가진 사람은 한 키 스트로크로 두 부분 모두를 되찾으므로, Basic 인증은 HTTPS 연결에만 속합니다.
WebSocket 핸드셰이크(RFC 6455): 서버는 변형된 키를 에코해서 클라이언트를 들었음을 증명합니다. 클라이언트의 Sec-WebSocket-Key에 고정된 매직 GUID를 이어 붙이고, 그 결과의 SHA-1을 구한 뒤, 다이제스트를 base64 인코딩합니다. 이것은 표준 Base64입니다. 패딩 포함. URL이 아니라 헤더에 살기 때문이죠:
function websocket_accept_key(string $clientKey): string
{
return base64_encode(hash('sha1', $clientKey . '258EAFA5-E914-47DA-95CA-C5AB0DC85B11', true));
}
$accept = websocket_accept_key('dGhlIHNhbXBsZSBub25jZQ==');
var_dump($accept); // string(28) "s3pPLMBiTxaQ9kYGzzhZRbK+xOo="
그 예시는 RFC 자체의 것이므로, 유용한 자가 테스트가 됩니다. 당신의 구현이 같은 28자를 만든다면, WebSocket 계층이 제대로 말하고 있는 것입니다.
이메일: 본래의 용도
이 글의 나머지는 모두 하나의 사실에서 내려온 후손입니다. SMTP는 7비트 ASCII를 실어 나르도록 설계되었고, 사람들은 바이너리를 보내고 싶었으니까요. MIME 표준의 답, RFC 2045 6.8절에서 그것은 Base64를 Content-Transfer-Encoding으로 쓰는 것이었고, 이미 만났던 두 가지 내부 규칙이 따라옵니다. 최대 76자의 줄, 그리고 알파벳 밖의 모든 문자를 무시하는 디코더. 그래서 PDF 첨부파일은 이렇게 여행합니다:
$pdf = file_get_contents('/var/www/uploads/report.pdf');
$attachment = chunk_split(base64_encode($pdf), 76, "\r\n");
// 이제 메일 라이브러리가 $attachment를 MIME 부분에 넣고,
// Content-Transfer-Encoding: base64를 붙입니다
실용적인 숫자: 인코딩 자체의 비용은 33퍼센트이고, 76자마다 붙는 CRLF가 조금 더 추가되므로, 100 KB 첨부파일은 약 137 KB의 텍스트로 출항합니다. PHP에서 이메일을 쓸 때, 라이브러리들(PHPMailer와 그 안정한 친척들)이 래핑을 당신 대신 해 주며, 당신은 원시 바이너리를 건네기만 합니다. 언젠가 원시 .eml 파일에서 76자 너비의 문자 벽을 본다면, 이제 그것을 만든 정확한 알고리즘을 아실 것입니다.
키와 인증서를 위한 PEM 아머
키와 인증서는 문자 벽보다 더 많은 것이 필요합니다. 라벨이요. PEM 아머는 BEGIN 줄, 64자로 래핑된 Base64 블록, END 줄이며, Privacy-Enhanced Mail(RFC 1421)에서 이어받아 OpenSSL이 살려 온 관례입니다. PHP의 openssl 확장은 이 모양을 직접 만들고 소비합니다:
$res = openssl_pkey_new([
'private_key_bits' => 2048,
'private_key_type' => OPENSSL_KEYTYPE_RSA,
]);
openssl_pkey_export($res, $pem);
// $pem은 이미 아머가 걸려 있음: BEGIN 라벨, 64자 줄, END 라벨
재미있는 경우는, 아머를 손으로 다시 짜야 할 때입니다. 예를 들어 API에서 원시 DER 바이트를 받아, PEM만 읽는 도구를 위해 PEM 파일이 필요할 때. 관례는 줄당 64자, LF 줄바꿈, 그리고 내용물을 이름 붙이는 라벨입니다:
$rearmored = "-----BEGIN CERTIFICATE-----\n"
. chunk_split(base64_encode($der), 64, "\n")
. "-----END CERTIFICATE-----\n";
var_dump(openssl_x509_parse($rearmored) !== false); // bool(true): 아머가 유효하다
라벨을 잘못 쓰면, Base64가 아무리 완벽해도 파일은 쓰레기가 됩니다. 그리고 줄 길이를 잘못 쓰면, 디코더가 줄바꿈을 무시하므로 대부분 도구는 여전히 읽겠지만, 차이 비교 도구와 사람들이 고생합니다. 64가 그 숫자입니다.
설정 파일, 환경 변수, 데이터베이스
Base64는 텍스트 컨테이너이므로, 그렇지 않으면 컨테이너를 깨뜨릴 값들에 대한 몰래 반입 도구가 됩니다. 세미콜론과 따옴표로 가득한 데이터베이스 DSN, .env 파일 안의 JWT, TEXT 칼럼 안의 바이너리 블롭: 모두 하나의 긴 안전한 문자열이 됩니다.
환경 변수 방식은 두 단계의 의식입니다. 한 번, 설정을 짓는 기계에서 인코딩합니다:
echo 'DB_DSN_B64=' . base64_encode('pg:host=db;password=qu"ote') . PHP_EOL;
그다음, 애플리케이션의 모든 부팅마다 시작 시점에 디코딩하고 검증합니다. 반쪽짜리로 붙여 넣힌 설정이 수수께끼 대신 확실히 실패하게 하려고요:
$dsn = base64_decode(getenv('DB_DSN_B64') ?: '', true);
if ($dsn === false) {
exit('DB_DSN_B64 is not valid Base64.');
}
데이터베이스에서는 같은 아이디어가 바이너리를 텍스트 칼럼에 저장합니다. 크기 청구서가 적용됩니다. 저장되는 값은 블롭보다 약 33퍼센트 크므로, 1 MB 파일은 칼럼에서 약 1.33 MB를 차지하고, 칼럼 타입을 고를 때 그것을 염두에 두어야 합니다. 그리고 어디서나 그렇듯 같은 경고: 이것은 포맷 안전이지, 비밀이 아닙니다. 설정을 읽거나 칼럼을 쿼리할 수 있는 사람은 한 번의 호출로 역전시킬 수 있습니다. 값이 민감하다면 암호화하세요. Base64는 그것을 이식 가능하게 할 뿐입니다.
큰 데이터와 단단한 메모리
인코딩은 당신에게 비용을 청구하는 방향입니다. 출력이 입력보다 세 분의 일 크므로, 2 GB 바이너리는 메모리에서 2.66 GB 인코딩 문자열을 원합니다. 긴 수명의 웹 프로세스나 메모리가 제한된 호스트에서는, 이것은 한꺼번에 통째로 읽기 대신 스트림할 이유이며, PHP는 두 가지를 줍니다.
첫 번째는 convert.base64-encode 스트림 필터입니다. 함수의 스트리밍 쌍둥이지요. 연관 배열로 파라미터를 지원합니다. 래핑 너비는 line-length, 구분자는 line-break-chars. 문자열 전체를 담아 두지 않고 chunk_split() 효과를 재현합니다:
$in = fopen('/var/www/uploads/big.bin', 'rb');
$out = fopen('/var/www/uploads/big.b64', 'wb');
stream_filter_append($out, 'convert.base64-encode', STREAM_FILTER_WRITE, [
'line-length' => 64,
'line-break-chars' => "\n",
]);
stream_copy_to_stream($in, $out);
fclose($in);
fclose($out);
두 번째는 전설적인 57바이트 트릭이며, PHP 전설의 작은 조각입니다. 76자 MIME 줄은 원본 데이터 바이트 57개를 정확히 담으므로, 입력 파일을 57바이트 배수의 청크로 읽으면 각 청크가 독립적으로 인코딩되고, 청크 사이에 끌어 나눌 남은 비트가 없습니다. 8151바이트 청크로 읽으면(57 곱하기 143: 출력 76자 완결 줄 143줄, PHP의 전통적인 8192바이트 I/O 버퍼와 가까움), 파일이 MIME 완벽하게 스트림으로 나가는 동안 메모리는 평평하게 유지됩니다:
$in = fopen('/var/www/uploads/big.bin', 'rb');
$out = fopen('/var/www/uploads/big.b64', 'wb');
while (!feof($in)) {
$plain = fread($in, 57 * 143);
$encoded = chunk_split(base64_encode($plain), 76, "\r\n");
fwrite($out, $encoded);
}
fclose($in);
fclose($out);
어느 쪽을 고르나요? 배관 공사를 PHP가 소유하게 하고, 정확한 청크 경계는 신경 쓰지 않는다면 필터를. 결정론적인 MIME 출력, 진행 상황 훅, 버퍼 크기에 대한 하드 상한이 필요하다면 57바이트 루프를. 어느 쪽이든, 메모리 점유는 한 파일이 아니라 한 청크에 머뭅니다.
PHP 특유의 함정
실제 PHP 코드베이스에서 나타나는 함정들을 한곳에 모아 정리했습니다:
- 이중 인코딩. 전형적인 케이스: 이미 Base64인 값(환경 변수, 데이터베이스, 이전 스크립트에서 온 것)을 아무도 검사하지 않아서
base64_encode()를 한 번 더 통과시킵니다. 결과는 한 번 디코딩하면… 또 Base64가 나옵니다. 치료법은 경계에서의 왕복 검사, 아니면 코드베이스의 모든 인코딩을 소유하는 하나의 잘 알려진 함수입니다. - URL의
+. 표준 출력에는+와/가 들어 있습니다. 폼 인코딩된 쿼리 문자열에서는 플러스가 당신의 코드가 보기 전에 공백이 되고, 경로에서는 구분자입니다. URL에 얽히는 모든 것에는 base64url을 내보내거나, 값을 통째로rawurlencode()로 퍼센트 인코딩하세요. - 끝의 구분자.
chunk_split()는 줄 길이의 정확한 배수여도 결과를 구분자로 끝냅니다. 끝의 빈 줄은 보통 해롭지 않지만(디코더가 무시하므로), 천진한 줄 카운터와 차이 비교 도구를 걸리게 합니다. 소비자가 까다로우면 구분자를rtrim()하세요. - 래핑 불일치. 76자 MIME 줄로 쓰고, 소비자가 64자 PEM 줄을 기대하는 경우(또는 그 반대)는 디코딩 문제가 아닙니다. 디코더가 줄바꿈을 무시하니까요. 하지만 줄 기반 도구와 사람이 읽는 문제입니다. 목적지가 기대하는 관례를 고르고, 그것에 붙어 있으세요.
- 끝의 줄바꿈은 데이터입니다.
base64_encode()는 모든 바이트를 인코딩하며, 텍스트 파일 끝의 줄바꿈도 포함됩니다. 두 시스템이 똑같이 보이는 텍스트에 대해 "다른" Base64를 만든다면, 끝의\n가 단골 용의자입니다. - Base64는 암호화가 아닙니다. 비밀번호가 데이터베이스에 닿기 전에 인코딩한다고 그것이 보호되는 것이 아니라, 포맷되는 것입니다. "암호화된" 칼럼은 쿼리 접근을 가진 사람에게는 한 번의 함수 호출만큼만 평문을 가립니다. 진짜 시크릿은 암호화하거나 해시하세요. Base64는 전송용 분장일 뿐입니다.
- 메모리는 세 분의 일 더 커집니다. 32비트 PHP 빌드나 메모리 제한이 빡빡한 호스트에서는, 큰 바이너리를 인코딩하는 것이 아예 실패할 수 있습니다.
memory_limit을 조정하기 전에, 위에서 보인 대로 스트림하세요. - 줄바꿈은 절대 추가되지 않습니다. 함수 설명의 "MIME base64"는 "MIME 래핑된 출력"을 뜻하지 않습니다. 출력이 76자 줄을 필요로 한다면,
chunk_split()나 필터로 당신이 추가합니다.
base64_encode의 짧은 역사
PHP 이야기의 인코딩 쪽은, 가장 좋은 의미로, 거의 상쾌할 정도로 지루합니다. base64_encode()는 PHP 4에서 매개변수 하나, 옵션 없이 코어 함수로 도착했고, 그 이후 단 하나도 얻지 않았습니다. strict 모드는 필요 없었습니다(데이터를 만드는 쪽이 당신인데, 엄격할 것이 뭐가 있냐고요), 패딩 옵션은 추가된 적 없고, 래핑 일부터 첫날부터 chunk_split()에 위임했죠. 그래서 두 함수는 여전히 매뉴얼의 "See Also" 목록에서 함께 있습니다.
매뉴얼은 33퍼센트 수치를 사람들이 기억하는 한 내내 싣고 있었습니다. "Base64 인코딩된 데이터는 원본 데이터보다 약 33% 더 많은 공간을 차지한다"는 문장이요. 그 문장은 오늘도 그대로 있고, 그것이 이 글에 그 숫자가 등장하는 이유입니다. 스트림 필터 convert.base64-encode는 나중에 합류했고, 그것도 자라면서 벗어던져야 할 버그가 있었습니다. 2015년, PHP는 필터가 메모리 스트림에서 읽기 모드로 동작할 때 마지막 패딩 문자를 빠뜨릴 수 있는 결함(bug #68532)을 고쳤는데, 이것은 바로 함수 기반 경로가 결코 겪지 않는 종류의 조용한 손상입니다. PHP 8.0이 네이티브 string 매개변수와 반환 타입을 추가했고, 변경 로그는 거기서 끝납니다. 함수 하나, 매개변수 하나, 20년, 옵션 제로: 표면을 처음부터 제대로 짰다는 것에 대한 기념비.
재미있는 PHP 사실들
레퍼런스가 기이함 없이는 완성되지 않으므로:
- 빈 항등식.
base64_encode('')은''입니다. 패딩도, 출력도, 뜻밖의 결과는 없습니다: 빈 것이 들어가면 빈 것이 나옵니다. - 이상한 주소. PHP 매뉴얼은
base64_encode()를 "기타 기본 확장" 책의 "URLs" 장에 등재합니다. "인코딩" 장은 없어요. "URLs"에서parse_url()옆에 그들을 찾을 수 있습니다. - 알파벳은 한 번도 움직이지 않았습니다. PHP 4부터 PHP의 인코더는 같은 64글자 알파벳을 내보내고 있습니다. 2001년 Windows 상자에서 PHP 4 스크립트가 만든 Base64 문자열은, 오늘 Linux의 PHP 8.4에서 동일하게 디코딩됩니다. 이것이 25년의 전적을 가진 상호운용입니다.
- 바이트 1개와 3개는 길이에서 동일해 보입니다. 둘 다 문자 4개를 낳고, 패딩만이 그것들을 구별합니다. 바로 그래서 이 글의 크기 표가 존재합니다.
- 57은 마법의 숫자입니다. 76자 MIME 줄은 원본 데이터 바이트 57개를 정확히 담고, 그 우연한 일치 덕분에 큰 데이터 절의 스트리밍 청크 루프가 읽기 사이에 상태를 끌고 다니지 않고 가능해집니다.
- 이것은 dial-up 형제가 있습니다.
base64_encode()의 "See Also"에는convert_uuencode()까지 등재되어 있는데, uuencode의 PHP 래퍼입니다. MIME이 Base64를 표준화하기 전에 이메일 바이너리를 인코딩했던 포맷이지요. String Functions 장에 살고 있지만, 이것은 이 함수의 본래 목적의 화석 기록입니다. - 오래된 브라우저들은 패딩을 까탈스럽게 대했습니다. 2004년 php.net 노트 하나에, Internet Explorer가
=를 포함하는 쿠키 이름을 거부했다는 보고서가 있습니다. 그래서 베테랑 코드들은 쿠키에 저장된 Base64에서 끝의 패딩을 떼어 내곤 했죠. 모던한 세팅에서는 그 트릭이 필요 없지만, 당신이 이어받을 수 있는 기이한rtrim($x, '=')호출을 설명해 줍니다.
반대편
이것이 인코딩 쪽입니다. 그리고 둘 중 더 쉬운 쪽이죠. 함수는 실패하지 않고, 출력은 결정론적이며, 포맷은 처음부터 끝까지 당신이 통제하는 것입니다. 더 어려운 방향은 다른 사람들의 Base64를 받는 쪽입니다. 그들의 패딩 선택, 그들의 줄바꿈, 그들의 URL-safe 방언, 그들의 손상된 붙여넣기. 바로 거기에 디코더가 strict 모드, 검증 파이프라인, 그리고 모든 것에 대한 건강한 의심을 필요로 하는 곳입니다. 이 페이지에서 링크된 PHP에서의 Base64 디코딩은 디코딩 쪽을 같은 깊이로 다룹니다.
마지막 업데이트: 2026-09-08
관련 문서: PHP에서의 Base64 디코딩: 완전한 가이드