인코딩 · 2026년 9월 5일

URL 인코딩 — 왜 %20이 존재하고, 언제 인코딩하고, API를 망가뜨리는 함정

URL 인코딩은 안전하지 않은 문자를 퍼센트 인코딩된 시퀀스로 변환합니다. 간단한 것 같지만 — 이중 인코딩, 쿼리 매개변수 vs 경로, 90%의 버그를 방지하는 유일한 규칙 — 복잡해집니다.

입력하거나 클릭한 모든 URL은 URL 인코딩을 거쳤습니다. “my file.txt”의 공백은 %20이 됩니다. 쿼리 문자열의 &는 %26이 됩니다. 경로의 /는 /로 남지만, 인코더가 어떤 문자가 안전하고 어떤 문자가 안전하지 않은지 알기 때문입니다.

URL 인코딩(공식적으로 “퍼센트 인코딩”)은 예약되지 않은 문자 집합에 없는 문자를 URL로 표현하는 방법입니다. 간단한 변환이지만, 가장 많은 API 버그가 존재하는 곳은 엣지 케이스입니다.

예약되지 않은 문자

이러한 문자는 URL에서 항상 안전하며 인코딩이 필요하지 않습니다:

A-Z a-z 0-9 - _ . ~

그 외 모든 것 — 공백, 슬래시, 앰퍼서드, 등호, 비ASCII 문자 — 은 퍼센트 기호 뒤에 두 개의 16진 숫자로 인코딩해야 합니다:

  • 공백 → %20
  • & → %26
  • = → %3D
  • / → %2F
  • ? → %3F
  • # → %23

인코딩은 문자의 UTF-8 바이트 값으로, 각각 두 개의 16진 숫자로 표현됩니다. é(UTF-8: 0xC3 0xA9)와 같은 다중 바이트 문자는 %C3%A9가 됩니다.

인코딩이 중요한 곳

쿼리 매개변수. 여기가 URL 인코딩이 가장 많은 버그를 일으키는 곳입니다. 쿼리 값이 & 또는 =를 포함하면 URL 파서가 해당 문자에서 분할하고 매개변수 구조를 손상시킵니다:

/search?q=cats&dogs     ← 모호함: "dogs"가 별도 매개변수?
/search?q=cats%26dogs   ← 정확: "cats&dogs"가 단일 값

모든 HTTP 라이브러리에는 쿼리 매개변수를 인코딩하는 함수가 있습니다. 사용하세요. 절대 수동으로 쿼리 문자열에 문자열을 연결하지 마세요.

경로. 파일명의 공백은 인코딩이 필요합니다:

/download/my file.pdf     ← 손상됨
/download/my%20file.pdf   ← 작동함

하지만 경로 세그먼트 내의 슬래시도 인코딩이 필요합니다:

/file/path/segment   ← 두 세그먼트: /로 분할된 "file/path"
/file%2Fpath/segment ← 한 세그먼트: "file/path"

비ASCII 문자. URL은 ASCII 전용입니다. 비ASCII 문자(강세된 문자, CJK 문자, 이모지)는 퍼센트 인코딩해야 합니다:

  • café → caf%C3%A9
  • 日本語 → %E6%97%A5%E6%9C%AC%E8%AA%9E
  • 🎉 → %F0%9F%8E%89

대부분의 브라우저는 주소 표시줄에 디코딩된 버전을 표시하지만, 실제로 네트워크를 통해 전송되는 바이트는 인코딩됩니다.

이중 인코딩 함정

가장 흔한 URL 인코딩 버그: 이미 인코딩된 문자열을 인코딩하는 것.

경로 /hello%20world를 가져가세요. 다시 URL 인코딩하면 %는 %25가 됩니다:

/hello%20world      ← 원본 (정확)
/hello%2520world    ← 이중 인코딩 (손상)

서버는 %25를 %로 디코딩하고, %20을 보고 공백으로 디코딩합니다. /hello world가 됩니다 — 서버가 단일 디코딩을 하는 경우에만. 두 번 디코딩하면(일부는这样做합니다) 원본을 얻게 됩니다. 동작은 일관성 없고 예측 불가능합니다.

규칙: 한 번 인코딩하고, 한 번 디코딩하세요. 인코딩된 문자열을 받으면 재인코딩 전에 디코딩하세요. HTTP 라이브러리의 URL 빌더가 원시 값 또는 사전 인코딩된 값을 기대하는지 확인하세요 — 대부분의 프레임워크에서 path와 rawPath의 차이는 중요합니다.

쿼리 문자열 인코딩

쿼리 문자열에는 고유한 인코딩 규칙이 있습니다. 경로와의 주요 차이점: +는 쿼리 문자열에서 공백을 나타냅니다(application/x-www-form-urlencoded), 하지만 %20도 공백을 나타냅니다. 둘 다 유효하지만 다른 표준에서 나옵니다:

  • application/x-www-form-urlencoded(양식 제출) — 공백에는 +
  • 퍼센트 인코딩(URL) — 공백에는 %20

대부분의 최신 API는 둘 다 받아들입니다. 하지만 양식 제출에서 쿼리 문자열을 분석하는 경우 + → 공백. URL을 구축하는 경우 %20 → 공백. 혼동하면 공백이 + 기호로 바뀌거나 그 반대의 미묘한 버그가 발생합니다.

사용 중인 언어의 URL 인코딩 함수를 사용하세요. 차이를 처리해 줍니다.

인코딩 vs 이스케이프

URL 인코딩은 HTML 이스케이프가 아닙니다. 서로 다른 문제를 해결합니다:

  • URL 인코딩 (%20) — URL 내 문자용. 문자가 URL 구문으로 해석되는 것을 방지.
  • HTML 이스케이프 (&, <) — HTML 내 문자용. 문자가 HTML 태그나 엔티티로 해석되는 것을 방지.

HTML href 내의 URL은 둘 다 필요합니다: 매개변수 값을 URL 인코딩하고, 전체 URL을 HTML 인코딩:

<a href="/search?q=cats%26d&amp;page=1">

%26는 &가 쿼리 구분자로 해석되는 것을 방지합니다. &amp;는 &가 HTML 엔티티 시작으로 해석되는 것을 방지합니다.

일반적인 함정

다른 맥락에서의 공백. URL에서는 %20, 양식 본문에서는 +, 실수로 이중 인코딩한 경우 %2520. 어떤 맥락에 있는지 알아두세요.

해시 프래그먼트. # 뒤의 모든 것은 서버로 전송되지 않습니다. 프래그먼트가 포함된 URL을 인코딩하고 프래그먼트가 ? 또는 &를 포함하면 서버는它을 보지 못합니다. 프래그먼트는 클라이언트 전용입니다.

퍼센트 기호 인코딩. % → %25. 데이터에 리터럴 %가 나타나는 경우(예: %가 포함된 비밀번호) 인코딩해야 합니다. 인코딩하지 않으면 파서는 그 뒤의 두 문자를 16진 시퀀스로 해석합니다.

유니코드 정규화. 일부 시스템은 인코딩 전에 유니코드를 정규화합니다. café(결합 악센트)와 café(합성 é)는 다른 퍼센트 시퀀스로 인코딩됩니다. 이것은 파일명과 국제화된 도메인 이름에 문제를 일으킵니다.

빠른 참조 | 문자 | 퍼센트 인코딩 | 맥락 | |——|——————|–––––| | 공백 | %20 | URL | | 공백 | + | 양식 본문 | | & | %26 | 쿼리 문자열 | | = | %3D | 쿼리 문자열 | | / | %2F | 경로 세그먼트 | | ? | %3F | 쿼리 문자열 | | # | %23 | 경로/쿼리 | | % | %25 | 어디서든 | | + | %2B | 쿼리 문자열 |

시도해보기

디코딩해야 하는 URL 또는 인코딩된 문자열, 인코딩해야 하는 원시 데이터가 있다면 로컬 도구를 사용하여 변환이 브라우저에서 이루어지도록 하세요 — 데이터는 어디에도 전송되지 않습니다.