Effitizer 프로젝트의 기존 코드를 보다가 new URLSearchParams라는 생소한 표현을 발견했다. 어딘가에서 import해 온 흔적이 보이지 않아서 구글에 검색해보니 주소창의 경로를 다룰 수 있는 브라우저의 내장 객체였다. URL 객체와 URLSearchParams를 비교한 제로초 님의 블로그 글과 공식 문서 MDN을 읽고 공부한 내용을 작성해본다.
둘 다 IE에서는 동작하지 않고 모던 브라우저에서만 가능한 기능이라고 한다. (IE는 올해 6월부로 bye했으니..) 프론트엔드 개발자는 웹을 다루니 웹 주소를 자유자재로 조작할 줄 안다면 편리할 것이다. 또한 이렇게 내장된 객체들을 활용하면 queryString 파싱과 같은 귀찮은 작업을 처리하는 것도 편해진다.
new 키워드로 인스턴스를 생성하고 조작할 경로를 인자로 전달한다. URL 객체만의 내장 프로퍼티로 주소의 경로를 조작할 수 있다.
// new 키워드로 URL 객체 생성
const url = new URL('https://www.zerocho.com:8080/category/HTML?hi=yena');
// 여러 프로퍼티로 주소의 일부 구성요소 조회하기
url.href; // 전체 웹 주소 반환 (https://www.zerocho.com:8080/category/HTML)
url.origin; // protocol에서 port까지 반환 (https://www.zerocho.com:8080)
url.protocol; // http:인지 https:인지 반환 (https:)
url.host; // www.부터 port까지 반환 (www.zerocho.com:8080)
url.hostname; // host에서 port만 빼고 반환 (www.zerocho.com)
url.port; // port 번호 반환 (8080)
url.pathname; // 상대경로 반환 (/category/HTML)
url.search; // '?'부터 hash('#')를 제외하고 반환 (?hi=yena)
url.searchParams; // (X) 위에처럼 조회가 안 됨 => *메서드로 조작하는 프로퍼티 (하단 참고!)
URL 객체는 다음과 같은 WHATWG 방식으로 구성된다.
(다시 보니 '주소'가 이 방식으로 구성되어 있다고 보는 게 맞겠다)
(두 개의 origin 칸은 병합해서 하나의 origin 칸으로 보자)
new URL('추가할 상대 경로', '기본 경로');
const url2 = new URL('./category/HTML', 'https://www.zerocho.com');
url2.href; // 전체 경로 반환 (https://www.zerocho.com/category/HTML)
url.search = '?hello=zerocho';
url.href; // 전체 경로 반환 (https://www.zerocho.com/category/HTML?hello=zerocho)
const url3 = new URL('https://www.zerocho.com?hello=zerocho&hi=yena&hi=js');
url3.search; // 조회 가능 - '?'부터 hash('#') 제외하고 반환 (?hello=zerocho&hi=yena&hi=js)
url3.searchParams; // (X) 불가능 - 출력하면 빈 객체({}) 반환
searchParams는 메서드로 조작이 가능하다고 하였다. 여러 메서드를 활용하여 '?' 이하 경로의 조회/수정/삭제를 해보자. (참고: searchParams로 수정된 경로를 확인할 때는 기본 경로까지 모두 보여주는 url.href보다 '?'부터 보여주는 url.search가 더 편하다)
// 조회하기
url3.searchParams.get('hello'); // 특정 param 하나 조회 (zerocho)
url3.searchParams.getAll('hi'); // 특정 param의 여러 값을 모두 조회 (2개 이상일 경우 배열: ['yena', 'js'])
// 추가하기 (같은 키로 여러 값을 추가하는 것도 가능)
url3.searchParams.append('bye', 'java');
url3.search; // '?'부터 전체 반환 (?hello=zerocho&hi=yena&hi=js&bye=java)
url3.searchParams.append('bye', 'ruby');
url3.search; // 똑같은 'bye'로 한번 더 추가 (?hello=zerocho&hi=yena&hi=js&bye=java&bye=ruby)
// 수정하기 (set: 기본적으로 추가 역할, 기존 키가 있다면 전부 삭제하고 새로운 내용으로 교체)
url3.searchParams.set('bye', 'python');
url3.search; // 기존의 bye가 모두 사라지고 python 하나로 교체 (?hello=zerocho&hi=yena&hi=js&bye=python)
// 삭제하기
url3.searchParams.delete('bye');
url3.search; // 특정 키로 삭제 (?hello=zerocho&hi=yena&hi=js)
const searchParams = new URLSearchParams('hello=zerocho&hi=yena&hi=js');
searchParams.set('bye', 'C#');
// URLSearchParams 객체로 만들어진 주소는 문자열로 변환해줘야 한다.
searchParams.toString(); // 문자열로 변환된 값 (hello=zerocho&hi=yena&hi=js&bye=C%23)
// 'C#'의 '#'이 '%23'으로 변경되었다.
(feat. URLSearchParams에 인자를 전달하는 다양한 방법)
let url = new URL('https://example.com?foo=1&bar=2');
// url.search로 url 주소를 검색할 때
let params1 = new URLSearchParams(url.search);
// 문자열 리터럴 전달하기
let params2 = new URLSearchParams("foo=1&bar=2");
let params2a = new URLSearchParams("?foo=1&bar=2");
// 2차원 배열 전달하기
let params3 = new URLSearchParams([["foo", "1"], ["bar", "2"]]);
// 객체 전달하기
let params4 = new URLSearchParams({ "foo": "1", "bar": "2" });
// params로 추가할 내용을 객체로 선언 (값은 전부 문자열화)
const add_params = {
c: 'a',
d: new String(2),
e: false.toString(),
};
// 기존 url 뒤에 새로운 params 추가
const new_params = new URLSearchParams([
// url.searchParams의 entries(): 기존 url의 params를 각각 배열로 변환
// Array.from 메서드로 하나의 배열로 만듦
...Array.from(url.searchParams.entries()), // [["a", "hello"], ["b", "world"]]
// Object.entries(객체): 객체를 바로 2차원 배열로 반환
...Object.entries(add_params), // [["c", "a"], ["d", "2"], ["e", "false"]]
]).toString(); // URLSearchParams 객체로 만들어진 값은 반드시 따로 문자열화 해줘야 한다!
// '&'로 연결된 새로운 params 반환
console.log(new_params); // a=hello&b=world&c=a&d=2&e=false
위의 내용을 고차 함수로 커스텀한 것이다. (출처: MDN)
// url에 params가 없을 때를 대비하여 params에 기본값으로 빈 객체 할당
const addSearchParams = (url, params = {}) => {
return (
new URL(
`${url.origin}$url.pathname}?
${new URLSearchParams([
...Array.from(url.searchParams.entries()),
...Object.entries(params),
])
.toString()}`
);
);
}