본문으로 건너뛰기

인증

이 가이드는 인증 패턴을 다루며 TanStack Start로 자체 인증 시스템을 구현하는 방법을 보여줍니다.

📋 시작하기 전에: 파트너 솔루션과 호스티드 서비스를 포함한 모든 사용 가능한 옵션은 인증 개요를 확인하세요.

인증 접근 방식

TanStack Start 애플리케이션에서 인증을 위한 여러 옵션을 사용할 수 있습니다:

호스티드 솔루션:

  1. Clerk - UI 컴포넌트를 갖춘 완전한 인증 플랫폼
  2. WorkOS - SSO 및 컴플라이언스 기능에 중점을 둔 엔터프라이즈용
  3. Better Auth - 오픈소스 TypeScript 라이브러리
  4. Auth.js - 80개 이상의 OAuth 제공자를 지원하는 오픈소스 라이브러리

DIY 구현의 이점:

  • 완전한 제어: 인증 흐름을 완전히 사용자 정의할 수 있습니다
  • 벤더 종속 없음: 인증 로직과 사용자 데이터를 직접 소유합니다
  • 맞춤 요구 사항: 특정 비즈니스 로직이나 컴플라이언스 요구 사항을 구현합니다
  • 비용 제어: 사용자당 가격 책정이나 사용량 제한이 없습니다

인증에는 비밀번호 보안, 세션 관리, 요청 제한, CSRF 보호, 다양한 공격 벡터 등 많은 고려 사항이 포함됩니다.

핵심 개념

인증과 권한 부여

  • 인증: 이 사용자는 누구입니까? (로그인/로그아웃)
  • 권한 부여: 이 사용자는 무엇을 할 수 있습니까? (권한/역할)

TanStack Start는 서버 함수, 세션, 라우트 보호를 통해 두 가지 모두를 위한 도구를 제공합니다.

데이터/API 경계를 먼저 보호하세요. 개인 데이터를 반환하거나 변경하는 모든 서버 함수, 서버 라우트 또는 기타 API 엔드포인트는 요청 자체에 권한 부여를 수행해야 합니다. beforeLoad은 라우트 UX에 유용합니다: 사용자가 사용할 수 없는 화면에 들어오지 못하게 하고, 어차피 실패할 작업을 트리거하지 않게 해줍니다. 이는 데이터의 보안 경계가 아닙니다. 서버 측 패턴은 인증 서버 프리미티브를 참고하세요.

필수 구성 요소

1. 인증을 위한 서버 함수

서버 함수는 민감한 인증 로직을 서버에서 안전하게 처리합니다:

import { createServerFn } from '@tanstack/react-start'
import { redirect } from '@tanstack/react-router'

// Login server function
export const loginFn = createServerFn({ method: 'POST' })
.validator((data: { email: string; password: string }) => data)
.handler(async ({ data }) => {
// Verify credentials (replace with your auth logic)
const user = await authenticateUser(data.email, data.password)

if (!user) {
return { error: 'Invalid credentials' }
}

// Create session
const session = await useAppSession()
await session.update({
userId: user.id,
email: user.email,
})

// Redirect to protected area
throw redirect({ to: '/dashboard' })
})

// Logout server function
export const logoutFn = createServerFn({ method: 'POST' }).handler(async () => {
const session = await useAppSession()
await session.clear()
throw redirect({ to: '/' })
})

// Get current user
export const getCurrentUserFn = createServerFn({ method: 'GET' }).handler(
async () => {
const session = await useAppSession()
const userId = session.data.userId

if (!userId) {
return null
}

return await getUserById(userId)
},
)

2. 세션 관리

TanStack Start는 보안이 적용된 HTTP-only 쿠키 세션을 제공합니다:

// utils/session.ts
import { useSession } from '@tanstack/react-start/server'

type SessionData = {
userId?: string
email?: string
role?: string
}

export function useAppSession() {
return useSession<SessionData>({
// Session configuration
name: 'app-session',
password: process.env.SESSION_SECRET!, // At least 32 characters
// Optional: customize cookie settings
cookie: {
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
httpOnly: true,
},
})
}

3. 인증 컨텍스트

애플리케이션 전체에서 인증 상태를 공유합니다:

// contexts/auth.tsx
import { createContext, useContext, ReactNode } from 'react'
import { useServerFn } from '@tanstack/react-start'
import { getCurrentUserFn } from '../server/auth'

type User = {
id: string
email: string
role: string
}

type AuthContextType = {
user: User | null
isLoading: boolean
refetch: () => void
}

const AuthContext = createContext<AuthContextType | undefined>(undefined)

export function AuthProvider({ children }: { children: ReactNode }) {
const { data: user, isLoading, refetch } = useServerFn(getCurrentUserFn)

return (
<AuthContext.Provider value={{ user, isLoading, refetch }}>
{children}
</AuthContext.Provider>
)
}

export function useAuth() {
const context = useContext(AuthContext)
if (!context) {
throw new Error('useAuth must be used within AuthProvider')
}
return context
}

4. 라우트 보호

beforeLoad를 사용하여 라우트를 보호하세요:

// routes/_authed.tsx - Layout route for protected pages
import { createFileRoute, redirect } from '@tanstack/react-router'
import { getCurrentUserFn } from '../server/auth'

export const Route = createFileRoute('/_authed')({
beforeLoad: async ({ location }) => {
const user = await getCurrentUserFn()

if (!user) {
throw redirect({
to: '/login',
search: { redirect: location.href },
})
}

// Pass user to child routes
return { user }
},
})
// routes/_authed/dashboard.tsx - Protected route
import { createFileRoute } from '@tanstack/react-router'

export const Route = createFileRoute('/_authed/dashboard')({
component: DashboardComponent,
})

function DashboardComponent() {
const { user } = Route.useRouteContext()

return (
<div>
<h1>Welcome, {user.email}!</h1>
{/* Dashboard content */}
</div>
)
}

구현 패턴

기본 이메일/비밀번호 인증

// server/auth.ts
import bcrypt from 'bcryptjs'
import { createServerFn } from '@tanstack/react-start'

// User registration
export const registerFn = createServerFn({ method: 'POST' })
.validator((data: { email: string; password: string; name: string }) => data)
.handler(async ({ data }) => {
// Check if user exists
const existingUser = await getUserByEmail(data.email)
if (existingUser) {
return { error: 'User already exists' }
}

// Hash password
const hashedPassword = await bcrypt.hash(data.password, 12)

// Create user
const user = await createUser({
email: data.email,
password: hashedPassword,
name: data.name,
})

// Create session
const session = await useAppSession()
await session.update({ userId: user.id })

return { success: true, user: { id: user.id, email: user.email } }
})

async function authenticateUser(email: string, password: string) {
const user = await getUserByEmail(email)
if (!user) return null

const isValid = await bcrypt.compare(password, user.password)
return isValid ? user : null
}

역할 기반 접근 제어(RBAC)

// utils/auth.ts
export const roles = {
USER: 'user',
ADMIN: 'admin',
MODERATOR: 'moderator',
} as const

type Role = (typeof roles)[keyof typeof roles]

export function hasPermission(userRole: Role, requiredRole: Role): boolean {
const hierarchy = {
[roles.USER]: 0,
[roles.MODERATOR]: 1,
[roles.ADMIN]: 2,
}

return hierarchy[userRole] >= hierarchy[requiredRole]
}

// Protected route with role check
export const Route = createFileRoute('/_authed/admin/')({
beforeLoad: async ({ context }) => {
if (!hasPermission(context.user.role, roles.ADMIN)) {
throw redirect({ to: '/unauthorized' })
}
},
})

소셜 인증 통합

// Example with OAuth providers
export const authProviders = {
google: {
clientId: process.env.GOOGLE_CLIENT_ID!,
redirectUri: `${process.env.APP_URL}/auth/google/callback`,
},
github: {
clientId: process.env.GITHUB_CLIENT_ID!,
redirectUri: `${process.env.APP_URL}/auth/github/callback`,
},
}

export const initiateOAuthFn = createServerFn({ method: 'POST' })
.validator((data: { provider: 'google' | 'github' }) => data)
.handler(async ({ data }) => {
const provider = authProviders[data.provider]
const state = generateRandomState()

// Store state in session for CSRF protection
const session = await useAppSession()
await session.update({ oauthState: state })

// Generate OAuth URL
const authUrl = generateOAuthUrl(provider, state)

throw redirect({ href: authUrl })
})

비밀번호 재설정 흐름

// Password reset request
export const requestPasswordResetFn = createServerFn({ method: 'POST' })
.validator((data: { email: string }) => data)
.handler(async ({ data }) => {
const user = await getUserByEmail(data.email)
if (!user) {
// Don't reveal if email exists
return { success: true }
}

const token = generateSecureToken()
const expires = new Date(Date.now() + 60 * 60 * 1000) // 1 hour

await savePasswordResetToken(user.id, token, expires)
await sendPasswordResetEmail(user.email, token)

return { success: true }
})

// Password reset confirmation
export const resetPasswordFn = createServerFn({ method: 'POST' })
.validator((data: { token: string; newPassword: string }) => data)
.handler(async ({ data }) => {
const resetToken = await getPasswordResetToken(data.token)

if (!resetToken || resetToken.expires < new Date()) {
return { error: 'Invalid or expired token' }
}

const hashedPassword = await bcrypt.hash(data.newPassword, 12)
await updateUserPassword(resetToken.userId, hashedPassword)
await deletePasswordResetToken(data.token)

return { success: true }
})

보안 모범 사례

1. 비밀번호 보안

// Use strong hashing (bcrypt, scrypt, or argon2)
import bcrypt from 'bcryptjs'

const saltRounds = 12 // Adjust based on your security needs
const hashedPassword = await bcrypt.hash(password, saltRounds)

2. 세션 보안

// Use secure session configuration
export function useAppSession() {
return useSession({
name: 'app-session',
password: process.env.SESSION_SECRET!, // 32+ characters
cookie: {
secure: process.env.NODE_ENV === 'production', // HTTPS only in production
sameSite: 'lax', // CSRF protection
httpOnly: true, // XSS protection
maxAge: 7 * 24 * 60 * 60, // 7 days
},
})
}

3. 요청 제한

// Simple in-memory rate limiting (use Redis in production)
const loginAttempts = new Map<string, { count: number; resetTime: number }>()

export const rateLimitLogin = (ip: string): boolean => {
const now = Date.now()
const attempts = loginAttempts.get(ip)

if (!attempts || now > attempts.resetTime) {
loginAttempts.set(ip, { count: 1, resetTime: now + 15 * 60 * 1000 }) // 15 min
return true
}

if (attempts.count >= 5) {
return false // Too many attempts
}

attempts.count++
return true
}

4. 입력 유효성 검사

import { z } from 'zod'

const loginSchema = z.object({
email: z.string().email().max(255),
password: z.string().min(8).max(100),
})

export const loginFn = createServerFn({ method: 'POST' })
.validator((data) => loginSchema.parse(data))
.handler(async ({ data }) => {
// data is now validated
})

인증 테스트

서버 함수 단위 테스트

// __tests__/auth.test.ts
import { describe, it, expect, beforeEach } from 'vitest'
import { loginFn } from '../server/auth'

describe('Authentication', () => {
beforeEach(async () => {
await setupTestDatabase()
})

it('should login with valid credentials', async () => {
const result = await loginFn({
data: { email: 'test@example.com', password: 'password123' },
})

expect(result.error).toBeUndefined()
expect(result.user).toBeDefined()
})

it('should reject invalid credentials', async () => {
const result = await loginFn({
data: { email: 'test@example.com', password: 'wrongpassword' },
})

expect(result.error).toBe('Invalid credentials')
})
})

통합 테스트

// __tests__/auth-flow.test.tsx
import { render, screen, fireEvent, waitFor } from '@testing-library/react'
import { RouterProvider, createMemoryHistory } from '@tanstack/react-router'
import { router } from '../router'

describe('Authentication Flow', () => {
it('should redirect to login when accessing protected route', async () => {
const history = createMemoryHistory()
history.push('/dashboard') // Protected route

render(<RouterProvider router={router} history={history} />)

await waitFor(() => {
expect(screen.getByText('Login')).toBeInTheDocument()
})
})
})

일반 패턴

로딩 상태

function LoginForm() {
const [isLoading, setIsLoading] = useState(false)
const loginMutation = useServerFn(loginFn)

const handleSubmit = async (data: LoginData) => {
setIsLoading(true)
try {
await loginMutation.mutate(data)
} catch (error) {
// Handle error
} finally {
setIsLoading(false)
}
}

return (
<form onSubmit={handleSubmit}>
{/* Form fields */}
<button disabled={isLoading}>
{isLoading ? 'Logging in...' : 'Login'}
</button>
</form>
)
}

로그인 상태 유지 기능

export const loginFn = createServerFn({ method: 'POST' })
.validator(
(data: { email: string; password: string; rememberMe?: boolean }) => data,
)
.handler(async ({ data }) => {
const user = await authenticateUser(data.email, data.password)
if (!user) return { error: 'Invalid credentials' }

const session = await useAppSession()
await session.update(
{ userId: user.id },
{
// Extend session if remember me is checked
maxAge: data.rememberMe ? 30 * 24 * 60 * 60 : undefined, // 30 days vs session
},
)

return { success: true }
})

작업 예시

다양한 인증 패턴을 이해하려면 이 구현을 살펴보세요:

다른 솔루션에서의 마이그레이션

클라이언트 측 인증에서

클라이언트 측 인증(localStorage, context만 사용)에서 마이그레이션하는 경우:

  1. 인증 로직을 서버 함수로 옮깁니다
  2. localStorage를 서버 세션으로 대체합니다
  3. 라우트 보호가 beforeLoad를 사용하도록 업데이트합니다
  4. 적절한 보안 헤더와 CSRF 보호를 추가합니다

다른 프레임워크에서

  • Next.js: API routes를 서버 함수로 바꾸고, NextAuth 세션을 마이그레이션합니다
  • Remix: loaders/actions를 서버 함수로 변환하고, 세션 패턴을 조정합니다
  • SvelteKit: form actions를 서버 함수로 옮기고, 라우트 보호를 업데이트합니다

프로덕션 고려 사항

인증 방식을 선택할 때는 다음 요소를 고려합니다:

호스팅형 vs 직접 구현 비교

호스팅형 솔루션(Clerk, WorkOS, Better Auth):

  • 사전 구축된 보안 조치와 정기 업데이트
  • UI 컴포넌트와 사용자 관리 기능
  • 규정 준수 인증과 감사 추적
  • 지원과 문서
  • 사용자별 또는 구독 기반 가격

직접 구현:

  • 구현과 데이터에 대한 완전한 제어
  • 지속적인 구독 비용 없음
  • 맞춤 비즈니스 로직과 워크플로
  • 보안 업데이트와 모니터링에 대한 책임
  • 엣지 케이스와 공격 벡터를 처리해야 함

보안 고려 사항

인증 시스템은 다양한 보안 측면을 처리해야 합니다:

  • 비밀번호 해싱과 타이밍 공격 방지
  • 세션 관리와 고정 공격 방지
  • CSRF 및 XSS 보호
  • 속도 제한과 무차별 대입 공격 방지
  • OAuth 흐름 보안
  • 규정 준수 요구 사항(GDPR, CCPA 등)

다음 단계

인증을 구현할 때는 다음을 고려합니다:

  • 보안 검토: 구현이 보안 모범 사례를 따르는지 검토합니다
  • 성능: 사용자 조회와 세션 검증에 캐시를 추가합니다
  • 모니터링: 인증 이벤트에 대한 로깅과 모니터링을 추가합니다
  • 규정 준수: 개인 데이터를 저장하는 경우 관련 규정을 준수하도록 합니다

다른 인증 방식은 인증 개요를 확인합니다. 특정 통합 도움이 필요하면 실전 예제를 살펴봅니다.