실행 모델
코드가 실행되는 위치를 이해하는 것은 TanStack Start 애플리케이션을 구축하는 데 필수적입니다. 이 가이드에서는 TanStack Start의 실행 모델과 코드가 실행되는 위치를 제어하는 방법을 설명합니다.
핵심 원칙: 기본적으로 동형
TanStack Start의 모든 코드는 기본적으로 동형입니다 - 명시적으로 제한하지 않는 한 서버와 클라이언트 모두에서 실행되며 양쪽 번들에 포함됩니다.
// ✅ This runs on BOTH server and client
function formatPrice(price: number) {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
}).format(price)
}
// ✅ Route loaders are ISOMORPHIC
export const Route = createFileRoute('/products')({
loader: async () => {
// This runs on server during SSR AND on client during navigation
const response = await fetch('/api/products')
return response.json()
},
})
반드시 알아야 할 사항: 라우트
loader는 동형입니다. 즉, 서버에서만 실행되는 것이 아니라 서버와 클라이언트 모두에서 실행됩니다.
실행 경계
TanStack Start 애플리케이션은 두 환경에서 실행됩니다:
서버 환경
- 파일 시스템, 데이터베이스, 환경 변수에 접근할 수 있는 Node.js 런타임
- SSR 중 - 초기 페이지가 서버에서 렌더링됩니다
- API 요청 - 서버 함수가 서버 측에서 실행됩니다
- 빌드 시점 - 정적 생성 및 사전 렌더링이 수행됩니다
클라이언트 환경
- DOM, localStorage, 사용자 상호작용에 접근할 수 있는 브라우저 런타임
- 하이드레이션 후 - 초기 서버 렌더링 후 클라이언트가 제어를 이어받습니다
- 탐색 - 탐색 중 라우트 로더가 클라이언트 측에서 실행됩니다
- 사용자 상호작용 - 이벤트 핸들러, 양식 제출 등입니다
실행 제어 API
서버 전용 실행
| API | 사용 사례 | 클라이언트 동작 |
|---|---|---|
createServerFn() | RPC 호출, 데이터 변경 | 서버로 네트워크 요청 |
createServerOnlyFn(fn) | 유틸리티 함수 | 오류 발생 |
import { createServerFn, createServerOnlyFn } from '@tanstack/solid-start'
// RPC: Server execution, callable from client
const updateUser = createServerFn({ method: 'POST' })
.validator((data: UserData) => data)
.handler(async ({ data }) => {
// Only runs on server, but client can call it
return await db.users.update(data)
})
// Utility: Server-only, client crashes if called
const getEnvVar = createServerOnlyFn(() => process.env.DATABASE_URL)
클라이언트 전용 실행
| API | 사용 사례 | 서버 동작 |
|---|---|---|
createClientOnlyFn(fn) | 브라우저 유틸리티 | 오류 발생 |
<ClientOnly> | 브라우저 API가 필요한 컴포넌트 | 폴백 렌더링 |
import { createClientOnlyFn } from '@tanstack/solid-start'
import { ClientOnly } from '@tanstack/solid-router'
// Utility: Client-only, server crashes if called
const saveToStorage = createClientOnlyFn((key: string, value: any) => {
localStorage.setItem(key, JSON.stringify(value))
})
// Component: Only renders children after hydration
function Analytics() {
return (
<ClientOnly fallback={null}>
<GoogleAnalyticsScript />
</ClientOnly>
)
}
useHydrated 훅
하이드레이션에 의존하는 동작을 더 세밀하게 제어하려면 useHydrated 훅을 사용합니다. 이 훅은 클라이언트의 하이드레이션 여부를 나타내는 접근자(시그널)를 반환합니다:
import { useHydrated } from '@tanstack/solid-router'
function TimeZoneDisplay() {
const hydrated = useHydrated()
const timeZone = () =>
hydrated() ? Intl.DateTimeFormat().resolvedOptions().timeZone : 'UTC'
return <div>Your timezone: {timeZone()}</div>
}
동작:
- SSR 중: 항상
false을 반환합니다 - 첫 번째 클라이언트 렌더링:
false을 반환합니다 - 하이드레이션 후:
true을 반환합니다(이후 모든 렌더링에서도true로 유지됩니다)
서버 렌더링에 적절한 폴백을 제공하면서 브라우저 시간대, 로케일 또는 localStorage와 같은 클라이언트 측 데이터를 기반으로 콘텐츠를 조건부 렌더링해야 할 때 유용합니다.
환경별 구현
import { createIsomorphicFn } from '@tanstack/solid-start'
// Different implementation per environment
const getDeviceInfo = createIsomorphicFn()
.server(() => ({ type: 'server', platform: process.platform }))
.client(() => ({ type: 'client', userAgent: navigator.userAgent }))
아키텍처 패턴
점진적 향상
JavaScript 없이도 작동하고 클라이언트 측 기능으로 향상되는 컴포넌트를 구축합니다:
function SearchForm() {
const [query, setQuery] = createSignal('')
return (
<form action="/search" method="get">
<input
name="q"
value={query()}
onChange={(e) => setQuery(e.target.value)}
/>
<ClientOnly fallback={<button type="submit">Search</button>}>
<SearchButton onSearch={() => search(query())} />
</ClientOnly>
</form>
)
}
환경 인식 스토리지
const storage = createIsomorphicFn()
.server((key: string) => {
// Server: File-based cache
const fs = require('node:fs')
return JSON.parse(fs.readFileSync('.cache', 'utf-8'))[key]
})
.client((key: string) => {
// Client: localStorage
return JSON.parse(localStorage.getItem(key) || 'null')
})
RPC와 직접 함수 호출 비교
서버 함수와 서버 전용 함수를 각각 언제 사용해야 하는지 알아봅니다:
// createServerFn: RPC pattern - server execution, client callable
const fetchUser = createServerFn().handler(async () => await db.users.find())
// Usage from client component:
const user = await fetchUser() // ✅ Network request
// createServerOnlyFn: Crashes if called from client
const getSecret = createServerOnlyFn(() => process.env.SECRET)
// Usage from client:
const secret = getSecret() // ❌ Throws error
일반적인 안티 패턴
환경 변수 노출
// ❌ Exposes to client bundle
const apiKey = process.env.SECRET_KEY
// ✅ Server-only access
const apiKey = createServerOnlyFn(() => process.env.SECRET_KEY)
잘못된 로더 가정
// ❌ Assuming loader is server-only
export const Route = createFileRoute('/users')({
loader: () => {
// This runs on BOTH server and client!
const secret = process.env.SECRET // Exposed to client
return fetch(`/api/users?key=${secret}`)
},
})
// ✅ Use server function for server-only operations
const getUsersSecurely = createServerFn().handler(() => {
const secret = process.env.SECRET // Server-only
return fetch(`/api/users?key=${secret}`)
})
export const Route = createFileRoute('/users')({
loader: () => getUsersSecurely(), // Isomorphic call to server function
})
하이드레이션 불일치
// ❌ Different content server vs client
function CurrentTime() {
return <div>{new Date().toLocaleString()}</div>
}
// ✅ Consistent rendering
function CurrentTime() {
const [time, setTime] = createSignal<string>()
createEffect(() => {
setTime(new Date().toLocaleString())
})
return <div>{time() || 'Loading...'}</div>
}
수동 방식과 API 기반 환경 감지 비교
// Manual: You handle the logic
function logMessage(msg: string) {
if (typeof window === 'undefined') {
console.log(`[SERVER]: ${msg}`)
} else {
console.log(`[CLIENT]: ${msg}`)
}
}
// API: Framework handles it
const logMessage = createIsomorphicFn()
.server((msg) => console.log(`[SERVER]: ${msg}`))
.client((msg) => console.log(`[CLIENT]: ${msg}`))
아키텍처 결정 프레임워크
다음 경우 서버 전용을 선택합니다:
- 민감한 데이터(환경 변수, 시크릿)에 접근하는 경우
- 파일 시스템 작업
- 데이터베이스 연결
- 외부 API 키
다음 경우 클라이언트 전용을 선택합니다:
- DOM 조작
- 브라우저 API(localStorage, 위치 정보)
- 사용자 상호작용 처리
- 분석/추적
다음 경우 동형 방식을 선택합니다:
- 데이터 형식 지정/변환
- 비즈니스 로직
- 공유 유틸리티
- 라우트 로더(본질적으로 동형입니다)
보안 고려 사항
번들 분석
서버 전용 코드가 클라이언트 번들에 포함되지 않았는지 항상 확인합니다:
# Analyze client bundle
npm run build
# Check dist/client for any server-only imports
환경 변수 전략
- 클라이언트에 노출: 클라이언트에서 접근할 수 있는 변수에는
VITE_접두사를 사용합니다 - 서버 전용:
createServerOnlyFn()또는createServerFn()을 통해 접근합니다 - 절대 노출 금지: 데이터베이스 URL, API 키, 시크릿
오류 경계
서버/클라이언트 실행 오류를 적절하게 처리합니다:
function ErrorBoundary(props) {
return (
<ErrorBoundaryComponent
fallback={<div>Something went wrong</div>}
onError={(error) => {
if (typeof window === 'undefined') {
console.error('[SERVER ERROR]:', error)
} else {
console.error('[CLIENT ERROR]:', error)
}
}}
>
{props.children}
</ErrorBoundaryComponent>
)
}
안전하고 성능이 뛰어나며 유지 관리하기 쉬운 애플리케이션을 구축하려면 TanStack Start의 실행 모델을 이해하는 것이 매우 중요합니다. 기본적으로 동형인 접근 방식은 유연성을 제공하며, 실행 제어 API를 사용하면 필요할 때 정밀하게 제어할 수 있습니다.