URL 경로 세그먼트 추출, 라우트로 바꾸기

주소창이나 로그에서 복사한 긴 URL 한 줄을 라우트로 옮길 때, 어디까지가 경로이고 어디부터가 쿼리인지, 어떤 조각이 변수가 되는지 헷갈리는 분들을 위한 글입니다. 결론부터 말하면 URL의 분해는 규칙 몇 개로 딱 떨어집니다.
슬래시로 나뉜 경로 조각에만 인덱스가 붙고, ? 뒤의 쿼리는 경로 세그먼트로 세지 않으며, 세그먼트가 변수가 되는지 여부는 정해진 조건 하나로 판정됩니다. 이 글은 그 계산이 정확히 어떻게 이뤄지고 결과를 어떻게 읽는지만 다룹니다.
요약 ① URL은 프로토콜(https:)·호스트(hostname)·경로 세그먼트·쿼리로 나뉘고, 슬래시로 쪼갠 경로 조각에만 /0·/1 순서로 인덱스가 붙습니다. ②
?뒤의 쿼리는 경로 세그먼트로 세지 않고 별도의 키-값 테이블로 빠지며, 연속 슬래시나 후행 슬래시가 만든 빈 조각은 자동으로 걸러집니다. ③ 세그먼트가 숫자로만 되어 있거나 아홉 자 이상이면 동적 파라미터(:변수 / [변수])로 치환되고, 그 외에는 원문 그대로 남습니다.
긴 URL을 라우트로 옮길 때 조각을 잘못 세는 문제
예를 들어 https://shop.example.com/products/948213/reviews/?sort=recent 같은 URL을 라우트로 옮긴다고 해보겠습니다.
여기서 자주 막히는 지점이 세 가지입니다. ?sort=recent가 하나의 경로 조각인지, 맨 끝의 슬래시가 빈 조각을 하나 더 만들어 인덱스를 밀어내는지, 그리고 948213처럼 생긴 값만 변수로 잡아야 하는지입니다.
이 셋을 감으로 처리하면 라우트가 실제 요청과 어긋나기 쉽습니다. 다행히 이 판정은 전부 기계적인 규칙이라, 원리만 알면 매번 같은 답이 나옵니다.
도구 없이 브라우저만으로 분해하는 방법
이 계산은 브라우저에 내장된 URL API로 그대로 따라 할 수 있습니다. 개발자 도구(F12) 콘솔에 아래를 붙여넣으면 됩니다.
const u = new URL("https://shop.example.com/products/948213/reviews/?sort=recent");
u.protocol; // "https:"
u.hostname; // "shop.example.com"
u.pathname; // "/products/948213/reviews/"
u.pathname.split("/").filter(Boolean); // ["products", "948213", "reviews"]
[...u.searchParams]; // [["sort", "recent"]]
순서를 풀어보면 이렇습니다.
protocol과hostname이 프로토콜과 호스트로 먼저 떨어져 나옵니다.pathname을 슬래시(/)로 자른 뒤 빈 문자열을 걸러냅니다. 맨 앞 슬래시가 만든 빈 조각, 연속 슬래시, 후행 슬래시가 만든 빈 조각이 여기서 전부 사라집니다.- 남은 조각을 앞에서부터 0, 1, 2로 번호를 매깁니다. 그래서 위 예시는
products(/0),948213(/1),reviews(/2) 세 개입니다. ?뒤의 쿼리는searchParams로 따로 읽어 키-값으로 봅니다. 경로 조각 수에는 들어가지 않습니다.
마지막으로 각 조각이 변수인지 판정합니다. 규칙은 단순합니다. 숫자로만 이루어졌거나, 길이가 여덟 자를 넘으면(아홉 자 이상) 변수로 치환하고, 그렇지 않으면 원문 그대로 둡니다. 위 예시에서는 948213만 숫자라 변수가 되고 products·reviews는 그대로 남습니다.
저희 도구로 확인하기.
URL을 붙여넣으면 경로 세그먼트·쿼리와 Express·Next.js 라우트 패턴이 즉시 계산됩니다. 설치·로그인 없이 무료로 동작합니다.

결과를 읽을 때 흔히 하는 오해
가장 잦은 착각은 쿼리 파라미터를 경로 세그먼트로 세는 것입니다. ?sort=recent는 세그먼트가 아니라 쿼리 테이블에 따로 들어가므로, 인덱스 /2 다음에 /3으로 이어지지 않습니다.
두 번째는 후행 슬래시가 인덱스를 한 칸 밀어낼 것이라는 오해입니다. 빈 조각은 걸러지기 때문에 /reviews와 /reviews/는 세그먼트 결과가 같습니다.
세 번째는 "id처럼 생긴 값은 다 변수가 된다"는 기대입니다. 판정은 값의 의미가 아니라 모양(숫자 여부·길이)만 봅니다. 그래서 짧은 문자열 id(abc 같은)는 그대로 남고, 필요하면 변수명 칸에 이름을 직접 입력해 변수로 바꿀 수 있습니다.
같은 세그먼트가 두 프레임워크에서 어떻게 표기되는지는, 도구의 기본 예시 https://example.com/users/123/posts/abc 를 기준으로 보면 이렇습니다.
| 세그먼트 (인덱스) | 판정 | Express.js | Next.js App Router |
|---|---|---|---|
users (/0) | 문자열·여덟 자 이하 | users | users |
123 (/1) | 숫자로만 | :param1 | [param1] |
posts (/2) | 문자열·여덟 자 이하 | posts | posts |
abc (/3) | 문자열·여덟 자 이하 | abc | abc |
그래서 완성된 라우트는 Express에서는 /users/:param1/posts/abc, Next.js App Router에서는 app/users/[param1]/posts/abc/page.tsx가 됩니다.
기본 변수명은 인덱스에 따라 param0, param1처럼 자동으로 붙습니다. 이름을 바꾸기 전에는 :userId가 아니라 :param1으로 나오고, 칸에 원하는 이름을 입력하면 그때 :userId(Express)와 [userId](Next.js)로 함께 바뀝니다. introduction처럼 아홉 자 이상인 조각은 숫자가 아니어도 조건에 걸려 변수가 됩니다.
정리
- URL은 프로토콜·호스트·경로·쿼리로 나뉘고, 슬래시로 쪼갠 경로 조각에만 /0·/1 인덱스가 붙습니다.
?뒤의 쿼리는 경로 세그먼트가 아니라 별도의 키-값으로 읽으며, 세그먼트 개수에 포함되지 않습니다.- 연속 슬래시·후행 슬래시가 만든 빈 조각은 자동으로 걸러지므로 인덱스가 밀리지 않습니다.
- 세그먼트는 숫자로만 되어 있거나 아홉 자 이상일 때 변수로 치환되고, 나머지는 원문 그대로 남습니다. 직접 이름을 넣으면 조건과 무관하게 변수가 됩니다.
- 같은 세그먼트가 Express에서는
:변수, Next.js App Router에서는[변수]폴더로 표기됩니다.
URL을 붙여넣으면 경로 세그먼트·쿼리와 Express·Next.js 라우트 패턴이 즉시 계산됩니다. 설치·로그인 없이 무료로 동작합니다.
자주 묻는 질문
쿼리 파라미터(? 뒤)도 경로 세그먼트로 세나요?
아니요. ? 뒤의 쿼리는 경로가 아니라 쿼리 스트링이라 슬래시로 나누는 세그먼트에 포함되지 않습니다. 쿼리는 searchParams로 따로 읽혀 키-값 테이블로 표시되며, 경로 세그먼트의 인덱스(/0·/1 …)에 이어 붙지 않습니다.
후행 슬래시가 있으면 세그먼트 인덱스가 하나 밀리나요?
밀리지 않습니다. 경로를 슬래시로 자른 뒤 빈 문자열을 걸러내기 때문에, 맨 끝 슬래시나 중간의 연속 슬래시가 만든 빈 조각은 사라집니다. 그래서 /a/b/, /a/b, /a//b는 모두 세그먼트가 동일하게 잡힙니다.
어떤 조각은 변수가 되고 어떤 건 그대로인 기준이 뭔가요?
두 조건 중 하나에 해당하면 변수로 치환됩니다. 세그먼트가 숫자로만 되어 있거나, 길이가 여덟 자를 넘는(아홉 자 이상) 경우입니다. 이 판정은 값의 의미가 아니라 모양만 봅니다. 조건에 해당하지 않아도 변수명 칸에 이름을 직접 입력하면 그 조각은 변수로 바뀝니다.
프로토콜(http/https) 없이 경로만 입력해도 되나요?
됩니다. https://로 시작하지 않는 입력은 임시 호스트를 붙여 파싱하므로, /users/123/posts/abc처럼 경로만 넣어도 세그먼트가 분해됩니다. 표준 파싱이 실패하는 특수한 입력은 ?와 /로 수동 분리하는 폴백으로 처리됩니다.
