본문으로 건너뛰기

URL 재작성

URL 재작성을 사용하면 브라우저에 표시되는 URL과 라우터가 내부적으로 해석하는 URL 사이를 양방향으로 변환할 수 있습니다. 이 강력한 기능을 사용하면 라우트를 중복하거나 라우트 트리를 복잡하게 만들지 않고 로케일 접두사, 서브도메인 라우팅, 레거시 URL 마이그레이션, 멀티 테넌트 애플리케이션 같은 패턴을 구현할 수 있습니다.

URL 재작성을 사용하는 경우​

다음과 같은 경우 URL 재작성이 유용합니다.

  • i18n 로케일 접두사: 브라우저에는 /en/about을 표시하지만 내부적으로는 /about으로 라우팅합니다.
  • 서브도메인 라우팅: admin.example.com/users를 내부적으로 /admin/users로 라우팅합니다.
  • 레거시 URL 마이그레이션: 새 라우트에 매핑되는 /old-path 같은 이전 URL을 지원합니다.
  • 멀티 테넌트 애플리케이션: tenant1.example.com을 테넌트별 라우트로 라우팅합니다.
  • 사용자 지정 URL 스킴: 모든 URL 패턴을 라우트 구조에 맞게 변환합니다.

URL 재작성 작동 방식​

URL 재작성은 두 방향으로 작동합니다.

  1. 입력 재작성: 라우터가 해석하기 전에 브라우저에서 온 URL을 변환합니다.
  2. 출력 재작성: 브라우저에 기록하기 전에 라우터에서 온 URL을 변환합니다.
┌─────────────────────────────────────────────────────────────────┐
│ Browser URL Bar │
│ /en/about?q=test │
└─────────────────────────┬───────────────────────────────────────┘
│
▼ input rewrite
┌─────────────────────────────────────────────────────────────────┐
│ Router Internal URL │
│ /about?q=test │
│ │
│ (matches routes, runs loaders) │
└─────────────────────────┬───────────────────────────────────────┘
│
▼ output rewrite
┌─────────────────────────────────────────────────────────────────┐
│ Browser URL Bar │
│ /en/about?q=test │
└─────────────────────────────────────────────────────────────────┘

라우터는 location 객체에 두 개의 href 속성을 노출합니다.

  • location.href - 내부 URL(입력 재작성 후)
  • location.publicHref - 브라우저에 표시되는 외부 URL(출력 재작성 후)

기본 사용법​

라우터를 만들 때 재작성을 구성합니다.

import { createRouter } from '@tanstack/react-router'

const router = createRouter({
routeTree,
rewrite: {
input: ({ url }) => {
// Transform browser URL → router internal URL
// Return the modified URL, a new URL, or undefined to skip
return url
},
output: ({ url }) => {
// Transform router internal URL → browser URL
// Return the modified URL, a new URL, or undefined to skip
return url
},
},
})

input 및 output 함수는 URL 객체를 받고 다음 작업을 수행할 수 있습니다.

  • 동일한 url 객체를 변경하여 반환합니다.
  • 새 URL 인스턴스를 반환합니다.
  • 전체 href 문자열을 반환합니다(URL로 파싱됩니다).
  • 재작성을 건너뛰려면 undefined를 반환합니다.

일반적인 패턴​

패턴 1: i18n 로케일 접두사​

입력 시 로케일 접두사를 제거하고 출력 시 다시 추가합니다.

const locales = ['en', 'fr', 'es', 'de']
const defaultLocale = 'en'

// Get current locale (from cookie, localStorage, or detection)
function getLocale() {
return localStorage.getItem('locale') || defaultLocale
}

const router = createRouter({
routeTree,
rewrite: {
input: ({ url }) => {
// Check if pathname starts with a locale prefix
const segments = url.pathname.split('/').filter(Boolean)
const firstSegment = segments[0]

if (firstSegment && locales.includes(firstSegment)) {
// Strip the locale prefix: /en/about → /about
url.pathname = '/' + segments.slice(1).join('/') || '/'
}
return url
},
output: ({ url }) => {
const locale = getLocale()
// Add locale prefix: /about → /en/about
if (locale !== defaultLocale || true) {
// Always prefix, or conditionally skip default locale
url.pathname = `/${locale}${url.pathname === '/' ? '' : url.pathname}`
}
return url
},
},
})

프로덕션 i18n에는 localizeUrl 및 deLocalizeUrl 함수를 제공하는 Paraglide 같은 라이브러리 사용을 고려합니다. 통합 방법은 국제화 가이드를 참고합니다.

패턴 2: 서브도메인에서 경로로 라우팅​

서브도메인 요청을 경로 기반 라우트로 라우팅합니다.

const router = createRouter({
routeTree,
rewrite: {
input: ({ url }) => {
const subdomain = url.hostname.split('.')[0]

// admin.example.com/users → /admin/users
if (subdomain === 'admin') {
url.pathname = '/admin' + url.pathname
}
// api.example.com/v1/users → /api/v1/users
else if (subdomain === 'api') {
url.pathname = '/api' + url.pathname
}

return url
},
output: ({ url }) => {
// Reverse the transformation for link generation
if (url.pathname.startsWith('/admin')) {
url.hostname = 'admin.example.com'
url.pathname = url.pathname.replace(/^\/admin/, '') || '/'
} else if (url.pathname.startsWith('/api')) {
url.hostname = 'api.example.com'
url.pathname = url.pathname.replace(/^\/api/, '') || '/'
}
return url
},
},
})

패턴 3: 레거시 URL 마이그레이션​

새 라우트 구조를 유지하면서 이전 URL을 지원합니다.

const legacyPaths: Record<string, string> = {
'/old-about': '/about',
'/old-contact': '/contact',
'/blog-posts': '/blog',
'/user-profile': '/account/profile',
}

const router = createRouter({
routeTree,
rewrite: {
input: ({ url }) => {
const newPath = legacyPaths[url.pathname]
if (newPath) {
url.pathname = newPath
}
return url
},
// No output rewrite needed - new URLs will be used going forward
},
})

패턴 4: 멀티 테넌트 라우팅​

테넌트별 도메인을 통합 라우트 구조로 라우팅합니다.

const router = createRouter({
routeTree,
rewrite: {
input: ({ url }) => {
// Extract tenant from subdomain: acme.app.com → acme
const parts = url.hostname.split('.')
if (parts.length >= 3) {
const tenant = parts[0]
// Inject tenant into the path: /dashboard → /tenant/acme/dashboard
url.pathname = `/tenant/${tenant}${url.pathname}`
}
return url
},
output: ({ url }) => {
// Extract tenant from path and move to subdomain
const match = url.pathname.match(/^\/tenant\/([^/]+)(.*)$/)
if (match) {
const [, tenant, rest] = match
url.hostname = `${tenant}.app.com`
url.pathname = rest || '/'
}
return url
},
},
})

패턴 5: 검색 매개변수 변환​

재작성 중 검색 매개변수를 변환합니다.

const router = createRouter({
routeTree,
rewrite: {
input: ({ url }) => {
// Convert legacy search param format
// ?filter_status=active → ?status=active
const filterStatus = url.searchParams.get('filter_status')
if (filterStatus) {
url.searchParams.delete('filter_status')
url.searchParams.set('status', filterStatus)
}
return url
},
output: ({ url }) => {
// Optionally transform back for external display
return url
},
},
})

여러 재작성 조합​

서로 독립적인 재작성 변환이 여러 개 필요하면 composeRewrites를 사용하여 결합합니다.

import { composeRewrites } from '@tanstack/react-router'

const localeRewrite = {
input: ({ url }) => {
// Strip locale prefix
const match = url.pathname.match(/^\/(en|fr|es)(\/.*)$/)
if (match) {
url.pathname = match[2] || '/'
}
return url
},
output: ({ url }) => {
// Add locale prefix
url.pathname = `/en${url.pathname === '/' ? '' : url.pathname}`
return url
},
}

const legacyRewrite = {
input: ({ url }) => {
if (url.pathname === '/old-page') {
url.pathname = '/new-page'
}
return url
},
}

const router = createRouter({
routeTree,
rewrite: composeRewrites([localeRewrite, legacyRewrite]),
})

작업 순서:

  • 입력 재작성: 순서대로 실행합니다(처음부터 마지막까지).
  • 출력 재작성: 역순으로 실행합니다(마지막부터 처음까지).

이렇게 하면 조합된 재작성이 올바르게 "래핑 해제"됩니다. 위 예시에서는 다음과 같습니다.

  • 입력: 로케일이 /en을 제거한 다음 레거시 재작성이 /old-page를 리디렉션합니다.
  • 출력: 레거시 재작성이 먼저 실행되고(작업 없음), 로케일이 /en을 다시 추가합니다.

basepath와의 상호작용​

basepath를 구성하면 라우터는 내부적으로 이를 재작성으로 구현합니다. 사용자 지정 rewrite도 제공하면 두 재작성이 자동으로 조합됩니다.

const router = createRouter({
routeTree,
basepath: '/app',
rewrite: {
input: ({ url }) => {
// This runs AFTER basepath is stripped
// Browser: /app/en/about → After basepath: /en/about → Your rewrite: /about
return url
},
output: ({ url }) => {
// This runs BEFORE basepath is added
// Your rewrite: /about → After your rewrite: /en/about → Basepath adds: /app/en/about
return url
},
},
})

조합 순서는 다음을 보장합니다.

  1. 입력: 먼저 basepath를 제거한 다음 재작성을 실행합니다.
  2. 출력: 먼저 재작성을 실행한 다음 basepath를 추가합니다.

<Link> 컴포넌트는 href 속성을 생성할 때 출력 재작성을 자동으로 적용합니다.

// With locale rewrite configured (adds /en prefix)
<Link to="/about">About</Link>
// Renders: <a href="/en/about">About</a>

프로그래밍 방식 탐색​

navigate() 또는 router.navigate()를 통한 프로그래밍 방식 탐색에도 재작성이 적용됩니다.

const navigate = useNavigate()

// Navigates to /about internally, displays /en/about in browser
navigate({ to: '/about' })

출력 재작성이 출처(hostname)를 변경하면 <Link> 컴포넌트는 클라이언트 측 탐색 대신 표준 앵커 태그를 자동으로 렌더링합니다.

// Rewrite that changes hostname for /admin paths
const router = createRouter({
routeTree,
rewrite: {
output: ({ url }) => {
if (url.pathname.startsWith('/admin')) {
url.hostname = 'admin.example.com'
url.pathname = url.pathname.replace(/^\/admin/, '') || '/'
}
return url
},
},
})

// This link will be a hard navigation (full page load)
<Link to="/admin/dashboard">Admin Dashboard</Link>
// Renders: <a href="https://admin.example.com/dashboard">Admin Dashboard</a>

publicHref 속성​

라우터의 location 객체에는 외부 URL(출력 재작성 후)을 포함하는 publicHref 속성이 있습니다.

function MyComponent() {
const location = useLocation()

// Internal URL used for routing
console.log(location.href) // "/about"

// External URL shown in browser
console.log(location.publicHref) // "/en/about"

return (
<div>
{/* Use publicHref for sharing, canonical URLs, etc. */}
<ShareButton url={window.location.origin + location.publicHref} />
</div>
)
}

다음과 같이 실제 브라우저 URL이 필요할 때 publicHref를 사용합니다.

  • 소셜 공유
  • 표준 URL
  • 애널리틱스 추적
  • 링크를 클립보드에 복사

서버 측 고려 사항​

URL 재작성은 클라이언트와 서버 모두에 적용됩니다. TanStack Start를 사용할 때는 다음과 같습니다.

서버 미들웨어​

들어오는 요청을 파싱할 때 재작성이 적용됩니다.

// router.tsx
export const router = createRouter({
routeTree,
rewrite: {
input: ({ url }) => deLocalizeUrl(url),
output: ({ url }) => localizeUrl(url),
},
})

서버 핸들러는 동일한 재작성 구성을 사용하여 들어오는 URL을 파싱하고 올바른 외부 URL로 응답을 생성합니다.

SSR 하이드레이션​

라우터는 서버에서 렌더링된 HTML과 클라이언트 하이드레이션이 일관된 URL을 사용하도록 합니다. SSR 중에 publicHref가 직렬화되므로 클라이언트는 올바른 외부 URL로 하이드레이션할 수 있습니다.

API 레퍼런스​

rewrite 옵션​

  • 타입: LocationRewrite
  • 선택 사항입니다.
  • 브라우저와 라우터 사이의 양방향 URL 변환을 구성합니다.

LocationRewrite 타입​

type LocationRewrite = {
/**
* Transform the URL before the router interprets it.
* Called when reading from browser history.
*/
input?: LocationRewriteFunction

/**
* Transform the URL before it's written to browser history.
* Called when generating links and committing navigation.
*/
output?: LocationRewriteFunction
}

LocationRewriteFunction 타입​

type LocationRewriteFunction = (opts: { url: URL }) => undefined | string | URL

매개변수:

  • url: 현재 URL을 나타내는 URL 객체입니다.

반환값:

  • URL: 변환된 URL 객체입니다(변경된 동일 객체이거나 새 인스턴스일 수 있습니다).
  • string: URL로 파싱되는 전체 href 문자열입니다.
  • undefined: 재작성을 건너뛰고 원래 URL을 사용합니다.

composeRewrites 함수​

import { composeRewrites } from '@tanstack/react-router'

function composeRewrites(rewrites: Array<LocationRewrite>): LocationRewrite

여러 재작성 쌍을 하나의 재작성으로 결합합니다. 입력 재작성은 순서대로 실행되고 출력 재작성은 역순으로 실행됩니다.

예시:

const composedRewrite = composeRewrites([
{ input: rewrite1Input, output: rewrite1Output },
{ input: rewrite2Input, output: rewrite2Output },
])

// Input execution order: rewrite1Input → rewrite2Input
// Output execution order: rewrite2Output → rewrite1Output

예시​

완전히 작동하는 예시는 TanStack Router 저장소에서 확인할 수 있습니다.