developer
한글 URL이 %ED%95%9C처럼 바뀌는 이유: 퍼센트 인코딩 정리
주소창의 한글이 복사하면 긴 % 문자열로 바뀌는 원리, encodeURI와 encodeURIComponent의 차이, 공백이 %20과 +로 갈리는 이유를 RFC 3986 기준으로 설명합니다.
modu.tools Editorial · ·
developer
주소창의 한글이 복사하면 긴 % 문자열로 바뀌는 원리, encodeURI와 encodeURIComponent의 차이, 공백이 %20과 +로 갈리는 이유를 RFC 3986 기준으로 설명합니다.
modu.tools Editorial · ·
브라우저 주소창에서는 modu.tools/검색?q=한글처럼 보이던 주소를 복사해 메신저에 붙여 넣으면 %EA%B2%80%EC%83%89?q=%ED%95%9C%EA%B8%80 같은 긴 문자열이 됩니다. 고장 난 것이 아니라 퍼센트 인코딩이라는 정상적인 변환입니다. 이 글은 왜 이런 변환이 필요한지, 개발할 때 어떤 함수를 써야 하는지 정리합니다.
URI의 일반 문법을 정한 RFC 3986은 URL에 그대로 쓸 수 있는 문자를 아주 좁게 정해 둡니다.
- . _ ~ 네 개: / ? # [ ] @ ! $ & ' ( ) * + , ; =예약 문자는 URL의 구조를 나누는 역할을 합니다. ?는 쿼리의 시작, &는 매개변수 구분, #는 조각(fragment)의 시작이죠. 그래서 데이터 안에 이런 문자가 들어가면 구조와 헷갈리지 않게 인코딩해야 합니다.
한글은 두 목록 어디에도 없습니다. 그래서 URL에 넣으려면 바이트로 바꾼 다음 각 바이트를 % 뒤에 16진수 두 자리로 적습니다. 이것이 퍼센트 인코딩입니다.
ED 95 9C가 됩니다.%를 붙이면 %ED%95%9C입니다.한글 한 글자가 UTF-8에서 3바이트라서, 인코딩하면 9글자(%XX × 3)로 늘어납니다. 한글 10자 검색어는 URL에서 90자가 되는 셈이에요. URL 길이 제한이 있는 환경(일부 서버, 공유 링크, 문자 메시지)에서 한글 주소가 생각보다 빨리 한도에 닿는 이유입니다. 바이트 계산은 한글 바이트 가이드에 자세히 정리했습니다.
RFC 3986은 새로운 URI 체계에서 비ASCII 문자를 먼저 UTF-8로 바꾼 뒤 퍼센트 인코딩하도록 권고합니다. 오래된 한국 사이트 중에는 EUC-KR 바이트로 인코딩하는 곳이 있어서, 같은 ’한’이 %C7%D1로 나오기도 합니다. 이런 주소를 UTF-8로 디코딩하면 글자가 깨집니다.
JavaScript에는 비슷한 함수가 두 개 있어서 자주 헷갈립니다.
| 함수 | 인코딩하지 않는 문자 | 용도 |
|---|---|---|
encodeURI |
예약 문자 대부분(:/?#&= 등) |
이미 완성된 URL 전체 |
encodeURIComponent |
영문, 숫자, -_.!~*'() |
URL의 한 조각(쿼리 값, 경로 조각) |
예를 들어 검색어 A&B 회사를 쿼리 값으로 넣는다고 해 봅시다.
encodeURI("A&B 회사") → A&B%20%ED%9A%8C%EC%82%AC : &가 그대로 남아서 서버는 이것을 두 개의 매개변수로 해석합니다.encodeURIComponent("A&B 회사") → A%26B%20%ED%9A%8C%EC%82%AC : &가 %26으로 바뀌어 값의 일부로 전달됩니다.규칙은 간단합니다. **값 하나를 넣을 때는 항상 encodeURIComponent**를 씁니다. 더 좋은 방법은 URLSearchParams나 URL 객체를 써서 인코딩을 맡기는 것입니다.
const url = new URL("https://modu.tools/search");
url.searchParams.set("q", "A&B 회사");
// https://modu.tools/search?q=A%26B+%ED%9A%8C%EC%82%AC
위 예시에서 URLSearchParams는 공백을 +로, encodeURIComponent는 %20으로 바꿨습니다. 둘 다 맞습니다. 다만 쓰이는 맥락이 다릅니다.
%20입니다. 경로(path)에서는 항상 이것을 씁니다.+로 바꾸도록 정합니다. 쿼리 문자열에서 흔히 보입니다.문제는 해석하는 쪽입니다. 경로에 들어간 +는 공백이 아니라 문자 그대로의 +입니다. 그래서 C++ 같은 값을 폼 방식으로 디코딩하면 C (공백 두 개)가 되어 버리는 버그가 생깁니다. 값에 +가 들어갈 수 있다면 반드시 %2B로 인코딩해야 합니다.
이미 인코딩된 문자열을 한 번 더 인코딩하면 %가 %25로 바뀝니다. %ED가 %25ED가 되는 식이죠. 링크를 열었는데 한글 대신 %ED%95%9C가 그대로 화면에 보인다면 이중 인코딩을 의심하세요. 인코딩은 URL을 만드는 마지막 단계에서 한 번만, 디코딩은 받는 쪽에서 한 번만 하는 것이 원칙입니다.
한글.kr 같은 국제화 도메인 이름(IDN)은 퍼센트 인코딩이 아니라 퓨니코드라는 별도 방식으로 바뀝니다. 도메인 부분은 xn--로 시작하는 ASCII 문자열이 되고, 경로와 쿼리 부분만 퍼센트 인코딩됩니다. 한 URL 안에서 두 가지 방식이 함께 쓰일 수 있다는 점을 알아 두면 디버깅할 때 덜 헷갈립니다.
encodeURIComponent 또는 URLSearchParams로 인코딩했나요?&, =, +, #이 들어갈 가능성을 테스트했나요?+와 %20 중 무엇으로 기대하는지 확인했나요?modu.tools의 데이터 워크벤치에서는 URL을 붙여 넣으면 인코딩과 디코딩 결과를 바로 비교할 수 있습니다. 모든 처리는 브라우저 안에서 이뤄집니다.
주소를 만들거나 고칠 때 아래 순서로 확인하면 대부분의 사고를 막을 수 있습니다.
# 뒤), 아니면 URL 전체인지에 따라 쓸 함수가 달라집니다.강남역 맛집, 제품명 C++ 입문.encodeURIComponent를 적용한다. 강남역 맛집은 %EA%B0%95%EB%82%A8%EC%97%AD%20%EB%A7%9B%EC%A7%91이 되고, C++ 입문은 C%2B%2B%20%EC%9E%85%EB%AC%B8이 됩니다. +가 %2B로 바뀌었는지 꼭 보세요.https://modu.tools/search?q= + 인코딩된 값처럼 이어 붙이거나, 더 안전하게는 URL과 searchParams를 씁니다.%가 없는 정상 한글에는 아무 일도 없지만, %25가 섞인 이중 인코딩 문자열은 한 단계씩만 풀어야 원문이 복구됩니다.목표 URL: https://modu.tools/search?q=한글&sort=최신
| 단계 | 값 | 결과 |
|---|---|---|
| 원본 q | 한글 | (아직 인코딩 전) |
| encodeURIComponent(q) | 한글 | %ED%95%9C%EA%B8%80 |
| 원본 sort | 최신 | (아직 인코딩 전) |
| encodeURIComponent(sort) | 최신 | %EC%B5%9C%EC%8B%A0 |
| 조립 | — | https://modu.tools/search?q=%ED%95%9C%EA%B8%80&sort=%EC%B5%9C%EC%8B%A0 |
같은 작업을 URLSearchParams로 하면 공백이 +로 바뀌는 점만 다릅니다. 경로에 한글을 넣을 때는 폼 인코딩이 아니라 퍼센트 인코딩(%20)을 써야 하므로, 경로와 쿼리를 같은 함수로 처리하지 마세요.
| 실수 | 화면에 보이는 증상 | 고치는 방법 |
|---|---|---|
쿼리 값에 encodeURI 사용 |
&나 =가 그대로 남아 매개변수가 쪼개짐 |
encodeURIComponent 또는 searchParams |
| 이미 인코딩된 주소를 다시 인코딩 | 주소에 %25ED처럼 %25가 보임 |
인코딩 여부를 먼저 검사하고 한 번만 적용 |
경로의 +를 공백으로 디코딩 |
C++이 C 로 바뀜 |
경로는 %20/%2B만 쓰고 +↔공백 치환을 하지 않음 |
| EUC-KR로 인코딩된 옛 링크를 UTF-8로 디코딩 | `` 같은 깨진 글자 | 서버·문서의 인코딩을 확인한 뒤 맞춤 디코딩 |
전체를 encodeURIComponent |
https%3A%2F%2F...처럼 스킴까지 인코딩됨 |
URL 전체는 encodeURI 또는 URL 객체 |
많은 서버와 프록시는 요청 줄 길이를 8KB 안팎으로 제한합니다. 한글 검색어 200자는 UTF-8로 600바이트, 퍼센트 인코딩하면 약 1,800자가 됩니다. 영문 200자와 같은 ’글자 수’라도 URL 길이는 세 배 가까이 늘어날 수 있어요. 공유용 짧은 링크, SMS에 넣을 주소, 구형 장비의 QR 코드라면 한글 경로 대신 짧은 ASCII 슬러그를 쓰는 편이 안전합니다.
브라우저 주소창은 한글을 사람이 읽기 쉬운 형태로 보여 주지만, 복사·네트워크 전송·서버 로그에는 인코딩된 형태가 남는 경우가 많습니다. 디버깅할 때는 주소창 표시와 실제 요청 줄을 구분해 보세요.
%ed와 %ED는 같은가요? RFC 3986은 16진수 대소문자를 같은 것으로 보지만, 일관성을 위해 대문자를 쓰도록 권고합니다.
한글 URL을 그대로 공유해도 되나요? 최신 브라우저와 메신저는 대부분 처리하지만, 오래된 시스템이나 이메일 클라이언트에서는 링크가 끊길 수 있습니다. 공유용이라면 인코딩된 주소가 안전합니다.
~는 왜 인코딩하지 않나요?
RFC 3986에서 ~는 예약되지 않은 문자라 그대로 써도 됩니다.