인증에 대한 이해는 애플리케이션의 데이터를 보호하는 데 매우 중요합니다. 이 페이지에서는 인증을 구현할 때 React와 Next.js의 어떤 기능을 사용해야 하는지 안내합니다.
시작하기 전에, 과정을 세 가지 개념으로 나눠보면 도움이 됩니다.
인증 (Authentication): 사용자가 본인이 맞는지 확인하는 과정으로, 주로 사용자 이름과 비밀번호 같은 정보로 신원을 증명합니다.
세션 관리(Session Management): 여러 요청 간에 사용자의 인증 상태를 유지하고 추적합니다.
권한 부여(Authorization): 사용자가 접근할 수 있는 경로와 데이터를 결정합니다.
이 다이어그램은 React와 Next.js 기능을 사용한 인증 흐름을 보여줍니다.
이 페이지의 예제에서는 교육적인 목적을 위해 기본적인 사용자 이름과 비밀번호 인증 과정을 설명합니다. 사용자 맞춤 인증 솔루션을 구현할 수도 있지만, 보안과 간편함을 위해 인증 라이브러리 사용을 권장합니다. 이러한 라이브러리는 인증, 세션 관리, 권한 부여를 위한 내장 솔루션을 제공하며, 소셜 로그인, 다단계 인증, 역할 기반 접근 제어와 같은 추가 기능도 포함되어 있습니다. 인증 라이브러리 목록은 ‘인증 라이브러리’ 섹션에서 확인할 수 있습니다.
React의 서버 액션과 useFormState
를 활용하여 <form>
요소를 사용하여 사용자 자격 증명을 캡처하고, 폼 필드를 검증하며, 인증 공급자의 API 또는 데이터베이스를 호출할 수 있습니다.
서버 액션은 항상 서버에서 실행되므로 인증 로직을 처리하는 안전한 환경을 제공합니다.
다음은 회원가입/로그인 기능을 구현하는 단계입니다:
app/ui/signup-form.tsx
import { signup } from '@/app/actions/auth'
export function SignupForm() {
return (
<form action={signup}>
<div>
<label htmlFor="name">Name</label>
<input id="name" name="name" placeholder="Name" />
</div>
<div>
<label htmlFor="email">Email</label>
<input id="email" name="email" type="email" placeholder="Email" />
</div>
<div>
<label htmlFor="password">Password</label>
<input id="password" name="password" type="password" />
</div>
<button type="submit">Sign Up</button>
</form>
)
}
app/actions/auth.tsx
export async function signup(formData: FormData) {}
예를 들어, Zod
를 사용하여 적절한 오류 메시지와 함께 폼 스키마를 정의할 수 있습니다:
app/lib/definitions.ts
import { z } from 'zod'
export const SignupFormSchema = z.object({
name: z
.string()
.min(2, { message: 'Name must be at least 2 characters long.' })
.trim(),
email: z.string().email({ message: 'Please enter a valid email.' }).trim(),
password: z
.string()
.min(8, { message: 'Be at least 8 characters long' })
.regex(/[a-zA-Z]/, { message: 'Contain at least one letter.' })
.regex(/[0-9]/, { message: 'Contain at least one number.' })
.regex(/[^a-zA-Z0-9]/, {
message: 'Contain at least one special character.',
})
.trim(),
})
export type FormState =
| {
errors?: {
name?: string[]
email?: string[]
password?: string[]
}
message?: string
}
| undefined
인증 공급자의 API나 데이터베이스에 불필요한 호출을 방지하기 위해, 정의된 스키마와 일치하지 않는 폼 필드가 있을 경우 서버 액션에서 조기에 반환할 수 있습니다.
app/actions/auth.ts
import { SignupFormSchema, FormState } from '@/app/lib/definitions'
export async function signup(state: FormState, formData: FormData) {
// Validate form fields
const validatedFields = SignupFormSchema.safeParse({
name: formData.get('name'),
email: formData.get('email'),
password: formData.get('password'),
})
// If any form fields are invalid, return early
if (!validatedFields.success) {
return {
errors: validatedFields.error.flatten().fieldErrors,
}
}
// Call the provider or db to create a user...
}
다시 <SignupForm />
으로 돌아가서, React의 useFormState
훅을 사용하여 폼 제출 중에 검증 오류를 표시할 수 있습니다
app/ui/signup-form.tsx
'use client'
import { useFormState, useFormStatus } from 'react-dom'
import { signup } from '@/app/actions/auth'
export function SignupForm() {
const [state, action] = useFormState(signup, undefined)
return (
<form action={action}>
<div>
<label htmlFor="name">Name</label>
<input id="name" name="name" placeholder="Name" />
</div>
{state?.errors?.name && <p>{state.errors.name}</p>}
<div>
<label htmlFor="email">Email</label>
<input id="email" name="email" placeholder="Email" />
</div>
{state?.errors?.email && <p>{state.errors.email}</p>}
<div>
<label htmlFor="password">Password</label>
<input id="password" name="password" type="password" />
</div>
{state?.errors?.password && (
<div>
<p>Password must:</p>
<ul>
{state.errors.password.map((error) => (
<li key={error}>- {error}</li>
))}
</ul>
</div>
)}
<SubmitButton />
</form>
)
}
function SubmitButton() {
const { pending } = useFormStatus()
return (
<button disabled={pending} type="submit">
Sign Up
</button>
)
}
참고 사항
이 예제에서는 Next.js 앱 라우터에 포함된 React의 useFormState
훅을 사용합니다. React 19를 사용 중이라면 useActionState
를 대신 사용하세요. 자세한 내용은 React 문서를 참조하세요.
React 19에서는 useFormStatus
가 반환하는 객체에 data
, method
, action
과 같은 추가 키가 포함됩니다. React 19가 아닌 경우에는 pending
키만 사용할 수 있습니다. 또한 React 19에서는 useActionState
가 반환된 상태에 pending
키도 포함합니다.
데이터를 수정하기 전에 사용자가 해당 작업을 수행할 권한이 있는지도 항상 확인해야 합니다. 자세한 내용은 '인증 및 권한 부여'를 참조하세요.
이전 예제에서 이어서 설명
app/actions/auth.ts
export async function signup(state: FormState, formData: FormData) {
// 1. Validate form fields
// ...
// 2. Prepare data for insertion into database
const { name, email, password } = validatedFields.data
// e.g. Hash the user's password before storing it
const hashedPassword = await bcrypt.hash(password, 10)
// 3. Insert the user into the database or call an Auth Library's API
const data = await db
.insert(users)
.values({
name,
email,
password: hashedPassword,
})
.returning({ id: users.id })
const user = data[0]
if (!user) {
return {
message: 'An error occurred while creating your account.',
}
}
// TODO:
// 4. Create user session
// 5. Redirect user
}
사용자 계정을 성공적으로 생성하거나 자격 증명을 확인한 후, 사용자의 인증 상태를 관리하기 위한 세션을 생성할 수 있습니다. 세션 관리 방식에 따라 세션은 쿠키나 데이터베이스, 또는 두 곳 모두에 저장될 수 있습니다. 더 자세한 내용은 '세션 관리' 섹션에서 확인하세요.
팁
위 예제는 교육 목적으로 인증 단계를 상세히 설명하여 다소 장황해 보일 수 있습니다. 이를 통해 안전한 인증 솔루션을 직접 구현하는 과정이 얼마나 복잡해질 수 있는지 강조하고 있으며, 인증 라이브러리를 사용하여 과정을 단순화하는 것도 고려해 보세요.
또한 사용자 경험을 개선하기 위해, 회원가입 단계 초기에 중복 이메일이나 사용자 이름을 미리 확인하는 것이 좋습니다. 예를 들어, 사용자가 사용자 이름을 입력하는 중이거나 입력란이 포커스를 잃을 때 확인을 수행할 수 있습니다. 이를 통해 불필요한 폼 제출을 방지하고 즉각적인 피드백을 제공할 수 있습니다. 요청 빈도 관리는 use-debounce와 같은 라이브러리를 사용하여 디바운스 처리로 가능합니다.
세션 관리는 사용자 인증 상태가 여러 요청 간에 유지되도록 보장하는 역할을 합니다. 여기에는 세션이나 토큰을 생성하고, 저장하고, 갱신하고, 삭제하는 과정이 포함됩니다.
세션에는 두 가지 유형이 있습니다.
참고 사항
두 가지 방식 중 하나 또는 둘 다 사용할 수 있지만, iron-session이나 Jose와 같은 세션 관리 라이브러리를 사용하는 것을 권장합니다.
무상태 세션을 생성하고 관리하려면 다음 단계들을 따라야 합니다.
세션 서명에 사용할 비밀 키를 생성하고, 환경 변수로 저장합니다.
세션 관리 라이브러리를 사용해 세션 데이터를 암호화/복호화하는 로직을 작성합니다.
Next.js의 쿠키 API를 사용하여 쿠키를 관리합니다.
위 내용에 더해, 사용자가 애플리케이션으로 돌아올 때 세션을 갱신하는 기능과, 로그아웃 시 세션을 삭제하는 기능도 추가하는 것을 고려해 보세요.
참고사항
인증 라이브러리에 세션 관리가 포함되어 있는지 확인하세요.
비밀 키 생성
비밀 키 생성 세션을 서명하기 위해 비밀 키를 생성하는 방법에는 여러 가지가 있습니다. 예를 들어, 터미널에서 openssl 명령어를 사용하여 생성할 수 있습니다
openssl rand -base64 32
이 명령어는 32자 길이의 랜덤 문자열을 생성하며, 이를 비밀 키로 사용하고 환경 변수 파일에 저장할 수 있습니다.
.env
SESSION_SECRET=your_secret_key``
그런 다음 세션 관리 로직에서 이 키를 참조할 수 있습니다.
app/lib/session.js
const secretKey = process.env.SESSION_SECRET
세션 암호화 및 복호화
다음으로, 선호하는 세션 관리 라이브러리를 사용하여 세션을 암호화하고 복호화할 수 있습니다. 이전 예제에서 이어서, Edge Runtime
과 호환되는 Jose
와 React의 서버 전용 패키지를 사용하여 세션 관리 로직이 서버에서만 실행되도록 합니다."
app/lib/session.ts
import 'server-only'
import { SignJWT, jwtVerify } from 'jose'
import { SessionPayload } from '@/app/lib/definitions'
const secretKey = process.env.SESSION_SECRET
const encodedKey = new TextEncoder().encode(secretKey)
export async function encrypt(payload: SessionPayload) {
return new SignJWT(payload)
.setProtectedHeader({ alg: 'HS256' })
.setIssuedAt()
.setExpirationTime('7d')
.sign(encodedKey)
}
export async function decrypt(session: string | undefined = '') {
try {
const { payload } = await jwtVerify(session, encodedKey, {
algorithms: ['HS256'],
})
return payload
} catch (error) {
console.log('Failed to verify session')
}
}
팁
페이로드에는 이후 요청에서 사용될 최소한의 고유 사용자 데이터, 예를 들어 사용자 ID, 역할 등을 포함해야 합니다. 개인 식별 정보(전화번호, 이메일 주소, 신용카드 정보 등)나 비밀번호와 같은 민감한 데이터는 포함해서는 안 됩니다.
쿠키 설정(권장 옵션)
세션을 쿠키에 저장하려면 Next.js의 쿠키 API를 사용합니다. 쿠키는 서버에서 설정해야 하며, 다음과 같은 권장 옵션을 포함해야 합니다.
HttpOnly: 클라이언트 측 JavaScript가 쿠키에 접근하는 것을 방지합니다.
Secure: 쿠키를 전송할 때 HTTPS를 사용합니다.
SameSite: 쿠키가 교차 사이트 요청과 함께 전송될 수 있는지를 지정합니다.
Max-Age 또는 Expires: 일정 기간 후 쿠키를 삭제합니다.
Path: 쿠키의 URL 경로를 정의합니다."
각 옵션에 대한 자세한 내용은 MDN을 참조하세요.
app/lib/session.ts
import 'server-only'
import { cookies } from 'next/headers'
export async function createSession(userId: string) {
const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000)
// 2. Encrypt the session ID
const session = await encrypt({ userId, expiresAt })
// 3. Store the session in cookies for optimistic auth checks
const cookieStore = await cookies()
cookieStore().set('session', session, {
httpOnly: true,
secure: true,
expires: expiresAt,
sameSite: 'lax',
path: '/',
})
}
서버 액션에서 createSession()
함수를 호출하고, redirect()
API를 사용하여 사용자를 적절한 페이지로 리디렉션할 수 있습니다.
app/actions/auth.ts
import { createSession } from '@/app/lib/session'
export async function signup(state: FormState, formData: FormData) {
// Previous steps:
// 1. Validate form fields
// 2. Prepare data for insertion into database
// 3. Insert the user into the database or call an Library API
// Current steps:
// 4. Create user session
await createSession(user.id)
// 5. Redirect user
redirect('/profile')
}
팁
세션의 만료 시간을 연장할 수도 있습니다. 이는 사용자가 애플리케이션에 다시 접근했을 때 로그인이 유지되도록 하는 데 유용합니다.
app/lib/session.ts
import 'server-only'
import { cookies } from 'next/headers'
import { decrypt } from '@/app/lib/session'
export async function updateSession() {
const session = (await cookies()).get('session')?.value
const payload = await decrypt(session)
if (!session || !payload) {
return null
}
const expires = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000)
const cookieStore = await cookies()
cookieStore().set('session', session, {
httpOnly: true,
secure: true,
expires: expires,
sameSite: 'lax',
path: '/',
})
}
팁: 사용자 세션을 확장하는 데 사용할 수 있는 새로 고침(refresh) 토큰
인증 라이브러리가 지원하는지 확인하세요.
세션을 삭제하려면 쿠키를 삭제하세요.
app/lib/session.ts
import 'server-only'
import { cookies } from 'next/headers'
export async function deleteSession() {
const cookieStore = await cookies()
cookieStore().delete('session')
}
그런 다음 애플리케이션에서 deleteSession()
함수를 재사용할 수 있습니다(예: 로그아웃 시)
app/actions/auth.ts
import { cookies } from 'next/headers'
import { deleteSession } from '@/app/lib/session'
export async function logout() {
deleteSession()
redirect('/login')
}
데이터베이스 세션을 생성하고 관리하려면 다음 단계를 따라야 합니다.
세션 데이터를 저장할 데이터베이스에 테이블을 생성합니다(또는 인증 라이브러리가 이를 처리하는지 확인합니다).
세션을 삽입, 업데이트 및 삭제하는 기능을 구현합니다.
세션 ID를 사용자의 브라우저에 저장하기 전에 암호화하고, 데이터베이스와 쿠키가 동기화되도록 합니다(선택 사항이지만 미들웨어에서 낙관적인 인증 검사를 위해 권장됨)."
예시
app/lib/session.ts
import cookies from 'next/headers'
import { db } from '@/app/lib/db'
import { encrypt } from '@/app/lib/session'
export async function createSession(id: number) {
const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000)
// 1. Create a session in the database
const data = await db
.insert(sessions)
.values({
userId: id,
expiresAt,
})
// Return the session ID
.returning({ id: sessions.id })
const sessionId = data[0].id
// 2. Encrypt the session ID
const session = await encrypt({ sessionId, expiresAt })
// 3. Store the session in cookies for optimistic auth checks
const cookieStore = await cookies()
cookieStore().set('session', session, {
httpOnly: true,
secure: true,
expires: expiresAt,
sameSite: 'lax',
path: '/',
})
}
팁
세션 관리 구현 후, 사용자가 애플리케이션 내에서 접근할 수 있는 것과 수행할 수 있는 작업을 제어하기 위해 권한 부여 로직을 추가해야 합니다. 권한 부여 섹션으로 이동하여 더 자세히 알아보세요.
사용자가 인증되고 세션이 생성되면, 애플리케이션 내에서 사용자가 접근하고 수행할 수 있는 권한을 제어하기 위해 권한 부여(authorization)를 구현할 수 있습니다.
주요 권한 부여 확인 유형은 두 가지입니다:
낙관적(Optimistic): 사용자가 쿠키에 저장된 세션 데이터를 사용하여 특정 경로에 접근하거나 행동을 수행할 수 있는 권한이 있는지 확인하는 방법입니다. 이러한 확인은 UI 요소를 보여주거나 숨기거나, 권한이나 역할에 따라 사용자를 리디렉션하는 등의 빠른 작업에 유용합니다.
안전(Secure): 사용자가 데이터베이스에 저장된 세션 데이터를 사용하여 특정 경로에 접근하거나 행동을 수행할 수 있는 권한이 있는지 확인하는 방법입니다. 이러한 체크는 더 안전하며, 민감한 데이터에 접근하거나 특정 작업을 수행해야 하는 경우에 사용됩니다.
두 경우 모두 다음을 권장합니다.
권한 부여 로직을 중앙 집중화하기 위한 데이터 접근 계층(Data Access Layer) 생성
필요한 데이터만 반환하기 위한 데이터 전송 객체(Data Transfer Objects, DTO) 사용
옵션으로 미들웨어를 사용하여 낙관적인 체크 수행
미들웨어를 사용하여 사용자의 권한에 따라 리다이렉트하는 경우가 있습니다. 여기 몇 가지 상황이 있습니다:
낙관적인 체크 수행: 미들웨어는 모든 경로에서 실행되므로, 리다이렉트 로직을 중앙 집중화하고 인가되지 않은 사용자를 미리 필터링하는 좋은 방법입니다.
사용자 간 데이터 공유가 있는 정적 경로 보호: 예를 들어, 유료 콘텐츠 뒤에 숨겨진 데이터와 같은 경우에 해당합니다.
그러나 미들웨어는 모든 경로에서 실행되기 때문에, 미리 가져온 경로를 포함하여 성능 문제를 방지하기 위해 데이터베이스 체크는 피하고 세션을 쿠키에서만 읽는 것이 중요합니다.
예시
middleware.ts
import { NextRequest, NextResponse } from 'next/server'
import { decrypt } from '@/app/lib/session'
import { cookies } from 'next/headers'
// 1. Specify protected and public routes
const protectedRoutes = ['/dashboard']
const publicRoutes = ['/login', '/signup', '/']
export default async function middleware(req: NextRequest) {
// 2. Check if the current route is protected or public
const path = req.nextUrl.pathname
const isProtectedRoute = protectedRoutes.includes(path)
const isPublicRoute = publicRoutes.includes(path)
// 3. Decrypt the session from the cookie
const cookie = (await cookies()).get('session')?.value
const session = await decrypt(cookie)
// 4. Redirect to /login if the user is not authenticated
if (isProtectedRoute && !session?.userId) {
return NextResponse.redirect(new URL('/login', req.nextUrl))
}
// 5. Redirect to /dashboard if the user is authenticated
if (
isPublicRoute &&
session?.userId &&
!req.nextUrl.pathname.startsWith('/dashboard')
) {
return NextResponse.redirect(new URL('/dashboard', req.nextUrl))
}
return NextResponse.next()
}
// Routes Middleware should not run on
export const config = {
matcher: ['/((?!api|_next/static|_next/image|.*\\.png$).*)'],
}
미들웨어는 초기 체크에 유용할 수 있지만, 데이터를 보호하는 데 있어 유일한 방어선이 되어서는 안 됩니다. 보안 검사는 데이터 소스에 최대한 가깝게 수행해야 하며, 더 많은 정보는 데이터 접근 계층(Data Access Layer)을 참조하세요.
팁:
미들웨어에서는 req.cookies.get('session').value를 사용하여 쿠키를 읽을 수 있습니다.
미들웨어는 엣지 런타임(Edge Runtime)을 사용하므로, 인증(auth) 라이브러리와 세션 관리 라이브러리가 호환되는지 확인하세요.
미들웨어의 matcher
속성을 사용하여 미들웨어가 실행될 경로를 지정할 수 있습니다. 하지만 인증(auth)의 경우, 모든 경로에서 미들웨어가 실행되는 것이 권장됩니다.
데이터 요청과 인증 로직을 중앙에서 관리할 수 있도록 DAL(Data Access Layer)을 생성하는 것이 좋습니다.
DAL에는 사용자가 애플리케이션과 상호작용할 때 세션을 확인하는 기능이 포함되어야 합니다. 최소한 이 기능은 세션의 유효성을 검사하고, 이후 요청을 처리할 때 필요한 사용자 정보를 반환하거나 리다이렉트 하는 역할을 해야 합니다.
예를 들어, verifySession()
함수를 포함한 별도의 파일을 만들어 DAL을 구성할 수 있습니다. 그런 다음 React의 cache
API를 사용하여 React 렌더링 동안 해당 함수의 반환 값을 메모이제이션하여 성능을 최적화할 수 있습니다.
app/lib/dal.ts
import 'server-only'
import { cookies } from 'next/headers'
import { decrypt } from '@/app/lib/session'
export const verifySession = cache(async () => {
const cookie = (await cookies()).get('session')?.value
const session = await decrypt(cookie)
if (!session?.userId) {
redirect('/login')
}
return { isAuth: true, userId: session.userId }
})
그런 다음 데이터 요청, 서버 작업(Server Actions), 경로 핸들러(Route Handlers)에서 verifySession()
함수를 호출할 수 있습니다.
app/lib/dal.ts
export const getUser = cache(async () => {
const session = await verifySession()
if (!session) return null
try {
const data = await db.query.users.findMany({
where: eq(users.id, session.userId),
// Explicitly return the columns you need rather than the whole user object
columns: {
id: true,
name: true,
email: true,
},
})
const user = data[0]
return user
} catch (error) {
console.log('Failed to fetch user')
return null
}
})
팁:
DAL(Data Access Layer)은 요청 시점에 데이터를 보호하는 데 사용할 수 있습니다. 하지만 사용자가 공유하는 정적 라우트의 경우, 데이터는 요청 시점이 아닌 빌드 시점에 가져옵니다. 정적 라우트를 보호하려면 미들웨어를 사용하세요.
안전한 검사를 위해, 세션 ID를 데이터베이스와 비교하여 세션이 유효한지 확인할 수 있습니다. React의 캐시 함수(cache function)를 사용하여 렌더링 중 중복된 데이터베이스 요청을 방지하세요.
관련된 데이터 요청을 JavaScript 클래스에 통합하고, 메서드 호출 전에 verifySession()
을 실행하도록 설정하는 것도 좋은 방법입니다.
데이터를 가져올 때, 애플리케이션에서 실제로 사용하는 필요한 데이터만 반환하는 것이 좋습니다. 예를 들어, 사용자 데이터를 가져올 때 비밀번호, 전화번호 등 전체 사용자 객체가 아니라 사용자 ID와 이름 같은 필요한 정보만 반환하는 것이 좋습니다.
하지만 반환되는 데이터 구조를 제어할 수 없거나, 팀 작업 중 전체 객체가 클라이언트에 전달되지 않도록 하고 싶을 때는 클라이언트에 노출해도 안전한 필드를 지정하는 전략을 사용할 수 있습니다.
app/lib/dto.ts
import 'server-only'
import { getUser } from '@/app/lib/dal'
function canSeeUsername(viewer: User) {
return true
}
function canSeePhoneNumber(viewer: User, team: string) {
return viewer.isAdmin || team === viewer.team
}
export async function getProfileDTO(slug: string) {
const data = await db.query.users.findMany({
where: eq(users.slug, slug),
// Return specific columns here
})
const user = data[0]
const currentUser = await getUser(user.id)
// Or return only what's specific to the query here
return {
username: canSeeUsername(currentUser) ? user.username : null,
phonenumber: canSeePhoneNumber(currentUser, user.team)
? user.phonenumber
: null,
}
}
데이터 요청과 인증 로직을 데이터 접근 계층(DAL)에 중앙 집중화하고 DTO(Data Transfer Object)를 사용하면, 모든 데이터 요청의 보안과 일관성을 보장할 수 있습니다. 이를 통해 애플리케이션이 확장되더라도 유지보수, 감사, 디버깅이 쉬워집니다.
참고사항
DTO(Data Transfer Object)를 정의하는 방법에는 toJSON()
메서드를 사용하는 것부터 개별 함수, JS 클래스를 사용하는 것까지 다양한 방식이 있습니다. 이는 JavaScript 패턴이지 React나 Next.js의 기능이 아니므로, 애플리케이션에 가장 적합한 패턴을 찾기 위해 연구해 보는 것이 좋습니다.
Next.js 보안 모범 사례에 대해 더 알고 싶다면, 우리의 "Next.js 보안" 기사에서 자세히 알아보세요.
서버 컴포넌트에서의 인증 검사는 역할 기반 접근 제어에 유용합니다. 예를 들어, 사용자의 역할에 따라 조건부로 컴포넌트를 렌더링할 수 있습니다:
app/dashboard/page.tsx
import { verifySession } from '@/app/lib/dal'
export default function Dashboard() {
const session = await verifySession()
const userRole = session?.user?.role // Assuming 'role' is part of the session object
if (userRole === 'admin') {
return <AdminDashboard />
} else if (userRole === 'user') {
return <UserDashboard />
} else {
redirect('/login')
}
}
예시에서는 DAL에서 제공하는 verifySession() 함수를 사용하여 'admin', 'user', 및 권한 없는 역할을 확인합니다. 이 패턴은 각 사용자가 자신의 역할에 적합한 컴포넌트와만 상호작용하도록 보장합니다.
부분 렌더링(Partial Rendering) 때문에 레이아웃에서 체크를 수행할 때 주의해야 합니다. 레이아웃은 내비게이션 시 다시 렌더링되지 않으므로, 사용자의 세션이 모든 경로 변경 시마다 체크되지 않습니다.
대신, 데이터 자원이나 조건부로 렌더링될 컴포넌트에 가까운 곳에서 체크를 수행해야 합니다.
예를 들어, 사용자 데이터를 가져와서 내비게이션에 사용자 이미지를 표시하는 공유 레이아웃을 고려해 보세요. 인증 검사를 레이아웃에서 수행하는 대신, 레이아웃에서 사용자 데이터를 가져오는 getUser()
를 호출하고, 인증 체크는 DAL에서 수행해야 합니다.
이렇게 하면 애플리케이션 내에서 getUser()
가 호출되는 모든 위치에서 인증 검사가 수행되므로, 개발자가 사용자가 데이터에 접근할 권한이 있는지 확인하는 것을 잊는 것을 방지할 수 있습니다.
app/layout.tsx
export default async function Layout({
children,
}: {
children: React.ReactNode;
}) {
const user = await getUser();
return (
// ...
)
}
app/lib/dal.ts
export const getUser = cache(async () => {
const session = await verifySession()
if (!session) return null
// Get user ID from session and fetch data
})
참고사항
서버 액션을 공개 API 엔드포인트와 동일한 보안 고려 사항으로 취급하고, 사용자가 변형(mutation)을 수행할 수 있는 권한이 있는지 확인해야 합니다.
app/lib/actions.ts
'use server'
import { verifySession } from '@/app/lib/dal'
export async function serverAction(formData: FormData) {
const session = await verifySession()
const userRole = session?.user?.role
// Return early if user is not authorized to perform the action
if (userRole !== 'admin') {
return null
}
// Proceed with the action for authorized users
}
라우트 핸들러를 공개 API 엔드포인트와 동일한 보안 고려 사항으로 취급하고, 사용자가 해당 라우트 핸들러에 접근할 수 있는 권한이 있는지 확인해야 합니다.
예시
app/api/route.ts
import { verifySession } from '@/app/lib/dal'
export async function GET() {
// User authentication and role verification
const session = await verifySession()
// Check if the user is authenticated
if (!session) {
// User is not authenticated
return new Response(null, { status: 401 })
}
// Check if the user has the 'admin' role
if (session.user.role !== 'admin') {
// User is authenticated but does not have the right permissions
return new Response(null, { status: 403 })
}
// Continue for authorized users
}
위의 예시는 두 단계의 보안 검사를 포함한 라우트 핸들러를 보여줍니다. 먼저 활성 세션이 있는지 확인한 후, 로그인한 사용자가 'admin'인지 검증합니다.
인증을 위한 컨텍스트 제공자(context providers)
를 사용하는 것은 중첩 구조(interleaving)
덕분에 효과적입니다. 하지만 React 컨텍스트는 서버 컴포넌트(Server Components)에서 지원되지 않기 때문에 클라이언트 컴포넌트(Client Components)에만 적용 가능합니다.
이러한 방식은 동작하지만, 모든 자식 서버 컴포넌트는 먼저 서버에서 렌더링되며, 이 경우 컨텍스트 제공자의 세션 데이터에 접근할 수 없습니다.
app/layout.ts
import { ContextProvider } from 'auth-lib'
export default function RootLayout({ children }) {
return (
<html lang="en">
<body>
<ContextProvider>{children}</ContextProvider>
</body>
</html>
)
}
"use client";
import { useSession } from "auth-lib";
export default function Profile() {
const { userId } = useSession();
const { data } = useSWR(`/api/user/${userId}`, fetcher)
return (
// ...
);
}
클라이언트 컴포넌트에서 세션 데이터가 필요한 경우(예: 클라이언트 측 데이터 페칭을 위한 경우), React의 taintUniqueValue API를 사용하여 민감한 세션 데이터가 클라이언트에 노출되는 것을 방지해야 합니다.