개발자 도구9 분 읽기|MJ민재

이미지가 안 뜨는데 알고 보니 Base64 문제였던 썰 — 인코딩 완전 정복

이메일 템플릿 이미지가 모든 클라이언트에서 깨져서 두 시간 삽질했는데, 원인이 Base64 변형 하나를 잘못 쓴 거였어요. 이 글에서는 Base64가 비트 레벨에서 어떻게 동작하는지, 언제 쓰면 좋고 언제 쓰면 오히려 안 좋은지, URL-safe 변형이 왜 필요한지, 크기 오버헤드 계산법까지 전부 정리했어요.

이 글에서 알 수 있는 것

  • 3바이트 입력 → 4문자 출력 알고리즘, Base64가 비트 레벨에서 어떻게 동작하는지
  • 언제 쓰면 좋고 언제 쓰면 오히려 성능이 나빠지는지 — 크기 오버헤드 실제 수치로
  • 표준 Base64 vs URL-safe Base64: JWT, 쿼리 파라미터, 파일명에서 왜 다른 버전이 필요한지

이메일 템플릿 작업을 하다가 이미지가 모든 클라이언트에서 깨져 보이는 문제가 생겼어요. img 태그는 멀쩡해 보이고, URL도 이상 없어 보이고, HTML 구조도 문제없었어요. 서버도 확인하고, CDN도 확인하고, HTML도 세 번 읽었는데 원인을 못 찾겠는 거예요. 두 시간쯤 지나서 src 값을 디코더에 붙여넣어 봤더니 바로 보였어요. 표준 Base64 문자열을 URL 컨텍스트에 그냥 넣은 거였어요. + 기호가 공백으로, / 기호가 경로 구분자로 해석되면서 데이터가 조용히 뭉개졌던 거죠. 두 시간, 문자 하나. 그날 Base64를 제대로 공부했어요.

Base64가 뭔지, 그리고 뭐가 아닌지

Base64는 바이너리 데이터를 텍스트로 바꾸는 인코딩이에요. 이미지, 오디오, PDF 같은 바이트 스트림을 64개 출력 가능한 ASCII 문자로 표현해요. 그 목적이 전부예요. 텍스트만 처리할 수 있는 시스템 — 이메일, URL, HTML 속성, JSON 문자열 — 에서 바이너리 데이터를 안전하게 주고받기 위한 거예요.

64개 문자는 이렇게 구성돼요: A-Z (26개), a-z (26개), 0-9 (10개), + 와 / (2개). 합쳐서 64. 패딩에는 = 를 써요. 어떤 바이너리 데이터든 이 문자들만으로 표현할 수 있어요.

원본 텍스트:   안녕 (UTF-8)
바이트:        EC 95 88 EB 85 95
Base64 출력:   7JWI65WV

원본 텍스트:   Hello
Base64 출력:   SGVsbG8=

원본 JSON:     {"id": 1}
Base64 출력:   eyJpZCI6IDF9

지금 이 도구를 사용해 보세요:

Base64 인코더/디코더

알고리즘이 실제로 어떻게 동작하나

Base64는 입력을 3바이트(24비트) 단위로 처리해요. 24비트를 6비트씩 4개로 잘라서 각각 64개 문자 중 하나로 매핑하는 거예요. 그래서 입력 3바이트가 항상 출력 4문자가 되는 거고요.

'Man' 인코딩 (3바이트: 77, 97, 110):

바이너리: 01001101 01100001 01101110
         └──────────────────────┘
               24비트 합계

6비트씩 4개로 분리:
 010011 | 010110 | 000101 | 101110
   19   |   22   |    5   |   46
    T   |    W   |    F   |    u

결과: 'TWFu'

패딩 예시 — 'Ma' 인코딩 (2바이트):
바이너리: 01001101 01100001 [없는 바이트 → 0으로 채움]
 010011 | 010110 | 000100 | (패딩)
    T   |    W   |    E   |    =
결과: 'TWE='

패딩 예시 — 'M' 인코딩 (1바이트):
바이너리: 01001101 [없는 2바이트 → 0으로 채움]
 010011 | 010000 | (패딩) | (패딩)
    T   |    Q   |    =   |    =
결과: 'TQ=='

끝에 붙는 = 기호는 디코더한테 실제 데이터 바이트가 몇 개인지 알려줘요. = 하나면 마지막 그룹이 2바이트, == 이면 1바이트였다는 뜻이에요. = 없으면 입력이 3의 배수였다는 거고요.

언제 쓰면 좋고 언제 쓰면 손해인가

Base64는 도구예요. 무조건 좋거나 무조건 나쁜 게 아니라 상황에 따라 달라요. 명확하게 정리했어요:

용도Base64?이유
HTML/CSS 안의 작은 아이콘 (data URI)써도 좋음HTTP 요청 하나를 아낄 수 있어서, 33% 오버헤드보다 이득이 커요
JWT 토큰 헤더 + 페이로드필수JWT 명세에서 Base64url 인코딩을 요구해요. 선택의 여지가 없어요
이메일 첨부 파일 (MIME)필수SMTP는 7비트 ASCII만 전송해요. 바이너리를 안전하게 보내려면 Base64가 필요해요
JSON API 응답 안의 이미지작은 것만썸네일 5KB 이하는 괜찮아요. 그 이상이면 URL 참조로 처리하세요
웹 페이지의 큰 이미지쓰면 안 됨33% 오버헤드 + 브라우저 파싱 블록. CDN URL로 서빙하는 게 맞아요
설정에 비밀번호나 API 키 숨기기절대 안 됨Base64는 암호화가 아니에요. 10초면 디코딩돼요
URL 쿼리 파라미터URL-safe 변형만표준 Base64 (+, /) 는 URL에서 깨져요. -, _ 쓰는 URL-safe 버전 써야 해요
💡

data URI 쓸 때 5KB 기준

이미지가 5KB 이하면 Base64 data URI로 인라인하면 네트워크 요청 하나를 줄일 수 있어요. 5KB 넘으면 오버헤드가 더 커서 별도 파일로 CDN에서 서빙하는 게 나아요. SVG는 Base64보다 원본 마크업을 인라인으로 넣는 게 대부분 더 좋아요.

표준 Base64 vs URL-safe Base64

표준 Base64는 알파벳에 + 와 / 를 써요. 이 두 문자가 URL, HTML 폼, 쿼리 파라미터에서 특수하게 해석돼서 데이터가 조용히 깨지는 거예요. 제가 두 시간을 날린 이유예요.

표준 Base64 알파벳:
A-Z a-z 0-9 + /  (= 패딩)

URL-safe Base64 알파벳 (RFC 4648 §5):
A-Z a-z 0-9 - _  (= 패딩, 생략 가능)

같은 데이터, 다른 인코딩:
표준:      SGVsbG8+V29ybGQ=  ← + 기호가 URL에서 깨져요
URL-safe:  SGVsbG8-V29ybGQ=  ← 어디서나 안전해요

URL-safe를 써야 하는 경우:
- JWT 토큰 (헤더.페이로드.서명 전부 URL-safe)
- 쿼리 파라미터 (?token=SGVsbG8-V29ybGQ)
- 파일명이나 URL 경로 구성 요소
- HTML 속성 값에 들어가는 경우

표준 Base64로 충분한 경우:
- 이메일 MIME 첨부 파일
- HTTP 응답 바디 (URL 아닌 경우)
- CSS/HTML 안의 data URI
⚠️

Base64는 암호화가 아니에요 — 비밀번호 숨기려고 쓰면 안 돼요

Base64 결과물이 영문 대소문자 숫자로 뒤섞여 있어서 암호화된 것처럼 보이는데, 전혀 아니에요. 누구나 10초 만에 디코딩할 수 있어요. API 키, 비밀번호, 개인정보를 Base64로 인코딩해서 '안전하다'고 생각하는 건 보안 취약점이에요. 데이터를 보호해야 하면 실제 암호화를 쓰세요. 대칭 암호화는 AES-256, 비밀번호 해싱은 bcrypt나 argon2예요. Base64는 전송 포맷이에요. 보안 도구가 아니에요.

크기 오버헤드 계산

33% 오버헤드라는 말을 정확히 이해하면 의사결정이 쉬워져요. 입력 3바이트 → 출력 4문자, 각 문자는 1바이트. 비율이 4/3 = 1.333이니까 항상 정확히 약 33% 커지는 거예요.

크기 오버헤드 실제 계산:

원본 크기     Base64 크기    오버헤드
1 KB          1,368 B        +344 B (+33.6%)
10 KB         13,336 B       +3,336 B (+33.4%)
100 KB        133,400 B      +33,400 B (+33.4%)
1 MB          1.33 MB        +334 KB (+33.4%)

실제 예시 — 200x200 PNG 아이콘:
원본 파일:      ~15 KB
Base64 변환:    ~20 KB
추가 용량:      ~5 KB
아낀 HTTP 요청: 1회
빠른 네트워크 왕복 시간: 20~50ms
결론: 인라인하는 게 이득. 5KB < 요청 1회.

실제 예시 — 히어로 이미지:
원본 파일:      ~250 KB
Base64 변환:    ~333 KB
추가 용량:      ~83 KB
아낀 HTTP 요청: 1회
결론: 인라인하면 손해. 83KB 다운로드 > 캐시된 요청 1회.

자주 묻는 질문

자주 묻는 질문

자바스크립트에서 Base64 인코딩/디코딩 어떻게 해요?

브라우저에서는 btoa()로 인코딩, atob()으로 디코딩해요. 이건 표준 Base64만 지원해요. URL-safe 버전이 필요하면 인코딩 후 + 를 - 로, / 를 _ 로 바꾸면 돼요. 이미지나 파일 같은 실제 바이너리 데이터는 btoa()가 ASCII 문자열만 제대로 처리하기 때문에 Uint8Array나 FileReader를 써야 해요. Node.js에서는 Buffer.from(string).toString('base64')로 인코딩하고 Buffer.from(base64string, 'base64').toString('utf8')로 디코딩해요.

Base64 문자열 끝에 == 이 붙는 건 왜예요?

패딩이에요. Base64는 3바이트 단위로 처리하는데, 대부분의 입력 길이는 3의 배수가 아니에요. = 하나는 마지막 그룹에서 1바이트가 부족해서 채운 것, == 는 2바이트가 부족했다는 뜻이에요. = 없으면 입력 길이가 3의 배수였다는 거고요. 디코더가 원본 데이터 길이를 정확히 알 수 있게 해주는 장치예요.

Base64랑 hex 인코딩이랑 뭐가 달라요?

둘 다 바이너리를 텍스트로 변환하는데, 알파벳 크기와 크기 오버헤드가 달라요. hex는 16개 문자를 써서 1바이트를 2자리 hex로 표현해요. 크기가 2배로 늘어나요. Base64는 64개 문자로 3바이트를 4문자로 표현해서 33% 늘어나요. Base64가 더 효율적이에요. hex는 체크섬, 메모리 주소, 색상 코드처럼 사람이 읽을 저수준 바이너리 표현에 쓰고, Base64는 데이터 전송에 써요.

이미지, PDF 같은 파일도 Base64로 인코딩할 수 있나요?

어떤 바이너리 파일이든 가능해요. Base64는 바이트 값이 뭘 나타내는지 신경 안 써요. 그냥 바이트를 64개 문자로 변환하는 거니까요. 이메일에 어떤 파일이든 첨부할 수 있는 이유가 이거예요. MIME 프로토콜이 원본 바이트를 Base64로 인코딩해서 텍스트로 보내고, 받는 쪽 클라이언트가 디코딩해서 원본 파일로 복원하는 거예요.

JWT가 왜 Base64를 쓰나요? 암호화 아닌가요?

JWT는 Base64url, 즉 URL-safe Base64를 써요. 이유가 두 가지예요. 첫째, JWT 토큰은 HTTP 헤더, URL, 쿠키 안에서 이동하는데 전부 텍스트 전용 환경이에요. Base64가 바이너리 서명과 JSON 페이로드를 그 환경에서 안전하게 만들어줘요. 둘째, 토큰이 작아야 해요. hex 인코딩은 크기가 2배라서 안 맞아요. 중요한 건, JWT 페이로드는 인코딩만 된 거지 암호화가 된 게 아니에요. 누구나 디코딩해서 읽을 수 있어요. 서명은 변조를 감지해주는 거지 내용을 숨기는 게 아니에요.

Base64 인코더/디코더

텍스트나 파일을 Base64로 인코딩하거나 디코딩하세요 — 표준, URL-safe 버전 모두 지원

지금 바로 인코딩/디코딩

이 글에서 다룬 도구 바로 사용하기

MJ

민재

개발자 겸 테크 라이터. 개발 도구와 파일 변환 기술을 깊이 있게 다룹니다.

이 글이 도움이 되셨나요? 새 가이드 알림 받기

스팸 없이, 새 소식만 보내드립니다. 언제든 취소 가능. · 구독 시 개인정보처리방침에 동의합니다.

이런 글도 좋아하실 수 있어요

84+

제공 도구

100+

블로그 글

English & 한국어

지원 언어

이 페이지를 즐겨찾기하세요! 매주 새로운 무료 도구가 추가됩니다.