본문으로 건너뛰기

CDN 에셋 URL

실험적 기능: transformAssets은 실험적 기능이며 변경될 수 있습니다.

런타임에 TanStack Start가 매니페스트로 관리되는 에셋 URL을 다시 작성해야 할 때 이 가이드를 사용합니다. 가장 일반적인 사용 사례는 서버가 시작될 때만 오리진을 알 수 있거나 요청마다 오리진이 달라지는 CDN에서 JavaScript와 CSS를 제공하는 것입니다.

이 가이드는 에셋 URL 재작성에 관한 내용입니다. CSS 가져오기 패턴을 선택하고 CSS 인라이닝을 구성하는 방법은 CSS 스타일링 가이드를 참조하세요.

transformAssets이 다시 작성하는 항목

createStartHandlertransformAssets 옵션은 Start가 SSR 매니페스트에서 관리하는 URL을 다시 작성합니다:

  • JavaScript 프리로드 링크(모듈 출력에는 <link rel="modulepreload">, IIFE 출력에는 <link rel="preload" as="script">)
  • 매니페스트로 관리되는 CSS의 <link rel="stylesheet"> 태그
  • 클라이언트 진입점 스크립트 URL
  • CSS URL 템플릿이 활성화된 경우 인라인된 CSS 내부의 url(...)@import URL

앱의 모든 URL을 다시 작성하지는 않습니다. 특히 ?url로 가져와 라우트 head() 함수에서 반환하는 CSS를 포함하여 임의의 라우트 head().links 항목은 다시 작성하지 않습니다. 주요 제외 항목은 다시 작성되지 않는 항목을 참조하세요.

정적 CDN 접두사 사용

Start가 관리하는 모든 에셋에 동일한 URL 접두사를 추가해야 하는 경우 문자열을 전달합니다.

// src/server.ts
import {
createStartHandler,
defaultStreamHandler,
} from '@tanstack/react-start/server'
import { createServerEntry } from '@tanstack/react-start/server-entry'

const handler = createStartHandler({
handler: defaultStreamHandler,
transformAssets: process.env.CDN_ORIGIN || '',
})

export default createServerEntry({ fetch: handler })

CDN_ORIGINhttps://cdn.example.com이고 에셋 URL이 /assets/index-abc123.js이면 Start는 https://cdn.example.com/assets/index-abc123.js을 렌더링합니다.

문자열이 비어 있거나 설정되지 않은 경우 URL은 변경되지 않습니다.

교차 출처 속성 추가

매니페스트로 관리되는 <link> 태그에 crossOrigin도 설정해야 하는 경우 객체 축약형을 사용합니다.

// src/server.ts
import {
createStartHandler,
defaultStreamHandler,
} from '@tanstack/react-start/server'
import { createServerEntry } from '@tanstack/react-start/server-entry'

const handler = createStartHandler({
handler: defaultStreamHandler,
transformAssets: {
prefix: process.env.CDN_ORIGIN || '',
crossOrigin: 'anonymous',
},
})

export default createServerEntry({ fetch: handler })

crossOrigin은 지원되는 모든 링크 종류에 적용할 하나의 값이나 HeadContent assetCrossOrigin 형태와 일치하는 종류별 레코드를 허용합니다.

transformAssets: {
prefix: 'https://cdn.example.com',
crossOrigin: {
script: 'anonymous',
stylesheet: 'use-credentials',
},
}

종류별 레코드에 나열되지 않은 종류에는 crossOrigin 속성이 적용되지 않습니다. 문자열 축약형과 객체 축약형은 기본적으로 캐시됩니다.

앱 셸의 HeadContent에서도 교차 출처 동작을 설정할 수 있습니다:

<HeadContent assetCrossOrigin="anonymous" />

or:

<HeadContent
assetCrossOrigin={{
script: 'anonymous',
stylesheet: 'use-credentials',
}}
/>

transformAssetsassetCrossOrigin가 모두 교차 출처 값을 설정하면, assetCrossOrigintransformAssets의 값을 재정의합니다. assetCrossOrigin는 매니페스트에서 관리하는 스크립트 및 스타일시트 링크에만 적용되며, 라우트 head() 함수에서 반환된 임의의 링크에는 적용되지 않습니다.

애셋별 로직에 콜백 사용하기

출력이 애셋 종류나 URL에 따라 달라지는 경우 콜백을 전달합니다. 콜백은 문자열, { href, crossOrigin? } 또는 둘 중 하나의 Promise를 반환합니다.

// src/server.ts
import {
createStartHandler,
defaultStreamHandler,
} from '@tanstack/react-start/server'
import { createServerEntry } from '@tanstack/react-start/server-entry'

const handler = createStartHandler({
handler: defaultStreamHandler,
transformAssets: (asset) => {
const href = `https://cdn.example.com${asset.url}`

if (asset.kind === 'script') {
return {
href,
crossOrigin: 'anonymous',
}
}

return { href }
},
})

export default createServerEntry({ fetch: handler })

kind 필드는 변환 중인 애셋 URL을 알려 줍니다.

kind설명
'script'JavaScript 프리로드 또는 클라이언트 진입점 스크립트 URL
'stylesheet'매니페스트에서 관리하는 CSS 스타일시트 URL
'css-url'인라인 CSS 내부의 url(...) 또는 @import URL

kind === 'css-url'의 경우 컨텍스트에는 CSS 콘텐츠가 인라인되는 매니페스트 스타일시트 href인 stylesheetHref도 포함됩니다.

crossOrigin는 매니페스트에서 관리하는 스크립트 및 스타일시트 태그에 적용됩니다. CSS 내부 URL의 경우 { href }를 반환하는 것은 문자열을 반환하는 것과 같습니다.

기본적으로 프로덕션에서는 첫 번째 요청 후 콜백 결과가 캐시됩니다. 변환이 요청별 데이터에 따라 달라지는 경우에만 cache: false가 포함된 객체 형식을 사용합니다.

요청별 CDN 선택 처리하기

CDN 출처가 요청 헤더, 테넌트 또는 리전 등 현재 요청에 따라 달라지는 경우 cache: false가 포함된 객체 형식을 사용합니다.

// src/server.ts
import {
createStartHandler,
defaultStreamHandler,
getRequest,
} from '@tanstack/react-start/server'
import { createServerEntry } from '@tanstack/react-start/server-entry'

const handler = createStartHandler({
handler: defaultStreamHandler,
transformAssets: {
transform: ({ kind, url }) => {
const region = getRequest().headers.get('x-region') || 'us'
const cdnBase =
region === 'eu'
? 'https://cdn-eu.example.com'
: 'https://cdn-us.example.com'

if (kind === 'script') {
return {
href: `${cdnBase}${url}`,
crossOrigin: 'anonymous',
}
}

return { href: `${cdnBase}${url}` }
},
cache: false,
},
})

export default createServerEntry({ fetch: handler })

객체 형식은 다음 속성을 허용합니다:

속성타입설명
transformstring | (asset) => string | { href, crossOrigin? } | Promise<...>위의 축약 형식과 동일한 문자열 접두사 또는 콜백입니다.
createTransform(ctx: { warmup: true } | { warmup: false; request: Request }) => (asset) => string | { href, crossOrigin? } | Promise<...>매니페스트 계산마다 한 번 실행되고 애셋별 변환을 반환하는 비동기 팩토리입니다. transform와 함께 사용할 수 없습니다.
cacheboolean변환된 매니페스트를 캐시할지 여부입니다. 기본값은 true입니다.
warmupbooleantrue인 경우 프로덕션의 서버 시작 시 캐시된 매니페스트를 미리 준비합니다. 기본값은 false입니다.

매니페스트 계산마다 한 번 비동기 작업을 수행한 다음, 그 결과로 여러 URL을 변환해야 할 때는 createTransform을 사용합니다.

transformAssets: {
cache: false,
async createTransform(ctx) {
if (ctx.warmup) {
return ({ url }) => ({ href: url })
}

const region = ctx.request.headers.get('x-region') || 'us'
const cdnBase = await fetchCdnBaseForRegion(region)

return (asset) => {
if (asset.kind === 'script') {
return {
href: `${cdnBase}${asset.url}`,
crossOrigin: 'anonymous',
}
}

return { href: `${cdnBase}${asset.url}` }
}
},
}

정적 CDN 접두사에는 문자열 또는 객체 축약형을 사용하는 것이 좋습니다. 더 간단하며 기본적으로 캐시된 매니페스트를 사용합니다.

인라인 CSS 내부의 URL 변환

Start의 CSS 인라이닝을 활성화하면 Start는 인라인 CSS 콘텐츠 내부의 URL에도 transformAssets을 실행할 수 있습니다. 이는 글꼴 및 배경 이미지와 같은 상대 및 루트 상대 url(...)@import 값을 포함합니다.

Start는 런타임에 CSS를 파싱하지 않으므로 빌드 타임 CSS URL 템플릿을 사용하도록 설정해야 합니다:

tanstackStart({
server: {
build: {
inlineCss: {
enabled: true,
transformAssets: true,
},
},
},
})

inlineCss: true을 전달해도 라우트 CSS는 인라인되지만, 런타임 CSS URL 변환에 필요한 템플릿 메타데이터는 생성되지 않습니다.

상대 CSS URL은 변환이 실행되기 전에 생성된 스타일시트 href를 기준으로 확인됩니다.

/* emitted stylesheet href: /assets/dashboard.css */
.card {
background-image: url('./dot.svg');
}

콜백은 kind: 'css-url'이 포함된 /assets/dot.svg을 받습니다. 예를 들어 JavaScript 및 CSS 파일은 하나의 CDN 원본에서 제공하고, 인라인 CSS 내부에서 참조되는 글꼴 또는 이미지 URL은 다른 원본에서 제공할 수 있습니다.

const handler = createStartHandler({
handler: defaultStreamHandler,
transformAssets: (asset) => {
if (asset.kind === 'css-url') {
return `https://static-assets.example.com${asset.url}`
}

return `https://cdn.example.com${asset.url}`
},
})

asset.kind === 'css-url'이면 URL은 url(...) 또는 @import 참조와 같이 인라인 CSS 파일 내부에서 가져온 것입니다. 콜백 컨텍스트에는 해당 URL을 포함한 생성된 스타일시트를 식별하는 stylesheetHref도 포함됩니다. 소스 스타일시트에 따라 변환이 달라져야 할 때 사용합니다.

transformAssets: (asset) => {
if (asset.kind === 'css-url') {
const cdnBase = asset.stylesheetHref.includes('/admin-')
? 'https://admin-cdn.example.com'
: 'https://cdn.example.com'

return `${cdnBase}${asset.url}`
}

return `https://cdn.example.com${asset.url}`
}

CSS 내부의 절대 URL, 프로토콜 상대 URL, 데이터 URL 및 해시 참조는 변경되지 않으며 transformAssets에 전달되지 않습니다. 빌드에서 CSS URL 템플릿을 활성화하지 않았다면 인라인 CSS 내부의 URL은 런타임에도 변경되지 않습니다.

URL 재작성의 캐시 시점 선택

대부분의 앱에서는 모든 요청에 동일한 CDN URL을 사용합니다. 이 경우 기본 캐시 동작을 유지합니다. Start는 프로덕션에서 변환된 매니페스트를 한 번 계산한 다음 이후 요청에 재사용합니다.

리전, 테넌트, 헤더 또는 쿠키에 따라 CDN을 선택하는 경우처럼 요청마다 결과가 달라질 수 있을 때만 캐시를 끕니다.

형식기본 캐시동작
문자열 접두사true한 번 계산되며 프로덕션에서 캐시됩니다.
객체 축약형true한 번 계산되며 프로덕션에서 캐시됩니다.
콜백true첫 번째 요청에서 한 번 실행되며 프로덕션에서 캐시됩니다.
cache: true이 있거나 생략된 객체true위와 동일합니다.
cache: false이 있는 객체false기본 매니페스트를 깊은 복제하고 모든 요청을 변환합니다.

변환이 요청별 데이터에 의존할 때만 cache: false을 사용합니다. 정적 CDN 접두사에는 기본값인 cache: true이 더 빠르고 간단합니다.

첫 번째 사용자 요청 중에 최초 캐시 재작성을 수행하지 않으려면 warmup: true을 설정합니다. Start는 서버가 시작될 때 백그라운드에서 변환된 매니페스트를 계산합니다.

transformAssets: {
transform: process.env.CDN_ORIGIN || '',
cache: true,
warmup: true,
}

개발 모드 또는 cache: false인 경우 워밍업은 아무 효과가 없습니다.

참고: 개발 모드(TSS_DEV_SERVER)에서는 cache 설정과 관계없이 항상 캐시를 건너뛰므로 언제나 최신 매니페스트를 가져옵니다.

클라이언트 탐색 청크를 CDN에 유지합니다

transformAssets은 SSR HTML의 URL, 즉 스크립트 프리로드 힌트, 스타일시트 링크, 클라이언트 진입점 스크립트를 재작성합니다. 즉, 브라우저가 초기 페이지를 로드할 때 CDN에서 해당 자산을 가져올 수 있습니다.

사용자가 클라이언트 측에서 탐색하면 TanStack Router는 번들러가 경로를 삽입한 import() 호출을 사용해 라우트 청크를 지연 로드합니다. 해당 비동기 청크 URL이 transformAssets에서 CDN으로 재작성하는 클라이언트 진입점 스크립트를 기준으로 해석되도록 번들러를 구성합니다.

Vite

Vite의 기본 base: '/'을 사용하면 지연 라우트 청크 경로는 /assets/about-abc123.js처럼 절대 경로이며, CDN이 아닌 앱 서버 원본을 기준으로 해석됩니다.

Vite 빌드에서는 Vite가 클라이언트 측 청크에 대한 상대 import 경로를 생성하도록 base: ''을 설정합니다.

vite.config.ts
import { defineConfig } from 'vite'

export default defineConfig({
base: '',
// ... plugins, etc.
})

base: ''을 사용하면 transformAssets을 통해 CDN에서 클라이언트 진입점 스크립트를 로드할 수 있으며, 상대 import() 호출은 동일한 CDN 원본을 기준으로 해석됩니다. 따라서 클라이언트 측 탐색 중에 지연 로드되는 라우트 청크가 CDN에 유지됩니다.

'./' 대신 빈 문자열을 사용하는 것이 중요합니다. 둘 다 상대적인 클라이언트 측 import를 생성하지만, base: ''은 SSR 매니페스트의 루트 상대 경로를 보존하므로 transformAssets에서 CDN 원본을 올바르게 앞에 추가할 수 있습니다.

base 설정초기 로드 시 SSR 자산클라이언트 측 탐색 청크
'/' (기본값)transformAssets을 통한 CDN앱 서버
''transformAssets을 통한 CDNCDN, 진입점 모듈 기준 상대 경로

Vite에서 transformAssets을 사용하며 초기 로드 자산과 클라이언트 탐색 청크를 동일한 CDN에서 제공하려면 항상 base: ''을 사용합니다.

Rsbuild

Rsbuild 빌드에서는 Rspack이 로드된 클라이언트 진입점 스크립트에서 비동기 청크 URL을 파생하도록 output.assetPrefix'auto'으로 설정합니다.

rsbuild.config.ts
import { defineConfig } from '@rsbuild/core'

export default defineConfig({
output: {
assetPrefix: 'auto',
},
// ... plugins, etc.
})

assetPrefix: 'auto'을 사용하면 transformAssets을 통해 CDN에서 클라이언트 진입점 스크립트를 로드할 수 있으며, 클라이언트 측 탐색 중 비동기 라우트 청크는 해당 진입점 스크립트를 기준으로 해석됩니다.

재작성하지 않는 항목

transformAssets은 Start 매니페스트에서 관리하는 자산과, CSS URL 템플릿이 활성화된 경우 Start가 HTML에 인라인으로 삽입하는 CSS 내부의 URL을 재작성합니다.

라우트 head() 함수에서 반환된 임의의 링크는 재작성하지 않습니다.

import { createRootRoute } from '@tanstack/react-router'
import appCss from '../styles/app.css?url'

export const Route = createRootRoute({
head: () => ({
links: [{ rel: 'stylesheet', href: appCss }],
}),
})

이 스타일시트가 CDN URL을 사용해야 한다면 해당 URL에 번들러 수준 옵션이나 빌드 시점 구성을 사용합니다. Start가 생성된 스타일시트 URL을 관리하도록 하려면 CSS를 부수 효과 또는 CSS 모듈로 대신 가져옵니다. CSS 패턴 선택을 참조하세요.

transformAssets는 컴포넌트에서 직접 가져온 애셋 URL도 다시 작성하지 않습니다:

// This import resolves to a URL at build time by your bundler.
import logo from './logo.svg'

function Header() {
return <img src={logo} /> // This URL is not affected by transformAssets.
}

이러한 애셋 import에는 번들러에서 제공하는 URL 제어 기능을 사용합니다.

Vite

Vite 빌드에서는 vite.config.ts에서 Vite의 experimental.renderBuiltUrl을 사용합니다.

vite.config.ts
import { defineConfig } from 'vite'

export default defineConfig({
experimental: {
renderBuiltUrl(filename, { hostType }) {
if (hostType === 'js') {
return { relative: true }
}

return `https://cdn.example.com/${filename}`
},
},
})

Rsbuild

빌드 시점에 CDN origin을 알 수 있는 Rsbuild 빌드에서는 rsbuild.config.ts에서 output.assetPrefix을 사용합니다.

rsbuild.config.ts
import { defineConfig } from '@rsbuild/core'

export default defineConfig({
output: {
assetPrefix: 'https://cdn.example.com/',
},
})