인코딩 완전 정복

강정우·2026년 4월 15일

CS

목록 보기
1/1
post-thumbnail

Base64와 인코딩 완전 정복 — 바이너리→텍스트 변환의 모든 것

Base64는 주로 어디에 쓰일까?

Base64는 바이너리 데이터를 텍스트(ASCII) 형태로 변환해야 할 때 사용한다. 텍스트만 허용되는 채널에서 바이너리를 안전하게 전송·저장하는 것이 핵심 목적이다.

대표적인 사용처

이메일 (MIME) — SMTP는 7-bit ASCII 기반 프로토콜이다. 이미지·첨부파일 같은 바이너리를 본문에 실으려면 base64로 인코딩해야 한다. Content-Transfer-Encoding: base64 헤더가 붙는 경우가 바로 이것이다.

웹/API 통신 — JSON이나 XML 같은 텍스트 포맷 안에 이미지·PDF 등 바이너리 파일을 넣을 때 활용된다. Anthropic API에 이미지를 보낼 때 source.type: "base64"로 넣는 패턴이 대표적이다. Data URI(data:image/png;base64,…)로 HTML/CSS에 이미지를 인라인 삽입할 때도 동일한 원리다.

인증 헤더 — HTTP Basic Authentication에서 username:password 문자열을 base64로 변환해 Authorization: Basic <encoded> 형태로 전송한다. 암호화가 아니라 단순 변환이므로 보안 목적은 아니다.

인증서·키 저장 — PEM 형식의 SSL/TLS 인증서, SSH 공개키 등이 base64로 표현되어 -----BEGIN CERTIFICATE----- 블록 안에 들어간다.

쿠키·토큰 — JWT의 header/payload 부분이 base64url로 처리되고, 쿠키에 구조화된 값을 담을 때도 종종 활용된다.

정리하면, base64 자체는 암호화나 압축이 아니라 "바이너리 → 텍스트 안전 변환"이 본질이며, 텍스트 전용 프로토콜·포맷과 바이너리 사이의 브릿지 역할을 한다. 다만 원본 대비 약 33% 크기가 늘어나는 오버헤드가 있어서, 대용량 파일에는 멀티파트 전송이 더 효율적이다.


Base64 말고 다른 종류들

문자 인코딩 (Character Encoding)

텍스트를 바이트로 표현하는 방식이다. ASCII가 영문 128자만 다루던 한계를 넘어 다국어를 지원하는 방향으로 발전해 왔다.

  • ASCII — 7비트, 영문·숫자·기본 기호만 표현 (0~127)
  • EUC-KR / CP949 — 한글 완성형. 과거 한국 웹에서 널리 쓰였고, 지금도 레거시 시스템에서 만난다
  • UTF-8 — 가변 길이(1~4바이트) 유니코드 구현체. 현재 웹 표준이며 ASCII와 하위 호환된다
  • UTF-16 — 2 또는 4바이트. JavaScript 내부 문자열, Windows API 등에서 채택
  • UTF-32 — 고정 4바이트. 처리는 단순하지만 메모리 낭비가 커서 실무에선 드물다
  • ISO-8859-1 (Latin-1) — 서유럽어 확장. HTTP 기본 charset으로 오래 활용되었다

바이너리→텍스트 (Binary-to-Text)

base64와 같은 계열로, 바이너리를 텍스트 안전 문자열로 바꾼다.

  • Base32 — A~Z + 2~7 사용. 대소문자 구분 없는 환경(DNS, OTP 시크릿)에 적합
  • Base16 (Hex) — 0~9, A~F. MAC 주소, 해시값, 색상 코드(#FF5733) 등에서 일상적으로 볼 수 있다
  • Base85 (Ascii85) — base64보다 효율적(약 25% 오버헤드). PDF 내부, Git 바이너리 패치에 쓰인다
  • Quoted-Printable — 이메일에서 대부분 ASCII인 텍스트에 간헐적 비ASCII 문자가 섞일 때 활용. =EC=9D=B4 같은 형태
  • UUencode — Unix 시절 이메일 첨부 방식. base64/MIME에 거의 대체되었다

웹/URL

  • Percent-encoding (URL encoding) — URL에 넣을 수 없는 문자를 %XX 형태로 변환. 예: 공백 → %20, 한글 → %ED%95%9C
  • HTML Entity encoding — HTML 특수문자를 안전하게 표현. <&lt;, &&amp;, 숫자 참조 &#44032;

전송/압축 (Transfer Encoding)

전송 효율이나 스트리밍을 위한 방식이다.

  • Chunked Transfer Encoding — HTTP/1.1에서 전체 크기를 모른 채 응답을 조각 단위로 전송
  • gzip / deflate / br (Brotli) — HTTP Content-Encoding 헤더로 지정. 엄밀히는 압축이지만 전송 계층으로 분류되기도 한다

미디어 (Audio/Video/Image)

  • 영상 — H.264, H.265(HEVC), VP9, AV1
  • 음성 — AAC, Opus, MP3, FLAC
  • 이미지 — JPEG, PNG, WebP, AVIF

이들은 "코덱"이라고도 부르며, 손실/무손실 압축과 디코딩 규격을 함께 정의한다.

보안 관련 변환

변환과 암호화는 다르지만, 실무에서 함께 언급되는 경우가 많다.

  • 해싱 — SHA-256, MD5 등. 단방향 처리라 디코딩 불가. 무결성 검증·비밀번호 저장용
  • 암호화 — AES, RSA, ChaCha20 등. 키가 있어야 복호화 가능. 파이프라인에서 인코딩과 결합되어 활용된다 (예: 암호화 → base64 → JSON 전송)

핵심 구분은 목적이다. 문자 표현이면 UTF-8 계열, 바이너리의 텍스트 안전 변환이면 base64 계열, 전송 효율이면 gzip/chunked, 미디어 압축이면 코덱.


바이너리→텍스트 인코딩, 실제로 어디서 쓰이나?

바이너리→텍스트 변환이 추상적으로 느껴지는 이유는, 대부분 "이미 변환된 결과물"을 매일 보면서도 그 사실을 의식하지 못하기 때문이다.

핵심 전제: 왜 필요한가?

세상에는 "텍스트만 통과시키는 통로"가 생각보다 많다. JSON, XML, HTTP 헤더, 이메일 본문, 환경변수, 설정 파일, 소스코드 — 이런 곳에 바이너리(이미지, 인증서, 암호화 결과물 등)를 넣으려면 텍스트로 바꿔야 한다. 그 변환기가 base64, hex 같은 바이너리→텍스트 기법이다.

1. SSH 키 / SSL 인증서 (Base64)

~/.ssh/id_rsa.pub를 열어보면 이런 형태다:

ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAB... user@host

가운데 긴 문자열이 base64이다. 공개키의 실체는 바이너리 숫자(큰 정수)인데, 이걸 텍스트 파일에 한 줄로 저장하고 터미널에 복사·붙여넣기 하려면 텍스트여야 한다. NHN Cloud 인스턴스에 SSH 키 등록할 때 콘솔에 붙여넣는 것도 같은 이유다.

SSL 인증서(*.pem)도 마찬가지다:

-----BEGIN CERTIFICATE-----
MIIDdzCCAl+gAwIBAgIEAgAAuTANBgkq...
-----END CERTIFICATE-----

nginx 설정에서 ssl_certificate 경로를 지정하면 nginx가 이 base64를 디코딩해서 바이너리 인증서로 해석한다.

2. 이메일 첨부파일 (Base64)

이메일로 이미지를 보내면, 실제 전송되는 원문(raw message)은 이렇다:

Content-Type: image/png; name="photo.png"
Content-Transfer-Encoding: base64

iVBORw0KGgoAAAANSUhEUgAAAPAAAADwCAYAAAA+VemSAAA...

SMTP는 7-bit 텍스트 프로토콜이라 PNG 바이너리를 그대로 보낼 수 없다. 메일 클라이언트가 자동으로 base64 처리해서 전송하고, 수신 측에서 복원하는 것이다. 우리가 의식하지 못할 뿐 매일 일어나는 일이다.

3. API 요청 안에 파일 담기 (Base64)

Anthropic API에 이미지를 보내는 코드가 대표적이다:

{
  "type": "image",
  "source": {
    "type": "base64",
    "media_type": "image/jpeg",
    "data": "/9j/4AAQSkZJRgABAQ..."
  }
}

JSON은 텍스트 포맷이라 바이너리 바이트를 직접 넣을 수 없다. multipart/form-data로 파일을 따로 보내는 방법도 있지만, JSON body 하나로 깔끔하게 처리하고 싶을 때 base64가 활용된다.

4. Data URI — HTML/CSS 안에 이미지 삽입 (Base64)

<img src="data:image/png;base64,iVBORw0KGgo..." />

별도 HTTP 요청 없이 HTML 자체에 이미지를 포함시킨다. 아이콘 같은 작은 이미지에 적용하면 네트워크 왕복을 줄일 수 있다. 이메일 HTML 템플릿에서 특히 많이 쓰인다 (외부 이미지 URL이 차단되는 메일 클라이언트 대응).

5. JWT 토큰 (Base64url)

로그인 후 받는 JWT를 .으로 쪼개면 세 파트다:

eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxMjN9.서명부분

앞 두 파트를 base64url 디코딩하면:

{"alg":"HS256"}
{"user_id":123}

JSON 객체를 HTTP 헤더(Authorization: Bearer ...)나 URL 파라미터에 넣어야 하는데, JSON을 그대로 넣으면 {, ", : 같은 특수문자가 문제를 일으킨다. base64url로 감싸면 알파벳·숫자·-·_만 남아서 어디에든 안전하게 들어간다.

6. Git 바이너리 diff (Base85)

Git에서 이미지 파일이 변경되면 diff에 이런 내용이 나타난다:

diff --git a/icon.png b/icon.png
GIT binary patch
literal 2345
zcmV+^3E7mhP)<h;3K|Lk000e1NJLTq...

텍스트 기반 diff/patch 포맷 안에 바이너리 변경분을 담기 위해 base85를 채택한다.

7. Hex (Base16) — 해시값, MAC 주소, 디버깅

매일 접하는 것들이다:

sha256sum file.tar.gz
# e3b0c44298fc1c149afbf4c8996fb924...  ← hex로 표현된 해시

ip link show
# link/ether 3a:2b:1c:4d:5e:6f  ← MAC 주소도 hex

hexdump -C /dev/urandom | head
# 바이너리 파일 내용을 사람이 읽을 수 있게 hex로 표시

해시 함수의 출력은 바이너리(32바이트 등)인데, 터미널에 표시하거나 로그에 기록하려면 hex 문자열로 변환한다.

정리하면 패턴은 하나다: "텍스트만 허용되는 곳에 바이너리를 넣어야 할 때." JSON, HTTP 헤더, 이메일, 설정 파일, 소스코드, 터미널 출력 — 이 모든 텍스트 전용 통로가 존재하는 한 바이너리→텍스트 기법은 계속 쓰인다. 눈에 잘 안 보이는 이유는 대부분 라이브러리나 프로토콜이 자동 처리해주기 때문이다.


바이너리는 0과 1인데 이게 이미지라고?

맞다. 바이너리는 0과 1의 나열이 맞고, 이미지도 음악도 영상도 결국 전부 0과 1이다. 핵심은 "해석 방식이 다르다"는 것이다.

컴퓨터에서 모든 것은 바이너리

텍스트 파일도 바이너리다. 예를 들어 A라는 글자는 내부적으로 01000001(65번)로 저장된다. 다만 텍스트 파일은 모든 바이트가 "사람이 읽을 수 있는 문자 범위(0~127)" 안에 있어서, 에디터로 열면 글자로 보이는 것이다.

반면 PNG 이미지 파일을 메모장으로 열어보면 ‰PNG\r\n... 뒤에 깨진 문자가 쏟아진다. 같은 0과 1인데 "문자로 해석할 수 없는 바이트"가 대부분이기 때문이다.

이미지가 바이너리인 구조

3×2 픽셀짜리 아주 작은 이미지가 있다고 하면, 각 픽셀은 RGB 값을 가진다:

픽셀(0,0) = 빨강 255, 초록 0, 파랑 0

이걸 바이너리로 쓰면:

11111111 00000000 00000000

이게 한 픽셀이고, 이런 바이트가 수십만~수백만 개 이어지면 사진 한 장이 된다. 거기에 파일 헤더(이미지 크기, 포맷 정보 등)가 앞에 붙고, 압축 알고리즘이 적용되면 PNG나 JPEG 파일이 되는 것이다.

"텍스트"와 "바이너리"의 실무적 구분

둘 다 0과 1인 건 같지만, 실무에서 말하는 구분은 이것이다:

텍스트 — 모든 바이트가 문자로 매핑되는 형태. JSON, HTML, 소스코드, 설정 파일 등. 에디터로 열면 사람이 읽을 수 있다.

바이너리 — 문자 범위 밖의 바이트가 포함된 형태. 이미지, 동영상, 실행파일, 압축 아카이브 등. 에디터로 열면 깨져 보인다.

그래서 base64가 필요한 이유

JSON 같은 텍스트 포맷은 "문자"만 담을 수 있게 설계되어 있다. 그런데 이미지 바이너리에는 0x00(null), 0x89, 0xFF 같은 바이트가 들어 있고, 이런 값들을 JSON 문자열 안에 그대로 넣으면 파싱이 깨진다.

base64가 하는 일:

원본 바이너리:  10001001 01010000 01001110 01000111 ...
                (0x89)    (P)      (N)      (G)

 ↓ base64 변환

텍스트 문자열:  "iVBORw0KGgo..."
                (A~Z, a~z, 0~9, +, / 만 사용)

0x89 같은 "텍스트로 표현 불가능한 바이트"까지 포함해서 전부 알파벳·숫자 조합으로 바꿔주는 것이다. 받는 쪽에서 다시 디코딩하면 원본이 그대로 복원된다.

바이너리→텍스트 변환은 "모든 바이트를 문자 안전 영역으로 옮기는 번역"이라고 생각하면 된다.


Django에서 base64를 만나는 경우

Django가 내부적으로 자동 처리하는 경우

CSRF 토큰{% csrf_token %}이 생성하는 토큰 값이 base64로 처리되어 있다. 내부적으로 랜덤 바이트를 생성한 뒤 변환해서 폼 hidden 필드나 쿠키에 넣는다. 바이너리 랜덤 값을 HTML과 쿠키(텍스트 통로)에 안전하게 담기 위해서다.

세션SESSION_ENGINE이 기본 DB 백엔드일 때, 세션 딕셔너리를 직렬화한 뒤 base64로 변환해서 저장한다. django_session 테이블의 session_data 컬럼을 직접 보면 base64 문자열이 들어 있는 것을 확인할 수 있다.

비밀번호 해싱PBKDF2로 해싱된 비밀번호가 DB에 저장될 때 형태가 이렇다:

pbkdf2_sha256$600000$salt$hash값

여기서 salt와 hash 부분이 base64로 표현된 바이너리다. 해시 함수 출력은 바이너리인데 DB varchar 컬럼(텍스트)에 저장해야 하기 때문이다.

Signed Cookie / signing 모듈django.core.signing이 HMAC 서명값을 base62/base64로 변환한다. 쿠키나 URL에 들어가야 하니까 텍스트 안전 형태가 필요하다.

API 개발할 때 직접 쓰는 경우

클라이언트에서 이미지를 JSON으로 받을 때 — 모바일 앱이나 프론트엔드가 파일을 multipart/form-data 대신 JSON body에 base64로 담아 보내는 경우:

import base64
from django.core.files.base import ContentFile

def upload_profile(request):
    data = json.loads(request.body)
    # "data:image/png;base64,iVBORw0KGgo..." 형태로 올 수 있음
    image_data = data['image'].split(',')[1]  # base64 부분만 추출
    binary = base64.b64decode(image_data)
    file = ContentFile(binary, name='profile.png')
    # 이후 모델에 저장하거나 Object Storage에 업로드

API 응답으로 작은 파일을 내려줄 때 — 별도 파일 다운로드 엔드포인트를 만들기 번거로울 때, 썸네일 같은 작은 이미지를 JSON 응답에 포함시키기도 한다:

import base64

def get_thumbnail(request, pk):
    obj = MyModel.objects.get(pk=pk)
    with obj.thumbnail.open('rb') as f:
        encoded = base64.b64encode(f.read()).decode('ascii')
    return JsonResponse({
        'thumbnail': f'data:image/jpeg;base64,{encoded}'
    })

외부 API 연동에서 Basic Auth — 외부 서비스를 호출할 때:

import base64
import requests

credentials = base64.b64encode(b'api_user:api_password').decode()
response = requests.get(url, headers={
    'Authorization': f'Basic {credentials}'
})

바이너리를 캐시에 저장할 때 — Redis 캐시에 바이너리를 넣어야 할 때 base64로 변환하면 직렬화 문제를 피할 수 있다.

공통 패턴

Django에서 base64가 등장하는 모든 경우를 관통하는 공통점은, DB 컬럼(텍스트), JSON 응답(텍스트), 쿠키(텍스트), HTTP 헤더(텍스트) 같은 텍스트 전용 통로에 바이너리 값(해시, 서명, 이미지, 랜덤 토큰)을 넣어야 하는 상황이라는 것이다. 프레임워크가 내부적으로 해주는 것과 개발자가 직접 하는 것의 차이만 있을 뿐, 원리는 동일하다.

profile
智(지)! 德(덕)! 體(체)!

0개의 댓글