본문으로 건너뛰기

인증 공급자 설정 방법

이 가이드에서는 널리 사용되는 인증 서비스(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>

무한 로딩 상태​

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

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

// 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 />,
})

일반적인 다음 단계​

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

<!-- TODO: Uncomment as how-to guides are created