가이드8분 분량

퍼센트 인코딩이란? (RFC 3986 쉽게 설명하기)

퍼센트 인코딩은 RFC 3986에 정의된 메커니즘으로, URI의 특수 문자를 퍼센트 기호(%)와 두 자리 16진수로 치환하여 인코딩합니다. 이를 통해 URL이 인터넷에서 안전하게 전송될 수 있는 유효한 ASCII 문자만 포함하도록 보장합니다.

퍼센트 인코딩이란?

URL 인코딩이라고도 불리는 퍼센트 인코딩은 URI(Uniform Resource Identifier)에서 허용되지 않거나 특별한 의미를 가지는 문자를 표현하는 표준 방식입니다. 인코딩이 필요한 각 문자를 퍼센트 기호(%)와 해당 문자의 바이트 값을 나타내는 두 자리 16진수로 치환하는 방식으로 동작합니다.

예를 들어, 공백 문자(바이트 값 0x20)는 %20으로 인코딩됩니다. 앳 기호 @(바이트 값 0x40)는 %40으로 인코딩됩니다. 이 메커니즘은 어떤 데이터든 URI 구문과 충돌하지 않고 URI에 안전하게 삽입될 수 있도록 보장합니다.

"퍼센트 인코딩"이라는 용어는 퍼센트 문자를 이스케이프 접두사로 사용하는 데서 유래했습니다. 공식 명세는 2005년 1월 국제 인터넷 표준화 기구(IETF)가 발표한 RFC 3986에 정의되어 있으며, Tim Berners-Lee, Roy Fielding, Larry Masinter가 작성했습니다.

퍼센트 인코딩의 동작 방식

인코딩 과정은 다음 단계를 따릅니다. 먼저, 문자를 문자 인코딩(현대 웹에서는 거의 항상 UTF-8)을 사용하여 바이트 표현으로 변환합니다. 그런 다음, 각 바이트를 퍼센트 기호와 두 자리 16진수로 표현합니다(관례상 대문자 A-F를 사용하지만, 디코더는 소문자도 허용해야 합니다).

// 공백 문자의 단계별 인코딩
// 문자: ' ' (공백)
// ASCII/UTF-8 바이트 값: 0x20 (10진수 32)
// 퍼센트 인코딩: %20

// 멀티바이트 문자의 단계별 인코딩: 예음 부호가 있는 e
// 문자: 예음 부호가 있는 e (U+00E9)
// UTF-8 바이트: 0xC3 0xA9
// 퍼센트 인코딩: %C3%A9

// 3바이트 문자의 단계별 인코딩
// 문자: 한중일 표의문자 (U+4E2D)
// UTF-8 바이트: 0xE4 0xB8 0xAD
// 퍼센트 인코딩: %E4%B8%AD

16진수는 반드시 두 자리씩 짝을 이루어야 합니다. 단독으로 쓰인 퍼센트 기호나 16진수가 아닌 문자가 뒤따르는 퍼센트 기호는 유효하지 않으며 디코딩 오류를 일으킵니다. 이는 JavaScript에서 "URI malformed" 오류가 발생하는 흔한 원인입니다.

퍼센트 인코딩은 항상 되돌릴 수 있습니다. 디코더는 퍼센트 기호를 읽고, 뒤따르는 두 문자를 16진수 바이트 값으로 취한 다음, 이를 원래 문자로 다시 변환합니다. 멀티바이트 UTF-8 문자의 경우, 연속된 인코딩 바이트들이 결합되어 함께 디코딩됩니다.

어떤 문자를 인코딩해야 하는가?

모든 문자가 퍼센트 인코딩을 필요로 하는 것은 아닙니다. RFC 3986은 두 가지 범주를 정의합니다. 인코딩이 절대 필요 없는 예약되지 않은(unreserved) 문자와, 예약된 용도로 사용되지 않을 때 인코딩해야 하는 예약된(reserved) 문자입니다. 그 밖의 모든 문자(공백, 비ASCII 문자, 제어 문자 포함)는 항상 인코딩해야 합니다.

인코딩이 필요 없는 문자(unreserved): 대문자(A-Z), 소문자(a-z), 숫자(0-9), 하이픈(-), 마침표(.), 밑줄(_), 물결표(~). 이 66개 문자는 인코딩 없이 URI의 어디에나 나타날 수 있습니다.

때때로 인코딩이 필요한 문자(reserved): : / ? # [ ] @ ! $ & ' ( ) * + , ; =. 이 문자들은 URI에서 특별한 구문적 의미를 가집니다. 구분자가 아닌 데이터로 사용될 때만 인코딩이 필요합니다.

항상 인코딩이 필요한 문자: 공백, 비ASCII 문자(127을 초과하는 모든 문자), 제어 문자(0-31 및 127), 그리고 < > { } | \ ^ ` " 같은 안전하지 않은 문자.

RFC 3986의 예약 문자 vs 예약되지 않은 문자

예약 문자와 예약되지 않은 문자의 구분은 URI가 동작하는 방식의 근간입니다. 예약 문자는 URI의 구조를 정의하는 구분자 역할을 합니다. 예를 들어, ://는 스킴과 오소리티(authority)를 구분하고, /는 경로 세그먼트를 구분하며, ?는 쿼리 문자열을 시작하고, #은 프래그먼트를 시작합니다.

범주 문자 인코딩 시점
예약되지 않음 A-Z a-z 0-9 - . _ ~ 절대 안 함
일반 구분자 : / ? # [ ] @ 구분자가 아닌 데이터로 사용될 때
하위 구분자 ! $ & ' ( ) * + , ; = 의미를 부여하는 구성 요소에서 데이터로 사용될 때
그 밖의 모든 문자 공백, 비ASCII, 제어 문자 등 항상

RFC 3986의 핵심 원칙 중 하나는, 예약 문자가 퍼센트 인코딩되어 있는지 아니면 그대로 나타나는지의 차이만 있는 URI는 서로 동등하지 않다는 것입니다. 예를 들어, /path/to/path%2Fto%2F/로 디코딩됨에도 불구하고 서로 다른 URI입니다. 전자는 두 개의 경로 세그먼트를 가지며, 후자는 리터럴 슬래시를 포함하는 하나의 경로 세그먼트를 가집니다.

퍼센트 인코딩 vs URL 인코딩: 같은 것인가?

"퍼센트 인코딩"과 "URL 인코딩"은 종종 혼용되며, 대부분의 맥락에서 같은 것을 의미합니다. 하지만 미묘한 역사적 구분이 있습니다. "URL 인코딩"은 때때로 HTML 폼에서 사용되는 더 오래된 application/x-www-form-urlencoded 형식을 가리킬 수 있는데, 이 형식은 공백을 %20 대신 +로 인코딩합니다.

application/x-www-form-urlencoded 형식은 HTML 명세에서 정의되었으며 RFC 3986보다 앞섭니다. 이 형식은 약간 다른 규칙을 가집니다. 공백은 +가 되고, 인코딩되지 않는 문자 집합도 조금 다릅니다. 현대적으로 "URL 인코딩"이라는 용어는 거의 항상 공백이 %20이 되는 RFC 3986 퍼센트 인코딩을 가리킵니다.

실무에서는 모든 URI 맥락(경로 세그먼트, REST API의 쿼리 매개변수, 프래그먼트 식별자)에서 RFC 3986 퍼센트 인코딩을 사용해야 합니다. application/x-www-form-urlencoded 형식은 HTML 폼 제출을 다루거나 특정 API가 요구하는 경우에만 사용하세요.

// RFC 3986 퍼센트 인코딩 (URI에 권장)
encodeURIComponent('hello world')  // "hello%20world"

// application/x-www-form-urlencoded (HTML 폼)
new URLSearchParams({q: 'hello world'}).toString()  // "q=hello+world"

// Python 동등 코드
from urllib.parse import quote, quote_plus
quote('hello world', safe='')   # "hello%20world"
quote_plus('hello world')       # "hello+world"

관련 글

무료 도구 사용해 보기