URL 씹고 뜯고 맛보고 즐기기

미누캉·2020년 8월 25일
3

자바스크립트에서 URL을 다루는 것은 쉽게 접할 수 있습니다. 특히 브라우저와 같은 클라이언트 영역에서는 필수적으로 다루는데요, 이러한 URL 구성요소들을 다루는 것을 외부 API 없이 (레거시 환경에서 폴리필이 필요 할 수도 있습니다) 자유롭게 씹고 뜯고 잘 다루는 것을 알아보겠습니다.

이 아티클에서 말하는 레거시란, 브라우저 기준 IE11 브라우저 이하의 환경을 말하며, Node.js V10 이전의 환경을 말합니다.

URL의 구성요소

URL의 구성요소는 위와 같습니다. 간단하게 사이트 하나 들어가서, 콘솔로 location[구성요소] 로 접근하여 해당하는 사이트의 URL 구성요소를 확인할 수 있습니다.

하지만 location 객체는 현재 페이지의 정보만 알 수 있지, 다른 URL에 대한 파싱된 구성요소를 확인 할 수 없습니다.

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']}

굳이 저런 포맷을 사용하는게 아니라면, 이제 더 이상 외부 모듈을 사용하지 않아도 됩니다. 알아보러 가볼까요?

URL 파싱 (현재)

현재는 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 객체를 만들때, 첫번째 인자로 정확한 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
profile
위버스 컴퍼니에서 프론트엔드 개발자로 일하고 있습니다.

1개의 댓글

comment-user-thumbnail
2020년 9월 27일

감사합니다!

답글 달기