R에서의 Base64 인코딩: 완전한 가이드
여행해야 하는 바이트가 있고, 길은 텍스트만 허용합니다. JSON 필드 안에 살아야 하는 JPEG. 환경 변수에 얹혀 있어야 하는 인증서. 자기 완결형 HTML 보고서 안을 여행해야 하는 플롯. Base64는 이 모든 것을 위한 패킹 테이프입니다: 어떤 바이트 순서든 64개의 무해한 문자로 이루어진 문자열이 되어, 당신이 던질 수 있는 어떤 텍스트 채널에서도 살아남습니다. 이 사이트의 홈 페이지가 해당 형식을 끝까지 다루므로, 여기 짧은 버전입니다: 바이트 3개가 들어가면 문자 4개가 나옵니다. A에서 Z까지, a에서 z까지, 0에서 9까지에서 고르되 +와 /가 더하고, 화물이 딱 나누어떨어지지 않으면 끝에 = 패딩이 조금 붙습니다.
R 특유의 반전입니다: base R에는 Base64 인코더가 아예 없습니다. base 패키리에 대기 중인 base64_encode()도 없고, 손 뻗어 쓸 한 줄짜리 내장 함수도 없습니다. 패키지를 고르게 되는데, 생태계는 진짜로 선택지를 줍니다. 다른 속도, 다른 줄바꿈 습관, 패딩에 대한 다른 의견과 함께요. 이 글을 다 읽으면 각 상황에서 어느 인코더를 골라야 하는지, 그리고 그중 어떤 것이 조용히 인코딩이 아닌 다른 일을 할지가 알게 될 것입니다.
인코더 지형도
인코딩을 맡는 패키지는 다섯 개이며, 일상적인 일꾼들, 암호학에 인접한 쪽, 그리고 작은 전문가로 나뉩니다. 2026년 현재 출연진은 이렇습니다:
| 패키지 | 버전 (2026) | 인코딩 진입점 | 줄바꿈 습관 | 언제 손을 대나 |
|---|---|---|---|---|
base64enc |
0.1-6 | base64encode() |
linewidth와 newline, 전부 당신이 정합니다 |
일상 문자열, MIME 줄바꿈 |
openssl |
2.4.2 | base64_encode() |
64자 줄, LF 줄바꿈, 끝에 줄바꿈 | PEM 파일, 기존 암호화 스택 |
b64 |
0.1.7 | encode(), encode_file() |
줄바꿈하지 않음; 필요 시 b64_chunk()와 b64_wrap() |
속도, 벡터, URL에 안전한 엔진 |
base64 |
2.0.2 | encode() |
기본적으로 64자 줄에 끝에 줄바꿈 | 파일-대-파일 잡무, 보고서 이미지 |
base64url |
1.4 | base64_urlencode() |
줄바꿈하지 않음, 패딩 없음, 문자열 입력 | URL에 안전한 문자열 |
인코더가 세 개 더, 당신이 이미 로드할 수 있는 패키지 안에 숨어 있습니다. jsonlite는 base64_enc()와 base64url_enc()를 내보내므로, 이미 JSON을 파싱하고 있다면 손에 인코더가 있을지 모릅니다. jose는 JWT 작업을 위한 base64url_encode()를 내보냅니다. 그리고 노련한 RCurl 패키지에는 여전히 libcurl을 감싸는 base64() 래퍼가 타고 있는데, 잘 작동하며 이전 시대에 속합니다. 마지막으로 base64 패키지는 이제 포장지(라벨)에서 자기 자신을 호환성 래퍼라 소개하며, 새 애플리케이션을 base64enc, openssl나 jsonlite로 가리킵니다.
설정하기
R가 아직 머신에 없다면, 운영체제에 들어 있습니다: Debian과 Ubuntu에는 r-base, Fedora에는 R, macOS에는 Homebrew나 공식 설치 프로그램, Windows에는 설치 프로그램. 그다음은 인코더로, CRAN에서 바로:
install.packages("base64enc")
install.packages("openssl")
install.packages("b64")
install.packages("base64url")
설치가 꼬이는 데는 둘 다 빌드 때입니다. openssl는 시스템의 OpenSSL를 대상으로 컴파일되므로, 맨 상태의 Linux 머신은 먼저 개발용 헤더를 원합니다 (sudo apt install libssl-dev), 아니면 Debian과 Ubuntu에서는 sudo apt install r-cran-openssl로 컴파일을 아예 건너뛸 수도 있습니다. 그리고 b64는 extendr로 감싼 Rust 엔진이므로, 소스 빌드는 Rust 툴체인을 원합니다 (sudo apt install cargo를 실행하면 rustc도 함께 가져옵니다). Windows와 macOS는 CRAN에서 미리 빌드된 바이너리를 받으므로 이 모든 것은 해당되지 않습니다.
첫 인코딩
인코딩 생활의 90%는 3줄 안에 들어갑니다. 디코딩 쪽이 스모크 테스트로 쓰는 그 유명한 문자열을 그대로 쓰면서요:
library(base64enc)
packed <- base64encode(charToRaw("Man"))
packed
#> [1] "TWFu"
identical(packed, "TWFu")
#> [1] TRUE
그 의식에서 눈여겨볼 것이 세 가지입니다. 첫째, 입력은 raw 벡터입니다: charToRaw()는 당신의 R 문자열에서 싸이게 될 바이트로 건너가는 다리이고, 이 표의 일상용 인코더 - base64enc, openssl, b64 - 는 전부 raw를 받습니다. 둘째, 출력은 디코더와 반대 방향입니다: 문자열 하나. 인코딩은 경계의 텍스트 쪽에서 끝나기 때문이죠. 셋째, 길이 계산이 실제로 작동하는 모습을 보시죠: 바이트 3개가 들어가면 문자 4개가 나옵니다. 화물이 딱 나누어떨어져서 패딩은 필요 없고, 화물이 나누어떨어지지 않으면 = 문자 한두 개가 끝에 내려앉습니다.
그리고 신뢰할 수 없는 인코더가 없는 것보다 나쁘므로, 두 방향이 일치한다는 것을 증명하는 라운드 트립입니다:
text <- "Hello, world!"
packed <- base64encode(charToRaw(text))
packed
#> [1] "SGVsbG8sIHdvcmxkIQ=="
identical(text, rawToChar(base64decode(packed)))
#> [1] TRUE
문자열, 바이트와 파일명 함정
자, 이제 함정입니다. R 개발자라면 누구나 한 번씩 빠지니까요. base64encode()는 문자 인수를 인코딩할 텍스트가 아니라 파일명으로 여깁니다. 문자열을 넘기면 그 파일을 찾아다니게 됩니다:
base64encode("Man")
#> Warning in file(what, "rb") :
#> cannot open file 'Man': No such file or directory
#> Error: cannot open the connection
base64encode(charToRaw("Man"))
#> [1] "TWFu"
경고가 바로 단서입니다: file("Man", "rb")를 시도한 것이죠. 뜻은 "Man이라는 파일을 raw로 읽기 위해 여는 것"입니다. 그래서 base64encode()와 함께라면 규율은 하나의 반사동작입니다: 항상 먼저 charToRaw(). 파일이 진정으로 의도한 것이라면, 그것이 바로 함수가 하고 있는 일이고, 출력은 파일의 바이트를 싸 넣은 것이므로, 때로는 정확히 당신이 원하는 것이 됩니다.
b64는 같은 질문에 다른 입장을 취합니다: 그 encode()는 문자 벡터를 그대로 받아, 각 요소를 UTF-8 텍스트로 여긴 데다, 벡터화되어 있습니다:
b64::encode("Man")
#> [1] "TWFu"
b64::encode(c("Man", "M"))
#> [1] "TWFu" "TQ=="
이 경계의 가장자리를 더 두 개 알아두어 할 가치가 있습니다. 빈 입력은 누구에게 물었는지에 따라 세 가지 다른 방식으로 인코딩됩니다:
base64encode(raw(0))
#> character(0)
b64::encode("")
#> [1] ""
base64url::base64_urlencode("")
#> [1] ""
base64enc는 빈 문자열이 아니라 길이가 0인 문자 벡터로 대답합니다. 그래서 문자열을 기대하고 character(0)를 받아드는 하류 코드는 뜻밖의 곳에서 실패합니다. 그리고 문자 집합 결정은 인코딩 쪽에도 살고 있습니다: charToRaw()는 문자열이 현재 입고 있는 인코딩 그대로를 싸 넣으므로, UTF-8 텍스트는 UTF-8 바이트로 여행을 하게 되고, 그것이 바로 선의 다른 끝이 기대하는 것입니다. 이모지도 포함. 최신 R은 U+FFFF 위 코드를 순수한 UTF-8로 저장하기 때문이죠:
emoji <- "\U0001F600"
nchar(emoji, type = "bytes")
#> [1] 4
round_trip <- base64decode(base64encode(charToRaw(emoji)))
identical(emoji, rawToChar(round_trip))
#> [1] TRUE
줄바꿈: MIME, PEM과 당신만의 너비
긴 Base64 문자열은 줄로 끊겨야 합니다. 세상의 가장 오래된 텍스트 채널들이 열 한계를 가지고 있었기 때문이고, MIME은 그것을 잊을 여력이 없었거든요. 당신이 만날 두 가지 역사적인 줄바꿈은 MIME(76자에서 줄바꿈, 줄 사이에 CRLF)과 PEM(64에서 줄바꿈)입니다. 모든 인코더는 둘 중 어느 것을, 혹은 아예 무엇을 쓸지에 대해 자기 생각을 가지고 있습니다. 그래서 이것은 인코딩 전에 계약을 고르는 섹션입니다.
먼저 크기 계산입니다. 줄바꿈은 출력이 얼마나 길어지는지를 아는 것이니까요. 바이트 3개마다 문자 4개가 나오므로, 인코딩된 형태의 길이는 단순한 올림으로 주어지므로:
nchar(base64encode(charToRaw(strrep("a", 100))))
#> [1] 136
4 * ceiling(100 / 3)
#> [1] 136
그것을 주머니에 넣고, 인코더들을 보겠습니다. base64enc가 가장 유연합니다: 기본적으로 끊기지 않은 한 줄을 내보내고, linewidth 인수를 주면 줄들의 벡터를 대신 건네줍니다:
long <- charToRaw(strrep("R", 100))
base64encode(long, linewidth = 76)
#> [1] "UlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJS"
#> [2] "UlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUg=="
base64encode(long, linewidth = 76, newline = "\r\n")
#> [1] "UlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJS\r\nUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUg=="
너비 76에서 100바이트에 두 줄, 기본은 벡터, newline을 추가하면 CRLF로 이어진 문자열 하나로. 끝에 붙는 빈 요소는 없습니다: 76자 두 줄으로 정확히 인코딩되는 114바이트는 두 줄로 돌아오고, 세 줄이 아닙니다.
openssl는 반대 극에 있습니다. linebreaks = TRUE는 64자에서 평범한 LF 줄바꿈으로 줄바꿈하고, 맨 마지막에 줄바꿈을 하나 더 얹습니다:
wrapped <- openssl::base64_encode(charToRaw(strrep("a", 100)), linebreaks = TRUE)
nchar(wrapped)
#> [1] 139 # 데이터 문자 136개에 줄바꿈 3개
nchar(strsplit(wrapped, "\n", fixed = TRUE)[[1]])
#> [1] 64 64 8
계산을 해 보세요: 데이터 문자 136개, 내부 줄바꿈 두 개, 끝에 줄바꿈 하나, 총 139. 그리고 그 끝 줄바꿈은 당신이 쓸 수 있는 가장 자연스러운 줄 세기에겐 보이지 않습니다. strsplit()가 끝에 붙은 빈 조각을 버리기 때문에, 벡터는 세 줄이라 말하지만 문자열은 네 줄을 실어 나르는 셈이죠. OpenSSL 줄바꿈 출력을 MIME 줄바꿈 출력과 diff해서 문자 수가 맞지 않는다면, 이것이 바로 그 유령입니다.
b64는 아예 줄바꿈하지 않습니다. 두 조작을 따로 건네주며, 청크 너비에는 하나의 규칙이 있습니다: 4의 배수. 엔진이 Base64 그룹을 반으로 자르는 것을 거부하기 때문이죠:
enc <- b64::encode(strrep("a", 100))
ch <- b64::b64_chunk(enc, 76)
b64::b64_wrap(ch, "\r\n")
#> [1] "YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFh\r\nYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYQ=="
b64::b64_chunk(enc, 75)
#> Error: Chunk size must be a multiple of 4.
그리고 파일 지향인 base64 패키지는 OpenSSL의 걸음을 따릅니다: 64자 줄, 끝에 줄바꿈, 기본적으로 켜져 있습니다:
writeBin(charToRaw(strrep("b", 200)), "big.bin")
base64::encode("big.bin", "big.b64")
con <- file("big.b64")
lines <- readLines(con)
close(con)
nchar(lines)
#> [1] 64 64 64 64 12 0
그 마지막 0은 끝 줄바꿈으로, readLines()가 빈 마지막 줄로 잡아 낸 것입니다. 이제 전체 전장을 한 번에 보겠습니다:
| 인코더 | 너비 | 줄바꿈 | 끝 줄바꿈 |
|---|---|---|---|
base64encode(x) |
한 줄 | 없음 | 없음 |
base64encode(x, linewidth = 76, newline = "\r\n") |
76 | CRLF | 없음 |
openssl::base64_encode(x, linebreaks = TRUE) |
64 | LF | 있음 |
b64::encode(x) |
한 줄 | 없음 (b64_chunk()와 b64_wrap()로 줄바꿈) |
없음 |
base64::encode(in, out) |
64 | LF | 있음 |
base64url::base64_urlencode(x) |
한 줄 | 없음 | 없음 |
URL에 안전한 Base64
표준 Base64는 알파벳의 마지막 두 칸을 +와 /에 씁니다. 그리고 그 둘은 바로 URL이 싫어하는 문자입니다: 더표는 %2B가 되고, 슬래시는 %2F가 되며, 패딩의 각 =는 %3D가 됩니다. RFC 4648 5절에 정의된 URL에 안전한 변형은 이 두 문자를 -와 _로 바꾸고 보통 패딩도 버리므로, 어디에든 붙여 넣을 수 있어야 할 토큰이 어디에든 붙여 넣을 수 있는 채로 남습니다. R에는 그 안으로 들어가는 문이 세 개 있습니다.
b64 엔진이 가장 완벽합니다: 엔진이란 구성해 둔 알파벳과 패딩 정책이며, 패키지는 필요한 네 가지를 동봉합니다. 같은 세 바이트를 표준 엔진과 URL에 안전한 엔진에 각각 먹여, 알파벳이 제 일을 하는 모습을 지켜 보세요:
bytes <- as.raw(c(0xfb, 0xef, 0xbe))
b64::encode(bytes)
#> [1] "++++"
b64::encode(bytes, engine("url_safe"))
#> [1] "----"
b64::encode(as.raw(0x4d), engine("url_safe"))
#> [1] "TQ=="
b64::encode(as.raw(0x4d), engine("url_safe_no_pad"))
#> [1] "TQ"
엔진이 네 개, "standard", "standard_no_pad", "url_safe", "url_safe_no_pad", 그리고 같은 엔진 객체가 양쪽 방향에서 작동하므로 코드가 대칭을 유지합니다. 패딩 없는 변형은 패딩이 실제로 나타날 때만 다릅니다. 위 1바이트 예처럼요.
전용 base64url 패키지는 단 하나의 목적을 가진 문입니다: 문자열이 들어가면 URL에 안전한 문자열이 나옵니다. 줄바꿈도 패딩도 하지 않습니다:
base64url::base64_urlencode("hello world")
#> [1] "aGVsbG8gd29ybGQ"
jose도 JWT 작업을 위한 자기 자신의 base64url_encode()를 내보내며, 디코딩 쪽에서 바라던 대로 raw 벡터를 돌려줍니다. 세 문 모두를 묶는 규칙이 하나 있습니다: 인코딩에 쓴 알파벳으로 디코딩해야 합니다. 표준 디코더에게 "----"를 건네면 첫 대시에서 실패합니다. 디코딩에 대한 자매 문서는 각 디코더가 더럽거나 안 맞는 입력을 어떻게 만나는지 다룹니다.
JWT: 토큰 만들기
JSON Web Token이 바로 URL에 안전한 Base64가 일상 도구가 된 곳입니다. JWT는 점으로 이어진 base64url 세 부분입니다: 서명 방식을 설명하는 헤더, JSON 클레임으로 된 페이로드, 그리고 둘을 묶는 서명. 인코딩 쪽에서는 세 가지를 모두 만들어 내는데, jose 패키지가 이 온전한 의식을 해냅니다. 그 API는 jwt_* 함수 (jwt_claim(), jwt_encode_hmac(), jwt_decode_hmac(), jwt_split())를 중심으로 지어졌고, 2.0 릴리스 (2026년 4월)는 ED25519 지원을 추가하고 typ 헤더 필드를 선택 사항으로 만들었습니다:
library(jose)
claim <- jwt_claim(iss = "app", sub = "1234567890", exp = Sys.time() + 3600)
token <- jwt_encode_hmac(claim, "0123456789abcdef")
sp <- jwt_split(token)
sp$header
#> $typ
#> [1] "JWT"
#>
#> $alg
#> [1] "HS256"
names(sp$payload)
#> [1] "iss" "sub" "exp" "iat"
jwt_claim()가 배경에서 한 일을 보세요: iat (발급 시각)는 기본적으로 현재 시간이므로, 요청 없이 페이로드에 등장했고, exp는 기본값이 아무것도 아니므로, 이것은 만료 없는 토큰을 의미합니다. 보통 당신이 원하는 것은 아니죠. exp를 의도적으로 설정하면, 나머지는 서명이 알아서 합니다. jwt_split()는 검사 도구입니다: 헤더, 이름 붙은 목록으로서의 페이로드, raw 서명. 검증은 일절 개입하지 않으며, 이 글 쌍의 디코딩 쪽이 설명하는 바로 그 엿보기 단계입니다.
검증 쪽은 jose가 자신의 값을 입증하는 곳입니다. jwt_decode_hmac()는 서명을 확인하고 시간 관련 클레임을 시행하며, 거절 스타일이 구체적입니다:
round <- jwt_decode_hmac(token, "0123456789abcdef")
round$sub
#> [1] "1234567890"
jwt_decode_hmac(token, "ffffffffffffffff")
#> Error: HMAC signature verification failed!
old <- jwt_claim(sub = "1234567890", exp = Sys.time() - 3600)
expired <- jwt_encode_hmac(old, "0123456789abcdef")
jwt_decode_hmac(expired, "0123456789abcdef")
#> Error: Token has expired on 2026-08-30 05:09:36
성공하면 클레임이 보통의 목록으로 돌아오니, round$sub 일당은 그냥 작동합니다. 미래의 nbf (유효 시작) 클레임은 자기만의 거절을 얻습니다: Token is not valid before .... 그리고 HMAC 토큰은 비대칭 jwt_decode_sig()로는 디코딩되지 않습니다. 그것은 Unsupported algorithm: HMAC로 답하며, 대신 공개 키를 원하거든요. 마지막 참고 두 가지. 첫째, 페이로드는 인코딩된 것이지 암호화된 것이 아닙니다: 모든 클레임을 누가든 읽을 수 있으니, 비밀스러운 것은 어디에도 넣을 수 없습니다. 둘째, jose 뒤에 httr2를 로드하면 마스킹 메시지를 주시하세요: httr2는 자기 자신의 jwt_claim(), jwt_encode_hmac(), jwt_encode_sig()를 내보내는데, exp 기본값이 5분 후인 OAuth 클라이언트 자격 증명을 위해 지어진 것들로, 세션의 나머지 동안 jose 버전을 가려 버립니다.
파일: 바이너리 들어가고, 텍스트 나가기
파일을 인코딩하는 것은 디코딩 쪽의 파일 작업의 거울상이고, b64 패키지가 가장 명백한 진입점을 가졌습니다. 거대한 중간 문자열 하나를 결코 만들지 않기 때문에 가장 빠르기도 합니다:
writeBin(charToRaw("file payload bytes"), "payload.bin")
enc <- b64::encode_file("payload.bin")
enc
#> [1] "ZmlsZSBwYXlsb2FkIGJ5dGVz"
cat(enc, file = "payload.b64")
readLines("payload.b64")
#> Warning in readLines("payload.b64") :
#> incomplete final line found on 'payload.b64'
#> [1] "ZmlsZSBwYXlsb2FkIGJ5dGVz"
경고는 위장한 기능입니다: cat()은 끝에 줄바꿈을 추가하지 않으므로 파일은 줄 가운데에서 끝나고, readLines()가 그렇게 알려 주는 것이죠. 그 파일이 끝에 줄바꿈에서 발작(panic)하는 b64::decode_file()로 소비될 때, 이 사실의 어느 쪽에 서 있는지 기억하세요. 디코딩 문서에 온전한 이야기가 있습니다. cat()이나 writeBin(charToRaw(enc), path)로 쓰면 그 모서리는 둔한 채로 남습니다.
base64 패키지는 순수한 파일-대-파일 옵션입니다. 짝을 이룬 함수 쌍을 가지고 있고, 기본적으로 OpenSSL식 줄바꿈을 씁니다:
base64::encode("payload.bin", "payload2.b64")
readLines("payload2.b64")
#> [1] "ZmlsZSBwYXlsb2FkIGJ5dGVz" ""
출력 경로를 돌려주므로 로깅에 편리합니다. 그리고 기계의 안쪽을 보고 싶을 때, 수동 파이프라인은 표의 어떤 인코더와도 동작합니다: 파일을 raw로 읽고, 인코딩하고, 텍스트를 쓰고, 끝:
bytes <- readBin("payload.bin", what = "raw", n = file.size("payload.bin"))
enc <- base64encode(bytes)
writeLines(enc, "payload3.b64")
identical(enc, readLines("payload3.b64"))
#> [1] TRUE
data URI와 자기 완결형 문서
data: URI 스킴 (RFC 2397)은 인코딩 쪽에서 가장 눈에 띄는 사용자입니다: 자기 자신의 내용을 싣는 문서, MIME 타입, "base64"라는 단어, 그리고 페이로드가 전부 하나의 속성 안에 들어갑니다. PNG 매직 바이트가 이 패턴을 알아볼 수 있게 해 줍니다: 현존하는 모든 Base64 PNG는 같은 문자로 시작합니다. 파일 헤더 89 50 4E 47은 항상 같은 방식으로 인코딩되기 때문이죠:
png_head <- as.raw(c(0x89, 0x50, 0x4e, 0x47))
uri <- paste0("data:image/png;base64,", base64encode(png_head))
uri
#> [1] "data:image/png;base64,iVBORw=="
tag <- paste0("<img src=\"", uri, "\" />")
HTML 파일에서 내장된 이미지를 찾기 위해 iVBOR를 grep해 본 적이 있다면, 그 지문이 작동하는 이유가 이것이겠지요. R Markdown 보고서, 단일 파일 대시보드, 크롤링한 페이지는 전부 같은 모양을 쓰며, 하나를 만드는 것도 같은 패턴을 따릅니다: 파일을 raw로 읽고, 인코딩하고, MIME 접두사 뒤에 붙여 넣는 것. 대가는 앞서의 크기 계산입니다: 원본보다 3분의 1 더 부푼 채로 당신의 HTML에 영원히 앉아 있거든요. 내장된 이미지는 슬림하게 유지하세요.
API와 웹 요청
이 글 쌍의 디코딩 쪽은 당신에게 Base64를 건네는 API를 만나고, 이 쪽은 그것을 원해서 오는 API를 만납니다. 패턴은 매번 동일합니다: 바이트를 인코딩하고, 문자열을 JSON 본체에 넣고, 보내는 것. 현대적인 HTTP 클라이언트는 httr2입니다:
library(httr2)
req <- request("https://httpbin.org/post")
req <- req_body_json(req, list(file = packed, name = "man.txt"))
req
#> <httr2_request>
#> POST https://httpbin.org/post
#> Body: JSON data
res <- req_perform(req)
res$status_code
#> [1] 200
길 위에 쓸 참고 세 가지입니다. 요청 생성자는 request()이며, httr2의 첫 릴리스부터 그 이름을 써 왔습니다. 응답을 읽을 때 응답 본문은 raw 벡터로 도착하므로, 파싱 전에 rawToChar()를 거치세요. 그리고 API는 방언을 쓸 수 있습니다: 어떤 것은 URL에 안전한 알파벳을 원하고, 어떤 것은 패딩을 벗긴 것을 원합니다. JSON API는 문자열 값 안에 =가 있어도 아무 문제가 없으므로, 패딩 문제가 URL 문제가 되는 것은 Base64가 경로나 쿼리로 여행할 때뿐입니다. 의심스러울 때는 머리 속의 스펙이 아니라 API의 예시를 읽어 보세요.
데이터베이스, 설정과 환경
데이터베이스 안의 Base64는 텍스트 열을 통해 밀수된 바이너리이며, 인코딩 쪽은 덩어리가 들어가기 전에 그것을 싸 넣는 일입니다. DBI와 RSQLite를 통한 SQLite와의 라운드 트립입니다:
library(DBI)
library(RSQLite)
db <- dbConnect(SQLite(), ":memory:")
dbExecute(db, "CREATE TABLE files (name TEXT, payload TEXT)")
stored <- base64encode(charToRaw("stored in a database"))
dbExecute(db, paste0("INSERT INTO files VALUES ('note.txt', '", stored, "')"))
row <- dbGetQuery(db, "SELECT * FROM files")
rawToChar(base64decode(row$payload))
#> [1] "stored in a database"
대안은 바이트를 BLOB로 네이티브 저장하는 것입니다. 그러면 Base64는 아예 불필요하며 열은 raw 벡터로 R에 돌아옵니다. TEXT 안의 Base64 변형은 이동성을 위해 존재합니다: 텍스트 편집기로 검사할 수 있고, diff할 수 있으며, 어떤 다른 언어든 바이너리 드라이버 없이 읽을 수 있거든요. 바로 그 논리가 그것을 설정 파일까지 데려가는데, 거기서 인증서나 비밀이 YAML, JSON, 또는 환경 변수 안에 문자열로 저장되어 있습니다:
Sys.setenv("API_CERT" = base64encode(charToRaw("LTSSECRET")))
Sys.getenv("API_CERT")
#> [1] "TFRTU0VDUkVU"
주의 하나를 여기에 얹어야 합니다. 설정 파일은 비밀이 사는 곳이니까요: Base64는 인코딩이지 암호화가 아닙니다. 설정 파일 안의 Base64 값은 그 파일을 읽을 수 있는 누구나 읽을 수 있습니다. 전송과 텍스트 편집기에는 살아남지만, 그것은 아무것도 보호하지 않습니다.
이메일
이메일은 76자 줄바꿈이 태어난 곳이며, MIME 첨부 파일은 여전히 그것을 입고 있습니다: Base64 콘텐츠, 줄 사이에 CRLF를 두고 76자에서 끊긴 채, Content-Transfer-Encoding: base64를 선언하는 부분 안에. 정확히 그 모양을 만들어 내는 인코더는, 줄바꿈 인수를 MIME 계약으로 설정한 base64encode()입니다:
body <- "The quick brown fox jumps over the lazy dog, and then it came back down again."
mime_part <- base64encode(charToRaw(body), linewidth = 76, newline = "\r\n")
strsplit(mime_part, "\r\n", fixed = TRUE)[[1]]
#> [1] "VGhlIHF1aWNrIGJyb3duIGZveCBqdW1wcyBvdmVyIHRoZSBsYXp5IGRvZywgYW5kIHRoZW4gaXQg"
#> [2] "Y2FtZSBiYWNrIGRvd24gYWdhaW4u"
R에는 일급의 메일 클라이언트는 없지만, MIME 부분을 손으로 만들거나 검사할 때, .eml 픽스처를 생성할 때, 또는 그것에서 첨부 파일을 파싱할 때마다 이 지점은 그대로 성립합니다: 이것이 바로 Base64가 가져야 할 모양이고, 이 글 쌍의 디코딩 쪽은 너그러운 디코더들이 돌아오는 길에 그것을 풀어 주는 모습을 보여 줍니다.
큰 페이로드와 문자열의 천장
R 문자열에는 2^31 - 1 바이트라는 단단한 천장이 있고, 인코딩된 형태가 원본보다 약 3분의 1 더 크기 때문에, 원시 데이터가 대략 1.5 GB인 파일은 그 한 줄 인코딩을 벽 너머로 밀어 버립니다. 실용적인 수는 base64enc가 2022년 긴 벡터 릴리스부터 제안해 온 그것과 같습니다: 출력을 하나의 문자열이 아니라 줄들로 유지하세요:
big <- raw(10 * 1024 * 1024)
packed <- base64encode(big, linewidth = 76)
length(packed)
#> [1] 183961
sum(nchar(packed))
#> [1] 13981016
0으로 된 10메가바이트는 76자를 넘지 않는 183961줄이 됩니다. 거대한 문자열 하나를 결코 얹고 있을 필요 없이, 줄별로 써 내거나 파이프를 통해 흘려보낼 수 있는 완전히 평범한 벡터죠. 데이터 프레임의 경우, 바이너리 값이 많은 열이라면, b64가 속도 챔피언입니다: Rust 엔진이 전체 열을 벡터화된 호출 한 번으로 인코딩하는데, 행마다 반복하는 것과는 극적인 차이입니다. 만들어야 할 인코딩된 값의 열이 있다면, 직접 system.time() 비교를 빠르게 돌려 보세요. 행 단위 루프와 벡터 호출 한 번 사이의 격차는 보통 중요할 만큼 큽니다.
명령줄
모든 것이 완전한 R 세션을 필요로 하는 것은 아닙니다. 클래식 Unix 도구는 Base64를 네이티브로 말합니다. 모든 플랫폼에서 플래그 없이 인코딩하고, Linux에서는 -d로, macOS와 BSD에서는 -D로 디코딩하죠:
echo -n "Hello, world!" | base64
#> SGVsbG8sIHdvcmxkIQ==
echo -n "SGVsbG8sIHdvcmxkIQ==" | base64 -d
#> Hello, world!
첫 줄의 -n를 보세요: 그것이 없으면 echo가 줄바꿈을 한 개 얹어, 출력이 13바이트 대신 14바이트를 인코딩하고 IQ== 대신 o=로 끝납니다. GNU base64는 대신 줄바꿈도 해 줍니다 (-w 76). MIME 모양의 입력을 기대하는 곳에 파이프할 때 편리하죠. 그리고 한 줄짜리 Rscript가 스크립트에서 쓰시는 같은 패키지로 같은 일을 해냅니다:
Rscript -e 'library(base64enc); writeLines(base64encode(charToRaw("Man")))'
#> TWFu
빠른 확인과 파이프에는 셸을, 결과가 데이터 프레임, 파일, 보고서에 살아야 할 때는 R을 쓰세요. 그리고 수메가바이트짜리 문자열을 터미널에 붙여 넣지 마세요: 명령줄 인수는 Base64보다 훨씬 일찍 ARG_MAX에 부딪히므로, 대신 파일로 파이프를 보내세요.
알아둘 만한 함정
이것이 인코딩 쪽이 R 개발자들을 물어뜯는 방식의 짧은 목록입니다. 전부 Base64 일반의 성질이 아니라 생태계 고유의 것입니다:
- 문자열은 파일명입니다.
base64encode("Man")는 Man이라는 파일을 여려고 합니다. 경고를 보면 파일 이름이 나오고, 오류는 연결 실패를 말합니다.base64encode()에는 항상 먼저charToRaw(). - 빈 입력은 세 가지 방식으로 인코딩됩니다.
base64encode(raw(0))는character(0)를 돌려주지만,b64::encode("")와base64url::base64_urlencode("")는""를 돌려줍니다. 하류의 문자열 코드는 길이가 0인 벡터를 기대하지 않습니다. - openssl은 줄바꿈을 한 뒤에 유령 줄을 하나 더 얹습니다.
linebreaks = TRUE는 64에서 LF로 줄바꿈하고,strsplit()가 조용히 버리는 끝 줄바꿈을 붙입니다. 그래서 안일한 줄 세기와 문자 세기가 한 줄만큼 어긋나죠. - 줄바꿈은 계약입니다. MIME은 CRLF를 둔 76, PEM은 64, JSON API는 대개 아무것도 원하지 않습니다. 상대 쪽이 기대하는 모양을 고르세요. 한 줄바꿈 방식을 관용하는 디코더가 다른 방식을 거부하기 때문입니다.
- URL에 안전한 알파벳은 양 끝이 맞아야 합니다. URL에 안전하게 인코딩한
"----"는 표준 디코더에서 첫 대시에서 실패하고, 문자열이 URL 안에 사는 순간 패딩은%3D가 됩니다. - b64_chunk는 4의 배수를 요구합니다. 다른 어떤 너비도
Chunk size must be a multiple of 4.를 얻습니다. Base64 그룹은 반으로 자를 수 없기 때문이죠. - 끝에 줄바꿈은 디코더를 발작(panic)시킬 수 있습니다.
b64::decode_file()가 읽도록 쓰는 파일은 줄바꿈 없이 끝나야 합니다.writeLines()가 아니라cat(). - JWT의 시간 클레임은 시행됩니다.
jwt_decode_hmac()는 만료된 토큰과 미래의nbf클레임을 거부하고,httr2를 나중에 로드하면jose의jwt_*함수들을 가려 버립니다. - 페이로드는 보입니다. JWT, data URI, 설정 파일 안의 Base64 클레임은 누구나 읽을 수 있습니다. 인코딩은 암호화가 아닙니다.
- 문자열의 천장은 벽이지 가이드라인이 아닙니다. R 문자열 하나당 약 1.5 GB의 원시 데이터가 한 줄 인코딩이 더 이상 맞지 않는 지점입니다. 큰 출력은 줄로 유지하거나 흘려보내세요.
최선의 실천
base64enc::base64encode()를 쓸 때는 인코딩 전에charToRaw()로 변환하세요. 문자 직접 입력과 벡터화가 필요하면, 문자열을 문자열로 대하는 패키지는b64::encode()입니다.- 일상의 일에는
base64enc::base64encode()를 기본으로 쓰고, 속도, 진정한 벡터화, 또는 URL에 안전한 엔진이 원하면b64에 손을 대며, 프로젝트에 이미 있고 PEM 모양의 출력을 원하면openssl를 쓰세요. - 줄바꿈은 패키지가 아니라 채널별로 고르세요: JSON과 URL에는 없음, MIME에는 CRLF를 둔 76, PEM식 블록에는 64. 그리고 일관되게 유지하세요. 파이프의 디코더 쪽이 무엇을 기대해야 할지 알아야 하니까요.
- URL, JWT, 파일명에 살 것에는 패딩 없는 URL에 안전한 알파벳을, 이메일에는 표준 알파벳을 쓰세요.
- JWT에는
exp(그리고iat)를 명시적으로 설정하고, 토큰을 신뢰하기 전에jwt_decode_hmac()로 검증하며, 모든 클레임이 공개된 텍스트라는 것을 기억하세요. - 인코딩 경로는 라운드 트립으로 테스트하세요:
identical(text, rawToChar(base64decode(base64encode(charToRaw(text))))). 한 줄이고, 알파벳, 패딩, 문자 집합 실수를 한꺼번에 잡아 주니까요. - 파일에는 전체 파일을 하나의 문자열로 읽는 것보다
b64::encode_file()이나base64::encode()를 우선하고, 엄격한 디코더가 읽을 출력이면cat()으로 쓰세요. - 패킹 테이프를 정직하게 유지하세요: Base64는 바이트가 여행하게 하지만, 비밀스럽게 만들지도, 작게 만들지도 않습니다. 목적이 비밀이면 먼저 암호화하고, 목적이 크기면 먼저 압축하세요.
Base64가 R에 들어온 길
형식은 R이 그것으로 아무것도 하기 훨씬 전에 이미 도착했습니다. 1987년 Privacy-Enhanced Mail 프로토콜을 위해 표준화되었고 (RFC 989), 1993년 MIME에 채택되었으며 (RFC 1521, 그리고 1996년 최종 RFC 2045 - 76자 줄바꿈을 여전히 정의하는 것), 2003년 RFC 3548에서 다듬어졌고, 2006년 RFC 4648에서 URL에 안전한 알파벳, 패딩 없는 옵션, 그리고 어린 남동생 Base32를 더한 현대적인 모양을 얻었습니다. R의 이야기는 2012년 9월에 시작되어, Simon Urbanek의 base64enc가 CRAN에 올라와 "이걸 Base64로 하려면?"에 대한 기본 답이 10년 넘게 조용히 자리 잡고 있었으며, 2015년에 checkUTF8()를, 2022년에는 긴 벡터 지원을 갖추었습니다. 암호학 세계는 openssl를 통해 들어왔는데, Jeroen Ooms의 시스템 OpenSSL를 둘러싼 오랜 래퍼로, 그 base64_encode()는 그때부터 PEM 모양의 옵션이었습니다. Ooms의 또 다른 오래된 base64 패키지는 2024년 10월 호환성 래퍼임을 명시하며 재발행되었고, 그 자체 설명이 이제 새 애플리케이션을 다른 곳으로 가리킵니다. 그러고 2024년 1월에 b64가 왔습니다. extendr로 지은 Rust 엔진으로 벡터화와 알파벳 한 마당을 가져왔고, 2026년 4월 jose는 2.0 버전을 릴리스해 jwt_* API에 ED25519 지원을 추가하며 서명된 토큰을 일급 시민으로 만들었습니다. 결과는 일마다 인코더 하나씩 갖춘 도구상자입니다: 일상 문자열, PEM 블록, 속도, URL, 파일, 그리고 토큰.
재미 코너
완전한 가이드라면 웃음으로 끝내야 하므로:
- 인터넷의 모든 Base64 PNG는 같은 문자로 시작합니다: 매직 헤더 89 50 4E 47은
iVBOR로 인코딩되므로, HTML 파일에서 그 지문을 grep하면 모든 내장 이미지를 찾아냅니다. 형식은 지문을 가질 수 있고, 이 지문은 접두사입니다. base64encode("Man")는 단어 Man을 인코딩하지 않습니다. Man이라는 이름을 가진 파일을 찾아다니며, 열 수 없다는 경고를 남기고 물러납니다. Base64 생태계에서 가장 R 특유의 함정 하나, 인수의 목록 속에 대놓고 숨어 있습니다.- OpenSSL 줄바꿈 문자열은 항상 끝 줄바꿈으로 끝나므로, PEM 블록의 마지막 줄은 결코 파일의 마지막 줄이 아닙니다. 그 줄바꿈은 문장 끝에 마침표를 두는데, 당신이 원하든 원하지 않든요.
- 빈 문자열은 세 가지 다른 방식으로 인코딩됩니다:
base64enc는 문자열 0개의 벡터를 돌려주고,b64와base64url는 빈 문자열을 돌려줍니다. R은 아무것도 만나고, R은 세 가지 답을 줍니다. - jose는 JWT 헤더를
alg보다 앞에typ로 씁니다. 대개의 손으로 쓴 예제는alg를 앞에 두죠. JSON은 키 순서를 신경 쓰지 않고, JWT 검증도 그것을 알지만, 당신의 문자열 diff는 모릅니다. - MIME의 76자 한계는 1993년 메시지 줄 길이에 대한 결정으로, 네 개의 RFC를 거쳐 당신이 보낸 모든 이메일 첨부 파일에까지 전해졌습니다. 오늘 당신이 설정하는 줄바꿈은 R이 색 플롯을 갖기 전에 논쟁되었던 것입니다.
b64는 당신이 본 적도 없는 알파벳을 디코딩합니다: BinHex, IMAP modified UTF-7, bcrypt, crypt, 그리고 URL에 안전한 쌍까지, 엔진 각 하나로. 당신의 JSON 필드를 싸 넣어 주는 그 Rust 코드가 1980년대 Macintosh 첨부 파일을 풀어 낼 수 있습니다.
마무리
일을 따라 인코더를 고르세요: 일상 문자열에는 base64enc, 채널이 모양을 가졌다면 linewidth와 newline를 곁들여; PEM식 64자 출력을 원하거나 이미 프로젝트에 있다면 openssl; 속도, 벡터, URL에 안전한 엔진이 원하면 b64; 그리고 파일 잡무와 URL에 안전한 문자열에는 작은 전문가 base64와 base64url. base64enc::base64encode()로 인코딩하기 전에 charToRaw()로 변환하고, 줄바꿈은 패키지가 아니라 채널별로 고르며, URL이나 JWT에 살 것에는 URL에 안전한 알파벳을 유지하고, 토큰은 jose로 서명하고 검증하며, 모든 경로를 라운드 트립으로 테스트하세요. 형식 자체는 정확히 두 곳에서만 무자비합니다: 알파벳과 줄바꿈. 그리고 인코더들이 대부분 다르는 곳은, 둘 중 하나를 틀렸을 때 그것을 얼마나 정직하게 알려 주느냐입니다. 그리고 반대 방향이 부르는 때가 오면 - 문자열이 도착해 그것을 분해해야 하고, 바이트가 무슨 의미인지 확인해야 하며, 조용히 실패하는 디코더들을 살아남아야 할 때 - 자매 문서가 R에서의 Base64 디코딩을 자세히 다룹니다.
마지막 업데이트: 2026-09-08
관련 문서: R에서의 Base64 디코딩: 완전한 가이드