본문으로 건너뛰기

실행 모델

코드가 실행되는 위치를 이해하는 것은 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를 사용하면 필요할 때 정밀하게 제어할 수 있습니다.