텍스트 인코딩이 존재하는 이유
컴퓨터는 데이터를 바이트로 전송하지만, 모든 바이트값이 모든 맥락에서 안전한 건 아닙니다. URL에는 공백이 들어갈 수 없고, HTML은 꺾쇠괄호를 태그로 해석하고, 이메일 시스템은 바이너리 데이터를 손상시킬 수 있습니다. 텍스트 인코딩은 안전하지 않은 문자를 안전한 표현으로 변환해서, 나중에 원본으로 되돌릴 수 있게 합니다.
웹 개발자에게 이 인코딩들은 선택이 아닌 필수입니다. 잘못된 인코딩은 깨진 링크, 글자 깨짐, 보안 취약점(XSS 공격), 프로덕션에서야 발견되는 데이터 손상의 원인이 됩니다.
Base64 인코딩
하는 일
Base64는 바이너리 데이터를 64개의 ASCII 문자(A-Z, a-z, 0-9, +, /)로 이루어진 문자열로 변환합니다. 입력 3바이트가 Base64 4문자가 되므로, 인코딩된 출력은 원본보다 약 33% 커집니다.
사용 시점
텍스트만 허용되는 맥락에 바이너리 데이터를 넣어야 할 때 Base64를 사용합니다.
- data URI로 HTML이나 CSS에 이미지 직접 삽입
- 이메일의 바이너리 첨부 파일 전송(MIME)
- 텍스트만 지원하는 JSON에 바이너리 데이터 저장
- 텍스트 기반 API를 통한 바이너리 데이터 전달
작동 원리
입력 바이트를 3개씩(24비트) 묶어 6비트 4그룹으로 나누고, 각 그룹을 64개 문자 중 하나에 매핑합니다. 입력 길이가 3의 배수가 아니면 패딩 문자(=)를 추가합니다.
예를 들어 "Hi"(2바이트)는 Base64로 "SGk="가 됩니다. "="는 2바이트가 3바이트 그룹을 채우지 못해서 추가된 패딩입니다.
흔한 실수
Base64는 인코딩이지 암호화가 아닙니다. 보안은 전혀 제공하지 않으며 누구나 즉시 디코딩할 수 있습니다. 민감한 데이터를 "숨기는" 용도로 쓰면 안 됩니다.
Base64는 데이터 크기를 약 33% 늘립니다. 큰 파일에서는 이 오버헤드가 무시 못 할 수준이 됩니다.
URL에서 Base64 데이터를 사용할 때는 URL-safe 변형("+"를 "-"로, "/"를 "_"로 대체)을 사용하세요.
URL 인코딩 (퍼센트 인코딩)
하는 일
URL 인코딩은 안전하지 않은 문자를 퍼센트 기호와 16진수 두 자리로 대체합니다. 공백은 %20, 앰퍼샌드는 %26, 슬래시는 %2F가 됩니다.
사용 시점
URL에 넣는 모든 데이터는 적절히 인코딩해야 합니다.
- 쿼리 파라미터 값: `?search=hello%20world`
- 특수 문자가 포함된 경로 세그먼트
- GET 요청으로 전송되는 폼 데이터
- URL의 일부가 되는 모든 사용자 입력
예약 문자와 비예약 문자
URL 구문은 특정 문자를 구조적 용도로 예약합니다. 앰퍼샌드(&)는 쿼리 파라미터를 구분하고, 등호(=)는 키와 값을 구분하고, 물음표(?)는 쿼리 문자열을 시작합니다. 이 문자들이 구조가 아니라 데이터로 나타날 때는 인코딩해야 합니다.
비예약 문자 — 문자, 숫자, 하이픈, 밑줄, 마침표, 틸드 — 는 인코딩이 필요 없습니다.
이중 인코딩
흔한 실수 중 하나가 이미 인코딩된 데이터를 다시 인코딩하는 것입니다. %20이 %2520이 됩니다. 인코딩 함수를 두 번 적용하거나, 프레임워크가 자동 인코딩하는 데이터를 수동으로도 인코딩했을 때 발생합니다.
어떤 계층이 인코딩을 담당하는지 파악하고, 정확히 한 번만 인코딩하세요.
HTML 엔티티
하는 일
HTML 엔티티 인코딩은 HTML에서 특별한 의미를 가진 문자를 이름 또는 숫자 참조로 대체합니다. <는 `<`, >는 `>`, &는 `&`, "는 `"`가 됩니다.
사용 시점
신뢰할 수 없는 텍스트를 HTML 문서에 삽입할 때 HTML 인코딩은 필수입니다. 인코딩 없이 꺾쇠괄호가 포함된 사용자 입력을 넣으면 HTML 태그로 해석되어 XSS 공격으로 이어질 수 있습니다.
- 사용자 생성 콘텐츠를 웹 페이지에 표시할 때
- HTML 속성에 동적 값을 삽입할 때
- 문서나 튜토리얼에서 코드 조각을 보여줄 때
- 애플리케이션 외부에서 온 모든 텍스트
이름 엔티티 vs 숫자 엔티티
HTML은 이름 엔티티(`&`, `<`, `©`)와 숫자 엔티티(`&`, `<`, `©`)를 모두 지원합니다. 이름 엔티티는 읽기 쉽지만 정해진 집합으로 제한됩니다. 숫자 엔티티는 10진수(`€`)나 16진수(`€`) 표기로 모든 유니코드 문자를 표현할 수 있습니다.
인코딩 결합
실제 데이터는 여러 인코딩 계층을 거치는 경우가 많습니다. 사용자가 앰퍼샌드가 포함된 검색어를 입력합니다. 브라우저가 HTTP 요청을 위해 URL 인코딩합니다. 서버가 디코딩한 후 HTML 응답에 HTML 엔티티 인코딩으로 포함합니다. JSON API 호출이 들어 있다면 JSON 이스케이프도 추가됩니다.
각 인코딩 계층은 올바른 순서로 적용되고 제거되어야 합니다. 순서가 섞이거나 한 계층의 디코딩을 빠뜨리면, 디버깅하기 어려운 깨진 출력이 만들어집니다.
보안 함의
인코딩은 인젝션 공격에 대한 첫 번째 방어선입니다. SQL 인젝션, XSS, 명령어 인젝션 모두 데이터가 코드로 해석되는 상황을 악용합니다. 올바른 인코딩은 데이터가 어떤 문자를 포함하든 데이터로만 남게 보장합니다.
맥락이 중요합니다. HTML 인코딩은 HTML 맥락에서 XSS를 방지하지만 JavaScript 맥락에서는 안 됩니다. URL 인코딩은 URL을 보호하지만 HTML 속성은 아닙니다. 데이터가 사용될 특정 맥락에 맞는 인코딩을 항상 적용하세요.
실용 워크플로
텍스트가 맥락 경계를 넘을 때는 이 체크리스트를 따르세요.
1. 대상 맥락을 파악한다 (URL, HTML, JSON, SQL) 2. 해당 맥락에서 특별한 문자가 무엇인지 확인한다 3. 경계에서 적절한 인코딩을 정확히 한 번 적용한다 4. 원시 텍스트 맥락으로 돌아갈 때만 디코딩한다 5. "이미 인코딩되어 있겠지"를 믿지 말고 경계에서 검증하거나 다시 인코딩한다
신뢰할 수 있는 인코딩/디코딩 도구를 가까이 두면 디버깅 시간을 줄이고 보안 취약점을 예방할 수 있습니다.