본문으로 건너뛰기

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
},
},
})

inputoutput 함수는 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에는 localizeUrldeLocalizeUrl 함수를 제공하는 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 저장소에서 확인할 수 있습니다.