URL에 인코딩이 필요한 이유
URL은 문자로 구성되어 있지만, 모든 문자를 직접 "안전하게" 사용할 수 있는 것은 아닙니다. URL은 인터넷을 통해 다양한 시스템을 거쳐 전송되는데, 이 시스템들은 특정 문자를 특별한 의미로 해석할 수 있습니다. 예를 들어 ?는 쿼리 문자열, &는 파라미터 구분자, #는 프래그먼트 식별자로 사용됩니다. 또한 중국어나 아랍어 같은 비ASCII 문자도 있으며, 오래된 HTTP 인프라에서는 일부 문자가 깨질 수 있습니다.
URL 인코딩(공식 명칭: 퍼센트 인코딩)은 안전하지 않은 문자를 %와 해당 문자의 바이트 값을 나타내는 두 자리 16진수로 대체하여 이 문제를 해결합니다.
공백 (0x20) → %20
골뱅이 기호 (0x40) → %40
슬래시 (0x2F) → %2F
RFC 표준
URL 인코딩은 RFC 3986(Uniform Resource Identifiers)에 정의되어 있습니다. 이 표준에 따르면:
비예약 문자 — 인코딩 없이 URL 어디서나 안전하게 사용할 수 있는 문자:
- A–Z, a–z, 0–9
-,_,.,~
예약 문자 — URI에서 특별한 의미를 가지며, 데이터로 사용할 때는 퍼센트 인코딩해야 하는 문자:
: / ? # [ ] @ ! $ & ' ( ) * + , ; =
그 외 모든 문자——공백, 비ASCII 문자, ", <, >, {, } 등——은 퍼센트 인코딩해야 합니다.
퍼센트 인코딩 실습
공백 인코딩
공백 문자(U+0020)는 %20으로 인코딩됩니다. 쿼리 문자열에서는 %20 대신 +를 사용하는 경우도 있습니다. 이는 HTML 폼 URL 인코딩(application/x-www-form-urlencoded) 표준에서 유래한 것으로, 공백을 +로 인코딩합니다. 두 가지는 서로 다릅니다:
%20— RFC 3986 퍼센트 인코딩(경로, 헤더의 공백)+— HTML 폼 인코딩(쿼리 문자열의 공백)
디코딩 시에는 어떤 방식이 적용되는지 반드시 파악하여 버그를 방지하세요.
비ASCII 문자
비ASCII 문자는 먼저 UTF-8로 인코딩된 후, 각 바이트가 퍼센트 인코딩됩니다:
한자 中 (U+4E2D)
UTF-8 바이트: 0xE4 0xB8 0xAD
퍼센트 인코딩: %E4%B8%AD
한국어 단어 안녕도 동일한 방식으로 UTF-8 바이트로 변환된 후 퍼센트 인코딩됩니다.
JavaScript 인코딩 함수
JavaScript는 URL 인코딩을 위한 여러 내장 함수를 제공합니다:
encodeURI()
전체 URI를 인코딩합니다——URL 구조를 유지하면서 전체 URL을 인코딩하도록 설계되었습니다. URI 구문의 일부인 문자(; , / ? : @ & = + $ #)는 인코딩하지 않습니다.
encodeURI("https://example.com/search?q=hello world&lang=한국어")
// "https://example.com/search?q=hello%20world&lang=%ED%95%9C%EA%B5%AD%EC%96%B4"
encodeURIComponent()
URI 컴포넌트를 인코딩합니다——쿼리 파라미터 값 같은 개별 값을 인코딩하도록 설계되었습니다. A–Z a–z 0–9 - _ . ! ~ * ' ( ) 외의 모든 것을 인코딩합니다.
encodeURIComponent("hello world & more")
// "hello%20world%20%26%20more"
encodeURIComponent("https://example.com")
// "https%3A%2F%2Fexample.com"
어떤 것을 사용해야 할까요
| 시나리오 | 함수 |
|---|---|
| 전체 URL 인코딩 | encodeURI() |
| 쿼리 파라미터 값 인코딩 | encodeURIComponent() |
| 경로 세그먼트 인코딩 | encodeURIComponent() |
| 다른 URL에 포함할 URL 인코딩 | encodeURIComponent() |
대응하는 디코딩 함수
decodeURI(encodedURI)
decodeURIComponent(encodedComponent)
더 이상 사용되지 않는 escape()와 unescape() 함수는 절대 사용하지 마세요——비ASCII 문자를 다르게 처리하여 잘못된 결과를 만들어냅니다.
흔한 함정과 주의사항
이중 인코딩
이미 인코딩된 문자열을 다시 인코딩하는 것은 흔한 버그입니다:
encodeURIComponent(encodeURIComponent("hello world"))
// "hello%2520world"
// %25는 %의 인코딩이므로 %20이 %2520이 됩니다
인코딩하기 전에 값이 이미 인코딩되어 있는지 반드시 확인하세요.
+ 대 %20 함정
+를 공백으로 사용하는 쿼리 문자열을 decodeURIComponent로 디코딩하면, +는 공백으로 디코딩되지 않습니다——먼저 +를 %20으로 교체하거나 URLSearchParams API를 사용해야 합니다:
new URLSearchParams("q=hello+world").get("q")
// "hello world" ✓
decodeURIComponent("hello+world")
// "hello+world" ✗ — 여전히 리터럴 더하기 기호
프래그먼트 식별자
URL의 # 문자는 프래그먼트 식별자(페이지 내 앵커)의 시작을 나타냅니다. 데이터에 #가 포함된 경우 %23으로 인코딩해야 합니다. 그렇지 않으면 브라우저는 그 이후의 모든 것을 프래그먼트로 처리합니다.
국제화 도메인 이름(IDN)
비ASCII 문자를 포함한 도메인 이름(예: bücher.de)은 퍼센트 인코딩이 아닌 Punycode 인코딩을 사용합니다. 브라우저는 IDN을 내부적으로 Punycode로 변환합니다: bücher.de → xn--bcher-kva.de.
URL 구조 참조
RFC 3986에 따르면 URL은 다음 컴포넌트로 구성됩니다:
scheme://userinfo@host:port/path?query#fragment
| 컴포넌트 | 예시 | 인코딩 규칙 |
|---|---|---|
| Scheme | https | 문자, 숫자, +, -, . |
| Host | example.com | 도메인 레이블 + 점 |
| Port | 8080 | 숫자만 |
| Path | /search/results | %XX로 인코딩(비예약 문자 + :@!$&'()*+,;= 제외) |
| Query | q=hello+world | 폼 데이터에서 공백은 +, 일반적으로는 %20 |
| Fragment | #section-2 | 서버로 전송되지 않음; 브라우저 전용 |
서버 측 고려사항
URL 정규화
서버는 처리 전에 URL을 정규화해야 합니다——예를 들어 %41(A로 디코딩)과 A를 동일하게 처리합니다. 그러나 일부 문자는 인코딩된 상태와 인코딩되지 않은 상태에서 다른 의미를 가집니다: 경로의 /와 %2F——보안상의 이유(경로 탐색 방지)로 많은 웹 서버는 이 둘을 다르게 처리합니다.
URL 파라미터를 통한 SQL 인젝션
URL 디코딩 후에도 URL 파라미터를 데이터베이스 쿼리에 사용하기 전에 반드시 살균하고 검증하세요. URL 인코딩은 보안 경계가 아닙니다.
도구 및 API
브라우저의 URL API
최신 브라우저와 Node.js는 URL을 구조화된 방식으로 다루기 위한 URL API를 제공합니다:
const url = new URL("https://example.com/search?q=hello world&page=1");
console.log(url.searchParams.get("q")); // "hello world" (자동 디코딩)
url.searchParams.set("q", "new value & special");
console.log(url.href);
// https://example.com/search?q=new+value+%26+special&page=1
URL API는 인코딩/디코딩을 투명하게 처리하므로, 일반적으로 encodeURIComponent를 수동으로 호출하는 것보다 권장되는 방식입니다.
요약
URL 인코딩은 모든 웹 개발자가 매일 접하는 기본 개념으로, 대부분 자신도 모르게 사용하고 있습니다. 기억해야 할 핵심 사항:
- 퍼센트 인코딩(
%XX)은 URI에서 안전하지 않은 문자를 인코딩하는 표준 메커니즘입니다. - 개별 값에는
encodeURIComponent()를, 전체 URL에는encodeURI()를 사용하세요. - 쿼리 문자열에서
+와%20의 차이를 인식하세요. - 이중 인코딩을 피하세요——인코딩하기 전에 문자열이 이미 인코딩되어 있는지 확인하세요.
- 프로그래밍 방식으로 URL을 다룰 때는 현대적인
URL과URLSearchParamsAPI를 우선 사용하세요.