URL 인코더·디코더
'파라미터'는 / ? & = 까지 이스케이프하므로 단일 쿼리 값에 사용하세요. '전체 URL'은 이를 유지해 URL 구조가 보존됩니다.
입력한 내용은 저장, 기록, 공유되지 않습니다.
텍스트를 URL용으로 퍼센트 인코딩하거나 다시 디코딩합니다. 단일 쿼리 값인지 전체 URL인지에 따라 필요한 이스케이프가 다르므로 모드를 선택할 수 있습니다.
사용 방법
- 인코딩 또는 디코딩을 선택합니다.
- 인코딩할 때 단일 값이면 파라미터를, 전체 주소면 전체 URL을 선택합니다.
- 텍스트를 붙여넣고 결과를 복사합니다.
모드가 두 가지인 이유
RFC 3986은 URL에 나올 수 있는 문자를 두 그룹으로 나눕니다. 비예약 문자, 즉 영문자, 숫자, 그리고 - _ . ~ 네 기호는 항상 그 자체를 뜻하며 절대 인코딩하지 않습니다. 예약 문자는 구조적인 의미를 가집니다. 스킴 뒤의 콜론, 경로 세그먼트 사이의 슬래시, 쿼리를 시작하는 물음표, 매개변수를 구분하는 앰퍼샌드와 등호, 프래그먼트를 시작하는 해시가 그것입니다.
두 모드의 차이는 이 두 번째 그룹을 어떻게 다루느냐뿐입니다. 컴포넌트 모드는 예약 문자까지 이스케이프합니다. 텍스트가 쿼리 값 하나나 경로 세그먼트 하나처럼 한 칸 안에 들어가 구조로 읽혀서는 안 될 때 씁니다. 전체 URL 모드는 예약 문자를 그대로 두므로, 주소 전체가 스킴과 슬래시, 구분자의 역할을 유지한 채 변환됩니다.
잘못된 모드를 쓰면 대개 바로 티가 납니다. 주소 전체를 컴포넌트 모드로 변환하면 https://example.com이 https%3A%2F%2Fexample.com이 되어 더 이상 열리지 않습니다. 값 하나를 전체 URL 모드로 변환하면 그 안에 섞인 앰퍼샌드가 매개변수를 조용히 둘로 쪼갭니다.
문제를 일으키는 문자들
가장 먼저 마주치는 것은 공백입니다. 경로나 쿼리에서는 %20이 됩니다. HTML 폼 제출에서는 오래된 application/x-www-form-urlencoded 규칙에 따라 +가 되며, 두 관례 모두 지금도 매일 쓰입니다. 디코딩할 때 '+를 공백으로' 옵션이 있는 이유입니다. 폼 쿼리 문자열이면 켜고, 경로라면 끄세요. 경로에서 +는 글자 그대로의 더하기 기호이므로 공백으로 바꾸면 값이 망가집니다.
값 안의 앰퍼샌드와 등호는 반드시 이스케이프해야 합니다. 그렇지 않으면 서버가 쿼리를 엉뚱한 곳에서 나눕니다. 이런 문자가 우연히 들어간 검색어나 Base64 덩어리가 대표적인 예입니다. 해시는 아예 서버로 전송되지 않습니다. 그 뒤는 모두 브라우저가 처리하는 프래그먼트이므로, 매개변수 안의 이스케이프되지 않은 #은 요청을 조용히 잘라 버립니다.
값 안의 슬래시는 특히 주의해야 합니다. %2F로 퍼센트 인코딩하면 합법이지만, 라우팅 전에 이를 디코딩하는 서버와 프록시가 적지 않아 여전히 경로 구분자로 읽힐 수 있습니다. 값에 슬래시가 들어갈 수 있다면 경로가 아니라 쿼리에 넣어 문제 자체를 피하세요.
이중 인코딩
퍼센트 인코딩은 멱등하지 않습니다. 퍼센트 기호 자체도 인코딩이 필요한 문자라 %25가 되므로, 이미 인코딩된 문자열을 다시 인코딩하면 %20이 %2520이 됩니다. 이 문자열은 인코딩을 한 번 더 거친 URL의 눈에 보이는 흔적이며, 한 번 알아보고 나면 어디서든 눈에 띕니다.
애플리케이션 코드가 값을 인코딩한 뒤, 원시 텍스트를 받는다고 가정한 프레임워크, HTTP 클라이언트, 템플릿이 다시 인코딩할 때 생깁니다. 해결책은 두 단계 중 하나를 없애는 것이지, 받는 쪽에서 두 번 디코딩하는 것이 아닙니다. 그렇게 하면 버그의 위치만 옮겨질 뿐입니다. 여기서 한 겹마다 한 번씩 디코딩하며 %2520이 %20이 되고 다시 공백이 되는지 보면, 실제로 몇 겹이었는지 알 수 있습니다.
비ASCII 문자와 퍼센트 인코딩이 다루지 않는 것
텍스트는 먼저 UTF-8로 변환된 뒤 각 바이트가 퍼센트 인코딩되므로, 한글 한 글자는 이스케이프 시퀀스 하나가 아니라 세 개가 됩니다. 이는 URL 표준이 요구하는 방식이며, 한국어, 일본어, 중국어, 악센트가 붙은 라틴 문자, 이모지가 여기서 깔끔하게 왕복되는 이유입니다.
도메인 이름은 예외이며, 이 구분이 중요합니다. URL의 호스트 부분은 퍼센트 인코딩하지 않습니다. 국제화 도메인 이름은 대신 Punycode로 변환되어 xn--로 시작하는 이름이 됩니다. 호스트 이름을 퍼센트 인코딩하면 인코딩된 것처럼 보이지만 어디로도 연결되지 않는 결과가 나옵니다. 이 도구가 다루는 것은 경로, 쿼리, 프래그먼트뿐입니다.
URL의 해부
URL은 대부분의 사람이 실제로 쓰는 것보다 많은 부분으로 이루어져 있습니다. 스킴, 선택적인 userinfo 구간, 호스트, 선택적인 포트, 경로, 쿼리, 프래그먼트입니다. 전부 풀어서 쓴 https://user:[email protected]:8443/a/b?x=1#top 에는 이 모두가 들어 있습니다. userinfo 형식은 HTTP에서 더 이상 권장되지 않으며 브라우저는 이를 제거하거나 경고를 띄웁니다. 악의적인 호스트를 익숙한 호스트처럼 보이게 만드는 데 쓰였기 때문입니다.
각 부분의 규칙은 서로 다릅니다. 호스트는 대소문자를 구분하지 않고 퍼센트 인코딩을 포함할 수 없습니다. 경로는 대부분의 서버에서 대소문자를 구분합니다. 쿼리는 문자열이라는 것 외에 표준 문법이 없고, key=value를 앰퍼샌드로 구분하는 방식은 명세가 아니라 관습일 뿐입니다. 그리고 프래그먼트는 브라우저 밖으로 나가지 않습니다.
사람들이 가장 놀라는 부분은 상대 경로 해석입니다. /a/page에서 b/c로 향하는 링크는 /a/b/c로 해석되지만, /a/page에서는 /a/page/b/c가 됩니다. 끝의 슬래시가 기준을 바꿔 버리는 것입니다. 끝 슬래시가 하나 빠지거나 더 붙었을 때 페이지의 모든 상대 리소스가 깨지는 이유가 여기에 있고, 두 형태 사이의 정규화 리다이렉트를 한 번 제대로 설정해 둘 가치가 있는 이유이기도 합니다.
쿼리 스트링에는 규칙이 아니라 관습만 있다
쿼리 스트링에서 배열을 표현하는 표준은 없습니다. 그래서 프레임워크마다 방식이 다릅니다. tags=a&tags=b는 같은 키를 반복하고, tags[]=a&tags[]=b는 대괄호를 붙이며, tags=a,b는 구분자를 씁니다. 셋 다 유효한 URL이고, 반대편에서 어떤 라이브러리를 쓰느냐에 따라 각각 다른 값으로 파싱됩니다.
중첩 구조나 불리언, 빈 값도 마찬가지입니다. flag=는 false일까요, 빈 문자열일까요, 아니면 존재하지만 값이 지정되지 않은 상태일까요? 답은 받는 쪽 파서가 정하는 것이므로, 이 부분이 중요한 API라면 문서로 명시하고 추측 대신 검증해야 합니다.
JavaScript에서는 URLSearchParams가 인코딩과 중복 키를 올바르게 처리하므로 문자열을 직접 조립하는 것보다 이쪽을 쓰는 편이 좋습니다. 다만 URLSearchParams는 폼 관습에 따라 공백을 플러스 기호로 인코딩하고 encodeURIComponent는 %20을 만든다는 점에 유의하세요. 쿼리 스트링에서는 둘 다 공백으로 디코딩되지만, 경로에서는 한쪽만 그렇습니다.
URL 처리에 숨어 있는 보안 문제
오픈 리다이렉트는 파라미터로 전달된 URL을 검사하지 않고 방문자를 그리로 보내 버리는 엔드포인트입니다. 피싱 링크가 피해자가 신뢰하는 도메인에서 시작되도록 만드는 데 쓰이며, 보안 점검에서 여전히 단골로 지적되는 항목입니다. 해결책은 이동 가능한 목적지의 허용 목록을 두거나, 절대 URL 대신 경로만 받는 것입니다.
서버 사이드 요청 위조는 같은 부류의 문제가 안쪽을 향한 경우입니다. 서버가 사용자가 준 URL을 가져오는데, 사용자가 내부 주소나 클라우드 메타데이터 엔드포인트, 혹은 그리로 향하는 리다이렉트를 건네는 것입니다. 여기서는 라이브러리 간 파싱 차이가 거듭 악용되어 왔습니다. 교묘하게 만든 URL이 어떤 호스트를 가리키는지에 대해 두 파서의 판단이 엇갈릴 수 있기 때문입니다.
호모그래프 공격은 국제화 도메인 이름을 악용합니다. 화면에 똑같이 그려지는 키릴 문자와 라틴 문자가 서로 다른 호스트를 만들어 냅니다. 브라우저는 이름에 여러 문자 체계가 섞이면 퓨니코드 형태로 표시해 방어합니다. 의심스러운 링크가 주소창에 xn--로 보이는 이유가 그것이고, 이 접두사는 알아 둘 만합니다.
정규 URL, 중복, 추적 파라미터
같은 페이지는 보통 여러 주소로 접근할 수 있습니다. www가 있는 것과 없는 것, 끝 슬래시가 있는 것과 없는 것, http와 https, index.html이 붙은 형태, 그리고 온갖 추적 파라미터가 덧붙은 형태입니다. 검색 엔진 입장에서는 같은 내용을 가진 별개의 페이지들처럼 보일 수 있고, 그러면 그 페이지가 쌓아 온 권위가 나뉘어 버립니다.
이를 바로잡는 장치는 두 가지입니다. 301 리다이렉트는 구조적인 변형들을 한 가지 형태로 정하고 나머지를 전부 그리로 보내 정리합니다. head 안의 canonical 링크 요소는 리다이렉트할 수 없는 변형들, 주로 파라미터가 붙은 경우에 대해 선호하는 주소를 지정합니다.
추적 파라미터는 따로 언급할 만합니다. utm_source를 비롯한 파라미터들은 유입 시스템이 붙이는 것이라 캠페인마다 서로 다른 URL을 만들어 냅니다. 이들은 분석 도구에 속하지 정규 URL에 속하지 않으며, 기록이 끝난 뒤 클라이언트 쪽에서 떼어 내면 공유되는 링크를 깔끔하게 유지할 수 있습니다. 일부 플랫폼이 자동으로 붙이는 fbclid와 gclid 파라미터도 마찬가지입니다.
오래가는 경로 설계하기
URL은 일종의 약속입니다. 다른 사람들이 그 주소로 링크를 걸고, 그 링크는 당신이 통제할 수 없기 때문입니다. 그래서 경로는 구현이 아니라 리소스를 설명해야 합니다. /page.php?id=4718이 아니라 /guides/subnetting이어야 하고, 주소를 서비스하는 기술에 묶어 버리는 확장자는 붙이지 않아야 합니다.
소문자와 하이픈으로 통일하세요. 서버가 대소문자를 구분한다는 것은 /About과 /about이 다른 페이지가 될 수 있다는 뜻이고, 그것이 의도된 경우는 결코 없습니다. 그리고 하이픈은 슬러그에서 단어를 구분하는 관례적인 기호입니다. 콘텐츠가 진짜 아카이브가 아니라면 경로에 날짜를 넣지 마세요. /2024/pricing은 내용이 최신이더라도 1년만 지나면 낡아 보입니다.
URL을 꼭 바꿔야 한다면 예전 주소를 영구 리다이렉트하고 그 리다이렉트를 계속 유지하세요. 이 목록에서 당신 쪽에서 되돌릴 수 없는 유일한 결정이 링크를 끊는 일입니다. 가장 중요한 링크는 당신이 이름조차 들어 본 적 없는 사이트에 걸려 있기 때문입니다.
자주 묻는 질문
- 파라미터와 전체 URL의 차이는 무엇인가요?
- 파라미터는 encodeURIComponent를 사용해 예약 문자 / ? & = : 까지 이스케이프하므로 단일 쿼리 값이나 경로 조각에 적합합니다. 전체 URL은 encodeURI를 사용해 이를 유지하므로 주소가 그대로 해석됩니다. 전체 URL을 파라미터 모드로 처리하면 https://가 https%3A%2F%2F가 되어 깨집니다.
- 쿼리 파라미터에는 어떤 것을 써야 하나요?
- 파라미터를 사용하세요. 값에 &나 =가 있으면 이스케이프하지 않을 경우 서버가 구분자로 읽어 엉뚱한 위치에서 잘립니다.
- 디코딩할 때 잘못된 이스케이프 오류가 나는 이유는?
- 퍼센트 기호 뒤에는 16진수 두 자리가 와야 합니다. "100%"처럼 단독으로 쓰인 % 나 %E4%B8처럼 잘린 시퀀스는 유효하지 않으므로 추측하지 않고 중단합니다.
- +를 공백으로 처리하는 옵션은 언제 필요한가요?
- 폼 전송은 공백을 %20이 아닌 +로 인코딩합니다. HTML 폼에서 온 쿼리 문자열을 디코딩할 때 켜세요. 경로 조각에서는 +가 실제 더하기 기호이므로 꺼두어야 합니다.
- 한글도 처리되나요?
- 네. 퍼센트 인코딩 전에 UTF-8로 변환되므로 한국어, 일본어, 중국어, 이모지 모두 정확하게 왕복 변환됩니다.
마지막 업데이트:
진행 단계, 예상 결과, 실제 결과를 적어 주세요. 비밀 정보와 개인 정보는 삭제해 주세요.
