SPA 모드
SPA 모드란 대체 무엇인가요?
SEO, 크롤러 또는 성능상의 이유로 SSR이 필요하지 않은 애플리케이션에서는 애플리케이션을 클라이언트에서만 부트스트랩하는 데 필요한 html, head, body 태그를 포함하고 애플리케이션의 "셸"을 담은 정적 HTML(또는 특정 라우트에 대해 사전 렌더링된 HTML)을 사용자에게 제공하는 것이 바람직할 수 있습니다.
SSR 없이 Start를 사용하는 이유는 무엇인가요?
SSR을 사용하지 않는다고 해서 서버 측 기능을 포기하는 것은 아닙니다! SPA 모드는 실제로 서버 함수 및/또는 서버 라우트 같은 서버 측 기능이나 다른 외부 API와도 매우 잘 어울립니다. 이는 단지 JavaScript를 사용하여 클라이언트에서 렌더링하기 전까지 초기 문서에 애플리케이션의 완전히 렌더링된 HTML이 포함되지 않는다는 의미입니다.
SPA 모드의 이점
- 배포가 더 쉽습니다 - 정적 애셋을 제공할 수 있는 CDN만 있으면 됩니다.
- 호스팅 비용이 더 저렴합니다 - CDN은 Lambda 함수나 장기 실행 프로세스에 비해 저렴합니다.
- 클라이언트 전용 방식이 더 간단합니다 - SSR이 없으므로 하이드레이션, 렌더링 및 라우팅에서 문제가 발생할 가능성이 줄어듭니다.
SPA 모드의 주의 사항
- 전체 콘텐츠 표시까지 더 오래 걸립니다 - 셸 아래의 콘텐츠를 렌더링하려면 먼저 모든 JS를 다운로드하고 실행해야 하므로 전체 콘텐츠 표시까지 걸리는 시간이 더 깁니다.
- SEO 친화성이 떨어집니다 - 로봇, 크롤러 및 링크 미리보기 도구가 JS를 실행하도록 구성되어 있고 애플리케이션이 합리적인 시간 내에 렌더링될 수 있지 않다면, 애플리케이션을 색인하는 데 더 어려움을 겪을 수 있습니다.
어떻게 작동하나요?
SPA 모드를 활성화한 후 Start 빌드를 실행하면 셸을 생성하기 위한 추가 사전 렌더링 단계가 이어서 수행됩니다. 이 과정은 다음과 같이 진행됩니다:
- 애플리케이션의 루트 라우트만 사전 렌더링합니다
- 애플리케이션이 일반적으로 일치하는 라우트를 렌더링하는 위치에 라우터에 구성된 대기 폴백 컴포넌트가 대신 렌더링됩니다.
- 생성된 HTML은
/_shell.html(구성 가능)이라는 정적 HTML 페이지에 저장됩니다 - 모든 404 요청을 SPA 모드 셸로 리디렉션하도록 기본 재작성 규칙이 구성됩니다
[!NOTE] 다른 라우트도 사전 렌더링할 수 있으며 SPA 모드에서는 가능한 한 많이 사전 렌더링하는 것이 권장되지만, SPA 모드가 작동하는 데 필수는 아닙니다.
SPA 모드 구성하기
SPA 모드를 구성하려면 Start 플러그인 옵션에 몇 가지 옵션을 추가할 수 있습니다:
Vite
export default defineConfig({
plugins: [
tanstackStart({
spa: {
enabled: true,
},
}),
],
})
Rsbuild
export default defineConfig({
plugins: [
tanstackStart({
spa: {
enabled: true,
},
}),
],
})
필수 리디렉션 사용하기
순수 클라이언트 측 SPA를 호스트나 CDN에 배포할 때 URL이 SPA 셸로 올바르게 재작성되도록 리디렉션을 사용해야 하는 경우가 많습니다. 모든 배포에서는 다음 우선순위를 이 순서대로 고려해야 합니다:
- 정적 애셋이 존재하면 항상 제공되도록 합니다(예: /about.html). 이는 일반적으로 대부분 CDN의 기본 동작입니다
- (선택 사항) 특정 하위 경로가 동적 서버 핸들러로 라우팅되도록 허용 목록에 추가합니다(예: /api/**). 이에 대해서는 아래에서 자세히 설명합니다
- 모든 404 요청이 SPA 셸로 재작성되도록 합니다(예: /_shell.html로 연결되는 포괄 리디렉션). 셸 출력 경로를 사용자 지정 경로로 구성했다면 해당 경로를 대신 사용합니다
기본 리디렉션 예시
Netlify의 _redirects 파일을 사용하여 모든 404 요청을 SPA 셸로 재작성해 보겠습니다.
# Catch all other 404 requests and rewrite them to the SPA shell
/* /_shell.html 200
서버 함수 및 서버 라우트 허용하기
이번에도 Netlify의 _redirects 파일을 사용하여 특정 하위 경로가 서버로 라우팅되도록 허용 목록에 추가할 수 있습니다.
# Allow requests to /_serverFn/* to be routed through to the server (If you have configured your server function base path to be something other than /_serverFn, use that instead)
/_serverFn/* /_serverFn/:splat 200
# Allow any requests to /api/* to be routed through to the server (Server routes can be created at any path, so you must ensure that any server routes you want to use are under this path, or simply add additional redirects for each server route base you want to expose)
/api/* /api/:splat 200
# Catch all other 404 requests and rewrite them to the SPA shell
/* /_shell.html 200
셸 마스크 경로
SPA 셸을 생성하는 데 사용되는 기본 pathname은 /입니다. 이를 셸 마스크 경로라고 합니다. 일치한 라우트는 포함되지 않으므로 셸을 생성하는 데 사용되는 pathname은 대부분 중요하지 않지만, 구성할 수 있습니다.
[!NOTE] 셸 마스크 경로로 기본값인
/을 유지하는 것이 좋습니다.
Vite
export default defineConfig({
plugins: [
tanstackStart({
spa: {
maskPath: '/app',
},
}),
],
})
Rsbuild
export default defineConfig({
plugins: [
tanstackStart({
spa: {
maskPath: '/app',
},
}),
],
})
프리렌더링 옵션
프리렌더 옵션은 SPA 셸의 프리렌더링 동작을 구성하는 데 사용되며, 프리렌더링 가이드에 있는 것과 동일한 프리렌더 옵션을 받습니다.
기본적으로 다음 prerender 옵션이 설정됩니다:
outputPath:/_shell.htmlcrawlLinks:falseretryCount:0
즉, 기본적으로 추가 프리렌더링을 위해 따라갈 링크를 찾도록 셸을 크롤링하지 않으며, 프리렌더링 실패를 재시도하지 않습니다.
자체 프리렌더 옵션을 제공하여 언제든지 이 옵션을 재정의할 수 있습니다:
Vite
export default defineConfig({
plugins: [
tanstackStart({
spa: {
prerender: {
outputPath: '/custom-shell',
crawlLinks: true,
retryCount: 3,
},
},
}),
],
})
Rsbuild
export default defineConfig({
plugins: [
tanstackStart({
spa: {
prerender: {
outputPath: '/custom-shell',
crawlLinks: true,
retryCount: 3,
},
},
}),
],
})
SPA 모드에서 렌더링 사용자 지정
다음과 같은 작업을 원하는 경우 SPA 셸의 HTML 출력을 사용자 지정하면 유용합니다:
- SPA 라우트에 공통 head 태그 제공
- 사용자 지정 대기 fallback 컴포넌트 제공
- 셸의 HTML, CSS, JS에 관한 모든 항목 변경
이 과정을 간단하게 처리할 수 있도록 router 인스턴스에서 isShell() 함수를 제공합니다:
// src/routes/root.tsx
export default function Root() {
const isShell = useRouter().isShell()
if (isShell) console.log('Rendering the shell!')
}
이 boolean을 사용하여 현재 라우트가 셸인지 여부에 따라 서로 다른 UI를 조건부로 렌더링할 수 있습니다. 단, 셸을 하이드레이션한 후에는 라우터가 즉시 첫 번째 라우트로 이동하고 isShell()은 false을 반환한다는 점에 유의하세요. 이를 올바르게 처리하지 않으면 스타일이 적용되지 않은 콘텐츠가 순간적으로 표시될 수 있습니다.
셸의 동적 데이터
셸은 애플리케이션의 SSR 빌드를 사용하여 프리렌더링되므로 루트 라우트에 정의된 모든 loaders 또는 서버 전용 기능은 프리렌더링 과정에서 실행되며, 해당 데이터가 셸에 포함됩니다.
즉, loader 또는 서버 전용 기능을 사용하여 셸에서 동적 데이터를 사용할 수 있습니다.
// src/routes/__root.tsx
export const RootRoute = createRootRoute({
loader: async () => {
return {
name: 'Tanner',
}
},
component: Root,
})
export default function Root() {
const { name } = useLoaderData()
return (
<html>
<body>
<h1>Hello, {name}!</h1>
<Outlet />
</body>
</html>
)
}