파일 업로드
  1. base64hub
  2. 코드로 다루는 Base64
  3. Dart
코드

Dart에서 Base64 사용하기

Dart의 dart:convert 라이브러리에는 두 알파벳용 Base64 코덱과 패딩을 고쳐 주는 normalize 메서드가 들어 있습니다. 아래 출력은 Dart 3.13 기준입니다.

문자열 인코딩과 디코딩

Dart 문자열은 내부적으로 UTF-16이므로, 인코딩하기 전에 utf8.encode로 UTF-8 바이트로 바꾸세요.

import 'dart:convert';

void main() {
  final enc = base64Encode(utf8.encode('héllo wörld'));
  print(enc);                            // aMOpbGxvIHfDtnJsZA==
  print(utf8.decode(base64Decode(enc))); // héllo wörld
}

base64Encode(bytes)는 base64.encode(bytes)의, base64Decode(s)는 base64.decode(s)의 축약형입니다. 디코딩한 바이트가 UTF-8이 아니면 utf8.decode가 FormatException: Invalid UTF-8 byte를 던집니다. 대신 대체 문자를 받으려면 allowMalformed: true를 넘기세요.

같은 두 호출로 HTTP Basic Auth 헤더도 만들 수 있습니다. 예를 들어 package:http용은 다음과 같습니다.

final headers = {'Authorization': 'Basic ${base64Encode(utf8.encode('user:pass'))}'};
// Authorization: Basic dXNlcjpwYXNz

Basic Auth 헤더 생성기는 어떤 자격 증명이든 같은 값을 보여 줍니다.

URL-safe Base64와 normalize()

base64Url은 -_ 알파벳을 쓰는 두 번째 코덱입니다. 두 인코더 모두 항상 패딩을 붙이므로, 패딩이 없어야 하는 형식이라면 제거하세요.

final d = utf8.encode('<<???>>');
print(base64Url.encode(d));                     // PDw_Pz8-Pg==
print(base64Url.encode(d).replaceAll('=', '')); // PDw_Pz8-Pg

디코딩에서는 Dart가 대부분의 언어보다 친절합니다. 두 디코더 모두 두 알파벳을 받지만 패딩은 올바라야 합니다. normalize는 패딩을 고치고 알파벳도 변환합니다.

base64Url.normalize('PDw_Pz8-Pg');   // PDw_Pz8-Pg==
base64.normalize('PDw_Pz8-Pg');      // PDw/Pz8+Pg==

utf8.decode(base64Url.decode(base64Url.normalize('PDw_Pz8-Pg')));  // <<???>>
utf8.decode(base64Decode('PDw_Pz8-Pg=='));                         // <<???>>

normalize는 입력이 올바른 Base64가 아니면 FormatException을 던지므로 검사기 역할도 겸합니다. JWT 구간은 패딩 없는 Base64URL입니다. Base64와 Base64URL 비교와 패딩 설명을 참고하거나 JWT 디코더에 붙여넣어 보세요.

normalize만 있으면 JWT 페이로드를 한 줄로 읽을 수 있습니다.

const jwt = 'eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0In0.sig';
final payload = utf8.decode(base64Url.decode(base64Url.normalize(jwt.split('.')[1])));
print(payload);                        // {"sub":"1234"}
print(jsonDecode(payload)['sub']);     // 1234

이 코드는 클레임을 읽기만 할 뿐 서명은 검증하지 않습니다. 따라서 클라이언트에서 권한 판단에 이 클레임을 믿어서는 절대 안 됩니다.

파일 인코딩과 디코딩

import 'dart:io';

final bytes = await File('hello.txt').readAsBytes();
print(base64Encode(bytes));                              // aGVsbG8K
await File('copy.txt').writeAsBytes(base64Decode('aGVsbG8K'));

큰 파일 스트리밍

base64.encoder와 base64.decoder는 스트림 변환기이므로 파일을 조각 단위로 변환할 수 있습니다.

// encode: bytes -> Base64 text -> ASCII bytes on disk
await File('big.bin')
    .openRead()
    .transform(base64.encoder)
    .transform(ascii.encoder)
    .pipe(File('big.b64').openWrite());

// decode: ASCII bytes -> String chunks -> bytes
await File('big.b64')
    .openRead()
    .transform(ascii.decoder)
    .transform(base64.decoder)
    .pipe(File('big.out').openWrite());

변환기는 조각 사이에 남는 바이트를 보관하므로 조각 크기는 상관없습니다. 두 방향 모두 300 KB 테스트 파일이 메모리 내 함수를 쓸 때와 바이트 단위로 똑같이 나왔습니다.

Flutter: 이미지와 Data URI

Base64 이미지를 표시하려면 다음과 같이 합니다.

Image.memory(base64Decode(b64))

에셋이나 선택한 파일에서 Base64를 만들려면 바이트를 읽고 base64Encode를 호출하세요. Data URI는 UriData가 접두사를 처리합니다.

UriData.parse('data:text/plain;base64,aGk=').contentAsBytes();  // [104, 105]
UriData.fromBytes(pngBytes, mimeType: 'image/png').toString();   // data:image/png;base64,...

Data URI를 base64Decode에 그대로 넘기면 data 뒤의 : 때문에 Invalid character (at character 5)로 실패합니다. 정체를 모르는 문자열에 무엇이 들어 있는지 확인하려면 Base64 이미지 변환으로 브라우저에서 미리 보세요.

검증과 FormatException 메시지

디코딩 오류는 모두 FormatException입니다. toString()에는 메시지, 입력, 문제 위치 아래의 캐럿이 들어 있고, e.message와 e.offset으로 각각 따로 얻을 수 있습니다. base64Decode의 경우:

입력메시지
aGkInvalid length, must be multiple of four (at character 4)
aInvalid length, must be multiple of four (at character 2)
aGk=!!Invalid padding character (at character 5)
aGk==Invalid padding character (at character 5)
aGl=Invalid encoding before padding (at character 4)
"aGVs\nbG8K"Invalid character (at character 5)

Dart는 많은 언어가 관대한 두 가지 점에서 엄격합니다. 줄 바꿈을 거부하고(MIME이나 PEM 입력이라면 제거하세요), 0이 아닌 패딩 비트(aGl=)도 거부합니다. 유효성 검사는 다음과 같습니다.

bool isBase64(String s) {
  try {
    base64Decode(s);
    return true;
  } on FormatException {
    return false;
  }
}

문자열이 실패했는데 메시지만으로 원인을 알기 어렵다면 Base64 검사기가 문제 지점을 표시해 줍니다. 원인은 흔한 Base64 오류에서 다룹니다.

함정

  • codeUnits 인코딩. base64Encode('é'.codeUnits)는 6Q==(UTF-8이 아닌 Latin-1)를 반환하고, '✓'.codeUnits는 ArgumentError: Invalid argument (Not a byte value at index 0: 0x2713)을 던집니다. 반드시 utf8.encode를 거치세요.
  • 다른 시스템에서 온 줄 바꿈. 이메일이나 Linux base64의 MIME 출력에는 \n이 들어 있습니다. 먼저 s.replaceAll(RegExp(r'\s'), '')로 공백을 제거하세요.
  • base64Encode 출력이 URL-safe라고 가정하기. +와 /를 씁니다. URL에 들어가는 값에는 base64Url.encode를 사용하세요.
  • MIME 줄 바꿈 없음. 코덱은 절대 줄 바꿈을 넣지 않습니다. 필요하면 직접 76자 단위로 나누세요.

자주 묻는 질문

Flutter에서 Base64 이미지를 표시하려면 어떻게 하나요?

바이트로 디코딩한 뒤 Image.memory(base64Decode(b64))를 사용하세요. 문자열이 Data URI라면 먼저 쉼표까지 잘라 내거나 UriData.parse(uri).contentAsBytes()를 쓰세요.

base64Decode가 'Invalid length, must be multiple of four'라고 하는 이유는 무엇인가요?

입력에 패딩이 없기 때문이며, JWT와 URL-safe 토큰에서 흔한 일입니다. 먼저 base64.normalize(s)나 base64Url.normalize(s)를 호출하면 빠진 =를 채워 줍니다.

Dart의 base64Decode는 URL-safe 입력을 받나요?

네. 디코더는 +/와 -_를 모두 받으므로 base64Decode('PDw_Pz8-Pg==')도 동작합니다. 다른 것은 인코더뿐입니다. base64는 +/를, base64Url은 -_를 씁니다.

계속 읽기