btoa('안녕')는 멈추는데, 받은 Base64는 풀어도 읽을 수 없다면 어디서 달라졌는지부터 나눠 보세요. Base64는 바이트를 옮기는 표현이지, 한글 인코딩을 알아서 찾아 주는 복구 프로그램이 아닙니다.
정정 — 2026년 10월 10일: 이전 글은 “같은 값을 UTF-8로 풀면 원문이 돌아온다”, “영문은 두 경로가 같다”, “URL-safe는 패딩을 뗀다”를 조건 없이 설명했습니다. 복구 가능 조건·ASCII와 Latin-1의 차이·받는 형식의 패딩 조건을 보완했습니다. 실제 화면과 측정 시간을 흉내 낸 카드 등은 본문에서 제외했습니다. 남은 표지는 AI 생성 개념 이미지이며 실제 오류 캡처나 시험 기록이 아닙니다.
먼저 구분하기: 입력 오류, 잘못 읽은 바이트, 이미 잃은 원문
| 지금 보이는 현상 | 먼저 확인할 것 | 할 수 있는 일 |
|---|---|---|
btoa('안녕')에서 오류 | 문자열을 바이트로 바꿨는가 | TextEncoder로 UTF-8 바이트를 만든 뒤 Base64 인코딩 |
atob 결과가 한글 대신 다른 문자처럼 보임 | 원본 바이트의 문자 인코딩 | 원래 UTF-8이면 바이트 배열을 UTF-8로 다시 읽기 |
| Base64는 풀리지만 UTF-8 오류 | 텍스트인지, 다른 인코딩·파일인지 | 보내는 쪽의 형식 확인. 이미지·압축 파일에는 전용 도구 사용 |
값이 잘렸거나 �로 바뀐 뒤 저장됨 | 손상 전 원본이 남아 있는가 | 원본에서 다시 받기. 없는 바이트를 Base64로 복구하지 못함 |
원래 바이트가 잘렸거나 이미 다른 문자로 덮어써졌다면, 인코딩을 반복해도 잃은 글자를 알아낼 수 없습니다. �가 원문에 실제로 포함된 문자일 수도 있으므로 모양만으로 손상 여부를 단정하지 마세요.
다섯 공개 샘플로 문제 지점 찾기
Base64 도구에서 디코딩을 선택한 뒤 아래 값을 하나씩 넣어 보세요. 모두 이 글에서 만든 비민감 테스트 값입니다.
| 입력 Base64 | 바이트의 의미 | 이 UTF-8 텍스트 도구의 결과 |
|---|---|---|
SGVsbG8= | ASCII Hello | Hello |
7JWI64WV | UTF-8 안녕 | 안녕 |
Y2Fmw6k= | UTF-8 café | café |
Y2Fm6Q== | Latin-1 café의 바이트 | UTF-8 텍스트가 아니라는 오류 |
77u/ | UTF-8의 U+FEFF 한 문자 | 화면에서는 비어 보일 수 있지만 BOM 보존 안내와 1 코드 단위 |
btoa는 각 값을 0~255의 바이트로 취급합니다. 한글 안(U+C548)은 그 범위를 넘으므로 오류가 납니다. ASCII인 Hello는 직접 호출과 UTF-8 경유가 같지만, café는 다릅니다. btoa('café')의 Y2Fm6Q==는 UTF-8이 아닙니다. 악센트가 있는 문자까지 “영문이면 괜찮다”고 묶으면 놓치는 경우입니다. MDN의 btoa 설명에서도 문자와 바이트 변환을 구분합니다.
atob는 바이트 값이 들어 있는 문자열을 돌려줍니다. 이것을 바로 읽을 텍스트로 쓰지 말고, 원래 인코딩에 맞는 decoder를 거쳐야 합니다. PiPi 도구는 UTF-8 텍스트만 읽으며 잘못된 UTF-8을 대체 문자로 조용히 바꾸지 않습니다. 다른 문자 인코딩을 자동 추측하는 기능은 없습니다.
작은 브라우저 예제: 한글과 이모지 왕복 확인
다음 코드는 정상적인 짧은 샘플 문자열과 패딩 있는 표준 Base64를 위한 예제입니다. URL-safe 입력 정규화나 파일 복구 코드는 아닙니다.
const text = "안녕하세요 🌏";
const bytes = new TextEncoder().encode(text);
const base64 = btoa(
Array.from(bytes, byte => String.fromCharCode(byte)).join("")
);
const decodedBytes = Uint8Array.from(atob(base64), char => char.charCodeAt(0));
const restored = new TextDecoder("utf-8", {
fatal: true,
ignoreBOM: true
}).decode(decodedBytes);
console.log(base64); // 7JWI64WV7ZWY7IS47JqUIPCfjI8=
console.log(restored === text); // true
이 샘플은 JavaScript 문자열 길이로는 8 코드 단위, UTF-8로는 20바이트 → 28자의 Base64입니다. 화면의 글자수·바이트 수·인코딩 출력 길이는 서로 다른 값입니다.
fatal: true는 잘못된 UTF-8을 오류로 알립니다. ignoreBOM: true는 이름과 달리 시작의 U+FEFF를 출력에 포함합니다. 77u/를 도구에서 풀면 보이지 않아도 복사할 결과에는 그 문자가 남습니다. 자동 BOM 삭제 도구가 아닙니다. Encoding Standard, MDN ignoreBOM.
TextEncoder는 짝 없는 UTF-16 surrogate를 대체 문자로 바꿀 수 있습니다. 이 글의 코드는 그런 입력을 위한 일반 변환기가 아니며, PiPi 도구는 짝 없는 입력을 결과 없이 거절합니다. 원문 손상과 변환 성공을 혼동하지 마세요.
URL-safe와 패딩: 받는 API가 원하는 형식 확인
base64url은 표준 Base64의 +, / 대신 -, _를 쓰는 알파벳입니다. 패딩 생략은 받는 형식의 규칙을 따라야 합니다. “URL-safe니까 =를 지우면 어느 API에서나 된다”는 뜻은 아닙니다. RFC 4648 §3.2·§5.
이 도구의 URL-safe 옵션은 패딩을 생략합니다. 위 샘플은 7JWI64WV7ZWY7IS47JqUIPCfjI8이 됩니다. Decode에서는 URL-safe 알파벳과 빠진 패딩을 정규화하지만, 이 도구가 받아들였다는 이유로 다른 API의 규칙까지 만족한다고 판단하지 마세요. 도구는 Base64 입력의 공백·줄바꿈을 제거하며, 디코딩된 텍스트의 공백과 줄바꿈을 지우는 것은 아닙니다.
JWT의 header·payload 조각을 읽는 것은 서명 검증이나 신뢰성 판정이 아닙니다. 공개 테스트 값만 JWT 디코더에서 살펴보세요. URL에 값을 넣는 문맥과 퍼센트 인코딩은 URL 인코딩 가이드에서 따로 확인할 수 있습니다.
“약 33% 증가”는 글자수가 아닌 바이트 기준
패딩 있는 표준 Base64의 출력 길이는 4 × ceil(입력 바이트 수 / 3) 입니다. ceil은 올림입니다. 긴 입력은 대략 4/3배가 되지만 짧은 입력에는 패딩 비중이 큽니다.
Hello: 5바이트 → 8자, 원래보다 60% 증가.안녕: 6바이트 → 8자, 약 33% 증가. 한글 두 글자를 두 바이트로 세면 안 됩니다.안녕하세요 🌏: 20바이트 → 28자, 40% 증가.
Base64는 암호화가 아닙니다. 운영 토큰·비밀번호·비공개 JSON·개인정보를 넣지 마세요. 변환 자체가 브라우저에서 실행된다는 설명은 페이지 전체나 브라우저 환경의 비밀 보호를 보증하지 않습니다. 비민감 샘플을 쓰고, 비밀은 승인된 로컬 작업 절차를 따르세요.