블로그 목록
frontend

URLPattern API로 URL 매칭하기 — 라우팅 정규식 직접 짜던 거 그만

간단한 클라이언트 라우터를 직접 만들다 보면 결국 이 문제에 부딪힌다. /users/42/posts/7 같은 경로에서 42랑 7을 어떻게 뽑지?

내가 늘 하던 방식은 두 가지였다. split('/')로 쪼개서 인덱스로 집어오거나, 아니면 정규식을 손으로 짜는 거다.

const re = /^\/users\/([^/]+)\/posts\/([^/]+)$/;
const m = '/users/42/posts/7'.match(re);
// m[1] = '42', m[2] = '7'

돌아는 간다. 근데 경로가 하나 바뀔 때마다 정규식을 다시 들여다봐야 하고, m[1], m[2]가 뭐였는지 매번 세야 한다. 쿼리스트링이나 선택적 세그먼트까지 끼면 금방 지저분해진다.

URLPattern이 이걸 대신 해준다

브라우저에 URLPattern이라는 내장 객체가 있다. 경로를 Express 라우트처럼 :이름 문법으로 쓰면, 매칭도 해주고 이름 붙은 그룹까지 꺼내준다.

const pattern = new URLPattern({ pathname: '/users/:userId/posts/:postId' });

const result = pattern.exec('https://example.com/users/42/posts/7');
console.log(result.pathname.groups);
// { userId: '42', postId: '7' }

정규식 캡처 그룹을 숫자로 세는 대신 userId, postId로 바로 접근한다. 매칭 여부만 알고 싶으면 pattern.test(url)이 true/false를 돌려준다.

와일드카드랑 선택적 세그먼트

문법이 생각보다 풍부하다. *는 와일드카드, ?는 선택적 세그먼트다.

// 선택적 세그먼트 — /posts, /posts/42 둘 다 매칭
new URLPattern({ pathname: '/posts/:id?' });

// 와일드카드 — /files/ 아래 뭐든
const p = new URLPattern({ pathname: '/files/*' });
p.exec('https://x.com/files/a/b/c.png').pathname.groups[0];
// 'a/b/c.png'

pathname만 쓰면 전체 URL을 넣어도 경로 부분만 본다. 필요하면 protocol, hostname, search 같은 다른 조각에도 각각 패턴을 걸 수 있다.

실전에서 쓰는 모양

라우트 테이블을 배열로 두고 순회하는 식으로 쓰면 미니 라우터가 된다.

const routes = [
  { pattern: new URLPattern({ pathname: '/users/:id' }), page: 'user' },
  { pattern: new URLPattern({ pathname: '/posts/:id' }), page: 'post' },
];

function resolve(url) {
  for (const route of routes) {
    const match = route.pattern.exec(url);
    if (match) return { page: route.page, params: match.pathname.groups };
  }
  return { page: '404', params: {} };
}

Service Worker에서 요청을 가로채 분기할 때도 똑같은 패턴이 깔끔하게 들어맞는다.

정리

예전엔 Firefox가 빠져서 눈치를 봤는데, 이제 세 브라우저 다 지원한다(Chrome·Safari·Firefox 139부터). 혹시 구형 환경을 받쳐야 하면 urlpattern-polyfill 하나 넣으면 된다.

손으로 짠 정규식은 "이게 뭘 매칭하는 거였지"를 한 번 더 읽어야 하지만, URLPattern은 라우트 정의가 그대로 읽힌다. 경로에서 값 몇 개 뽑자고 match()[1]을 세던 일은 이제 안 해도 된다.