블로그 목록

URL 인코딩(퍼센트 인코딩)과 HTML 엔티티 인코딩의 차이와 올바른 사용법

검색 결과 주소를 복사해 붙여 넣으면 %EC%84%9C%EC%9A%B8처럼 알아보기 어려운 문자가 나오고, 게시판 소스를 보면 < 같은 표기가 보입니다. 둘 다 "문자를 다른 모양으로 바꾼다"는 점은 같지만, URL 인코딩(퍼센트 인코딩)과 HTML 엔티티 인코딩은 쓰이는 곳과 목적이 전혀 다릅니다. 둘을 섞어 쓰면 링크가 깨지거나 보안 문제가 생길 수 있습니다. 이 글에서는 두 방식을 예제와 함께 비교하고, 실무에서 자주 하는 실수를 정리합니다.

1. URL 인코딩(퍼센트 인코딩)

URL의 문법은 RFC 3986에 정의되어 있습니다. URL 안에서 ?는 쿼리의 시작, &는 파라미터 구분, =는 이름과 값의 구분, #은 프래그먼트의 시작처럼 특별한 역할을 하는 예약 문자가 있습니다. 또 공백이나 한글처럼 URL에 그대로 쓸 수 없는 문자도 있습니다.

퍼센트 인코딩은 이런 문자를 UTF-8 바이트로 바꾼 뒤, 각 바이트를 %와 16진수 두 자리로 표기합니다. 영문자, 숫자와 - . _ ~는 RFC 3986에서 인코딩하지 않아도 되는 문자(unreserved)로 정해져 있습니다.

  • 공백 → %20
  • & → %26
  • 한글 "가"(UTF-8 EA B0 80) → %EA%B0%80

예제: 검색어가 들어간 URL

검색어 서울 날씨 & 미세먼지를 쿼리 값으로 넣는다고 해 보겠습니다. 인코딩하지 않고 붙이면 ?q=서울 날씨 & 미세먼지가 되어, 서버는 & 뒤를 새로운 파라미터로 해석해 검색어가 "서울 날씨 "까지만 전달될 수 있습니다. 값만 인코딩하면 다음과 같습니다.

encodeURIComponent("서울 날씨 & 미세먼지");
// "%EC%84%9C%EC%9A%B8%20%EB%82%A0%EC%94%A8%20%26%20%EB%AF%B8%EC%84%B8%EB%A8%BC%EC%A7%80"

// URL 객체를 쓰면 구분자를 직접 다룰 필요가 없습니다
const url = new URL("https://example.com/search");
url.searchParams.set("q", "서울 날씨 & 미세먼지");
url.href;
// "https://example.com/search?q=%EC%84%9C%EC%9A%B8+%EB%82%A0%EC%94%A8+%26+%EB%AF%B8..." (뒷부분 생략)

두 번째 결과에서는 공백이 %20이 아니라 +로 바뀐 것을 볼 수 있습니다. URLSearchParams는 HTML 폼 전송 방식(application/x-www-form-urlencoded) 규칙을 따르기 때문입니다. 서버 대부분은 두 표기를 모두 공백으로 해석하지만, 경로(path)에서는 +가 공백으로 해석되지 않는다는 점에 주의합니다.

2. HTML 엔티티 인코딩

HTML 문서 안에서 <는 태그의 시작, &는 문자 참조의 시작을 뜻합니다. 사용자가 입력한 문자열을 HTML로 그대로 끼워 넣으면 브라우저가 이를 태그나 스크립트로 해석할 수 있고, 이것이 XSS(크로스 사이트 스크립팅)의 대표적인 원인입니다. HTML 엔티티 인코딩은 이런 문자를 문자 참조로 바꿔 "글자로만" 표시되게 합니다.

  • < → &lt;, > → &gt;, & → &amp;
  • " → &quot;, ' → &#x27; (속성값 안에서 특히 중요)

예를 들어 댓글에 <b>안녕</b>을 입력했을 때, 인코딩하면 화면에 태그 문자 그대로 보이고, 인코딩하지 않으면 굵은 글씨로 해석됩니다. 굵은 글씨는 무해하지만 같은 경로로 스크립트가 들어갈 수 있다는 것이 문제입니다.

function escapeHtml(text) {
  return text
    .replaceAll("&", "&amp;") // &를 가장 먼저 바꿔야 이중 변환을 피합니다
    .replaceAll("<", "&lt;")
    .replaceAll(">", "&gt;")
    .replaceAll('"', "&quot;")
    .replaceAll("'", "&#x27;");
}

다만 브라우저에서 DOM을 직접 다룬다면, 문자열을 직접 이스케이프하기보다 element.textContent = userInput처럼 텍스트로 넣는 API를 쓰는 편이 더 단순하고 안전합니다.

3. 두 인코딩이 함께 쓰이는 흐름

실제 서비스에서는 두 인코딩이 한 흐름 안에 차례로 등장하는 경우가 많습니다.

  1. 사용자가 검색창에 <b>서울</b>을 입력합니다.
  2. 브라우저는 이를 URL 쿼리 값으로 보내기 위해 퍼센트 인코딩합니다: ?q=%3Cb%3E%EC%84%9C%EC%9A%B8%3C%2Fb%3E
  3. 서버는 쿼리 값을 디코딩해 원래 문자열 <b>서울</b>을 얻습니다.
  4. 검색 결과 페이지에 "검색어: ..."로 보여 줄 때는 HTML 엔티티 인코딩해 &lt;b&gt;서울&lt;/b&gt;로 출력합니다.

즉, URL 인코딩은 "주소로 보낼 때", HTML 엔티티 인코딩은 "HTML로 출력할 때" 각각 적용되며, 한쪽이 다른 쪽을 대신하지 않습니다.

4. 한눈에 비교

항목URL 인코딩HTML 엔티티 인코딩
적용 위치URL의 경로, 쿼리 값 등HTML 문서의 텍스트, 속성값
표기 형식% + 16진수 2자리 (예: %20)&이름; 또는 &#숫자; (예: &lt;)
목적URL 구분자 오해석 방지, 비ASCII 문자 전달마크업으로 해석되는 것 방지(XSS 방어)
JS 도구encodeURIComponent(), URL, URLSearchParamstextContent 사용 또는 직접 이스케이프

5. 자주 하는 실수

  • encodeURI와 encodeURIComponent 혼동: encodeURI()는 URL 전체용이라 ?, &, =, /를 인코딩하지 않습니다. 쿼리 값 하나를 인코딩할 때는 encodeURIComponent()를 씁니다.
  • 이중 인코딩: 이미 인코딩된 %20을 한 번 더 인코딩하면 %2520이 됩니다. 어느 단계에서 인코딩하는지 한 곳으로 정해 두는 것이 좋습니다.
  • URL 인코딩으로 XSS를 막으려는 시도: 두 인코딩은 대체 관계가 아닙니다. 또 HTML 엔티티 인코딩만으로 href에 들어간 javascript: 주소까지 막을 수는 없으므로, 링크 값은 허용할 스킴(https: 등)을 따로 검사해야 합니다.
  • 잘못된 문자열을 디코딩하기: decodeURIComponent("%E0%A4%A")처럼 형식이 깨진 값을 넣으면 URIError가 발생합니다. 외부에서 들어온 값을 디코딩할 때는 예외 처리를 함께 둡니다.
  • &를 나중에 바꾸기: 직접 이스케이프 함수를 만들 때 &를 마지막에 바꾸면 이미 만든 &lt;가 &amp;lt;로 망가집니다.

6. 정리

URL에 값을 넣을 때는 퍼센트 인코딩을, HTML에 사용자 문자열을 넣을 때는 엔티티 인코딩(또는 textContent)을 사용합니다. 인코딩은 "데이터가 들어가는 위치"의 문법에 맞춰 고르는 것이 원칙입니다.

인코딩 결과를 직접 확인해 보고 싶다면 anyTools의 URL 변환기를 쓸 수 있으며, 입력값은 서버로 전송되지 않고 브라우저에서 처리됩니다. 바이너리를 문자로 옮기는 다른 인코딩은 Base64 인코딩 이해하기에서 설명합니다.

참고 자료