URL 인코딩이 무엇이고 왜 필요할까?
웹 개발을 하다 보면 한글이나 공백이 들어간 URL이 깨지거나, 쿼리 스트링에 특수 문자가 포함되어 의도치 않은 동작을 하는 경우를 종종 마주치게 되는데요. 이런 문제의 원인은 대부분 URL 인코딩과 관련이 있습니다.
이번 포스팅에서는 URL 인코딩이 무엇이고 왜 필요한지부터 짚어보겠습니다. 어떤 문자는 그대로 써도 되고 어떤 문자는 변환해야 하는지, 그리고 자바스크립트의 encodeURI와 encodeURIComponent 함수가 어떻게 다른지도 함께 살펴보겠습니다.
URL 인코딩이 왜 필요할까요?
URL은 인터넷에서 자원의 위치를 가리키는 주소인데요. RFC 3986 표준에 따라 URL에 사용할 수 있는 문자는 ASCII 범위 안의 일부 문자로 한정되어 있습니다.
예를 들어, 공백이나 한글, 또는 ?, &, = 같은 일부 특수 문자는 URL 안에서 특별한 의미를 가지거나 아예 허용되지 않는데요.
만약 검색어가 “hello world”라면 이를 URL에 그대로 넣어 https://example.com/search?q=hello world 형태로 만들면 어떻게 될까요?
https://example.com/search?q=hello world
^
공백이 있어서 깨짐
브라우저에 따라 자동으로 처리해주기도 하지만 서버나 다른 시스템에서는 공백을 URL의 끝으로 해석하거나 잘못된 요청으로 거절할 수 있습니다.
이런 문제를 해결하기 위해 등장한 것이 바로 URL 인코딩(URL encoding) 또는 퍼센트 인코딩(percent-encoding)입니다.
URL에 사용할 수 없거나 특별한 의미를 가진 문자를 %XX 형태로 변환해서 안전하게 표현하는 방식인데요.
https://example.com/search?q=hello%20world
여기서 %20은 공백 문자(ASCII 코드 32, 16진수 0x20)를 인코딩한 결과입니다.
이렇게 변환하면 URL이 깨지지 않고 어떤 시스템에서도 안전하게 전달됩니다.
URL에서 안전한 문자와 그렇지 않은 문자
그렇다면 어떤 문자는 그대로 써도 되고, 어떤 문자는 인코딩이 필요할까요? RFC 3986은 URL 문자를 크게 세 부류로 나누는데요.
첫 번째는 예약되지 않은 문자(unreserved characters)입니다. 이 문자들은 URL의 어느 위치에서도 그대로 사용할 수 있고, 인코딩할 필요가 없습니다.
A-Z, a-z, 0-9, - . _ ~
영문 알파벳 52자, 숫자 10자, 그리고 네 가지 특수 문자(-, ., _, ~)를 합쳐 총 66자가 여기에 해당됩니다.
NanoID가 기본 알파벳으로 64자 중 대부분을 이 영역에서 고른 이유도 바로 이 때문입니다.
두 번째는 예약된 문자(reserved characters)입니다. 이 문자들은 URL 안에서 특별한 구분자 역할을 하기 때문에 그 의미로 사용할 때는 인코딩하지 않지만 데이터의 일부로 사용할 때는 반드시 인코딩해야 합니다.
: / ? # [ ] @ (일반 구분자)
! $ & ' ( ) * + , ; = (보조 구분자)
예를 들어, ?는 경로와 쿼리 스트링을 구분하는 역할을 하는데요.
만약 검색어 자체에 ?가 들어가야 한다면 %3F로 인코딩해야 의도한 의미를 지킬 수 있습니다.
세 번째는 그 외의 모든 문자입니다. 공백, 한글, 이모지, 그리고 위 두 부류에 속하지 않는 모든 문자가 여기에 해당되는데, 이들은 URL에 포함시키려면 반드시 인코딩해야 합니다.
퍼센트 인코딩의 동작 방식
퍼센트 인코딩의 규칙은 의외로 단순합니다.
인코딩이 필요한 문자를 UTF-8로 바이트 단위로 표현한 뒤, 각 바이트를 %XX 형태(XX는 16진수 두 자리)로 바꾸는 것이 전부인데요.
예를 들어, 공백 문자는 ASCII 코드로 0x20이므로 %20이 되고, 한글 “안”은 UTF-8로 세 바이트(0xEC, 0x95, 0x88)이므로 %EC%95%88이 됩니다.
"안녕" → %EC%95%88%EB%85%95
이렇게 한 글자가 여러 바이트로 표현되다 보니 한글이 들어간 URL은 길이가 꽤 늘어나게 됩니다.
자바스크립트의 encodeURI와 encodeURIComponent
자바스크립트는 URL 인코딩을 위해 두 가지 내장 함수를 제공하는데요. 이름이 비슷해서 헷갈리기 쉽지만 동작 방식이 분명하게 다릅니다.
먼저 encodeURI는 완성된 URL 전체를 인코딩할 때 사용하는 함수입니다.
이 함수는 URL 구조를 깨뜨리지 않기 위해 예약된 문자(:, /, ?, #, &, = 등)는 인코딩하지 않고 그대로 둡니다.
const url = "https://example.com/search?q=hello world&lang=ko";
console.log(encodeURI(url));
// https://example.com/search?q=hello%20world&lang=ko
공백은 %20으로 변환되었지만 :, /, ?, &, =는 그대로 유지된 것을 볼 수 있습니다.
반면 encodeURIComponent는 URL의 일부 구성 요소(component)를 인코딩할 때 사용하는 함수입니다.
쿼리 스트링의 값이나 경로의 일부 같은 부분 문자열을 인코딩하기 위한 용도라서 예약된 문자까지 모두 인코딩합니다.
const query = "hello world&lang=ko";
console.log(encodeURIComponent(query));
// hello%20world%26lang%3Dko
&가 %26으로, =가 %3D로 인코딩된 것을 확인할 수 있습니다.
두 함수를 잘못 선택하면 어떤 문제가 생길까요? 검색어를 인코딩하지 않고 URL에 그대로 붙이면 의도치 않은 결과를 낳을 수 있는데요.
const keyword = "javascript&python";
const wrongUrl = `https://example.com/search?q=${keyword}`;
// https://example.com/search?q=javascript&python
// → q 매개변수의 값은 "javascript", python이라는 별도 매개변수가 추가됨
&가 인코딩되지 않아서 python이 별도의 쿼리 매개변수처럼 해석되어 버립니다.
이때는 encodeURIComponent를 사용해야 의도한 대로 동작합니다.
const keyword = "javascript&python";
const correctUrl = `https://example.com/search?q=${encodeURIComponent(keyword)}`;
// https://example.com/search?q=javascript%26python
정리하자면, URL 전체를 다룰 때는 encodeURI를 쓰고, 쿼리 값처럼 부분 문자열을 다룰 때는 encodeURIComponent를 쓴다고 기억해두면 됩니다.
실무에서는 사용자 입력이나 동적인 값을 URL에 끼워넣을 일이 더 많기 때문에 encodeURIComponent가 훨씬 자주 쓰이는 편입니다.
디코딩과 URLSearchParams
인코딩된 URL을 다시 원래 형태로 되돌리려면 어떻게 할까요?
자바스크립트는 인코딩 함수와 정확히 짝을 이루는 디코딩 함수도 제공하는데요.
URL 전체를 디코딩할 때는 decodeURI()를, 부분 문자열을 디코딩할 때는 decodeURIComponent()를 사용합니다.
decodeURI("https://example.com/search?q=hello%20world&lang=ko");
// https://example.com/search?q=hello world&lang=ko
decodeURIComponent("hello%20world%26lang%3Dko");
// hello world&lang=ko
다만 쿼리 스트링을 직접 인코딩하고 디코딩하는 일은 생각보다 까다로운데요. 다행히 자바스크립트에는 URLSearchParams라는 표준 API가 있어서 쿼리 매개변수를 객체처럼 다룰 수 있습니다.
const params = new URLSearchParams();
params.set("q", "javascript&python");
params.set("lang", "ko");
const url = `https://example.com/search?${params.toString()}`;
console.log(url);
// https://example.com/search?q=javascript%26python&lang=ko
URLSearchParams는 값을 자동으로 인코딩해주기 때문에 encodeURIComponent를 직접 호출할 필요가 없습니다.
여기에 URL API까지 함께 쓰면 URL 자체를 객체로 다룰 수 있어서 훨씬 깔끔하게 작업할 수 있습니다.
흔히 저지르는 실수
URL 인코딩을 다룰 때 자주 마주치는 실수 몇 가지를 짚어볼게요.
가장 흔한 실수는 인코딩을 두 번 하는 것입니다.
이미 인코딩된 문자열을 다시 인코딩하면 %가 %25로 변환되어 의도치 않은 결과가 나오는데요.
const encoded = encodeURIComponent("hello world");
// hello%20world
const doubleEncoded = encodeURIComponent(encoded);
// hello%2520world ← %가 다시 %25로 인코딩됨
서버에서 디코딩할 때는 한 번만 디코딩하므로 클라이언트에서도 한 번만 인코딩해야 합니다.
또 두 함수를 혼동하는 실수도 자주 마주치게 되는데요.
URL 전체에 encodeURIComponent를 쓰면 https://의 :와 /까지 인코딩되어 URL 구조가 망가지게 됩니다.
const wrongUrl = encodeURIComponent("https://example.com/path");
// https%3A%2F%2Fexample.com%2Fpath
한글이 들어간 URL을 다룰 때는 한 가지 더 주의할 점이 있는데요. 콘솔이나 브라우저 주소창에는 한글이 그대로 보이지만 실제로는 내부적으로 인코딩된 형태로 전송됩니다. 주소창에 보이는 모습과 실제 네트워크로 나가는 값이 다를 수 있다는 점을 항상 의식하고 있어야 디버깅이 수월해집니다.
마치며
지금까지 URL 인코딩이 왜 필요하고 어떻게 동작하는지, 자바스크립트에서 이를 다루는 두 함수의 차이점까지 살펴보았습니다. URL 인코딩은 일상적인 웹 개발에서 자주 마주치지만 잘 모르고 지나치기 쉬운 주제인데요. 어떤 문자가 안전하고 어떤 문자가 변환되어야 하는지 한 번 제대로 이해해두면 URL이 깨지거나 쿼리 매개변수가 이상하게 동작하는 문제를 훨씬 빠르게 진단할 수 있습니다.
더 자세한 명세는 RFC 3986을 참고하세요.
This work is licensed under
CC BY 4.0