개발 이야기3분 읽기

URL 인코딩, 어디를 얼마나 해야 하나

경로와 쿼리는 규칙이 다르고, `+`는 한쪽에서만 공백입니다. 이중 인코딩과 한글 도메인의 Punycode까지 정리했습니다.

URL에 값을 넣을 때 인코딩을 어디까지 해야 하는지는 매번 애매합니다. 함수 이름이 비슷한 것이 여럿이고, 위치에 따라 규칙도 다릅니다.

인코딩하지 않아도 되는 문자

퍼센트 인코딩은 문자를 바이트로 바꾼 뒤 각 바이트를 % 두 자리 16진수로 적는 방식입니다. 한글은 UTF-8로 세 바이트이므로 한 글자가 %EC%95%88처럼 아홉 글자가 됩니다.

인코딩하지 않아도 되는 문자는 정해져 있습니다.

A-Z  a-z  0-9  -  .  _  ~

나머지는 두 부류입니다.

  • 예약 문자: / ? # [ ] @ ! $ & ' ( ) * + , ; = 구조를 나타내는 데 쓰입니다. 구분자로 쓸 때는 그대로 두고, 값 안에 들어갈 때는 인코딩합니다
  • 그 외 — 공백, 한글, 이모지 등. 항상 인코딩합니다

핵심은 예약 문자 쪽입니다. 쿼리에서 &는 항목을 나누는 구분자이므로 그대로 둡니다. 그런데 값 안에 &가 들어간다면 %26으로 바꿔야 합니다. 그러지 않으면 값 하나가 두 개로 쪼개집니다.

값만 인코딩한다

그래서 URL 전체를 한 번에 인코딩하는 것은 대개 잘못입니다. 조립할 때 값 단위로 인코딩하고, 구분자는 직접 씁니다.

자바스크립트의 두 함수가 이 구분에 대응합니다.

  • encodeURIComponent() — 예약 문자까지 인코딩합니다. 값 하나를 넣을 때 씁니다
  • encodeURI() — 예약 문자는 그대로 둡니다. 이미 완성된 URL에서 공백이나 한글만 정리할 때 씁니다

값에 encodeURI()를 쓰면 &=가 살아남아 구조가 깨집니다. 반대로 완성된 URL에 encodeURIComponent()를 쓰면 ://까지 인코딩되어 문자열 하나가 됩니다.

가능하면 문자열을 직접 잇기보다 URLSearchParams나 각 언어의 URL 빌더를 씁니다. 값 인코딩을 알아서 처리합니다.

공백과 +

같은 공백이 자리에 따라 다르게 표기됩니다.

  • 경로에서는 %20입니다. +는 그냥 더하기 기호입니다
  • 쿼리 문자열에서는 %20+가 모두 공백으로 읽힙니다. HTML 폼이 보내는 application/x-www-form-urlencoded 형식이 공백을 +로 적기 때문입니다

여기서 문제가 생깁니다. 쿼리 값에 진짜 더하기 기호를 넣어야 한다면 %2B로 인코딩해야 합니다. 그러지 않으면 받는 쪽에서 공백이 됩니다. 전화번호 +82 10-1234-5678이나 Base64 값(+가 들어갑니다)을 쿼리로 넘길 때 자주 겪는 일입니다.

이중 인코딩

이미 인코딩된 값을 한 번 더 인코딩하면 %%25로 바뀝니다.

안       → %EC%95%88
%EC%95%88 → %25EC%2595%2588

받는 쪽에서 한 번만 디코딩하면 %EC%95%88이라는 문자열이 그대로 남습니다. 값이 여러 계층(프론트엔드 → API 게이트웨이 → 백엔드)을 지날 때, 각 계층이 인코딩을 한 번씩 더 하면서 생깁니다.

%25가 보이면 이중 인코딩을 의심합니다. 규칙은 인코딩한 쪽이 한 번, 디코딩하는 쪽이 한 번입니다. 중간에서 값을 다시 조립한다면 그 계층에서 디코딩과 인코딩이 짝을 이루는지 확인합니다.

도메인은 퍼센트 인코딩을 쓰지 않는다

주소의 호스트 부분은 규칙이 다릅니다. DNS는 ASCII만 다루므로, 한글이나 다른 문자가 들어간 도메인은 Punycode로 변환됩니다.

한국.kr  →  xn--3e0b707e.kr

xn-- 접두사가 변환된 도메인이라는 표시입니다. 브라우저 주소창에는 원래 글자로 보여 주지만, 실제로 요청이 나갈 때는 변환된 형태입니다.

여기에 알아 둘 것이 있습니다. 서로 다른 문자 중에 화면에서 똑같아 보이는 것들이 있습니다. 키릴 문자 а와 라틴 문자 a는 눈으로 구분되지 않지만 다른 문자입니다. 이걸 이용해 유명 도메인과 똑같아 보이는 주소를 만들 수 있습니다.

브라우저는 이런 경우 주소창에 Punycode 형태를 그대로 노출하는 식으로 방어합니다. 의심스러운 주소는 Punycode로 변환해 보면 xn--로 시작하는지 바로 확인됩니다.

확인할 때

  • 쿼리 값에 &, =, +, #이 들어가면 인코딩되었는지 봅니다
  • %25가 보이면 인코딩이 한 번 더 걸린 것입니다
  • 값이 잘려 들어왔다면 구분자로 해석된 문자가 있는지 봅니다
  • 호스트에 xn--이 보이면 원래 어떤 글자였는지 확인합니다

tools.onuel.devURL 인코딩디코딩이 있고, 주소를 조각별로 나눠 보여 주는 URL 파서Punycode 변환도 함께 있습니다.

  • #URL
  • #인코딩
  • #