본문으로 건너뛰기

인증 공급자 설정 방법

이 가이드에서는 널리 사용되는 인증 서비스(Auth0, Clerk, Supabase)를 TanStack Router와 통합하는 방법을 다룹니다.

빠른 시작

인증 공급자를 선택하고 해당 SDK를 설치한 다음, 공급자의 컨텍스트로 라우터를 감싸고 인증 상태를 TanStack Router의 컨텍스트 시스템에 맞게 조정합니다.


Auth0 통합

1. Auth0 설치

npm install @auth0/auth0-react

2. 환경 변수 설정

.env 파일에 추가합니다.

VITE_AUTH0_DOMAIN=your-auth0-domain.auth0.com
VITE_AUTH0_CLIENT_ID=your_auth0_client_id

3. Auth0 래퍼 만들기

src/auth/auth0.tsx를 만듭니다.

import { Auth0Provider, useAuth0 } from '@auth0/auth0-react'
import { createContext, useContext } from 'react'

interface Auth0ContextType {
isAuthenticated: boolean
user: any
login: () => void
logout: () => void
isLoading: boolean
}

const Auth0Context = createContext<Auth0ContextType | undefined>(undefined)

export function Auth0Wrapper({ children }: { children: React.ReactNode }) {
return (
<Auth0Provider
domain={import.meta.env.VITE_AUTH0_DOMAIN}
clientId={import.meta.env.VITE_AUTH0_CLIENT_ID}
authorizationParams={{
redirect_uri: window.location.origin,
}}
>
<Auth0ContextProvider>{children}</Auth0ContextProvider>
</Auth0Provider>
)
}

function Auth0ContextProvider({ children }: { children: React.ReactNode }) {
const { isAuthenticated, user, loginWithRedirect, logout, isLoading } =
useAuth0()

const contextValue = {
isAuthenticated,
user,
login: loginWithRedirect,
logout: () =>
logout({ logoutParams: { returnTo: window.location.origin } }),
isLoading,
}

return (
<Auth0Context.Provider value={contextValue}>
{children}
</Auth0Context.Provider>
)
}

export function useAuth0Context() {
const context = useContext(Auth0Context)
if (context === undefined) {
throw new Error('useAuth0Context must be used within Auth0Wrapper')
}
return context
}

4. 앱 구성 업데이트

src/App.tsx를 업데이트합니다.

import { RouterProvider } from '@tanstack/react-router'
import { Auth0Wrapper, useAuth0Context } from './auth/auth0'
import { router } from './router'

function InnerApp() {
const auth = useAuth0Context()

if (auth.isLoading) {
return (
<div className="flex items-center justify-center min-h-screen">
Loading...
</div>
)
}

return <RouterProvider router={router} context={{ auth }} />
}

function App() {
return (
<Auth0Wrapper>
<InnerApp />
</Auth0Wrapper>
)
}

export default App

5. 보호된 라우트 만들기

src/routes/_authenticated.tsx를 만듭니다.

import { createFileRoute, redirect, Outlet } from '@tanstack/react-router'

export const Route = createFileRoute('/_authenticated')({
beforeLoad: ({ context, location }) => {
if (!context.auth.isAuthenticated) {
// Auth0 handles login redirects, so just trigger login
context.auth.login()
return
}
},
component: () => <Outlet />,
})

Clerk 통합

1. Clerk 설치

npm install @clerk/clerk-react

2. 환경 변수 설정

.env 파일에 추가합니다.

VITE_CLERK_PUBLISHABLE_KEY=pk_test_your_clerk_key

3. Clerk 래퍼 만들기

src/auth/clerk.tsx를 만듭니다.

import { ClerkProvider, useUser, useAuth } from '@clerk/clerk-react'

export function ClerkWrapper({ children }: { children: React.ReactNode }) {
return (
<ClerkProvider publishableKey={import.meta.env.VITE_CLERK_PUBLISHABLE_KEY}>
{children}
</ClerkProvider>
)
}

export function useClerkAuth() {
const { isSignedIn, isLoaded } = useAuth()
const { user } = useUser()

return {
isAuthenticated: isSignedIn,
user: user
? {
id: user.id,
username:
user.username || user.primaryEmailAddress?.emailAddress || '',
email: user.primaryEmailAddress?.emailAddress || '',
}
: null,
isLoading: !isLoaded,
login: () => {
// Clerk handles login through components
window.location.href = '/sign-in'
},
logout: () => {
// Clerk handles logout through components
window.location.href = '/sign-out'
},
}
}

4. Clerk 인증 라우트 만들기

src/routes/sign-in.tsx를 만듭니다.

import { createFileRoute } from '@tanstack/react-router'
import { SignIn } from '@clerk/clerk-react'

export const Route = createFileRoute('/sign-in')({
component: () => (
<div className="flex items-center justify-center min-h-screen">
<SignIn redirectUrl="/dashboard" signUpUrl="/sign-up" />
</div>
),
})

src/routes/sign-up.tsx를 만듭니다.

import { createFileRoute } from '@tanstack/react-router'
import { SignUp } from '@clerk/clerk-react'

export const Route = createFileRoute('/sign-up')({
component: () => (
<div className="flex items-center justify-center min-h-screen">
<SignUp redirectUrl="/dashboard" signInUrl="/sign-in" />
</div>
),
})

5. 앱 구성 업데이트

src/App.tsx를 업데이트합니다.

import { RouterProvider } from '@tanstack/react-router'
import { ClerkWrapper, useClerkAuth } from './auth/clerk'
import { router } from './router'

function InnerApp() {
const auth = useClerkAuth()

if (auth.isLoading) {
return (
<div className="flex items-center justify-center min-h-screen">
Loading...
</div>
)
}

return <RouterProvider router={router} context={{ auth }} />
}

function App() {
return (
<ClerkWrapper>
<InnerApp />
</ClerkWrapper>
)
}

export default App

6. 보호된 라우트 만들기

src/routes/_authenticated.tsx를 만듭니다.

import { createFileRoute, redirect, Outlet } from '@tanstack/react-router'

export const Route = createFileRoute('/_authenticated')({
beforeLoad: ({ context, location }) => {
if (!context.auth.isAuthenticated) {
throw redirect({
to: '/sign-in',
search: {
redirect: location.href,
},
})
}
},
component: () => <Outlet />,
})

Supabase 통합

1. Supabase 설치

npm install @supabase/supabase-js

2. 환경 변수 설정

.env 파일에 추가합니다.

VITE_SUPABASE_URL=https://your-project.supabase.co
VITE_SUPABASE_ANON_KEY=your_supabase_anon_key

3. Supabase 클라이언트 만들기

src/auth/supabase.tsx를 만듭니다.

import { createClient } from '@supabase/supabase-js'
import { createContext, useContext, useEffect, useState } from 'react'

const supabase = createClient(
import.meta.env.VITE_SUPABASE_URL,
import.meta.env.VITE_SUPABASE_ANON_KEY,
)

interface SupabaseAuthState {
isAuthenticated: boolean
user: any
login: (email: string, password: string) => Promise<void>
logout: () => Promise<void>
isLoading: boolean
}

const SupabaseAuthContext = createContext<SupabaseAuthState | undefined>(
undefined,
)

export function SupabaseAuthProvider({
children,
}: {
children: React.ReactNode
}) {
const [user, setUser] = useState(null)
const [isAuthenticated, setIsAuthenticated] = useState(false)
const [isLoading, setIsLoading] = useState(true)

useEffect(() => {
// Get initial session
supabase.auth.getSession().then(({ data: { session } }) => {
setUser(session?.user ?? null)
setIsAuthenticated(!!session?.user)
setIsLoading(false)
})

// Listen for auth changes
const {
data: { subscription },
} = supabase.auth.onAuthStateChange((_event, session) => {
setUser(session?.user ?? null)
setIsAuthenticated(!!session?.user)
setIsLoading(false)
})

return () => subscription.unsubscribe()
}, [])

const login = async (email: string, password: string) => {
const { error } = await supabase.auth.signInWithPassword({
email,
password,
})
if (error) throw error
}

const logout = async () => {
const { error } = await supabase.auth.signOut()
if (error) throw error
}

return (
<SupabaseAuthContext.Provider
value={{
isAuthenticated,
user,
login,
logout,
isLoading,
}}
>
{children}
</SupabaseAuthContext.Provider>
)
}

export function useSupabaseAuth() {
const context = useContext(SupabaseAuthContext)
if (context === undefined) {
throw new Error('useSupabaseAuth must be used within SupabaseAuthProvider')
}
return context
}

4. 로그인 컴포넌트 만들기

src/routes/login.tsx를 만듭니다.

import { createFileRoute, redirect } from '@tanstack/react-router'
import { useState } from 'react'

export const Route = createFileRoute('/login')({
validateSearch: (search) => ({
redirect: (search.redirect as string) || '/dashboard',
}),
beforeLoad: ({ context, search }) => {
if (context.auth.isAuthenticated) {
throw redirect({ to: search.redirect })
}
},
component: LoginComponent,
})

function LoginComponent() {
const { auth } = Route.useRouteContext()
const { redirect } = Route.useSearch()
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const [isLoading, setIsLoading] = useState(false)
const [error, setError] = useState('')

const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
setIsLoading(true)
setError('')

try {
await auth.login(email, password)
// Supabase auth will automatically update context
window.location.href = redirect
} catch (err: any) {
setError(err.message || 'Login failed')
} finally {
setIsLoading(false)
}
}

return (
<div className="min-h-screen flex items-center justify-center">
<form
onSubmit={handleSubmit}
className="max-w-md w-full space-y-4 p-6 border rounded-lg"
>
<h1 className="text-2xl font-bold text-center">Sign In</h1>

{error && (
<div className="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded">
{error}
</div>
)}

<div>
<label htmlFor="email" className="block text-sm font-medium mb-1">
Email
</label>
<input
id="email"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
required
/>
</div>

<div>
<label htmlFor="password" className="block text-sm font-medium mb-1">
Password
</label>
<input
id="password"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
required
/>
</div>

<button
type="submit"
disabled={isLoading}
className="w-full bg-blue-600 text-white py-2 px-4 rounded-md hover:bg-blue-700 disabled:opacity-50"
>
{isLoading ? 'Signing in...' : 'Sign In'}
</button>
</form>
</div>
)
}

5. 앱 구성 업데이트

src/App.tsx를 업데이트합니다.

import { RouterProvider } from '@tanstack/react-router'
import { SupabaseAuthProvider, useSupabaseAuth } from './auth/supabase'
import { router } from './router'

function InnerApp() {
const auth = useSupabaseAuth()

if (auth.isLoading) {
return (
<div className="flex items-center justify-center min-h-screen">
Loading...
</div>
)
}

return <RouterProvider router={router} context={{ auth }} />
}

function App() {
return (
<SupabaseAuthProvider>
<InnerApp />
</SupabaseAuthProvider>
)
}

export default App

공급자 비교

기능Auth0ClerkSupabase
설정 복잡도보통낮음보통
UI 컴포넌트기본뛰어남없음
사용자 지정높음보통높음
가격부분 무료부분 무료부분 무료
소셜 로그인
엔터프라이즈 기능
데이터베이스 포함

선택 기준

  • Auth0: 복잡한 엔터프라이즈 요구 사항과 광범위한 사용자 지정
  • Clerk: 뛰어난 UI 컴포넌트를 사용한 빠른 설정
  • Supabase: 데이터베이스와 실시간 기능을 포함한 풀스택 솔루션

일반적인 문제

공급자 컨텍스트를 사용할 수 없음

문제: 컴포넌트에서 인증 컨텍스트가 undefined입니다.

해결 방법: 공급자 래퍼가 RouterProvider보다 상위에 있는지 확인합니다.

// ✅ Correct order
<AuthProvider>
<InnerApp>
<RouterProvider />
</InnerApp>
</AuthProvider>

// ❌ Wrong order
<RouterProvider>
<AuthProvider />
</RouterProvider>

무한 로딩 상태

문제: 앱이 로딩 화면에서 멈춥니다.

해결 방법: 인증 공급자가 isLoadingfalse로 올바르게 설정하는지 확인합니다.

// Add timeout fallback
useEffect(() => {
const timeout = setTimeout(() => {
if (isLoading) {
setIsLoading(false)
}
}, 5000)
return () => clearTimeout(timeout)
}, [isLoading])

Auth0의 리디렉션 루프

문제: 로그인 라우트와 보호된 라우트 사이에서 계속 리디렉션됩니다.

해결 방법: Auth0의 자동 리디렉션을 올바르게 처리합니다.

export const Route = createFileRoute('/_authenticated')({
beforeLoad: ({ context }) => {
if (!context.auth.isAuthenticated && !context.auth.isLoading) {
context.auth.login()
// Don't throw redirect, let Auth0 handle it
return
}
},
component: () => <Outlet />,
})

일반적인 다음 단계

인증 공급자를 통합한 후 다음 작업을 진행할 수 있습니다.