</>DevTools

%20URL 인코드/디코드

URL 컴포넌트 인코딩 및 디코딩

URL 인코딩(퍼센트 인코딩) 완전 정리

URL에 쓸 수 있는 문자는 생각보다 적습니다. 그래서 그 밖의 문자는 %XX 형태의 16진수로 바꿔서 넣는데, 이것이 퍼센트 인코딩입니다. 문제는 '어디에 넣느냐'에 따라 인코딩해야 하는 문자 집합이 달라진다는 점이고, JavaScript가 서로 다른 함수 네 개를 제공하는 이유도 여기에 있습니다.

예약 문자와 비예약 문자

RFC 3986은 URL 문자를 두 부류로 나눕니다. 비예약 문자(unreserved)는 A-Z a-z 0-9 와 - _ . ~ 네 개로, 언제나 그대로 써도 되고 인코딩하면 오히려 같은 URL이 다르게 취급될 수 있습니다. 예약 문자(reserved)는 : / ? # [ ] @ ! $ & ' ( ) * + , ; = 로, URL의 구조를 나타내는 구분자 역할을 합니다.

핵심은 예약 문자를 '구분자로 쓸 때는 그대로 두고, 데이터로 쓸 때는 인코딩해야 한다'는 것입니다. 예를 들어 쿼리 문자열을 나누는 &는 그대로 두지만, 값 안에 들어 있는 &는 %26으로 바꿔야 합니다. 그러지 않으면 파서가 그 지점에서 파라미터를 잘라 버립니다.

JavaScript 함수 네 개의 차이

encodeURI는 URL 전체를 인코딩한다고 가정하므로 구분자(: / ? # & =)를 건드리지 않습니다. encodeURIComponent는 값 하나를 인코딩한다고 가정하므로 구분자까지 모두 인코딩합니다. 실무에서 필요한 것은 거의 항상 encodeURIComponent입니다.

차이를 눈으로 확인
const value = "a&b=c/d?e";

encodeURI(value)          // "a&b=c/d?e"      ← 그대로, 값으로 쓰면 위험
encodeURIComponent(value) // "a%26b%3Dc%2Fd%3Fe"  ← 안전

// 올바른 조립
const url = "/search?q=" + encodeURIComponent(value);
함수인코딩하지 않는 문자쓰는 곳
encodeURIComponentA-Z a-z 0-9 - _ . ! ~ * ' ( )쿼리 파라미터 값, 경로 세그먼트 하나
encodeURI위 + : / ? # [ ] @ & = + $ ,이미 조립된 URL 전체
escape (폐기)유니코드를 %uXXXX로 잘못 처리쓰지 말 것
encodeURIComponent + 보정! ' ( ) * 까지 추가 인코딩RFC 3986 엄격 준수가 필요할 때

사용 방법

  1. 인코딩할 문자열이나 디코딩할 %XX 문자열을 입력합니다.
  2. Encode/Decode 방향을 고릅니다.
  3. 값 하나만 변환한다면 Component 방식(모든 예약 문자 인코딩)이 대개 정답입니다.
  4. 결과를 복사해 URL에 붙여넣습니다.

공백이 %20인가 +인가

둘 다 존재하는데 문맥이 다릅니다. %20은 RFC 3986의 정식 표기이고 URL 어디에서나 유효합니다. +는 HTML 폼이 application/x-www-form-urlencoded 형식으로 제출할 때 쓰는 관례로, 쿼리 문자열에서만 공백으로 해석됩니다.

실무에서 문제가 되는 지점은 경로입니다. 경로에 들어간 +는 공백이 아니라 문자 그대로의 플러스로 해석되므로, /files/my+file.pdf 는 이름에 +가 들어간 파일을 가리킵니다. 반대로 서버가 쿼리 파라미터를 파싱할 때 +를 공백으로 바꾸기 때문에, 값에 실제 플러스 기호를 담아야 한다면 %2B로 인코딩해야 합니다. 전화번호 +82나 Base64 문자열을 파라미터로 넘길 때 자주 겪는 버그입니다.

한글과 이모지 처리

퍼센트 인코딩은 바이트를 대상으로 하므로 문자를 먼저 UTF-8 바이트로 바꾼 뒤 각 바이트를 %XX로 표기합니다. 한글 한 글자는 UTF-8에서 3바이트이므로 '한'은 %ED%95%9C처럼 %가 세 번 나옵니다. 이모지는 4바이트여서 네 번 나옵니다.

브라우저 주소창은 한글을 사람이 읽기 쉽게 그대로 보여주지만, 복사해서 다른 곳에 붙이면 인코딩된 형태가 나오는 경우가 많습니다. 둘은 같은 URL이며 서버에 전달될 때는 인코딩된 형태입니다.

자주 생기는 실수

  • 이중 인코딩: 이미 인코딩된 문자열을 한 번 더 인코딩하면 %가 %25로 바뀌어 %2520 같은 형태가 됩니다. 디코딩 결과에 %25가 보이면 이중 인코딩을 의심하세요.
  • URL 전체에 encodeURIComponent 적용: https://가 https%3A%2F%2F로 바뀌어 링크가 깨집니다.
  • 쿼리 값에 그대로 넣은 # : 서버는 # 이후를 전달받지 못합니다. 프래그먼트는 브라우저 내부에서만 쓰이기 때문입니다.
  • 인코딩 후 다시 인코딩된 문자열을 DB에 저장: 저장은 원본으로, 인코딩은 출력 시점에 하는 것이 원칙입니다.
  • 비예약 문자를 굳이 인코딩: %41은 A와 같은 의미지만 캐시 키나 서명 검증에서 다른 문자열로 취급될 수 있습니다.

자주 묻는 질문

encodeURI와 encodeURIComponent 중 무엇을 써야 하나요?
값 하나를 URL에 끼워 넣는다면 encodeURIComponent입니다. 검색어, ID, 리다이렉트 대상 URL 등 파라미터 값은 모두 여기에 해당합니다. encodeURI는 이미 완성된 URL 문자열을 마지막에 한 번 정리할 때만 쓰며, 실무에서 필요한 경우는 드뭅니다.
리다이렉트할 URL을 파라미터로 넘기려면?
반드시 encodeURIComponent로 감싸야 합니다. ?next=https://example.com/a?b=c 처럼 그냥 넣으면 두 번째 ?부터가 원래 URL의 일부인지 새 파라미터인지 서버가 구분할 수 없습니다. ?next=https%3A%2F%2Fexample.com%2Fa%3Fb%3Dc 로 넘기면 명확해집니다. 덧붙여, 넘겨받은 리다이렉트 대상은 반드시 허용 목록으로 검증하세요. 검증 없이 이동시키면 오픈 리다이렉트 취약점이 됩니다.
왜 어떤 사이트 URL은 %EC%95%88 같은 문자로 가득한가요?
한글 등 ASCII 밖의 문자가 UTF-8 바이트 단위로 퍼센트 인코딩된 것입니다. 한글 한 글자가 3바이트라 세 개의 %XX로 표기되어 길어 보입니다. 브라우저 주소창에서는 대개 원래 글자로 보기 좋게 표시됩니다.
디코딩할 때 오류가 나는 이유는?
% 뒤에 16진수 두 자리가 오지 않으면 잘못된 인코딩입니다. 예를 들어 '100% 완료' 같은 문자열을 디코딩하려 하면 '% ' 부분에서 실패합니다. 인코딩되지 않은 원본 텍스트를 디코딩하려는 상황인지 확인하세요.
쿼리 파라미터를 직접 조립하는 게 최선인가요?
아닙니다. 브라우저와 Node.js 모두 URLSearchParams를 제공하며, 이쪽이 인코딩을 자동으로 처리하고 실수 여지가 적습니다. new URLSearchParams({q: '검색 어&'}).toString() 형태로 쓰면 됩니다. 다만 URLSearchParams는 공백을 +로 인코딩한다는 점을 기억하세요.
인코딩된 URL의 길이 제한이 있나요?
표준에는 없지만 현실적인 상한이 있습니다. 대체로 브라우저와 서버 구현이 2,000자 안팎에서 안전하며, 일부 서버는 8KB에서 헤더를 거부합니다. 인코딩은 길이를 최대 3배까지 늘리므로, 긴 데이터를 URL에 담기보다 POST 본문을 쓰는 편이 안전합니다.

💡 참고: 결과에 %25가 보이면 거의 항상 이중 인코딩입니다. 한 번 디코딩한 뒤 다시 확인해 보세요.

🔗관련 도구🔄 텍스트/데이터 변환