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

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

보내야 할 것이 있고, 길은 텍스트 전용입니다: 생바이트를 거부하는 JSON API, 7비트 기원을 기억하는 이메일 채널, 이름 붙일 수 없는 것은 목이 막혀 버리는 URL, 가장 평범한 문자만 받아들이는 설정 파일. base64의 압축 쪽으로 오신 것을 환영합니다. Swift는 메서드 호출 하나로 여러분의 바이트를 친근한 문자 벽으로 바꾸어 주고, 바이트 3개마다 문자 1개를 더하는 가산비를 붙이며, 두 시대가 줄 길이에 대해 각자의 의견을 가졌기 때문에 존재하는 몇 가지 래핑 옵션을 줍니다.

이 사이트의 홈 페이지가 이미 포맷을 자세히 설명합니다(인쇄 가능한 문자 64개, 입력 바이트 3개당 4개, 마지막 그룹에 최대 두 개의 = 패딩). 그래서 포맷 강의는 시작하기도 전에 끝납니다. 이 글에 함께 가져갈 사실 두 가지: base64는 압축이지, 잠금이 아니며, 그 압축은 데이터를 약 33퍼센트 늘리므로 크기 제한 근처에 있을 때마다 중요합니다. Swift에서 일 전체는 하나의 타입 Data, 실패할 수 없는 하나의 메서드 base64EncodedString(options:)를 통해 이루어집니다. 필요한 진짜 기술은 그 메서드 주변의 두 단계에서 무슨 일이 일어나는지 아는 것입니다. 메서드 자체는 절대 실패하지 않으니까요. 실패하는 것은 단계들이죠.

메서드 하나, 변명 제로

Swift에서 base64 관련 모든 것은 Foundation 프레임워크의 Data에 살고, 언어의 첫 출시 이래 거기서 살아 왔습니다 (Apple은 이 메서드를 iOS 8.0, macOS 10.10, tvOS 9.0, watchOS 2.0, visionOS 1.0부터 표기합니다). 파이프라인은 언제나 같은 세 단계입니다: 내용을 Data로, 메서드 호출, 문자열 발송.

import Foundation

let note = "Pack it, wrap it, ship it."
let packed = Data(note.utf8).base64EncodedString()
print(packed) // UGFjayBpdCwgd3JhcCBpdCwgc2hpcCBpdC4=

그 세 줄 안의 두 세부 사항이 더 가까이 볼 만합니다. 먼저, Data(note.utf8)는 조용한 단계입니다: utf8 뷰는 정의상 모든 Unicode 스칼라를 표현할 수 있어 절대 실패하지 않으며, 대부분의 예제에서 기본값인 이유입니다. 실패 가능한 사촌 note.data(using:)는 어떤 인코딩에서는 nil로 답할 수 있고 실제로 그렇게 하며, "어떤 바이트"라는 그 결정 전체는 아래에서 전용 섹션을 얻습니다. 여러분의 데이터가 사라질 수 있는 첫 번째 자리이기 때문입니다. 둘째, 메서드 자체는 실패할 수 없는 메서드입니다: 항상 답하고, 오류 경우가 없으며, 여러분에게 묻는 유일한 질문은 어떤 줄 래핑을 원하는가입니다. 형제 base64EncodedData(options:)도 있습니다: 문자열이 아니라 ASCII 바이트의 Data로 압축된 결과를 반환하며, 다음 정거장이 텍스트 필드보다 바이너리 API인 파이프라인을 위한 것입니다.

"Swift 앱에 base64 의존성이 필요하다"며 온 분도 반이나 될 텐데: 설치할 것이 아무것도 없습니다. Base64는 Foundation의 일부이고, Foundation은 툴체인의 일부이며, 툴체인은 모든 플랫폼에서 같은 방식으로 도착합니다. macOS에서는 Xcode나 커맨드라인 도구이고, Linux와 Windows에서는 swift.org의 인스톨러이며, 이 글 작성 시점의 현재 안정 라인인 6.3.x, 그리고 권장된 첫 문인 Swiftly 버전 관리자를 갖습니다. 공식 Docker 이미지는 컨테이너 진영을 커버합니다. 여러분의 Package.swift는 비워 두는 것이 맞습니다.

첫 번째 진짜 결정: 어떤 바이트인가?

base64 문자 하나도 만들어지기 전에, 여러분은 이미 가장 중요한 결정을 한 것입니다: base64는 바이트를 압축하고, 문자열은 바이트 형태를 고를 때까지 그저 문자열이니까요. UTF-8은 합리적인 기본값이며, 거의 모든 것의 정답입니다. 하지만 데이터가 레거시 시스템, 바이너리 프로토콜, Unicode의 모서리에서 온 순간, 그 선택은 더 이상 보이지 않습니다:

import Foundation

let phrase = "héllo"
print(phrase.data(using: .utf8)?.count ?? -1)              // 6
print(phrase.data(using: .ascii) == nil)                   // true
print(phrase.data(using: .utf16)?.count ?? -1)             // 12
print(phrase.data(using: .utf16LittleEndian)?.count ?? -1) // 10
print(phrase.data(using: .utf8)!.base64EncodedString())
// aMOpbGxv
print(phrase.data(using: .utf16LittleEndian)!.base64EncodedString())
// aADpAGwAbABvAA==
변환 "héllo"의 바이트 base64가 실어 나르는 것
.utf8 6 aMOpbGxv, 현대 API가 기대하는 표기
.ascii nil로 실패 강조 문자는 0x7F 위에 있어 ASCII가 거부한다
.utf16 12 UTF-8의 두 배 크기, 앞에 두 바이트 바이트 순서 마크가 동승
.utf16LittleEndian 10 BOM 태그 없는 같은 단어: 10바이트, 여기 나열된 BOM 없는 옵션 중 여전히 가장 무겁다

그 출력 속에 숨은 교훈 세 가지. data(using:) 형태는 실패 가능하고, .ascii가 실패의 단골 후보입니다. 그래서 강제 언래핑은 멀쩡한 문장이 크래시한 앱이 되는 길입니다. 단순 .utf16 변환은 앞쪽에 두 바이트 바이트 순서 마크를 붙입니다(리틀 엔디안 기계에서는 FF FE), 그 BOM은 압축된 출력에 실려 가서, 그것을 예상하지 않은 모든 디코더를 혼란스럽게 합니다. 그리고 크기 산술은 무자비합니다: 대충 고른 문자 인코딩은 두 배 데이터에 대해 base64 가산비를 물리므로, 질문은 "이게 인코딩되나?"가 아니라 "끝에 있는 쪽은 풀 때 무엇을 기대할까?"입니다. 황금 규칙: 여정의 양 끝은 base64가 시작되기 전에 바이트 형태에 합의해야 합니다. 디코더는 여러분이 무엇을 골랐는지 추측할 수 없고, 물어보지 않으니까요.

래핑: 두 개의 습관, 하나의 파라미터

메서드의 옵션은 전부 줄바꿈에 관한 것이며, 전부 존재하는 이유는 20세기 포맷 둘이 문자 줄 길이에 대해 합의하지 못했기 때문입니다. 1996년 이메일 표준 MIME은 base64를 CRLF 줄 끝으로 76자 래핑합니다. 1987년의 Privacy-Enhanced Mail 계보 PEM은 64자로 래핑하며, 그것이 바로 인증서와 키 안에서, 여러분의 서버가 설정 디렉토리에 보관하는 -----BEGIN CERTIFICATE----- 블록에서 발견하는 형태입니다.

import Foundation

let certBytes = Data((0..<300).map { UInt8($0 % 256) })
let raw = certBytes.base64EncodedString()
let pemStyle = certBytes.base64EncodedString(options: [.lineLength64Characters, .endLineWithLineFeed])
let mimeStyle = certBytes.base64EncodedString(options: [.lineLength76Characters,
  .endLineWithCarriageReturn, .endLineWithLineFeed])
print(raw.count)                                        // 한 줄에 400문자
print(pemStyle.components(separatedBy: "\n").count)    // 최대 64자의 7줄
print(mimeStyle.components(separatedBy: "\r\n").count) // 최대 76자의 6줄
옵션 역할 주의할 점
.lineLength64Characters 64문자 뒤에 줄을 자른다, PEM 습관 다르게 말하기 전까지 줄 끝은 CRLF
.lineLength76Characters 76문자 뒤에 줄을 자른다, MIME 습관 같은 CRLF 기본값
.endLineWithCarriageReturn 줄 끝에 캐리지 리턴을 포함 혼자면 CR 전용, 옛 Mac 스타일로, 원하는 것이 드묾
.endLineWithLineFeed 줄 끝에 라인 피드를 포함 CRLF를 뜻하면 두 옵션 모두 전달

여기 사람들을 놀라게 하는 기본값: 줄 끝을 고르지 않고 어떤 .lineLength 옵션을 달라고 하면, 받는 줄 끝은 CRLF, 즉 캐리지 리턴 더하기 라인 피드의 완전한 쌍입니다. 메서드에는 집안 스타일이 있고, 그 집안 스타일은 1996년 것입니다. LF 전용이 필요하다면? .endLineWithLineFeed 하나만으로 명시적으로 대가를 치르세요. 기록으로 남길 집안 규칙 하나: 마지막 줄에는 뒤따르는 줄 끝이 붙지 않습니다. 래핑된 결과는 어떤 옵션을 골랐든 마지막 데이터 문자나 = 패드로 끝나므로, 끝에 고아 빈 줄 없이 연결하고 붙여넣을 수 있습니다. 그리고 옵션이 전혀 없으면 출력은 끊기지 않은 한 줄이며, 그것이 JSON 본문, URL, API 페이로드에 딱 맞는 형태입니다: 현대 Swift 앱이 실제로 대부분 시간을 하는 일이죠.

Base64url: 여행할 수 있는 문자열

표준 알파벳은 JSON의 모범 시민이지만, URL의 최악의 시민입니다. 쿼리 문자열에서 +는 폼 파싱이 공백으로 읽고, /는 경로 구분자, =는 키와 값을 구분하므로, 표준 알파벳을 퍼센트 인코딩하면 짧아지기보다 더 길고 더 추해집니다. RFC 4648의 5절은 정확히 그것을 고치기 위해 존재합니다: "URL 및 파일명 안전 알파벳". 여기서 +는 -가 되고, /는 _가 되며, = 패딩은 보통 생략됩니다. URL에서 패드는 보통 %3D가 되어 목적을 무너뜨리니까요. RFC는 액자 속에 걸 만한 경고도 덧붙입니다: 이 인코딩은 "base64 인코딩과 같은 것으로 여겨져서는 안 된다"고요. YouTube 동영상 ID, JWT, 그리고 현대 API 식별자 대부분이 그것을 쓰므로, 사용하게 될 각오를 하세요.

import Foundation

extension Data {
  var base64URLEncoded: String {
    base64EncodedString()
      .replacingOccurrences(of: "+", with: "-")
      .replacingOccurrences(of: "/", with: "_")
      .replacingOccurrences(of: "=", with: "")
  }
}

let tricky = Data("The + / and = trio goes home.".utf8)
print(tricky.base64EncodedString())
// VGhlICsgLyBhbmQgPSB0cmlvIGdvZXMgaG9tZS4=
print(tricky.base64URLEncoded)
// VGhlICsgLyBhbmQgPSB0cmlvIGdvZXMgaG9tZS4

그 출력을 잘 보세요: 이 페이로드는 우연히 +도 /도 만들지 않아, 두 표기는 생략된 패딩으로만 다릅니다. 바이트 하나를 바꾸면 알파벳에서 갈라지는데, 그것이 핵심입니다. 대응 규칙 두 가지. 방언은 한 번만, 여러분의 데이터가 외부 세계를 만나는 경계에서 고르세요. 같은 문서 안에서 알파벳을 섞으면 안 됩니다: base64url을 받는 표준 디코더(또는 그 반대)는 입력을 거부하거나, 관대 모드에서는 이방 문자를 지워 잘못된 바이트를 건넵니다. 그리고 헬퍼에 정직한 이름을 지어, 다음 개발자가 그 문자열이 오타가 아니라 base64url임을 알게 하세요. 같은 익스텐션은 새로운 툴체인에서 더 짧아질 수 있습니다: 최신 SDK 베타는 이제 프레임워크 안에서 알파벳 교환을 하는 네이티브 .base64URLAlphabet 옵션을 포함하며, 그에 맞는 .omitPaddingCharacter 옵션도 있고, 오픈소스 Foundation은 같은 옵션을 더 늦은 툴체인을 위한 가용성 마커 뒤에 담고 있습니다. 그것들이 여러분의 최소 배포 대상에 도달할 때까지, 4줄 익스텐션이 이식 가능한 답안이며, 구성상 모든 플랫폼에서 계속 동작합니다.

JSON과 API: 요청하지도 않은 Base64

이것은 Codable로 일하는 사람들 중 가장 많은 사람을 놀라게 하므로, 전용 섹션을 받을 자격이 있습니다: JSONEncoder의 Data 프로퍼티 기본 전략은 이미 base64입니다. Codable 구조체에 Data 필드가 있으면, 인코더는 표준 base64로 자동으로 압축하고, JSONDecoder는 돌아오는 길에 자동으로 풉니다. 옵션도, 설정도, 의식도 없이.

import Foundation

struct Snapshot: Codable {
  let name: String
  let icon: Data
}

let snap = Snapshot(name: "cat", icon: Data("🐱".utf8))
let json = try JSONEncoder().encode(snap)
print(String(decoding: json, as: UTF8.self))
// 아이콘은 "8J+QsQ=="로 와이어를 건넜다

icon 프로퍼티가 8J+QsQ==로 와이어를 건넌 것은 그것이 집안 스타일이기 때문입니다. 대안은 있고, 실제로 만날 두 가지는 .custom와 더 새로운 .deferredToData입니다: 전자는 데이터와 인코더를 건네 표현 방식을 여러분이 결정하게 하고, 후자는 데이터 인스턴스 자체에게 맡깁니다. API가 표준 대신 base64url을 원하는 순간, .custom가 바로 앞 섹션의 익스텐션을 꽂는 자리입니다:

import Foundation

extension Data {
  var base64URLEncoded: String {
    base64EncodedString()
      .replacingOccurrences(of: "+", with: "-")
      .replacingOccurrences(of: "/", with: "_")
      .replacingOccurrences(of: "=", with: "")
  }
}

struct Snapshot: Codable {
  let name: String
  let icon: Data
}

let encoder = JSONEncoder()
encoder.dataEncodingStrategy = .custom { data, enc in
  var container = enc.singleValueContainer()
  try container.encode(data.base64URLEncoded)
}
let json = try encoder.encode(Snapshot(name: "cat", icon: Data("🐱".utf8)))
print(String(decoding: json, as: UTF8.self))
// 아이콘은 "8J-QsQ"로 와이어를 건넜다

동작하는 기능과 프로덕션 사고를 가르는 경고 하나: JSON 문자열에는 생 줄바꿈을 넣을 수 없습니다. 페이로드를 .lineLength 옵션으로 래핑해 이스케이프 없이 JSON 문서에 삽입했다면, 여러분은 JSON 값을 만든 것이 아니라 base64 억양이 있는 문법 오류를 만든 것입니다. 파서가 그것을 증명할 겁니다. 래핑된 출력은 이메일 본문과 인증서 파일에 속합니다. JSON, URL, 쿼리 문자열 안에서 사는 것은 전부 평범한 래핑 없는 문자열입니다.

Data URI: 문자열 속의 이미지

웹이 사랑하는 트릭은 파일 바이트를 URL에 직접 넣어 넣는 것입니다: data:{mime};base64,{payload}. Swift에서 하나 만드는 것은 읽기, 인코딩, 문자열 연결입니다:

import Foundation

let gif = Data(base64Encoded: "R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7")!
let uri = "data:image/gif;base64," + gif.base64EncodedString()
print(uri.hasPrefix("data:image/gif;base64,R0lGODlh")) // true
print(gif.count) // 42

예제는 유명한 42바이트 투명 GIF(더 작은 비투명 것도 있지만, 모두가 넣는 것은 이 것)를, 브라우저가 두 번째 요청 없이 렌더링하는 data URI로 다시 만듭니다. Apple 플랫폼에서는 반대 방향이 한 줄입니다: 압축한 그 Data가 UIImage(data:)나 NSImage(data:)로 바로 이어집니다. 교부 조건은 크기이며, 복리로 붙습니다: 100킬로바이트 이미지는 data:image/png;base64, 접두사조차 더하기 전에 133,000문자를 넘는 문자열이 됩니다. Data URI는 아이콘, 아바타, 작은 에셋에서 빛나고, 히어로 사진에서는 조용히 대역폭을 부풀리니, 작은 것용으로 남겨 두세요.

JWT: 앞의 두 부분 밀봉하기

JSON Web Token의 인코딩 쪽은 밀봉 두 개와 서명이며, 그 밀봉은 패딩을 뺀 여러분의 base64url 익스텐션입니다. 포맷이 정확히 요구하는 것이죠. 헤더와 페이로드는 JSON 문서이며, 두 부분 모두 같은 처리를 받습니다:

import Foundation

extension Data {
  var base64URLEncoded: String {
    base64EncodedString()
      .replacingOccurrences(of: "+", with: "-")
      .replacingOccurrences(of: "/", with: "_")
      .replacingOccurrences(of: "=", with: "")
  }
}

func seal(_ text: String) -> String {
  Data(text.utf8).base64URLEncoded
}

let header = seal(#"{"alg":"HS256","typ":"JWT"}"#)
let claims = seal(#"{"sub":"42","role":"editor"}"#)
print("\(header).\(claims).signature-here")
// eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI0MiIsInJvbGUiOiJlZGl0b3IifQ.signature-here

리마인더 두 가지. 세 번째 점 구분 부분은 앞의 두 부분 위에서 계산된 암호학적 서명이며, 토큰에서 어떤 보장도 제공하는 유일한 부분입니다: 헤더와 클레임은 트렌치코트를 입은 평범한 JSON이므로, 시크릿은 절대 거기 들어가선 안 됩니다. 그리고 seal() 안에서 패딩이 사라지는 것을 눈여겨보세요: 반대편의 JWT 디코더(자매 글의 것 포함)는 나머지로 채워 다시 붙여 넣으므로, 여정의 두 방향은 공통 지점에서 만납니다.

HTTP 헤더: Basic과 그 외

오래된 Authorization: Basic 헤더는 사용자명과 비밀번호를, 콜론으로 이어, 표준 base64로 압축해서 원합니다. 헤더 안에서 +와 /는 해롭지 않고, 방언 질문이 생기지 않으니까요:

import Foundation

let credentials = "editor:s3cret"
let header = "Basic " + Data(credentials.utf8).base64EncodedString()
print("Authorization: " + header)
// Authorization: Basic ZWRpdG9yOnMzY3JldA==

어디서나 같은 큰 소리의 각주: 압축은 자체적으로 보안이 제로를 제공하며, 헤더는 그것을 실어 나르는 HTTPS 접속만큼만 안전합니다. 현대적인 사촌 Authorization: Bearer는 대신 JWT를 싣습니다. 그래서 JWT 섹션의 밀봉 레시피가 그곳의 와이어로 가는 것입니다. HTTP에서 방언 질문이 실제로 생기는 곳은 쿼리 문자열입니다: API가 식별자를 URL에 태워 보내게 한다면, 그 식별자는 base64url이어야 하고, 아무리 적어도 퍼센트 인코딩된 표준 base64여야 하며, +가 공백으로 읽혀도 상관없다는 듯 남겨 둔 생 표준 알파벳은 절대 안 됩니다.

이메일 첨부 파일: 76자 계약

앱이 SMTP의 7비트 기원을 살아남아야 할 첨부를 만든다면, 계약은 MIME 것입니다: CRLF 줄 끝으로 76자 래핑된 base64, 그리고 수신자에게 무엇을 기대할지 알려 주는 Content-Transfer-Encoding: base64 헤더. 옵션 섹션에서 이미 표기를 보았습니다. 여기 래핑된 본문의 완전한 형태입니다:

import Foundation

let attachment = Data((0..<400).map { UInt8(65 + $0 % 26) })
let body = attachment.base64EncodedString(options: [.lineLength76Characters,
  .endLineWithCarriageReturn, .endLineWithLineFeed])
let lines = body.components(separatedBy: "\r\n")
print(lines.count)                     // 8줄
print(lines.map { $0.count }.max() ?? 0) // 76, 가장 긴 줄
print(body.hasSuffix("\r\n"))           // false, 마지막 줄은 맨발로 남는다

이 방언의 크기 청구서는 유명합니다: 4/3 알파벳 세금에 76자마다 줄바꿈을 더하면 원래의 약 137퍼센트에 이르고, 옛날 메일 엔지니어링의 단축법인 "원래에 1.37을 곱하고 헤더 약 800바이트를 더하라"는 여전히 메일 클라이언트에서 첨부 크기를 눈대중할 때 동작합니다. 산술이 정확한 전설이며, 이 글에서 33퍼센트 가산비가 둘째 소수 자리를 내는 유일한 곳입니다.

설정, 환경, 데이터베이스: 밑줄 숨기기

base64의 유일한 미덕이 출력이 작고 예측 가능한 문자 인코딩이라는 조용한 일들이 있습니다: 바이너리 블록이나 구조화 값을 평문 텍스트를 원하는 자리에 숨겨 두는 일. 셸 설정 파일을 살아남아야 할 환경 변수, blob보다 varchar가 편한 데이터베이스의 열, base64 마커를 가진 LDAP 파일, 비트보다 문자를 더 신뢰하게 스캔되는 QR 코드. 패턴은 어디서나 같습니다: 바이트를 정하고, 인코딩하고, 문자열을 저장하고, 반대편에서 디코딩.

import Foundation

struct FeatureFlags: Codable {
  var betaToolbar: Bool
  var maxRetries: Int
}

do {
  let flags = FeatureFlags(betaToolbar: true, maxRetries: 5)
  let json = try JSONEncoder().encode(flags)
  let storable = json.base64EncodedString()
  print(storable)
  guard let packed = Data(base64Encoded: storable) else {
    print("decode failed, that is odd")
    exit(1)
  }
  let restored = try JSONDecoder().decode(FeatureFlags.self, from: packed)
  print(restored.betaToolbar, restored.maxRetries)
} catch {
  print(error)
}

함정 두 가지가 여기 삽니다. 첫째는 이중 래핑: 둘 다 "도움이 되는 듯이" 인코딩하는 통합 레이어 두 개. 그래서 저장한 값은 base64의 base64가 되고, 한 번 디코딩하는 사람은 문자 벽을 얻어 기능이 깨진다고 생각합니다. 정확히 한 번, 정확히 한 경계에서 인코딩하고, 코멘트에도 그렇게 적어 두세요. 둘째는 환경에 의한 방언 이탈: 값이 +와 /를 망가뜨리는 URL, 폼 필드, 셸을 통과할 수 있다면, 대신 base64url 표기를 저장하세요. 문자 인코딩이 이 포맷의 전부이니까요.

파일: .b64 왕복

"이 파일을 .b64 텍스트 파일로"라는 일은 읽기, 호출, 쓰기입니다:

import Foundation

let source = URL(fileURLWithPath: "photos/cat.png")
let archive = URL(fileURLWithPath: "photos/cat.b64")
let bytes = try Data(contentsOf: source)
try Data(bytes.base64EncodedString().utf8).write(to: archive)

// 나중에, 어쩌면 다른 프로세스에서
let packed = try String(contentsOf: archive, encoding: .utf8)
let restored = Data(base64Encoded:
  packed.trimmingCharacters(in: .whitespacesAndNewlines))
if let restored = restored {
  try restored.write(to: URL(fileURLWithPath: "photos/cat-copy.png"))
} else {
  print("the .b64 file was not base64 after all")
}

돌아가는 길의 trimmingCharacters는, 파일을 쓴 것이 무엇이든 줄 끝을 추가했을 수 있고, 엄격한 디코더가 끝 줄바꿈을 nil 판정으로 대하기 때문에 있습니다. 그 왕복은 바이트마다 그대로 돌아오는데, 처음 보낼 때 검증해야 합니다. 메모리 사용량이 흥미로울 만큼 큰 파일은, 전체 버퍼를 한꺼번에 인코딩하지 마세요. Base64는 스트리밍을 정확하게 만들어 주는 멋진 성질이 있습니다: 입력 바이트 3개가 독립적인 출력 문자 4개를 만들므로, 인코딩하는 각 청크가 3바이트의 배수만 하면, 연결된 출력이 파일 전체를 한 번에 인코딩한 것과 동일합니다. 정렬을 깨면 출력이 바뀝니다. 청크 경계가 3바이트 그룹을 스트림 한가운데에서 끊으니까요:

import Foundation

func streamEncode(_ input: InputStream, output: OutputStream, lineLength: Int = 76) throws {
  input.open()
  output.open()
  defer { input.close(); output.close() }
  var buffer = [UInt8](repeating: 0, count: 65_536)
  var pending = [UInt8]()
  var line = ""
  var lineCount = 0
  func addText(_ text: String) {
    line += text
    while line.count > lineLength {
      if lineCount > 0 { _ = output.write(Array("\r\n".utf8), maxLength: 2) }
      _ = output.write(Array(String(line.prefix(lineLength)).utf8), maxLength: lineLength)
      line = String(line.dropFirst(lineLength))
      lineCount += 1
    }
  }
  func flushGroup(_ group: [UInt8]) {
    addText(Data(group).base64EncodedString())
  }
  while input.hasBytesAvailable {
    let n = input.read(&buffer, maxLength: buffer.count)
    if n < 0 { throw CocoaError(.fileReadUnknown) }
    if n == 0 { break }
    pending.append(contentsOf: buffer[0..<n])
    let groups = pending.count / 3
    if groups > 0 {
      flushGroup(Array(pending[0..<(groups * 3)]))
      pending.removeFirst(groups * 3)
    }
  }
  if !pending.isEmpty {
    flushGroup(pending)
  }
  if !line.isEmpty {
    if lineCount > 0 { _ = output.write(Array("\r\n".utf8), maxLength: 2) }
    _ = output.write(Array(line.utf8), maxLength: line.utf8.count)
  }
}

파일이 아무리 커도 정점 메모리는 읽기 버퍼 하나와 현재 줄이며, 래핑된 출력은 원샷 .lineLength76Characters 표기와 정확히 일치합니다. 역할이 뒤집힌 같은 3의 배수 규칙을, 자매 글의 스트리밍 디코더가 의지하므로, 여정의 두 편은 하나의 산술 진실을 나눕니다.

큰 페이로드와 메모리 청구서

누군가 "이거 base64 가능해?"라고 물을 다음을 위해 산술을 해 보겠습니다. 입력 바이트 3개가 출력 문자 4개가 되므로, 크기는 4/3배로 커집니다: 100킬로바이트 파일은 133,336문자 문자열이, 10메가바이트 파일은 13,333,336문자가 되는 식입니다. 패딩은 마지막에 최대 두 문자를 추가하는데, 몇 바이트보다 큰 것에는 반올림 오차에 불과하며, 세금에서 면제되는 것은 빈 입력뿐입니다. 거기서 세무서는 무료 통행권 한 장을 주고, 결과는 빈 문자열이죠. 실용적 귀결 세 가지. 첫째, 시작 전에 예산을 세우세요: 페이로드가 이미 한계 근처(URL의 약 2,000문자 안락 구간, JSON 필드의 계약, 데이터베이스 열의 너비)에 있다면, 인코딩 후에가 아니라 전에 한계를 1.33으로 나눠야 합니다(래핑이 관여하면 1.37로). 둘째, 압축하는 동안 원래 바이트와 압축된 문자열을 동시에 쥐고 있으므로, 작업 집합은 원래의 약 2.33배이며, 그 숫자가 안락하지 않게 되면 위의 스트리밍 함수들이 탈출구입니다. 셋째, 세금은 실제로 일방적입니다: 압축할 때 내고, 누군가 풀 때 바이트가 집으로 돌아옵니다. 그래서 진짜 질문은 "base64가 비싸냐?"가 아니라, "내가 탄 텍스트 전용 길이 그것을 요구하느냐?"입니다.

무는 실수들

  • 실패 가능한 문자 인코딩 단계. String.data(using:)는 nil로 답할 수 있습니다(.ascii에 강조 문자를 넣어 보세요). 강제 언래핑은 잘못된 입력을 크래시한 앱으로 올리는 클래식한 업그레이드입니다. 쉬운 부분인 base64 호출만이 아니라 변환을 가드하세요.
  • CRLF 집안 스타일. 줄 끝 옵션 없는 .lineLength 옵션은 기본적으로 CRLF를 만듭니다. 포맷이 LF 전용을 원하는데 옵션을 빼먹었다면, 출력은 애초에 가져선 안 될 캐리지 리턴을 실고 다닙니다.
  • CR 전용 함정. .endLineWithCarriageReturn 혼자서는 옛 Mac 스타일의 CR 전용 줄 끝을 만듭니다. CRLF를 뜻했다면(MIME의 경우 그러합니다), 두 줄 끝 옵션을 모두 전달하세요.
  • JSON 안에서의 래핑. JSON 문자열 안의 생 줄바꿈은 무조건 잘못된 JSON입니다. 문서에 삽입된 래핑된 base64는 base64 억양이 있는 문법 오류입니다. 래핑된 출력은 이메일 본문과 인증서 파일에 두세요.
  • BOM 동승자. 단순 .utf16 변환은 앞쪽에 두 바이트 BOM을 붙이고, 그것은 압축된 출력에 실려 예상하지 않은 디코더들을 혼란에 빠뜨립니다. 태그 없는 UTF-16이 필요하면 .utf16LittleEndian나 .utf16BigEndian을 사용하세요.
  • 방언 이탈. 표준과 base64url은 다른 알파벳이며, RFC도 글로 그렇게 말합니다. 쿼리 문자열까지 살아남은 +는 공백이 되고, 관대한 표준 디코더에 도달한 -는 지워집니다. 경계에서 방언을 고르고 지켜 가세요.
  • 대소문자는 문자다. 알파벳은 A와 a를 구별합니다. 대소문자를 섞은 복사-붙여넣기나 열성적인 대문자 변환 호출은, 두 버전이 여전히 모든 알파벳 검사를 통과하므로 조용히 데이터를 손상시킵니다. Base64는 여권 번호처럼 대소문자를 구별합니다.
  • 정렬 규칙. 스트리밍 인코더는 3바이트 배수에서 청크를 자야 합니다. 정렬되지 않은 청크는 출력을 바꾸고, 그 변화는 조용합니다: 문자열은 여전히 디코딩되지만, 잘못된 데이터로요.
  • 이중 래핑. 둘 다 인코딩하는 레이어는 base64의 base64를 만듭니다. 한 번 디코딩하는 사람은 바이트가 있어야 할 자리에 문자를 보고, 사고는 스스로 쓰여 집니다.
  • 가용성 벽. 새 네이티브 옵션(.base64URLAlphabet, .omitPaddingCharacter)은 최신 SDK 베타와 가용성 마커 뒤의 오픈소스 Foundation에 있지만, 여러분의 CI가 닿는 모든 툴체인에는 없습니다. 채택한다면, 같은 소스가 더 오래된 Xcode와 Linux에서 빌드되도록 가용성 검사로 가드하세요. 현재 안정 툴체인에서, 4줄 익스텐션은 옵션이 없는 모든 곳에 컴파일됩니다.
  • Base64는 암호화가 아니다. 요구가 기밀성이라면, 여러분은 통째로 한 범주를 틀린 도구를 골랐습니다. Base64의 일은 바이트를 여행시키는 것이며, 정확히 그 일만 하고, 그 이상도 그 이하도 아닙니다.

실전에 내보내는 법

  • 바이트를 인코딩하세요. 소망이 아니라. 메서드를 호출하기 전에 바이트 형태를 정하세요. 기본은 UTF-8, 아니면 명시적으로 이름을 붙이고, 실패 가능한 data(using:) 단계를 가드하세요. 데이터가 실제로 사라지는 곳이 거기니까요.
  • 기본은 래핑 없음, 계약에 따라 래핑. 단순 한 줄 출력은 JSON, API, 대부분의 데이터베이스에 맞습니다: 수신 포맷이 요구할 때만 64/76 래핑 옵션을 손보고, CRLF를 뜻하면 두 줄 끝 옵션을 모두 대가로 치르세요.
  • 경계마다 방언 하나. 텍스트 중심 목적지엔 표준 base64, URL이나 파일명과 닿을 것엔 base64url, 같은 문서에 둘은 절대 함께. 변환을 한 번 쓰고, 정직하게 이름을 붙이고, 재사용하세요.
  • 가산비에 예산을 세우세요. 시작 전에 4/3으로 곱하세요(래핑이 걸리면 1.37), 작업 집합이 불편할 만큼 큰 페이로드는 3바이트 정렬 청크로 스트림하세요.
  • 포장 테이프를 잠금으로 쓰지 마세요. 요구가 비밀이면, base64 선반에서 멈추고 대신 암호화를 가져가세요.

패킹의 짧은 역사

압축에 쓰는 알파벳과 래핑하는 줄 길이는, 텍스트 전용 길이를 바이트가 얼마나 살아남을 수 있는지에 대한 40년 논쟁의 화석이며, Swift의 위치는 짧지만 흥미롭습니다:

  • 1980년대, 같은 기계 시대. 이 가문의 첫 인코더들은, 반대편도 자신과 같은 기계라고 가정한 시스템 사이에서 다이얼업으로 파일을 옮기려고 존재했습니다. UNIX의 uuencode는 대문자, 숫자, 구두점을 썼고, 설계자들은 계산력을 아끼는 트릭을 찾았습니다: 알파벳이 연속된 ASCII 자리에 놓여 있어, 인코딩은 룩업 테이블 없이 문자 그대로 "32를 더하기"였습니다. 1981년 TRS-80에서 태어난 사촌 BinHex는 Apple II로 건너뛰어, 1984년 클래식 Macintosh의 포맷이 되었고, 다른 베팅을 했습니다: 그 64문자는 7, O, W, g, o, 그리고 소문자의 거의 절반을 생략합니다.
  • 1987년, 알파벳이 주소를 얻다. 첫 Privacy-Enhanced Mail 사양 RFC 989는 여러분이 오늘 타이핑하는 정확히 그 64문자를 표준화하고, 출력을 한 줄 64자로 래핑했으며, =로 패딩을, *로 인코딩되었지만 암호화되지 않은 데이터를 표시했습니다. 여러분이 서버 설정에 붙여넣은 모든 PEM 스타일 블록은 이 문서의 후손입니다.
  • 1996년, 자유로운 시대. MIME(RFC 2045)은 알파벳을 이메일 첨부 파일에 가져와 래핑을 76자로 옮기고, 래핑을 안심하고 만들게 하는 규칙을 추가했습니다: 디코더는 줄바꿈을 무시해야 한다. 인코더는 래핑을 배우고, 디코더는 용서를 배웠습니다. Swift의 76문자 옵션은 정확히 그 논쟁의 살아 있는 기념품입니다.
  • 2003~2006년, 규칙이 굳어지다. RFC 3548(2003)은 패딩을 생략해서는 안 된다고(포맷이 다르게 말하지 않는 한) 선언하고, 디코더는 알파벳 밖의 문자를 거부해야 한다고 했습니다; RFC 4648(2006년 10월)은 가문을 정리하고 URL 안전 알파벳을 추가했는데, 긴 식별자가 특수 문자마다 퍼센트 이스케이프하지 않고 URL에서 살 수 있게 명시적으로요. "URL 방언에 패딩 없음" 관습도 같은 문서에서 태어났습니다: URL에서 패드 문자는 보통 %3D가 되어 목적을 무너뜨리니까요.
  • 2013~2014년, API는 이미 여기 있다. Apple의 NSData 클래스는 수년간 base64를 압축해 왔고, 네 가지 래핑 옵션을 가진 옵션 기반 API는 Swift가 존재하기도 전인 2013년, iOS 7에 도착했습니다. Swift 1.0이 2014년 9월 9일에 도착했을 때, 네 가지 래핑 옵션을 가진 실패하지 않는 인코더와 1987년의 64문자 알파벳을 상속받았고, 성격은 그때부터 변하지 않았습니다.
  • 2015년 12월 3일, 툴체인이 건물을 나서다. Swift는 그날 오픈소스화되었고, Foundation의 base64는 함께 Linux로, 나중에 Windows로 건너갔습니다. "Apple 기계 밖에서 Swift로 Base64 인코딩"은 10년이 채 되지 않습니다: 1987년에 시작된 파티에 온 아주 젊은 손님.
  • 2023~2026년, 재작성과 URL 방언. Foundation 재작성(swift-foundation 프로젝트)은 Data를 순수 Swift 코어로 옮겼고, 2025년 커뮤니티 피치가 네이티브 base64url과 패딩 생략 옵션을 추가했습니다. 이 글 작성 시점, 최신 SDK 베타와 오픈소스 툴체인은 인코딩 옵션을 출시하고, 가문의 나머지는 가용성 마커 뒤의 오픈소스 Foundation에서 성숙 중이며, 커뮤니티 익스텐션은 그 사이 이식 가능한 다리 역할을 유지합니다.

작은 즐거움들

  • 1메가바이트는 정확히 1,333,336개의 base64 문자로 압축됩니다. 4/3 세금에 패딩 문자 두 개, 자리수까지 정확히. 세금에서 통째로 벗어날 수 있는 유일한 입력은 빈 것입니다: 들어가는 것도 없고, 나오는 것도 없으니까요.
  • 인코더는 디코더가 갖지 못한 방식으로, 실패라는 개념이 없습니다. nil을 반환하지도, 던지지도, 거부하지도 않습니다. 파이프라인 전체에서 유일한 실패는 상류, 문자 인코딩 단계에 살고, 그래서 메서드는 사촌보다 훨씬 침착하게 느껴집니다.
  • héllo를 UTF-8로 인코딩하면 aMOpbGxv가 되고, UTF-16 리틀 엔디안으로 하면 aADpAGwAbABvAA==가 됩니다. 같은 단어, 서로 다른 여권 두 장, 둘 다 유효하지만, 서로 바꿀 수는 없습니다.
  • base64 세계의 테스트 단어는 foobar이며, Zm9vYmFy로 압축됩니다. 실제 세계에서 base64 예제를 본 적이 있다면, foobar가 연루될 확률은 적지 않습니다.
  • 유명한 1x1 투명 GIF는 42바이트이고 마법의 단어 GIF89a로 시작하며, 그래서 접두사 R0lGODlh는 지구에서 거의 모든 base64 문자열보다 더 많은 코드베이스에 등장합니다.
  • 여러분의 Codable 구조체는 몰랐을 텐데 수년간 base64를 보내고 있었을 겁니다: JSONEncoder의 기본 Data 전략은 표준 base64로 압축하고, 그래서 Data 필드는 숫자 배열이 아니라 패딩된 문자열로 와이어를 건넙니다.
  • 패딩은 영원히 두 문자를 넘지 않습니다. 1바이트 페이로드는 ==로, 2바이트 페이로드는 =로 끝나고, 3바이트 페이로드는 아무것도 없이 끝납니다. 마지막 그룹의 문법 전체는 손톱 하나에 들어갑니다.
  • Swift는 압축에 쓰는 알파벳보다 27살 어립니다. 이 언어는 2014년에 출시되었고, 64문자는 1987년에 표준화된 뒤 지금까지 변하지 않았습니다.

이것이 완결된 압축 공구함입니다: 실패할 수 없는 메서드 하나, 그 앞에 오는 실패 가능한 단계 하나, CRLF 집안 스타일을 가진 네 가지 래핑 옵션, 4줄 base64url 익스텐션, 스트리밍용 3바이트 정렬 규칙, 그리고 텍스트 전용 길이의 입장료인 4/3 가산비. 인코딩은 base64의 청구서를 치르는 곳이고, 여러분은 서명 전에 모든 항목을 이제 압니다. 여정을 뒤집어 다른 사람이 압축한 것을 여는 순간, nil이 돌아오고, 공백 판정과, 관대한 노브의 사각지대가 무대에 섭니다. 연관 디코딩 글은 왕복의 그 절반에서 온전한 쇼를 연출합니다. 문자들이 도착하기 시작하면, 여러분은 이미 어떻게 여는지를 정확히 알고 있을 것입니다.

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

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