검색 URL의 값에 &가 들어가 다른 파라미터로 잘리거나, A+B를 다시 읽었더니 A B가 되는 경우가 있습니다. 첫 문제는 어디를 인코딩할지, 둘째는 쿼리를 어떤 방식으로 읽는지에 달렸습니다. 아래 예시를 사이트 도구와 브라우저 API로 직접 비교해 보세요.
component냐 full이냐, 먼저 범위를 정하라
자바스크립트에는 URL 인코딩 함수가 둘입니다. 차이는 예약문자를 건드리느냐입니다.
encodeURIComponent(component 모드)는 / ? # & = 같은 예약문자까지 전부 퍼센트 인코딩합니다. 값 하나, 경로 한 조각을 다룰 때 씁니다. encodeURI(full 모드)는 그 구분자들을 보존하며, 아직 퍼센트 인코딩되지 않은 주소 텍스트에 적용합니다. MDN도 둘을 용도로 구분합니다. 이 두 함수를 혼동하면 쿼리 값의 경계가 달라질 수 있습니다.
쿼리 값에 예약문자가 있으면 component
한 가지 재현하기 쉬운 오류는 파라미터 값 안에 &나 =가 들어갈 때입니다.
값으로 a=1&b=2를 넣는다고 해봅시다. PiPi Worlds URL 인코딩 도구에서 component로 인코딩하면 a%3D1%26b%3D2가 나옵니다. =와 &가 각각 %3D, %26으로 바뀌어 하나의 값으로 안전하게 들어갑니다. 반면 full로 인코딩하면 a=1&b=2 그대로입니다. 이 값을 ?x=a=1&b=2처럼 붙이면 서버는 &b=2를 별개 파라미터로 읽어 데이터가 깨집니다. 값에는 component가 답입니다.
인코딩 전 주소 텍스트 전체라면 full
반대 상황도 있습니다. 주소의 구분자는 만들었지만 한글·공백 등은 아직 퍼센트 인코딩하지 않은 경우입니다.
https://pipi-worlds.com/길?q=안녕을 full로 인코딩하면 https://pipi-worlds.com/%EA%B8%B8?q=%EC%95%88%EB%85%95이 됩니다. ://, /, ?, =는 그대로 두고 한글만 퍼센트로 바꿉니다. 여기에 component를 쓰면 https%3A%2F%2Fpipi-worlds.com...처럼 스킴 구분자까지 인코딩됩니다. 이미 인코딩된 URL은 full로 다시 처리하지 마세요. 예를 들어 a%20b는 a%2520b로 바뀌어 원래 의미가 달라집니다. 새 쿼리 값은 URL.searchParams.set으로 추가하세요.
’+‘가 공백이 되는 함정
디코딩에서 사람을 헷갈리게 하는 게 +입니다. 같은 a+b가 출처에 따라 다르게 풀립니다.
+의 의미는 “URL인지 아닌지”만으로 결정되지 않습니다. 특히 URL의 쿼리 문자열을 URLSearchParams로 읽으면 application/x-www-form-urlencoded 규칙에 따라 원문 +가 공백이 됩니다. HTML 폼뿐 아니라 브라우저의 쿼리 API도 이 규칙을 사용합니다.
decodeURIComponent("a+b") // "a+b"
new URLSearchParams("q=a+b").get("q") // "a b"
new URLSearchParams("q=a%2Bb").get("q") // "a+b"
const url = new URL("https://example.com/search");
url.searchParams.set("q", "A+B & C");
url.searchParams.get("q") // "A+B & C"
// 직렬화된 q 값의 +는 %2B, 공백은 +, &는 %26
따라서 이미 조합된 쿼리 q=A+B를 값 그대로 보존해야 한다면 원문 +를 %2B로 인코딩해야 합니다. 가장 간단한 방법은 문자열을 직접 이어 붙이지 않고 URL.searchParams.set에 인코딩 전 값을 주는 것입니다. 사이트 도구의 “+를 공백으로”는 Component 모드에서 쿼리·폼 값 한 조각을 수동 디코딩할 때만 켜세요. Full URL 모드에서는 경로를 훼손하지 않도록 +를 모두 보존하므로, URL 속 쿼리 해석은 URLSearchParams로 하거나 그 값만 복사해 Component 모드로 확인해야 합니다.
%25가 보이면, 원본 값과 다시 비교하기
검색어 한글+A B를 전달하고 싶다고 가정합니다. 아래는 실제 검색 요청을 보내지 않는 가상의 시험입니다. 사이트 변환 도구에 넣을 원본 값은 다음 한 줄입니다.
한글+A B
URL 인코딩 도구에서 Component · 인코딩을 고르면 아래 문자열이 됩니다. 디코딩으로 되돌리면 원본과 같아야 합니다.
%ED%95%9C%EA%B8%80%2BA%20B
하지만 이 결과를 searchParams.set에 다시 넣으면 안 됩니다. MDN의 퍼센트 인코딩 설명처럼 set에는 인코딩 전 값을 줍니다. 이미 인코딩한 값의 %도 문자로 취급해 다시 인코딩하기 때문입니다. 다음 JavaScript는 메모리 안에서 문자열만 만들고 읽습니다. 주소를 열거나 요청을 보내는 코드는 없습니다.
const original = "한글+A B";
const direct = new URLSearchParams();
direct.set("q", original);
const twice = new URLSearchParams();
twice.set("q", encodeURIComponent(original));
console.log(direct.toString());
// q=%ED%95%9C%EA%B8%80%2BA+B
console.log(twice.toString());
// q=%25ED%2595%259C%25EA%25B8%2580%252BA%2520B
console.log(new URLSearchParams(direct.toString()).get("q"));
// 한글+A B
console.log(new URLSearchParams(twice.toString()).get("q"));
// %ED%95%9C%EA%B8%80%2BA%20B
직접 넣은 쿼리는 읽었을 때 한글+A B로 돌아옵니다. 미리 인코딩한 쿼리는 읽어도 퍼센트 문자열이 남아 원본과 다릅니다. 공백이 도구에서는 %20, 쿼리 직렬화에서는 +로 보이는 차이와 원본 값이 달라지는 문제를 구분하세요.
%25를 무조건 지우면 안 됩니다
원본이 할인 20%라면 쿼리에 %25가 생기는 것이 맞습니다. 더 나아가 원본이 문자 그대로 a%20b일 수도 있습니다. 그 경우 q=a%2520b를 한 번 읽어 a%20b로 돌려주는 것이 올바릅니다. “한글이 나올 때까지 디코딩”하거나 %25를 일괄 치환하면 의도된 문자를 바꿀 수 있습니다.
- 원본을 먼저 적습니다. 보내려던 값이 한글인지, 퍼센트 문자를 포함한 문자열인지 구분합니다. 비밀값 대신 위 시험값을 쓰세요.
- 인코딩을 맡는 단계를 확인합니다. 직접 값 한 조각을 변환할지,
URLSearchParams에 원문을 맡길지 정합니다. 두 경로를 무심코 겹치지 않습니다. - 읽은 값을 원본과 대조합니다. 위 예제의
get("q")결과를 확인합니다. 기대한 원본과 다르면 값을 만드는 쪽을 고치고, 이유 없이 수신 쪽에 추가 디코딩을 붙이지 않습니다.
이 왕복 비교는 이 예제의 문자열 보존 검사입니다. 실제 서버의 파서·서명 규칙·업무 처리까지 검증하거나 링크의 안전성을 보증하지 않습니다.
도구로 확인할 때와 실제 링크를 만들 때
PiPi Worlds URL 인코딩 도구는 component/full 범위를 고르고 + 해석을 비교하는 수동 변환기입니다. 전체 URL 모드의 encodeURI는 &, #, ? 같은 구분자를 남기므로, 아직 만들지 않은 쿼리 값까지 안전하게 조합해 주지는 않습니다. 새 링크를 만들 때는 위처럼 URL과 searchParams.set을 쓰세요. 변환 입력은 브라우저 안에서 처리되지만 페이지 분석 비컨은 별도로 동작할 수 있고, 값을 실제 URL에 넣어 방문하면 그 주소가 요청·기록에 남을 수 있습니다. 잘린 % 시퀀스는 오류로 표시합니다.