자바스크립트에서 URL을 다루는 것은 쉽게 접할 수 있습니다. 특히 브라우저와 같은 클라이언트 영역에서는 필수적으로 다루는데요, 이러한 URL 구성요소들을 다루는 것을 외부 API 없이 (레거시 환경에서 폴리필이 필요 할 수도 있습니다) 자유롭게 씹고 뜯고 잘 다루는 것을 알아보겠습니다.
이 아티클에서 말하는 레거시란, 브라우저 기준 IE11 브라우저 이하의 환경을 말하며, Node.js V10 이전의 환경을 말합니다.
URL의 구성요소는 위와 같습니다. 간단하게 사이트 하나 들어가서, 콘솔로 location[구성요소]
로 접근하여 해당하는 사이트의 URL 구성요소를 확인할 수 있습니다.
하지만 location
객체는 현재 페이지의 정보만 알 수 있지, 다른 URL에 대한 파싱된 구성요소를 확인 할 수 없습니다.
레거시 Node.js에서는 빌트인 되어 있는 url
모듈을 이용하여 위와 같은 파싱된 URL 구성 요소 객체를 만들 수 있습니다.
const url = require('url');
const location = url.parse('https://minukang.io/page?a=1');
console.log(location.href); // "https://minukang.io/page?a=1"
console.log(location.pathname); // "/page"
console.log(location.search); // "?a=1"
브라우저에서는 앵커 엘리먼트를 생성하면서 이를 구현할 수 있습니다.
const location = document.createElement('a');
location.href = 'https://minukang.io/page?a=1';
console.log(location.href); // "https://minukang.io/page?a=1"
console.log(location.pathname); // "/page"
console.log(location.search); // "?a=1"
URL 구성요소 중 search
에 해당하는 부분을 쿼리스트링(query string)이라고 합니다. 레거시 환경에서는 이를 딕셔너리 객체 형태로 간단하게 사용하게끔 파싱하는게 없습니다. 따라서 직접 파싱하거나, 외부 모듈을 사용합니다. 대표적인 모듈로 qs
, query-string
, querystring
... 등등이 수 많이 있습니다.
NPM Trends: qs-vs-query-string-vs-querystring-vs-querystringify-vs-urijs-vs-url-parse
위 모듈들의 차이는 누가 누가 이런 저런 포맷 파싱을 얼마나 더 기상천외하게 잘 하냐? 라는 정도의 차이입니다. 예를 들어, query-string
모듈은 다음과 같은 포맷의 쿼리 스트링을 파싱하여 사용 할 수 있습니다.
const queryString = require('query-string');
queryString.parse('foo=1,2,3', { arrayFormat: 'comma' });
// => {foo: ['1', '2', '3']}
굳이 저런 포맷을 사용하는게 아니라면, 이제 더 이상 외부 모듈을 사용하지 않아도 됩니다. 알아보러 가볼까요?
현재는 WHATWG에서 제정된 표준 URL 인터페이스를 사용하여, 위의 기능을 모두 누릴 수 있습니다.
레거시 브라우저에서는 whatwg-url 혹은 core-js/stable 을 폴리필로 등록합니다.
폴리필을 사용해야 하는 규모가 있는 프로젝트에서는
core-js/stable
을 사용하는 것을 추천드립니다.
Node.js 7이상 10미만 버전에서는 URL 생성자가 글로벌에 빌트인되어 있지 않아서 다음과 같이 불러와야 합니다.
const URL = require('url').URL;
const location = new URL('https://minukang.io/page?a=1');
console.log(location.href); // "https://minukang.io/page?a=1"
console.log(location.pathname); // "/page"
console.log(location.search); // "?a=1"
쿼리스트링 파싱은 URLSearchParams
라는 인터페이스로 정의됩니다. 이 인터페이스는 Map 을 상속한 형태로 되어 있고 URL 객체 내의 접근 키 값은 searchParams
입니다.
URLSearchParams
가 Map을 상속한 형태라고 표현한 이유는, 기본적으로 Map과 같은 get
, set
, has
, size
, [Symbol.iterator]
를 가지고 있고, 배열 형태의 쿼리스트링을 표현하기 위해 append
, getAll
도 가지고 있기 때문입니다.
const location = new URL('https://minukang.io/page?a=1');
console.log(location.searchParams.get('a')); // "1"
location.searchParams.set('a', '2');
console.log(location.href); // "https://minukang.io/page?a=2"
console.log(location.toString()); // === location.href
searchParams
의 내용을 변경하고 URL객체의 직렬화 결과가 알아서 변경사항을 적용하는 것을 확인 하실 수 있습니다.
또한 쿼리 스트링만 따로 파싱하고 싶을 때, 단독으로도 사용이 가능합니다.
// 첫번째 인자로 다음과 같은 형태의 값이 올 수 있습니다.
let search;
search = '?foo=1&bar=2'; // 쿼리 스트링 문자열 형태 (?은 빠져도됨)
/* or */ search = { foo: '1', bar: '2' }; // 오브젝트 리터럴 형태
/* or */ search = [['foo', '1'], ['bar', '2']] // 요소가 키-값 쌍인, Array
const searchParams = new URLSearchParams(search);
searchParams.set('hi', 'hello');
console.log(searchParams.toString()) // "foo=1&bar=2&hi=hello"
URL 생성자를 이용하여 URL 객체를 만들때, 첫번째 인자로 정확한 URL 규칙을 지켜야합니다. 지켜지지 않은 URL은 TypeError
를 발생시킵니다.
const u = new URL('hahahaha.com'); // 프로토콜이 빠진 잘못된 URL
// **Uncaught TypeError: Failed to construct 'URL': Invalid URL**
이를 활용하여 올바른 URL인지 판단하는 헬퍼를 만들 수 있습니다.
function isValidUrl (url) {
try {
return new URL(url) && true;
} catch (err) {
if (err instanceof TypeError) {
return false;
}
throw err;
}
}
console.log(isValidUrl('https://www.minukang.com')); // true
console.log(isValidUrl('hahahaha.com')); // false
URL 생성자는 두번째 인자로, 기준 경로를 받을 수 있습니다. 이 기준 경로를 이용하여, API endpoint와 같이 미리 정의된 경로를 합치는 작업을 하지 않아도 됩니다.
const API_ENDPOINT = 'https://api.minukang.com/api/';
const apiUrl = new URL('/v1/shop/products', API_ENDPOINT);
return apiUrl.toString(); // https://api.minukang.com/v1/shop/products
위와 같이 첫번째 인자를 절대 경로로 사용하면 기준 경로의 origin 값과 절대 경로를 합쳐줍니다. 그래서 첫번째 값이 절대 경로라 기준 경로(두번째 값)의 path(/api/
)가 사라진 것을 확인 하실 수 있습니다. 상대 경로를 사용한다면 다음과 같은 것도 할 수 있습니다.
const API_ENDPOINT = 'https://api.minukang.com/api/';
const apiUrl = new URL('./v1/shop/products', API_ENDPOINT);
return apiUrl.toString(); // https://api.minukang.com/api/v1/shop/products
상대 경로로 지정해주니 기준 경로의 path가 잘 살아 있음을 확인 할 수 있습니다. 그 외에 ../
처럼 상위 패스로 가는 것도 지정이 가능합니다.
const API_ENDPOINT = 'https://api.minukang.com/api/parent';
const apiUrl = new URL('../v1/shop/products', API_ENDPOINT);
return apiUrl.toString(); // https://api.minukang.com/api/v1/shop/products
감사합니다!