서버 함수
서버 함수란 무엇인가요?
서버 함수를 사용하면 로더, 컴포넌트, 훅 또는 다른 서버 함수 등 애플리케이션 어디에서나 호출할 수 있는 서버 전용 로직을 정의할 수 있습니다. 서버에서 실행되지만 클라이언트 코드에서 원활하게 호출할 수 있습니다.
import { createServerFn } from '@tanstack/react-start'
export const getServerTime = createServerFn().handler(async () => {
// This runs only on the server
return new Date().toISOString()
})
// Call from anywhere - components, loaders, hooks, etc.
const time = await getServerTime()
서버 함수는 네트워크 경계 전반에서 타입 안전성을 유지하면서 서버 기능(데이터베이스 접근, 환경 변수, 파일 시스템)을 제공합니다.
[!NOTE] 서버 함수는 TanStack Start 애플리케이션에서 호출하기 위한 것입니다. 앱 코드에서 쉽게 사용할 수 있으며, Start가 클라이언트/서버 경계 간 직렬화를 처리합니다. Start 앱 외부에서 호출할 수 있는 엔드포인트가 필요하다면 대신 서버 라우트를 사용하세요.
동일 출처 요청
서버 함수는 애플리케이션을 위한 동일 출처 RPC 엔드포인트입니다. 서버 함수에 대한 브라우저 요청은 Fetch Metadata(Sec-Fetch-Site), Origin 또는 Referer 헤더로 검증되는 동일한 출처에서 이루어져야 합니다. 공개 API 또는 교차 출처 요청을 의도적으로 지원하는 엔드포인트에는 서버 라우트를 사용하세요.
TanStack Start는 교차 사이트 요청으로부터 서버 함수를 보호하는 createCsrfMiddleware()를 제공합니다. 앱에서 src/start.ts를 정의하지 않으면 Start가 서버 함수에 이 미들웨어를 자동으로 설치합니다. src/start.ts을 정의하는 경우 미들웨어를 명시적으로 추가하세요:
// src/start.ts
import { createStart, createCsrfMiddleware } from '@tanstack/react-start'
const csrfMiddleware = createCsrfMiddleware({
filter: (ctx) => ctx.handlerType === 'serverFn',
})
export const startInstance = createStart(() => ({
requestMiddleware: [csrfMiddleware],
}))
기본적으로 Origin 및 Referer 검사는 들어오는 요청 URL의 origin과 비교합니다. 배포 환경에서 다른 공개 origin을 허용해야 한다면 CSRF 미들웨어에 createCsrfMiddleware({ origin: 'https://app.example.com' })으로 설정합니다.
[!TIP] 이러한 헤더(
Sec-Fetch-Site,Origin또는Referer)가 하나도 없는 요청은 기본적으로 거부됩니다. 배포 환경에서 이러한 헤더를 제거하며 동일 출처 서버 함수 요청을 보장하는 다른 계층이 있다면createCsrfMiddleware({ filter: (ctx) => ctx.handlerType === 'serverFn', allowRequestsWithoutOriginCheck: true })으로 명시적으로 허용할 수 있습니다.
기본 사용법
서버 함수는 createServerFn()으로 생성하며 HTTP 메서드를 지정할 수 있습니다:
import { createServerFn } from '@tanstack/react-start'
// GET request (default)
export const getData = createServerFn().handler(async () => {
return { message: 'Hello from server!' }
})
// POST request
export const saveData = createServerFn({ method: 'POST' }).handler(async () => {
// Server-only logic
return { success: true }
})
서버 함수를 호출할 위치
다음 위치에서 서버 함수를 호출합니다:
- 라우트 로더 - 데이터 가져오기에 적합합니다
- 컴포넌트 -
useServerFn()훅과 함께 사용합니다 - 다른 서버 함수 - 서버 로직을 조합합니다
- 이벤트 핸들러 - 폼 제출, 클릭 등을 처리합니다.
// In a route loader
export const Route = createFileRoute('/posts')({
loader: () => getServerPosts(),
})
// In a component
function PostList() {
const getPosts = useServerFn(getServerPosts)
const { data } = useQuery({
queryKey: ['posts'],
queryFn: () => getPosts(),
})
}
파일 구성
규모가 큰 애플리케이션에서는 서버 측 코드를 별도의 파일로 구성하는 방식을 고려합니다. 다음은 한 가지 방법입니다:
src/utils/
├── users.functions.ts # Server function wrappers (createServerFn)
├── users.server.ts # Server-only helpers (DB queries, internal logic)
└── schemas.ts # Shared validation schemas (client-safe)
.functions.ts-createServerFn래퍼를 내보내며 어디서든 안전하게 가져올 수 있습니다.server.ts- 서버 전용 코드이며 서버 함수 핸들러 내부에서만 가져옵니다.ts(접미사 없음) - 클라이언트에서 안전한 코드(타입, 스키마, 상수)입니다
예시
// users.server.ts - Server-only helpers
import { db } from '~/db'
export async function findUserById(id: string) {
return db.query.users.findFirst({ where: eq(users.id, id) })
}
// users.functions.ts - Server functions
import { createServerFn } from '@tanstack/react-start'
import { findUserById } from './users.server'
export const getUser = createServerFn({ method: 'GET' })
.validator((data: { id: string }) => data)
.handler(async ({ data }) => {
return findUserById(data.id)
})
정적 가져오기는 안전합니다
서버 함수는 클라이언트 컴포넌트를 포함한 모든 파일에서 정적으로 가져올 수 있습니다:
// ✅ Safe - build process handles environment shaking
import { getUser } from '~/utils/users.functions'
function UserProfile({ id }) {
const { data } = useQuery({
queryKey: ['user', id],
queryFn: () => getUser({ data: { id } }),
})
}
빌드 과정에서는 클라이언트 번들의 서버 함수 구현을 RPC 스텁으로 대체합니다. 실제 서버 코드는 브라우저에 절대 전달되지 않습니다.
[!WARNING] 서버 함수에는 동적 가져오기를 사용하지 않습니다:
// ❌ 번들러 문제를 일으킬 수 있습니다
const { getUser } = await import('~/utils/users.functions')
매개변수 및 유효성 검사
서버 함수는 단일 data 매개변수를 받습니다. 네트워크 경계를 통과하므로 유효성 검사를 통해 타입 안전성과 런타임 정확성을 보장합니다.
기본 매개변수
import { createServerFn } from '@tanstack/react-start'
export const greetUser = createServerFn({ method: 'GET' })
.validator((data: { name: string }) => data)
.handler(async ({ data }) => {
return `Hello, ${data.name}!`
})
await greetUser({ data: { name: 'John' } })
Zod를 사용한 유효성 검사
견고한 유효성 검사를 위해 Zod와 같은 스키마 라이브러리를 사용합니다:
import { createServerFn } from '@tanstack/react-start'
import { z } from 'zod'
const UserSchema = z.object({
name: z.string().min(1),
age: z.number().min(0),
})
export const createUser = createServerFn({ method: 'POST' })
.validator(UserSchema)
.handler(async ({ data }) => {
// data is fully typed and validated
return `Created user: ${data.name}, age ${data.age}`
})
폼 데이터
FormData로 폼 제출을 처리합니다:
export const submitForm = createServerFn({ method: 'POST' })
.validator((data) => {
if (!(data instanceof FormData)) {
throw new Error('Expected FormData')
}
return {
name: data.get('name')?.toString() || '',
email: data.get('email')?.toString() || '',
}
})
.handler(async ({ data }) => {
// Process form data
return { success: true }
})
직렬화 타입 검사
서버 함수의 입력과 출력은 네트워크 경계를 통과하므로, TypeScript는 직렬화 가능 여부를 검사합니다:
- 유효성 검사기 입력 타입은 직렬화 가능해야 합니다.
FormData서버 함수에는POST도 허용됩니다. - 핸들러 반환 타입은 직렬화 가능해야 합니다.
Response객체는 허용됩니다.
이 기본 동작을 strict 모드라고 합니다. 의도적으로 이러한 타입 수준의 직렬화 검사를 사용하지 않으려면 createServerFn에 strict 옵션을 전달합니다:
// Disable input and output serialization type checks
export const looseServerFn = createServerFn({ strict: false })
.validator((data: { value: unknown }) => data)
.handler(async ({ data }) => {
return data.value
})
// Disable only input serialization type checks
export const looseInputServerFn = createServerFn({
strict: { input: false },
})
.validator((data: { value: unknown }) => data)
.handler(async () => {
return { ok: true }
})
// Disable only output serialization type checks
export const looseOutputServerFn = createServerFn({
strict: { output: false },
}).handler(async () => {
return getCustomSerializedValue()
})
[!WARNING]
strict: false는 TypeScript의 직렬화 검사만 완화합니다. 클라이언트와 서버 간에 값을 전송할 때도 런타임 직렬화 계층에서 해당 값을 올바르게 처리해야 합니다. 특정 서버 함수에 기본 직렬화 가능성 규칙이 지나치게 제한적인 이유를 알고 있는 경우가 아니라면 기본값인strict: true를 사용하는 것이 좋습니다.
오류 처리 및 리디렉션
서버 함수는 오류, 리디렉션, 찾을 수 없음 응답을 throw할 수 있으며, 라우트 생명주기 또는 useServerFn()을 사용하는 컴포넌트에서 호출하면 자동으로 처리됩니다.
기본 오류
import { createServerFn } from '@tanstack/react-start'
export const riskyFunction = createServerFn().handler(async () => {
if (Math.random() > 0.5) {
throw new Error('Something went wrong!')
}
return { success: true }
})
// Errors are serialized to the client
try {
await riskyFunction()
} catch (error) {
console.log(error.message) // "Something went wrong!"
}
리디렉션
인증, 탐색 등에 리디렉션을 사용합니다:
import { createServerFn } from '@tanstack/react-start'
import { redirect } from '@tanstack/react-router'
export const requireAuth = createServerFn().handler(async () => {
const user = await getCurrentUser()
if (!user) {
throw redirect({ to: '/login' })
}
return user
})
찾을 수 없음
리소스가 없으면 찾을 수 없음 오류를 throw합니다:
import { createServerFn } from '@tanstack/react-start'
import { notFound } from '@tanstack/react-router'
export const getPost = createServerFn()
.validator((data: { id: string }) => data)
.handler(async ({ data }) => {
const post = await db.findPost(data.id)
if (!post) {
throw notFound()
}
return post
})
고급 주제
더 고급 서버 함수 패턴과 기능은 다음 전용 가이드를 참조합니다:
서버 컨텍스트 및 요청 처리
요청 헤더와 쿠키에 접근하고 응답을 사용자 지정합니다:
import { createServerFn } from '@tanstack/react-start'
import {
getRequest,
getRequestHeader,
setResponseHeaders,
setResponseStatus,
} from '@tanstack/react-start/server'
// Public, non-personalized data — safe to cache shared across users.
export const getPublicData = createServerFn({ method: 'GET' }).handler(
async () => {
setResponseHeaders(
new Headers({
// 'public' is correct ONLY when the response does not depend on identity.
// For anything tied to a session/user/tenant, see the authenticated example below.
'Cache-Control': 'public, max-age=300',
'CDN-Cache-Control': 'max-age=3600, stale-while-revalidate=600',
}),
)
setResponseStatus(200)
return fetchPublicData()
},
)
Cache-Control 안전성:
public는 사용자와 서비스 사이의 모든 CDN/프록시에 응답을 누구에게나 제공할 수 있다고 알립니다. 핸들러가 세션, 쿠키 또는 인증 헤더를 읽거나 어떤 방식으로든 신원에 따라 분기하는 경우,public을 사용하면 한 사용자의 응답이 캐시되어 다음 사용자에게 재전송됩니다(테넌트 간 데이터 유출). 인증된 응답에는private을 사용합니다:
// Authenticated data — must NOT be 'public'.
export const getMyOrders = createServerFn({ method: 'GET' }).handler(
async () => {
const session = await requireSession()
setResponseHeaders(
new Headers({
// 'private' = only the user-agent may cache. Vary by Cookie/Authorization
// so any intermediary that does cache keys by identity, not URL alone.
'Cache-Control': 'private, max-age=60',
Vary: 'Cookie, Authorization',
}),
)
return db.orders.findMany({ where: { userId: session.userId } })
},
)
// For sensitive data, opt out entirely:
// setResponseHeaders(new Headers({ 'Cache-Control': 'no-store' }))
사용 가능한 유틸리티:
getRequest()- 전체 Request 객체에 접근합니다getRequestHeader(name)- 특정 요청 헤더를 읽습니다setResponseHeader(name, value)- 단일 응답 헤더를 설정합니다setResponseHeaders(headers)- Headers 객체를 통해 여러 응답 헤더를 설정합니다setResponseStatus(code)- HTTP 상태 코드를 설정합니다
스트리밍
서버 함수에서 클라이언트로 타입이 지정된 데이터를 스트리밍합니다. 서버 함수에서 데이터 스트리밍 가이드를 참조하세요.
원시 응답
Response 객체, 바이너리 데이터 또는 사용자 지정 콘텐츠 유형을 반환합니다.
점진적 향상
HTML 폼에서 .url 속성을 활용하여 JavaScript 없이 서버 함수를 사용합니다.
미들웨어
인증, 로깅 및 공유 로직을 위해 서버 함수와 미들웨어를 조합합니다. 미들웨어 가이드를 참조하세요.
데이터를 제공하는 엔드포인트에서 해당 데이터를 보호하세요. 서버 함수는 호출 UI를 렌더링하는 라우트와 독립적으로 접근할 수 있는 API 엔드포인트입니다. 비공개 데이터를 읽거나 쓰는 모든 서버 함수에
authMiddleware또는 이에 상응하는 핸들러 내부 검사를 적용하세요.beforeLoad은 라우트 UX에 유용하지만 데이터 경계는 아닙니다. 인증 서버 프리미티브를 참조하세요.
정적 서버 함수
정적 생성을 위해 빌드 시 서버 함수 결과를 캐시합니다. 정적 서버 함수를 참조하세요.
서버 컴포넌트
서버 함수는 클라이언트가 조합할 수 있는 서버 렌더링 React 컴포넌트인 Server Components를 반환할 수 있습니다. 서버 컴포넌트를 참조하세요.
요청 취소
장시간 실행되는 작업의 요청 취소를 AbortSignal으로 처리합니다.
프로덕션 빌드용 함수 ID 생성
내부적으로 서버 함수는 생성된 안정적인 함수 ID로 지정됩니다. 이러한 ID는 클라이언트/SSR 빌드에 삽입되며, 서버가 런타임에 올바른 모듈을 찾아 가져오는 데 사용됩니다.
기본적으로 ID는 번들을 간결하게 유지하고 파일 경로 노출을 방지하기 위해 동일한 시드의 SHA256 해시로 생성됩니다.
두 서버 함수의 ID가 같아지면(사용자 지정 생성기를 사용하는 경우 포함), 시스템은 _1, _2 등과 같이 증가하는 접미사를 추가하여 중복을 제거합니다.
사용자 지정:
TanStack Start 빌드 도구 플러그인을 구성할 때 generateFunctionId 함수를 제공하여 프로덕션 빌드의 함수 ID 생성을 사용자 지정할 수 있습니다.
빌드 간에 ID가 안정적으로 유지되도록 결정론적 입력(파일 이름 + functionName)을 사용하는 것이 좋습니다.
이 사용자 지정 기능은 실험적이며 변경될 수 있다는 점에 유의하세요.
예시:
Vite
import { defineConfig } from 'vite'
import { tanstackStart } from '@tanstack/react-start/plugin/vite'
export default defineConfig({
plugins: [
tanstackStart({
serverFns: {
generateFunctionId: ({ filename, functionName }) => {
return crypto
.createHash('sha1')
.update(`${filename}--${functionName}`)
.digest('hex')
},
},
}),
],
})
Rsbuild
import { defineConfig } from '@rsbuild/core'
import { tanstackStart } from '@tanstack/react-start/plugin/rsbuild'
export default defineConfig({
plugins: [
tanstackStart({
serverFns: {
generateFunctionId: ({ filename, functionName }) => {
return crypto
.createHash('sha1')
.update(`${filename}--${functionName}`)
.digest('hex')
},
},
}),
],
})
참고: 서버 함수는 원활한 호출 패턴을 유지하면서 클라이언트 번들에서 서버 코드를 추출하는 컴파일 프로세스를 사용합니다. 클라이언트에서 호출은 서버로 보내는
fetch요청으로 변환됩니다.