URL 안에 한글, 공백, 특수 문자를 어떻게 표현하는지 — 답은 percent encoding (URL encoding) 이다. 이 가이드는 어떤 문자가 그대로 가는지, 어떤 문자는 인코딩이 강제되는지, 왜 공백이 어떨 땐 %20이고 어떨 땐 + 인지, JS 의 두 함수 (encodeURI/ encodeURIComponent) 차이까지 정리한다.
RFC 3986 의 문자 집합
URL 안에서 그대로 등장 가능한 문자는 두 부류.
Unreserved (인코딩 불필요)
A-Z a-z 0-9 - _ . ~모든 환경에서 그대로 유지. 인코딩하면 도리어 의미가 달라질 수 있음 (URL 정규화 시 이 문자들의 percent 인코딩은 자동 디코드).
Reserved (의미 충돌 시 인코딩 필요)
gen-delims: : / ? # [ ] @
sub-delims: ! $ & ' ( ) * + , ; =URL 의 구조 를 표현하는 문자들. 위치에 따라 다른 의미.
?가 path 안에 있으면 query 시작으로 오해. 인코딩 필수.&가 query value 안에 있으면 다음 파라미터로 오해.#가 어디든 있으면 fragment 시작.
그 외 (모두 인코딩)
한글·중국어·이모지·공백·제어 문자 — 전부 percent encode. UTF-8 바이트 열로 변환 후 각 바이트를 %HH.
예: 한 은 UTF-8 로 EC 95 9C → %EC%95%9C.
URL 인코딩 / 디코딩 에 임의 문자열을 넣으면 encode/decode 결과를 즉시 확인 가능 — 한 번에 어떤 바이트가 나오는지 본다.
공백 — %20 vs +
악명 높은 혼동.
- path: 공백은
%20.+는 그냥 plus 문자. - query string (application/x-www-form-urlencoded): 공백은
+.%20도 동작하지만+가 form 인코딩 표준. - fragment:
%20.
그래서 같은 "hello world" 가
https://example.com/hello%20world(path)https://example.com/?q=hello+world(form-style query)https://example.com/?q=hello%20world(RFC 3986 query — 더 안전)
모두 같은 의미로 해석된다. 단, encodeURIComponent("hello world") 는 항상 %20 만 출력 — JS 에서 + 가 필요하면 직접 replace.
JS 의 세 함수
encodeURI
URL 전체를 인코딩. 구조 문자 (:/?#[]@!$&'()*+,;=) 는 그대로 유지. 이미 만들어진 URL 을 손볼 때 사용.
encodeURI("https://example.com/검색?q=하늘")
// "https://example.com/%EA%B2%80%EC%83%89?q=%ED%95%98%EB%8A%98"? 와 = 는 그대로 — 구조 보존. 한글만 인코딩.
encodeURIComponent
URL 의 한 조각 (path segment, query value 등) 을 인코딩. 구조 문자도 모두 인코딩.
encodeURIComponent("https://example.com/검색?q=하늘")
// "https%3A%2F%2Fexample.com%2F%EA%B2%80%EC%83%89%3Fq%3D%ED%95%98%EB%8A%98"URL 을 다른 URL 의 query value 로 박을 때 (OAuth redirect, 짧은 링크 서비스) 필수.
경험 법칙
- query value 만 만들 때 →
encodeURIComponent. - 완성된 URL 을 안전하게 만들 때 →
encodeURI. - URL 조립 자체 →
URL/URLSearchParams객체 사용 (가장 안전).
const url = new URL("https://example.com/search");
url.searchParams.set("q", "hello world & more");
url.searchParams.set("page", "2");
url.toString();
// "https://example.com/search?q=hello+world+%26+more&page=2"URLSearchParams 는 form 스타일이라 공백을 + 로. 표준이고 일관됨.
국제화 도메인 — IDN / Punycode
도메인은 ASCII 만 허용. 한글.kr 같은 IDN 은 Punycode 로 변환된다.
한글.kr → xn--bj0bj06e.kr
münchen.de → xn--mnchen-3ya.de
example.한국 → example.xn--3e0b707e브라우저 주소창은 IDN 그대로 표시하지만, DNS 조회·HTTP host 헤더는 모두 Punycode. Punycode (국제화 도메인) 에 도메인을 넣으면 양방향 변환 결과를 본다.
IDN homograph 공격 — Cyrillic а (U+0430) 와 Latin a (U+0061) 가 시각적으로 동일. аpple.com (Cyrillic a) 같은 위장 가능. 브라우저는 의심스러운 혼합 시 Punycode 로 강제 표시.
URL 의 다섯 부분
https://user:pass@host.example.com:8080/path/to/resource?key=value#fragment
─┬─── ──┬──── ─┬─────────────── ─┬── ─┬──────────────── ─┬──────── ─┬─────
│ │ │ │ │ │ └ fragment
│ │ │ │ │ └ query
│ │ │ │ └ path
│ │ │ └ port
│ │ └ host (authority)
│ └ userinfo (deprecated for HTTP)
└ scheme각 부분의 인코딩 규칙이 다르다. URL 파서 가 분해해 보여주므로 어디서 인코딩이 깨졌는지 빠르게 찾을 수 있다.
흔한 함정
1. 이중 인코딩
이미 인코딩된 URL 에 encodeURIComponent 다시 — % 가 %25 로. 받는 쪽이 디코드 한 번 만 하면 깨진 결과.
2. + 를 무조건 공백으로 디코드
path 의 a+b 는 그냥 plus. query string 의 a+b 는 form 인코딩 컨텍스트에서 공백. decodeURIComponent 는 둘 다 plus 로 두므로, form 디코드는 str.replace(/\+/g, " ") 먼저 후 디코드.
3. base path 직접 문자열 join
baseUrl + "/" + userInput 같은 패턴은 userInput 의 ?·#·../ 에 취약 (path traversal, query injection). new URL(userInput, baseUrl) 사용 권장.
4. fetch 에 raw 한글 URL
fetch("https://example.com/한글") 은 브라우저가 자동 인코딩 해 주지만, server-side fetch (Node) 는 환경에 따라 다름. 명시적 인코딩 권장.
5. + 가 base64 와 충돌
Base64 출력에 + 가 나타남. URL 에 박으려면 base64url 변종 (+ → -, / → _) 사용. JWT 가 정확히 이 방식.
조립 예제 — 안전한 패턴
function buildSearchUrl(base, query, page) {
const url = new URL(base);
url.pathname = `${url.pathname.replace(/\/$/, "")}/search`;
url.searchParams.set("q", query);
url.searchParams.set("page", String(page));
return url.toString();
}
buildSearchUrl("https://example.com/", "한글 검색 & 결과", 2)
// "https://example.com/search?q=%ED%95%9C%EA%B8%80+%EA%B2%80%EC%83%89+%26+%EA%B2%B0%EA%B3%BC&page=2"문자열 concat 0 건. URL + URLSearchParams 만으로 모든 경우 처리.
요약
- unreserved (
A-Za-z0-9-._~) 만 그대로. 나머지는 percent encode. - 공백: path/fragment 는
%20, form-style query 는+. - query value →
encodeURIComponent, 완성 URL 손질 →encodeURI, 조립 →URL+URLSearchParams. - 한글 도메인은 Punycode. IDN homograph 주의.
- 이중 인코딩,
+컨텍스트, base64 + URL 충돌이 단골 사고.