CSS 스타일링
TanStack Start는 번들러가 지원하는 CSS 패턴을 지원하며, 그 위에 SSR 인식형 라우트 애셋 탐색 기능을 추가합니다.
이 가이드를 사용하여 React Start 앱에서 CSS를 가져오는 방법과 SSR 스타일시트 링크, Early Hints, CSS 인라이닝 같은 프로덕션 CSS 동작을 구성하는 방법을 선택합니다.
CSS 패턴 선택
Start는 CSS를 가져오는 방식에 따라 CSS를 다르게 처리합니다.
| 패턴 | 사용 시점 | SSR 동작 | 프로덕션 기능 |
|---|---|---|---|
import css from './app.css?url' | 라우트 head()에 스타일시트 URL을 넣으려는 경우 | head().links에서 렌더링됨 | 동적 Early Hints |
import './global.css' | 라우트 청크에 전역 CSS를 연결하려는 경우 | 일치하는 라우트의 Start 매니페스트에서 검색됨 | 정적 Early Hints, transformAssets, CSS 인라이닝 |
import styles from './card.module.css' | 라우트 청크에 범위가 지정된 클래스 이름을 연결하려는 경우 | 일치하는 라우트의 Start 매니페스트에서 검색됨 | 정적 Early Hints, transformAssets, CSS 인라이닝 |
스타일시트가 라우트 head 출력의 일부인 경우 ?url을 사용합니다. Start가 생성된 스타일시트를 라우트 애셋으로 처리하도록 하려면 부수 효과 CSS import 또는 CSS 모듈을 사용합니다.
명시적 스타일시트 링크에 ?url 사용
번들러가 생성된 스타일시트 URL을 반환하게 하고 <link rel="stylesheet">을 직접 렌더링하려면 ?url으로 CSS를 import합니다.
// src/routes/__root.tsx
/// <reference types="vite/client" />
import { createRootRoute } from '@tanstack/react-router'
import appCss from '../styles/app.css?url'
export const Route = createRootRoute({
head: () => ({
links: [{ rel: 'stylesheet', href: appCss }],
}),
})
이 패턴은 명시적인 전역 스타일시트에 유용하며, 특히 이미 스타일시트를 라우트 head() 출력의 일부로 포함하려는 경우에 적합합니다. CSS 파일은 번들러에서 생성되며, HeadContent은 최종 문서에 스타일시트 링크를 배치합니다.
?url 스타일시트 링크는 라우트 head 출력이며, Start 매니페스트에서 관리하는 스타일시트 애셋이 아닙니다. 따라서 다음과 같습니다:
- 라우트
head()이 실행될 때 검색됩니다. dynamicEarly Hints 단계에 라우트head().links항목으로 나타날 수 있습니다.- Start의 런타임
transformAssets옵션으로 다시 작성되지 않습니다. - Start CSS 인라이닝으로 인라인되지 않습니다.
Start의 매니페스트 관리형 CSS 기능보다 라우트 head를 명시적으로 제어하는 것이 더 중요할 때 이 패턴을 사용합니다.
전역 라우트 CSS에 부수 효과 Import 사용
전역 선택자, 전역 클래스 이름 또는 CSS 사용자 지정 속성을 라우트나 컴포넌트 모듈과 함께 번들링하려면 CSS를 변수에 할당하지 않고 import합니다.
// src/routes/index.tsx
import { createFileRoute } from '@tanstack/react-router'
import '../styles/global.css'
export const Route = createFileRoute('/')({
component: Home,
})
function Home() {
return <div className="global-container">Global CSS</div>
}
클래스 이름은 전역으로 유지됩니다. Start는 클라이언트 빌드에서 생성된 CSS 애셋을 검색하여 일치하는 라우트 매니페스트 항목에 연결합니다. SSR 중에는 HeadContent이 일치하는 라우트 트리의 스타일시트 링크를 렌더링하므로 페이지에 하이드레이션 전에 스타일이 적용됩니다.
CSS를 import하는 위치에 따라 로드 시점이 결정됩니다:
- 모든 페이지에 적용하려면 루트 라우트 또는 앱 셸에서 import합니다.
- 해당 레이아웃과 하위 라우트에 적용하려면 레이아웃 라우트에서 import합니다.
- 해당 라우트가 일치할 때만 로드하려면 리프 라우트에서 import합니다.
- 초기 라우트 렌더링에 CSS가 필요하지 않은 경우에만
import()또는React.lazy으로 로드된 컴포넌트에서 import합니다.
비동기 컴포넌트에서 import한 CSS는 해당 비동기 청크와 함께 로드됩니다. 초기 라우트 일치를 위한 Start의 정적 라우트 매니페스트에 포함되지 않으므로 HeadContent은 이를 초기 SSR 스타일시트 링크로 렌더링하지 않으며, 정적 Early Hints 또는 CSS 인라이닝에 사용할 수 없습니다.
부수 효과 CSS import는 앱 전체 CSS 재설정, 디자인 토큰, 전역 유틸리티 클래스 및 라우트 수준 전역 스타일에 적합합니다.
범위가 지정된 라우트 CSS에 CSS Modules 사용
Start의 라우트 에셋 탐색 관점에서 CSS modules는 부수 효과 CSS import와 동일하게 작동하지만, 클래스 이름의 범위는 번들러에서 지정됩니다.
// src/routes/modules.tsx
import { createFileRoute } from '@tanstack/react-router'
import styles from '../styles/card.module.css'
export const Route = createFileRoute('/modules')({
component: Modules,
})
function Modules() {
return <div className={styles.card}>Scoped CSS module</div>
}
생성된 스타일시트는 라우트 청크 그래프에서 탐색되고, 일치하는 라우트의 SSR 중에 연결되며, 클라이언트 탐색 시 라우트 청크가 로드될 때 함께 로드됩니다.
범위가 지정된 클래스 이름과 Start에서 관리하는 스타일시트 에셋을 사용하려면 라우트 또는 컴포넌트에 한정된 스타일링에 CSS modules를 사용합니다.
CSS가 탐색되는 시점 이해
선택하는 import 패턴에 따라 Start가 스타일시트를 확인할 수 있는 시점이 결정됩니다.
부수 효과 import와 CSS modules의 CSS는 빌드 시점에 탐색됩니다. Start는 클라이언트 빌드 출력을 검사하고, 라우트 청크용으로 생성된 CSS를 기록하며, SSR 중에 해당 매니페스트를 사용합니다. Start는 라우트 로더가 실행되기 전에 이미 이러한 스타일시트를 알고 있으므로, static Early Hints 단계에서 rel=preload; as=style 링크로 전송할 수 있습니다.
?url로 import한 CSS는 다릅니다. Start는 라우트 head()가 링크를 반환할 때만 해당 스타일시트를 확인하며, 이는 router.load() 이후에 발생합니다. 이러한 링크도 Early Hints로 전송할 수 있지만, 지원되는 다른 라우트 head().links 항목과 함께 dynamic 단계에서만 가능합니다.
다음 경험 법칙을 사용합니다:
- Start가 라우트 CSS를 최대한 일찍 탐색하도록 하려면 부수 효과 import 또는 CSS modules를 사용합니다.
- 명시적인 라우트
head()제어를 원하고 탐색이 늦어져도 괜찮다면?url를 사용합니다. - 정적 매니페스트 에셋과 동적 head 링크가 모두 포함된 하나의 통합 Early Hints 응답을 원한다면
dynamic단계에서allLinks를 사용합니다. - CSS 인라이닝이 활성화된 경우, 인라인된 매니페스트 관리 스타일시트 에셋은 HTML에 포함되므로 정적 Early Hints에서 제외됩니다.
콜백 및 응답 헤더 API에 대해서는 Early Hints 가이드를 참조합니다.
프로덕션에서 라우트 CSS 인라인 처리
실험적 기능: CSS 인라이닝은 실험적 기능이며 변경될 수 있습니다.
CSS 인라이닝은 프로덕션 빌드에서 Start 매니페스트가 관리하는 라우트 CSS를 서버에서 렌더링된 HTML 응답에 직접 포함합니다. 이렇게 하면 초기 라우트 일치에 필요한 CSS의 렌더링 차단 스타일시트 요청을 방지하여 첫 렌더링을 개선할 수 있습니다.
Start 플러그인 옵션의 server.build.inlineCss로 활성화합니다. true를 전달하는 것은 { enabled: true, transformAssets: false }의 축약형입니다.
Vite
import { defineConfig } from 'vite'
import { tanstackStart } from '@tanstack/react-start/plugin/vite'
export default defineConfig({
plugins: [
tanstackStart({
server: {
build: {
inlineCss: true,
},
},
}),
],
})
Rsbuild
import { defineConfig } from '@rsbuild/core'
import { tanstackStart } from '@tanstack/react-start/plugin/rsbuild'
export default defineConfig({
plugins: [
tanstackStart({
server: {
build: {
inlineCss: true,
},
},
}),
],
})
이 옵션은 프로덕션 빌드에만 영향을 줍니다. 개발 모드에서는 Start의 일반적인 개발 CSS 처리를 계속 사용합니다.
CSS 인라이닝은 부수 효과 import와 CSS modules에서 탐색된 CSS에 적용됩니다. 이러한 스타일시트는 Start 매니페스트 에셋이기 때문입니다. ?url로 import하고 head().links에서 반환한 CSS는 인라인 처리하지 않습니다. 이러한 링크는 매니페스트에서 관리되는 스타일시트 에셋이 아니라 동적 라우트 head 출력이기 때문입니다.
CSS 인라인이 활성화되면 Start는 클라이언트 빌드에 CSS 파일을 계속 생성합니다. 변경되는 것은 SSR 문서뿐입니다. Start 매니페스트가 관리하는 스타일시트 링크는 일치하는 라우트를 위한 단일 인라인 <style> 태그로 대체됩니다. 중복 스타일시트 링크와 하이드레이션 불일치를 방지하기 위해 하이드레이션 중에도 인라인 스타일이 유지됩니다.
런타임에서 인라인 제어
서버 엔트리에서 요청별로 인라인을 제어할 수 있습니다. 이는 server.build.inlineCss이 활성화된 상태로 생성된 빌드에만 영향을 줍니다.
// src/server.ts
import {
createStartHandler,
defaultStreamHandler,
} from '@tanstack/react-start/server'
import { createServerEntry } from '@tanstack/react-start/server-entry'
const handler = createStartHandler({
handler: defaultStreamHandler,
inlineCss: ({ request }) => request.headers.get('x-inline-css') !== 'false',
})
export default createServerEntry({ fetch: handler })
사용자 지정 런타임 래퍼의 경우, handler(request, { inlineCss })이 해당 요청에 대한 핸들러 수준의 inlineCss 설정보다 우선합니다.
export default createServerEntry({
fetch(request) {
return handler(request, {
inlineCss: request.headers.get('x-inline-css') !== 'false',
})
},
})
URL 기준 경로 재설정
인라인된 스타일시트에 상대 url(...) 또는 @import 참조가 포함되어 있으면 Start는 CSS를 삽입하기 전에 생성된 CSS 애셋 URL을 기준으로 해당 참조의 경로를 재설정합니다.
예를 들어 생성된 스타일시트가 /_build/assets/dashboard.css에서 제공되는 경우:
.card {
background-image: url('./dot.svg');
}
Start는 이를 다음과 같이 삽입합니다:
.card {
background-image: url(/_build/assets/dot.svg);
}
루트 상대 URL은 빌드 시 변경되지 않습니다. 절대 URL, 프로토콜 상대 URL, 데이터 URL 및 해시 참조도 변경되지 않습니다.
글꼴이나 배경 이미지에 CDN 오리진을 앞에 추가하는 경우처럼 런타임에서 CSS 내부 URL을 다시 작성해야 한다면 server.build.inlineCss: { enabled: true, transformAssets: true }을 사용하여 CSS URL 템플릿을 활성화합니다. 자세한 transformAssets 동작은 인라인된 CSS 내부의 URL 변환을 참조하세요.
장단점
CSS 인라인은 일치하는 라우트의 CSS가 충분히 작아 별도의 스타일시트 요청보다 HTML에 포함하는 비용이 더 적을 때 유용합니다. 초기 라우트가 별도 파일이었다면 캐시되었을 대용량 전역 스타일시트를 로드하는 경우에는 효과가 떨어질 수 있습니다.
활성화하기 전에 다음 장단점을 고려하세요:
- HTML 응답이 더 커집니다.
- 최초 로드 CSS를 더 이상 HTML 응답과 별도로 캐시할 수 없습니다.
- 엄격한 Content Security Policy 설정에서는 인라인 스타일을 허용해야 합니다. 인라인된 CSS를 포함하여 렌더링된
<style>태그에HeadContent이 nonce를 적용할 수 있도록 라우터에ssr.nonce을 구성합니다. - 초기 응답 후 클라이언트 탐색과 브라우저 캐싱을 위해 CSS 파일은 계속 생성됩니다.
배포 및 라우트 구조에서 요청 오버헤드 감소가 더 큰 HTML 응답보다 가치가 있을 때 CSS 인라인을 사용합니다.
프로덕션 CSS 동작 구성
프로덕션에서 필요한 동작에 따라 다음 옵션을 함께 사용합니다.
| 요구 사항 | 사용 |
|---|---|
| 라우트 head의 명시적 스타일시트 링크 | ?url에서 반환된 head().links import |
| 라우트 CSS용 SSR 링크 | 사이드 이펙트 import 또는 CSS 모듈 |
| 가장 이른 CSS 조기 힌트 | 정적 힌트가 있는 부수 효과 import 또는 CSS 모듈 |
| 리디렉션에 안전한 스타일시트 조기 힌트 | 동적 힌트가 있는 ?url import |
| 최초 로드 시 차단되는 CSS 요청 감소 | server.build.inlineCss |
| Start 애셋의 런타임 CDN 재작성 | transformAssets |