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

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

여러분에게는 바이트가 있고, 문자열이 필요합니다. 페이로드는 파일일 수도, 인증 자격 증명일 수도, 설정 토큰일 수도, 아니면 JSON 문서 안에 실려 가는 바이너리 덩어리일 수도 있고, 채널은 텍스트만 받아들입니다. Base64는 그것을 해결하는 교환입니다. 입력 바이트 3개가 64글자 알파벳에서 고른 문자 4개가 되므로, 출력은 언제나 4의 깨끗한 배수이며, 언제나 텍스트 전용 세계에서 안전합니다. 대가는 고정되어 있습니다. 문자가 33퍼센트 더 늘고, 마지막 청크가 짧으면 포맷이 끝에 = 패딩 문자 하나둘을 붙입니다. 이 가이드는 그 교환을 올바르게 하는 Dart 레시피입니다.

설치할 것은 없습니다. Base64는 2015년 Dart 1.13부터 dart:convert에 함께 들어 있고, API는 그때부터 지금까지 안정적입니다. 표준과 URL 안전, 두 알파벳 모두 10년 넘게 사용할 수 있었습니다. 홈 페이지에서 포맷을 깊이 있게 다루지만, 여기는 그 일의 인코딩 쪽입니다. 전체 API 표면, 가장 흔한 버그를 피하는 바이트 우선 원칙, 패딩과 알파벳 선택, 그리고 실무일들: JWT, data URI, 파일 업로드, HTTP 헤더, MIME, 설정, 스트림, 명령줄입니다. 디코딩, 즉 반대 방향은 이 글의 끝에 링크된 별도의 가이드가 담당합니다.

import 하나, 알파벳 둘, 패딩 규칙 하나

인코딩의 공개된 표면 전체는 dart:convert 안에 있습니다:

항목 알파벳 이런 때에 쓰면 좋은가
base64Encode(bytes) 표준: A-Z a-z 0-9 + /, 패딩됨 API, MIME, Basic 인증, 대부분의 소비자
base64UrlEncode(bytes) URL 안전: A-Z a-z 0-9 - _, 여전히 패딩됨 URL, 파일 이름, JWT, 객체 ID
base64.encode(bytes) 표준, 탑레벨 호출과 동일 스트림 변환과 코덱 파이프라인
Base64Encoder().convert(bytes) 표준 이름 붙인 인코더 인스턴스가 필요할 때

규칙 두 개가 네 행을 모두 덮습니다. 첫째, 입력은 바이트 값의 리스트, 0에서 255까지의 정수여야 합니다. 음수나 256 이상을 포함한 그 외의 모든 값은 잘못된 인덱스를 명시하는 ArgumentError를 던집니다. 둘째, 출력은 언제나 패딩됩니다. 패딩 없는 출력을 만들 플래그, 생성자, 옵션은 없습니다. 포맷의 패딩은 데이터의 속성이기 때문이고, 그것을 없애고 싶은 규격은 별도의 문서화된 단계로 제거하는 것이죠. 처음부터 끝까지 가능한 가장 작은 예제입니다:

import 'dart:convert';
void main() {
  final text = 'Dart is open source';
  final bytes = utf8.encode(text);
  final encoded = base64Encode(bytes);
  print(encoded); // RGFydCBpcyBvcGVuIHNvdXJjZQ==
}

바이트 우선: 여러분을 구하는 순서

가장 흔한 Dart base64 버그는 애초에 base64와 무관합니다. 연산의 순서에 관한 것입니다. Dart의 String은 UTF-16 코드 유니트의 연속이고, base64Encode(text.codeUnits)를 호출하면 수신자가 기대하는 바이트가 아니라 그 16비트 유니트들이 압축됩니다. 순수 ASCII에서는 두 경우가 우연히 일치하므로, 이 버그는 첫 강세 문자, 이모지, 혹은 CJK 텍스트가 도착할 때까지 숨어 있습니다. 그리고 인코더는 그 일을 거부합니다. 0x4e16 같은 코드 유니트는 바이트 값이 아니기 때문이죠:

import 'dart:convert';
void main() {
  final message = 'Héllo Wörld 世界';
  print(utf8.encode(message).length); // 20
  print(message.codeUnits.length); // 14
  print(base64Encode(utf8.encode(message)));
  try {
    base64Encode(message.codeUnits);
  } on ArgumentError catch (e) {
    print(e);
  }
}

ArgumentError는 범칙한 인덱스를 정확히 가리키므로, 실패는 조용한 것이 아니라 크게 울립니다. 지켜야 할 원칙: base64에 말을 걸기 전에 바이트가 무엇인지 결정하세요. 텍스트는 이름 붙인 인코딩을 거칩니다. 모던 데이터라면 utf8.encode이고, 그 결과로 나온 List<int>가 압축되는 것입니다. 파일이나 네트워크 소켓에서 온 바이트는 이미 Uint8List로 도착하는데, 이것은 어떤 변환도 없이 인코더에 딱 맞는 모양입니다.

패딩: 인코더의 일

Base64는 바이트 3개의 그룹을 문자 4개로 매핑하므로, 길이가 3의 배수가 아닌 페이로드는 끝에 부분 그룹을 남깁니다. 포맷은 그 부족분을 = 문자로 표시합니다. 입력 바이트 1개는 문자 4개에 패드 2개가, 2개는 문자 4개에 패드 1개가, 3개는 정확히 문자 4개가 됩니다. Dart 인코더는 그것을 여러분을 위해, 조건 없이 해 줍니다:

import 'dart:convert';
void main() {
  print(base64Encode([0x41])); // QQ==
  print(base64Encode([0x41, 0x42])); // QUI=
  print(base64Encode([0x41, 0x42, 0x43])); // QUJD
}

그 조건 없는 동작은 기능입니다. 출력은 언제나 합법적이고 스스로를 설명하는 base64 문자열이니까요. 규격이 패딩 없는 변형을 요구할 때, JWT가 보통의 이유인데, 제거는 여러분의 명시적이고 눈에 보이는 단계이지, 라이브러리 설정이 아닙니다:

base64UrlEncode(bytes).replaceAll('=', '')

제거를 규격의 경계가 있는 자리에 두고, 이름을 붙이고, 문서화하세요. 이 교환의 디코더 쪽, 즉 손상되거나 패딩이 벗겨진 입력이 어떻게 수리되는지는 디코딩 가이드에서 다룹니다.

URL 안전 Base64

표준 알파벳에는 +, /, =가 들어 있고, 이 세 문자는 URL 문법과 충돌합니다. 쿼리 구분자, 경로 구분자, 파라미터 구분선이죠. RFC 4648에서 base64url로 표준화된 URL 안전 알파벳은 +를 -로, /를 _로 바꾸어, 출력이 이스케이프 없이 경로 세그먼트, 쿼리 값, 파일 이름 안에 살 수 있게 합니다. 바뀌는 두 문자를 모두 사용하는 바이트에 대한 차이는 이렇습니다:

import 'dart:convert';
void main() {
  final tricky = [0xfb, 0xff, 0xfe, 0xf9];
  print(base64Encode(tricky)); // +//++Q==
  print(base64UrlEncode(tricky)); // -__--Q==
}

맛으로 고르지 말고, 소비자로 고르세요. 값이 URL, JWT, 파일 이름 안에 살 것이라면 base64UrlEncode로 인코딩하고, 규격이 패딩 없다면 패딩을 제거하세요. 값이 MIME 본문, Basic 인증 헤더, 혹은 "base64"라고 쓴 API 계약의 필드가 될 것이라면 표준 알파벳을 사용하세요. 그냥 base64는 표준을 의미하기 때문입니다. 엄격한 소비자 눈에는 두 알파벳이 서로 바꿔 쓸 수 있는 것이 아닙니다. 표준 base64를 기대하는 서버는 -가 담긴 페이로드를 400으로, 그보다 도움이 되는 것은 아무것도 없이 거부할 수 있습니다.

문자 집합: 어떤 바이트를 압축하고 있는가?

입력이 텍스트라면, 인코딩 단계가 base64가 볼 바이트를 결정합니다. 그리고 반대편의 소비자는 문자 집합을 가정하고 있죠. 여러분의 가정과 소비자의 가정이 다르면, 출력은 잘못된 바이트의 완벽하게 유효한 base64가 됩니다. 가장 나쁜 종류의 버그입니다. 아무것도 예외를 던지지 않으니까요. 어떤 모던한 교환이든, UTF-8이 기본입니다. 다른 싱글바이트 인코딩은 레거시 데이터를 위해 존재합니다:

인코딩 무엇에 쓸 때 무엇으로 인코딩
utf8 모던 텍스트, JSON, 웹의 모든 것 utf8.encode(text)
latin1 레거시 서양 싱글바이트 데이터 latin1.encode(text)
ascii 단순 7비트 텍스트 ascii.encode(text)
import 'dart:convert';
void main() {
  final modern = base64Encode(utf8.encode('Héllo'));
  final legacy = base64Encode(latin1.encode('Héllo'));
  print(modern); // SMOpbGxv
  print(legacy); // SOlsbG8=
}

같은 단어, 다른 바이트, 다른 base64. 길이에 눈여겨볼 일입니다. 강세 기호가 두 바이트 시퀀스라서 UTF-8은 Héllo에 바이트 6개를 쓰고, Latin-1은 5개에 들어맞힙니다. 소비자가 여러분이 쓰지 않은 인코딩으로 디코딩하면 모지바케가 나오는데, 데이터가 전송 중 손상된 것처럼 보입니다. 사실은 의도에서 손상된 것이죠.

JWT: 토큰 쓰기

JSON Web Token은 점을 구분으로 삼아 이어진 base64url 부분 3개, 헤더, 페이로드, 서명입니다. RFC 7515가 두 가지 세부 사항을 고정합니다. 알파벳은 URL 안전이고, 패딩은 생략된다는 것입니다. 토큰이 URL과 헤더 안에 있도록 설계되어 있기 때문이죠. HS256 알고리즘의 서명은 header.payload의 HMAC-SHA256이며, 그 자체는 패딩 없는 base64url입니다. crypto 패키지로 손수 만들어 보면 몇 줄이고, 보기에 더 투명합니다:

import 'dart:convert';
import 'package:crypto/crypto.dart';
String base64UrlNoPadding(List<int> bytes) {
  return base64UrlEncode(bytes).replaceAll('=', '');
}
String createJwt(Map<String, dynamic> header, Map<String, dynamic> payload,
    List<int> secretKey) {
  final signingInput =
      '${base64UrlNoPadding(utf8.encode(jsonEncode(header)))}.'
      '${base64UrlNoPadding(utf8.encode(jsonEncode(payload)))}';
  final mac = Hmac(sha256, secretKey).convert(utf8.encode(signingInput));
  final signature = base64UrlNoPadding(mac.bytes);
  return '$signingInput.$signature';
}
void main() {
  final token = createJwt(
    {'alg': 'HS256', 'typ': 'JWT'},
    {'sub': 'user-42', 'exp': 1893456000},
    utf8.encode('a-32-byte-secret-key-0123456789'),
  );
  print(token);
}

서명은 압축된 바로 그 바이트 위에서 계산되므로, 여러분이 내보내는 문자열과 같은 것을 서명하기만 하면, 반대편의 검증은 같은 단계의 반복입니다. 경고 세 가지. pub.dev의 옛 jwt 패키지는 2014년 것으로 널 세이프티보다 빠릅니다. 이코시즘의 작동하는 답은 crypto로 여기에 보이는 일을 하는 것입니다. alg: none인 토큰을 절대 내보내지 마세요. 그리고 절대 클라이언트가 알고리즘을 고르게 하지 마세요. 그리고 페이로드는 아무나 읽을 수 있다는 점도 기억하세요. 그래서 토큰이 증명하도록 의도된 것만 넣으세요.

Data URI: 텍스트 안에 실어 보내는 파일

RFC 2397에서 정의한 data URI는 페이로드가 데이터 자체인 URL입니다. data URI 안의 바이너리 콘텐츠는 base64 인코딩됩니다. 그래서 텍스트 문서가 이미지, 폰트, 첨부 파일을 내장해야 할 때 이 포맷이 어디든 등장합니다. HTML 속성, CSS, JSON, 설정 파일이죠. Dart는 URI 패키지 없이 네이티브로 그 URI를 만들 수 있습니다:

import 'dart:convert';
import 'dart:io';
Future<void> main() async {
  final png = await File('icon.png').readAsBytes();
  final imageUri = Uri.dataFromBytes(png, mimeType: 'image/png');
  print(imageUri); // data:image/png;base64,iVBOR...
  final note = Uri.dataFromString('Hello, Dart!');
  print(note); // data:,Hello,%20Dart!
}

Uri.dataFromBytes는 기본적으로 base64 인코딩을 합니다 (다른 형태를 위한 percentEncoded: true 옵트인이 있지만), 이것이 바이너리에 맞는 인코딩입니다. Uri.dataFromString는 기본적으로 퍼센트 인코딩을 합니다. 짧은 텍스트는 그렇게 하면 더 짧기 때문이죠. 바이트 압축 형태를 원하면 base64: true 플래그를 받아들입니다. 실무적 함정은 규모입니다. 페이로드는 33퍼센트 오버헤드와 함께 문서 안에 실려 다니므로, data URI는 작은 에셋, 아이콘과 섬네일을 위한 것이지, CSS를 통해 메가바이트를 실어 보내는 것이 아닙니다.

파일: 텍스트 채널을 위한 바이트 압축

일상적인 일: JSON, 설정 파일, 혹은 어떤 텍스트 전용 전송을 반드시 통과해야 하는 파일입니다. 패턴은 바이트를 읽고, 인코딩하고, 내장하는 것입니다:

import 'dart:convert';
import 'dart:io';
Future<void> main() async {
  final image = await File('photo.jpg').readAsBytes();
  final encoded = base64Encode(image);
  final upload = jsonEncode({
    'name': 'photo.jpg',
    'size': image.length,
    'data': encoded,
  });
  print('payload ${upload.length} chars for ${image.length} bytes');
}

머리에 둬야 할 숫자는 증가입니다. 2,000바이트 파일은 base64 문자 2,668개가 되고, JSON 키들이 합류하면 조금 더 늘어납니다. 함정 두 가지. 첫째, 입력이 이미 인코딩되어 있지는 않은지 확인하세요. 이미 base64인 문자열을 base64 인코딩하는 것은 고전적인 이중 인코딩 버그이며, 그것은 "성공적으로" 또 다른 base64 벽으로 디코딩됩니다. 둘째, 채널이 바이너리를 실을 수 있다면, multipart/form-data가 존재하는 이유가 바로 그것인데, 바이너리를 싣세요. 4분의 1 작고, base64 세금이 순수한 낭비이니까요.

HTTP와 API: 헤더와 페이로드

HTTP에서 가장 익숙한 인코딩 일은 Authorization: Basic 헤더입니다. Basic이라는 단어, 공백, 그리고 username:password의 표준 알파벳 base64:

import 'dart:convert';
import 'package:http/http.dart' as http;
Future<void> main() async {
  final credentials = base64Encode(utf8.encode('octocat:secret'));
  final client = http.Client();
  final response = await client.get(
    Uri.parse('https://httpbin.org/basic-auth/octocat/secret'),
    headers: {'Authorization': 'Basic $credentials'},
  );
  print(response.statusCode);
  client.close();
}

http 패키지로, dart pub add http 한 번이면, 헤더는 요청 안의 그냥 문자열입니다. 함정은 보안 틀에 있습니다. 여기의 base64는 보호가 아니라 은폐입니다. 누구든 한 단계로 되돌릴 수 있고, 바로 그래서 Basic 인증은 TLS 연결에만 속합니다. 거기서 보호를 하는 것은 인코딩이 아니라 전송이지요. API 페이로드 필드는 계약을 따르세요. base64라고 하면, 그것은 패딩이 있는 표준 알파벳이며, URL 안전 변형은 엄격한 소비자가 거부할 다른 것입니다.

이메일과 MIME: 76자 래핑

이메일이 바이너리를 실을 수 있게 하는 시스템 MIME은 base64를 콘텐츠 전송 인코딩으로 쓰며, RFC 2045는 인코딩된 줄이 76자를 넘어서지 말고 그 사이에는 CRLF를 두라고 규정합니다. 이 한도는 MIME 관례입니다 - 76자에 CRLF를 더해도 80열 디스플레이에 여유 있게 들어맞으니까요 - 그리고 모든 준수 인코더는 래핑합니다. Dart 인코더는 한 번에 끊기지 않는 문자열을 만들므로, 래핑은 짧은 후처리 단계입니다:

import 'dart:convert';
String wrapForMime(String base64Text, [int lineLength = 76]) {
  final buffer = StringBuffer();
  for (var i = 0; i < base64Text.length; i += lineLength) {
    final end = i + lineLength > base64Text.length
        ? base64Text.length
        : i + lineLength;
    buffer
      ..write(base64Text.substring(i, end))
      ..write('\r\n');
  }
  return buffer.toString();
}
void main() {
  final encoded = base64Encode(utf8.encode('Hello from an email attachment'));
  print(wrapForMime(encoded));
}

패딩까지 포함해 완성된 문자열을 래핑하고, 마지막 줄은 76까지 그대로 두세요. 절대로 해서는 안 될 한 가지가 있습니다. 한 글자라도 아끼려다 래핑 전에 패딩을 제거하는 것. 패드는 인코딩된 콘텐츠의 일부이며, 줄을 다시 조립하는 소비자는 그것 없이 오면 결과를 거부합니다.

설정: 시크릿을 한 줄로

따옴표, 줄바꿈, 혹은 다른 귀찮은 문자를 포함하는 토큰, 키, 자격 증명은 설정 한 줄이나 CI 변수에 깔끔히 들어가도록 때때로 base64 인코딩됩니다. 먼저 솔직한 틀: 이것은 암호화가 아니라 은폐이고, 레포지토리나 로그에 한 번이라도 닿는 것은 공개된 것입니다. 이 패턴은 깔끔함에는 쓰되, 결코 비밀에는 쓰지 마세요. 값을 인코딩하는 것은 호출 한 번입니다:

import 'dart:convert';
String forEnvFile(String secret) {
  return base64Encode(utf8.encode(secret));
}
void main() {
  final line = 'API_TOKEN_B64=${forEnvFile('sk-live-abc123')}';
  print(line); // API_TOKEN_B64=c2stbGl2ZS1hYmMxMjM=
}

값은 .env 파일, CI 시크릿, 혹은 컴파일 시 정의에 앉아 있고, 디코딩 한 번 이후 평문으로 돌아옵니다. 시크릿이 전송 중이거나 저장되어 있을 때 보호되어야 한다면, 시크릿 매니저나 암호화 라이브러리를 쓰세요. base64의 일은 여기서는 파이프라인의 텍스트 처리를 단순하게 유지하는 것, 그 이상도 그 이하도 아닙니다.

스트림: 청크 경계를 넘어

바이트가 청크로 도착할 때, 네트워크 읽기, 블록으로 처리하는 파일, 인코더는 여러분이 아무것도 맞출 것 없이 해냅니다. 코덱이 부분 그룹을 청크 경계를 넘어 싣고 가므로, 청크 크기는 3의 배수일 필요가 없습니다:

import 'dart:convert';
import 'dart:typed_data';
Future<void> main() async {
  final data = Uint8List(100000);
  for (var i = 0; i < data.length; i += 31) {
    data[i] = i % 256;
  }
  final chunks = <List<int>>[data.sublist(0, 777), data.sublist(777)];
  final encoded = await Stream.fromIterable(chunks)
      .transform(base64.encoder)
      .join();
  print('in: ${data.length}, out: ${encoded.length}'); // in: 100000, out: 133336
}

777바이트와 99,223바이트, 어색한 크기의 두 청크가 하나의 올바른 133,336자 문자열을 만듭니다. 인코더가 각 불완전 그룹의 남은 비트들을 다음 청크가 도착할 때까지 주차해 두고, 패딩은 끝에서만 내보내기 때문이죠. 싱크를 선호한다면, base64.encoder.startChunkedConversion은 같은 상태 머신을 ByteConversionSink로 줍니다. (여러분은 바이트 청크를 넣고, 그것은 문자열을 내보냅니다) 큰 출력을 한 번의 큰 문자열로 합치지 않고 파일이나 소켓에 쓰는 데는 자연스러운 궁합입니다.

빅데이터: 처리량과 메모리

크기 계산은 정확하고, 머릿속에 둘 값입니다. 출력 길이는 입력 길이를 3으로 나눈 값을 올림해서 4를 곱한 것입니다. 1바이트, 2바이트, 3바이트 모두 4글자를 쓰고, 그 뒤로는 고정 33퍼센트 오버헤드입니다. 버퍼를 예약하거나 진도를 보고해야 할 때를 위한 공식입니다:

import 'dart:convert';
int encodedLength(int n) => (n + 2) ~/ 3 * 4;
void main() {
  print(encodedLength(100000)); // 133336
}

속도는 제약이 아닙니다. 인코더는 테이블 룩업 한 번 지나가는 작업으로, 메가바이트를 밀리초에 처리합니다. 제약은 와이어에서 그리고 메모리에서 청구되는 크기 세금 그 자체, 그리고 인코딩된 형태가 문자열이라는 사실입니다. 규모가 크면 이 둘을 함께 마음에 두세요. 크게 자라날 수 있는 페이로드는 큰 리스트 하나, 큰 문자열 하나로 쌓는 대신 위와 같이 인코딩을 스트림하세요. 같은 데이터의 반복 전송이라면, 채널에 바이너리 모드가 있는지 물어보세요. 33퍼센트는 어떤 알고리즘도 환불하지 않는 영구할증금입니다.

명령줄 인코더

VM은 인코더로 깔끔한 CLI를 만들어 줍니다. 이 도구는 파일 인자나 표준 입력을 읽고, 표준 알파벳 인코딩을 인쇄합니다:

import 'dart:convert';
import 'dart:io';
Future<void> main(List<String> args) async {
  final bytes = await _read(args);
  stdout.writeln(base64Encode(bytes));
}
Future<List<int>> _read(List<String> args) async {
  if (args.isNotEmpty) {
    return File(args[0]).readAsBytes();
  }
  final all = <int>[];
  await for (final chunk in stdin) {
    all.addAll(chunk);
  }
  return all;
}

bin/encode.dart로 저장해 dart run bin/encode.dart photo.jpg > photo.b64로 실행하거나, cat config | dart run bin/encode.dart로 파이프하세요. 동료인 읽어서 편평하게 만드는 디코더는 디코딩 가이드의 첫 번째 예제이며, 두 스크립트는 함께, 바이너리를 텍스트 채널을 통해 움직이는 작지만 진정 유용한 도구 모음입니다.

나가는 길에 무는 함정들

  • codeUnits 함정. base64Encode(text.codeUnits)는 바이트가 아니라 UTF-16 유니트를 압축합니다. ASCII에서는 작동하지만, 255를 넘는 첫 코드 유니트에서 ArgumentError를 던집니다. 텍스트는 언제나 이름 붙인 인코딩으로 먼저 인코딩하세요.
  • 알파벳 불일치. 표준 알파벳을 기대하는 소비자에게 URL 안전 출력을 건네는 것은 곧 일어날 400입니다. 알파벳은 규격에서 정하고, 한 번 인코딩하고, 사후에 변환하지 마세요.
  • 패딩 가정. Dart는 언제나 패딩합니다. 규격이 패딩 없이를 원하면, 경계에서 명시적 단계로 replaceAll('=', '')를 제거하고, 계약에 그렇게 쓰세요.
  • 문자 집합 이탈. UTF-8으로 디코딩하는 소비자를 위해 Latin-1 바이트를 인코딩하면, 잘못된 데이터의 유효한 base64가 됩니다. 아무것도 예외를 던지지 않습니다. 텍스트가 그저 틀린 것이죠.
  • 이중 인코딩. 이미 base64인 값, 다른 설정에서 복사해 온 토큰을 base64 인코딩하는 것은, 고전적인 "또 다른 base64 벽으로 디코딩된다" 버그입니다.
  • 개인정보 착각. Base64는 포맷이지, 암호가 아닙니다. 위협 모델에 독자가 포함된다면, 답은 암호화이지 인코딩이 아닙니다.
  • 낡은 패키지. pub.dev의 오래된 jwt 패키지는 널 세이프티 이전의 것입니다. JWT 일에는, crypto와 위의 몇 줄이 유지되는 길입니다.

다른 것을 고를 때

  • HTTP로 하는 파일 업로드. multipart/form-data를 쓰세요. 생바이트를 실으니, 33퍼센트 세금을 통째로 건너뜁니다.
  • 큰 페이로드나 반복되는 페이로드. 먼저 압축하고, 그다음 인코딩하세요. gzip된 텍스트의 base64는 텍스트의 base64보다 극적으로 작고, 압축 해제 쪽은 이미 포맷을 알고 있죠.
  • URL 안의 짧은 텍스트.문자 몇 개에겐 퍼센트 인코딩이 더 짧고, 값을 사람이 읽을 수 있게 유지합니다. data URI는 기본으로 그것을 여러분을 대신해 하죠.
  • 디버그 출력과 로그. 16진수는 base64보다 50퍼센트 더 길다(원본 크기의 두 배, base64는 3분의 4배뿐)지만, 훑어보고, 차이 비교하고, 동료에게 건네기 훨씬 쉽습니다. 로그 속 바이너리 조각에는 보통 이쪽이 이깁니다.

모범 사례, 인코더의 목록

  • 바이트를 인코딩하세요. 코드 유니트는 절대. 텍스트는 이름 붙인 인코딩을 먼저 거칩니다.
  • 호출을 쓰기 전에, 소비자의 규격에서 알파벳을 고르세요.
  • 패딩은 패딩 없음을 규격이 요구하는 곳에서만, 경계에서의 눈에 보이는 단계로 제거하세요.
  • 문자 집합을 계약에 명시하세요. 반대편에 대해 아무것도 가정하지 마세요.
  • 크게 자라날 수 있는 것은 전부 스트림하세요.
  • base64를 민감한 데이터의 보호가 아니라, 텍스트 전용 채널의 포맷으로 대하세요.

두 알파벳, 짧은 역사

방금 쓴 포맷은 모든 Dart 릴리스보다 오래되었습니다. 여러분이 고를 수 있는 알파벳 선택지는 Dart가 도착하기 수십 년 전에 표준화되었습니다. 짧은 버전:

  • 1993, RFC 1521: MIME은 base64를 이메일의 콘텐츠 전송 인코딩으로 소개합니다. 표준 64글자 알파벳과, 이 글이 76자에서 감는 그 줄 한도와 함께. 바이너리를 텍스트 채널로 싣는다는 포맷의 일은 여기서 시작됩니다.
  • 1996, RFC 2045: base64의 패딩과 줄 길이 규칙을 내구성 있는 표준으로 만든 MIME 갱신.
  • 2006, RFC 4648: 이 인코딩이 MIME에서 빠져나와 독자적으로 표준화됩니다. URL 안전 알파벳과, 무효한 입력을 디코더가 거부해야 한다는 조언이 더해지죠. Dart에서 두 알파벳을 고를 수 있는 선택은 이 문서에서 옵니다.
  • 2015, RFC 7515: JSON Web Signatures는 패딩 없는 base64url을 지정합니다. 모든 JWT 뒤에 있는 관례죠.
  • 2015년 11월, Dart 1.13: base64가 dart:convert에 도착합니다. URL 안전 변형은 그다음 해 봄, Dart 1.16에서 따라오고, 위에서 쓰신 탑레벨 base64Encode와 base64UrlEncode 호출은 2018년 Dart 2.0에 도착합니다.
  • 오늘, Dart 3.13: 두 알파벳, 언제나 패딩, import 한 번 거리에, 2015년부터 같은 엄격하고 단순한 기계입니다.

33퍼센트 오버헤드도 1993년 이후 변하지 않았습니다. 이것은 수학의 속성입니다. 바이트 3개에 기호 4개. 여러분이 앞으로 쓸 모든 언어의 모든 구현은 그것을 동일하게 지불합니다.

인코딩 벤치에서 온 재미있는 사실들

  • 인코더는 껐다 켰다 할 수 없습니다. SDK에는 패딩 없는 출력을 위한 플래그가 없으니, "패드를 제거하는 것"은 언제나 여러분의 코드, 여러분의 경계, 맨눈에 보이는 자리에 있습니다.
  • 바이트 1개가 문자 4개가 됩니다. base64Encode([65])는 QQ==. 가능한 가장 짧은 base64 문자열은 4글자이며, 그중 정보를 실은 것은 처음 두 글자뿐입니다. 마지막 두 글자는 패딩이죠.
  • Dart의 두 인코더 모두 패딩합니다. base64UrlEncode도요. base64url의 "패딩 없음"은 알파벳의 속성이 아니라, RFC 7515의 소비자 관례입니다.
  • 표준 알파벳은 7비트 인쇄 가능하도록 설계되었고, 그때부터 지금까지 기본을 유지합니다. +와 /가 결국 URL 안전 대체 문자를 얻었다는 사실은 그것이 얼마나 중심이 되었는지를 보여주는 징조이지, 결함이 아닙니다.
  • Héllo Wörld 世界의 같은 UTF-8 바이트 20개가 SMOpbGxvIFfDtnJsZCDkuJbnlYw=로 압축되는 동안, 그 문자열의 코드 유니트 14개는 인코더를 인덱스 12에서 추락시킵니다. 같은 문자, 완전히 다른 출력 둘, 그중 하나는 오류입니다.
  • PEM 파일, 모든 TLS 인증서에 있는 -----BEGIN CERTIFICATE----- 블록은 헤더를 달고 64자에서 래핑된 base64이며, 이 포맷은 1987년의 것입니다. MIME이 이메일을 위해 base64를 발표한 것보다 6년 빠르죠.

이제 여러분은 인코딩 쪽 전체를 손에 넣었습니다. API 표면, 바이트 우선 원칙, 패딩과 알파벳 결정, 그리고 JWT, data URI, 파일, HTTP, MIME, 설정, 스트림, 셸을 위한 작동하는 패턴까지. 반대 방향, 즉 이 문자열들 중 하나를 분해하는 일은 디코더의 엄격함 전부, 퍼센트 이스케이프의 놀라움, 수리 도구와 함께, 바로 아래에 링크된 Base64 디코딩 가이드에서 다룹니다.

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

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