Next.js Authentication 가이드 정리 (App Router 기준)
원문: How to implement authentication in Next.js
기준 버전: Next.js 16 (App Router)
1. 전체 개요
Next.js 인증(Authentication) 가이드는 인증 관련 기능을 세 가지 개념으로 나누어 설명합니다.
- Authentication (인증): 사용자가 누구인지 확인하는 단계입니다. 예: 이메일 + 비밀번호로 로그인.
- Session Management (세션 관리): 한 번 인증된 사용자의 상태를 여러 요청 사이에서 유지하는 단계입니다.
- Authorization (인가): 인증된 사용자가 어떤 페이지와 데이터를 사용할 수 있는지 결정하는 단계입니다.
Next.js App Router에서는 다음과 같은 도구들을 조합하여 인증/인가를 구현하도록 안내합니다.
- React
<form>+ Server Actions +useActionState - 쿠키(
cookies()API) 기반 세션 관리 - Proxy(기존 Middleware 역할)로 낙관적(optimistic) 권한 체크
- 데이터 접근 계층(DAL) + DTO 패턴
- Server Components, Route Handlers, Context Provider 등
실전 서비스에서는 직접 모든 것을 구현하기보다는 Auth 라이브러리(Auth0, NextAuth.js, Clerk 등)를 사용하는 것을 강하게 권장합니다.
2. Authentication: 회원가입 / 로그인 흐름
2.1 Server Actions + <form> + useActionState
Next.js App Router에서는 다음 조합으로 회원가입/로그인 폼을 구현하는 패턴을 소개합니다.
-
app/ui/xxx-form.tsx<form action={signup}>처럼 Server Action을action으로 연결합니다.- 폼 필드:
name,email,password등 사용자의 입력을 받습니다.
-
app/actions/auth.tsexport async function signup(formData: FormData) { ... }형태의 Server Action을 정의합니다.- 입력 데이터 검증, 사용자 생성, 세션 생성, 리다이렉트까지 이 안에서 처리합니다.
-
useActionState로 에러 및 상태 관리- 클라이언트 컴포넌트에서
const [state, action, pending] = useActionState(signup, undefined)형태로 호출합니다. state.errors등에 서버 검증 에러를 담아서 폼 아래에 에러 메시지를 표시합니다.
- 클라이언트 컴포넌트에서
핵심 포인트: 인증 관련 로직은 항상 서버에서 실행되어야 하므로, Server Action은 인증 로직을 담기에 적합한 위치입니다.
2.2 서버에서 폼 검증 (Zod 예시)
폼 검증은 서버에서 수행하는 것을 기준으로 설명합니다.
-
Zod 스키마 정의
SignupFormSchema와 같이 이름, 이메일, 비밀번호에 대한 유효성 조건을 작성합니다.- 예: 이름 최소 길이, 이메일 포맷, 비밀번호 길이 + 문자/숫자/특수문자 포함 여부 등.
-
Server Action에서
safeParse사용SignupFormSchema.safeParse({ ... })로 폼 데이터를 검증합니다.- 실패 시
errors를 정리해서 즉시 반환하고, DB 호출을 하지 않습니다.
-
클라이언트에서 에러 출력
state?.errors?.password처럼 각 필드에 대한 에러 배열을 가져와서 메시지를 렌더링합니다.
이 패턴을 통해 불필요한 DB/API 호출을 줄이고, 에러 메시지를 세밀하게 제어할 수 있습니다.
2.3 사용자 생성 또는 로그인 검증
폼 검증이 통과되면 Server Action에서 다음 순서로 처리합니다.
-
DB 또는 Auth Provider 호출 전 데이터 준비
const { name, email, password } = validatedFields.databcrypt.hash등으로 비밀번호를 해싱한 뒤 DB에 저장합니다.
-
회원가입
- DB에
users테이블에 새로운 사용자 레코드를 삽입합니다. - 삽입 결과가 없으면 에러 메시지를 반환합니다.
- DB에
-
로그인
- 이메일로 사용자를 조회하고,
bcrypt.compare로 비밀번호를 비교합니다. - 실패 시 적절한 에러 메시지를 반환합니다.
- 이메일로 사용자를 조회하고,
-
세션 생성 + 리다이렉트는 Session Management 섹션에서 설명하는
createSession함수를 재사용
문서에서는 학습용 예제로 Auth를 직접 구현하지만, 실제 서비스에서는 Auth 라이브러리 사용을 권장합니다.
3. Session Management (세션 관리)
세션 관리는 서버가 “이 요청이 어떤 사용자로부터 왔는가?”를 알 수 있도록 상태를 유지하는 과정입니다.
문서에서는 두 가지 방식의 세션을 설명합니다.
-
Stateless Session
- 세션 정보를 암호화해서 쿠키 자체에 저장합니다.
- 서버는 쿠키를 복호화해서 사용자 정보를 확인합니다.
- 구현이 상대적으로 단순하지만, 구현 실수가 있으면 보안 이슈가 발생할 수 있습니다.
-
Database Session
- 실제 세션 데이터는 DB에 저장하고, 브라우저에는 세션 ID(토큰)만 쿠키로 저장합니다.
- 더 안전하고 유연하지만, 구현과 운영이 조금 더 복잡하고 리소스를 더 사용합니다.
문서에서는 직접 구현 예시를 보여주면서도, iron-session, Jose 같은 세션 관리 라이브러리 사용을 추천합니다.
3.1 Stateless Sessions 구현 흐름
(1) Secret Key 생성
- 세션 서명을 위한 비밀 키를 생성하고 환경 변수로 관리합니다.
- 예:
openssl rand -base64 32명령어로 랜덤 문자열 생성 후.env에SESSION_SECRET으로 저장.
openssl rand -base64 32
# .env
SESSION_SECRET=랜덤_비밀키
(2) 세션 암호화/복호화
jose라이브러리와server-only를 사용하여 서버에서만 JWT 기반 암호화를 수행합니다.encrypt(payload)userId,expiresAt등 최소한의 정보만 담아 JWT를 생성합니다.- HS256 알고리즘, 만료 시간(
7d등)을 설정합니다.
decrypt(session)- 쿠키에서 세션 값을 가져와 검증하고, 페이로드를 반환합니다.
- 검증 실패 시
null또는undefined를 반환합니다.
세션 payload에는 ID, role 등 최소한의 식별 정보만 넣고, 이메일/전화번호/비밀번호 같은 민감 데이터는 넣지 않습니다.
(3) 쿠키에 세션 저장 (권장 옵션)
cookies()API로 서버에서 쿠키를 설정합니다.- 권장 옵션:
httpOnly: true– JS에서 접근 불가, XSS로부터 보호secure: true– https에서만 전송sameSite: 'lax'등 – CSRF 완화expires/maxAge– 만료 시간path: '/'– 전체 앱에서 사용
// app/lib/session.ts (예시)
export async function createSession(userId: string) {
const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000);
const token = await encrypt({ userId, expiresAt });
const cookieStore = await cookies();
cookieStore.set('session', token, {
httpOnly: true,
secure: true,
sameSite: 'lax',
expires: expiresAt,
path: '/',
});
}
Server Action 안에서는 회원가입/로그인 성공 후 createSession을 호출하고, redirect('/dashboard') 등으로 이동합니다.
(4) 세션 연장 (update / refresh)
- 사용자가 다시 방문했을 때, 세션 만료 시간을 연장하려면:
- 쿠키에서 세션을 읽고 복호화
- 유효하면 새로운 만료 시간을 계산
- 같은 세션 값으로 쿠키를 다시 설정하여 만료 시간만 갱신
이 방식은 “로그인 유지하기” 기능을 구현할 때 유용합니다.
(5) 세션 삭제 (로그아웃)
- 로그아웃 시에는
cookies().delete('session')으로 세션 쿠키를 삭제합니다. - Server Action 또는 Route Handler에서
deleteSession()함수를 호출한 뒤, 로그인 페이지로 리다이렉트합니다.
3.2 Database Sessions 구현 흐름
Database Session은 다음 단계를 포함합니다.
-
세션 테이블 생성
- 예:
sessions(id, userId, expiresAt, createdAt, ... )
- 예:
-
세션 생성 로직
- 로그인 성공 후 DB에 세션 레코드를 삽입합니다.
- 삽입 결과에서 세션 ID를 가져옵니다.
- 세션 ID를 암호화하여 쿠키에 저장합니다.
-
요청마다 세션 조회
- 쿠키에서 세션 토큰을 복호화 →
sessionId획득 - DB에서 해당
sessionId가 유효한지, 만료되지 않았는지 확인합니다.
- 쿠키에서 세션 토큰을 복호화 →
-
활용 예시
- 여러 기기에서의 로그인 관리
- “모든 기기에서 로그아웃” 기능
- 마지막 로그인 시간, 활성 세션 수 추적 등
Database Session은 고급 기능이 필요할 때 고려하는 방식입니다.
4. Authorization (인가)
사용자가 인증된 후에는 어떤 데이터와 기능에 접근할 수 있는지를 결정하는 인가 단계가 필요합니다.
문서에서는 두 가지 유형의 인가 체크를 소개합니다.
-
Optimistic Check (낙관적 체크)
- 쿠키에 있는 세션 데이터만 보고 빠르게 판단합니다.
- Proxy에서 리다이렉트, UI 요소 show/hide 등 가벼운 용도에 적합합니다.
-
Secure Check (보안 중심 체크)
- 데이터베이스 세션과 실제 데이터 소스(DB 등)를 조회하여 판단합니다.
- 중요한 데이터 접근, 민감한 작업(삭제, 결제 등)에 사용합니다.
권장 패턴은 다음과 같습니다.
- **DAL(Data Access Layer)**에 권한 체크 로직을 모읍니다.
- **DTO(Data Transfer Object)**를 사용해 필요한 데이터만 반환합니다.
- Proxy는 빠른 1차 필터링용으로만 사용하고, 실제 보안은 DAL에서 수행합니다.
4.1 Proxy를 활용한 낙관적 체크 (선택 사항)
Proxy(기존 Middleware 역할)를 사용하여 다음과 같은 작업을 처리할 수 있습니다.
- 보호된 라우트 목록과 공개 라우트 목록을 정의
- 쿠키에서 세션을 읽고 복호화
- 로그인하지 않은 사용자를
/login으로 리다이렉트 - 이미 로그인한 사용자를
/dashboard등으로 보냄
단, Proxy는 모든 요청(프리페치 포함)에 대해 실행되므로, DB 조회는 피하고 쿠키 기반 세션만 읽는 것이 좋습니다.
4.2 Data Access Layer (DAL)
DAL은 데이터 조회 및 권한 체크를 한 곳에 모으는 계층입니다.
-
verifySession()함수 예시- 쿠키에서 세션을 읽고 복호화
- 세션이 없거나
userId가 없으면/login으로 리다이렉트 - 유효하면
{ isAuth: true, userId }등을 반환
-
이후 모든 데이터 조회/변경 함수에서
verifySession()을 먼저 호출하여 권한 체크를 일관되게 수행합니다.
이렇게 하면:
- 인증/인가 로직이 중앙 집중화됩니다.
- 누락된 권한 체크로 인한 보안 실수를 줄일 수 있습니다.
4.3 Data Transfer Object (DTO)
DTO는 “클라이언트에 어떤 필드까지 전달할지”를 제어하는 용도입니다.
- 예를 들어 사용자 데이터를 가져올 때:
- DB에는
id, name, email, phoneNumber, role, ...가 있어도 - 클라이언트에는
id, name정도만 노출할 수 있습니다.
- DB에는
또한, 현재 로그인한 사용자와 대상 사용자의 관계에 따라
- 팀이 같은지, 관리자인지 등에 따라
phoneNumber노출 여부를 동적으로 결정하는 패턴도 소개합니다.
이렇게 하면 민감한 정보가 클라이언트로 흘러가지 않도록 제어할 수 있습니다.
5. Server Components에서의 인증/인가
5.1 Server Components에서 역할 기반 렌더링
Server Component에서 verifySession()을 호출해 세션 정보를 가져온 뒤, 역할(role)에 따라 렌더링을 분기하는 패턴을 사용합니다.
// app/dashboard/page.tsx (예시)
export default async function DashboardPage() {
const session = await verifySession();
const role = session?.user?.role;
if (role === 'admin') return <AdminDashboard />;
if (role === 'user') return <UserDashboard />;
redirect('/login');
}
Server Components에서 체크하면, 민감한 UI 및 데이터를 클라이언트에 보내기 전에 필터링할 수 있습니다.
5.2 Layouts에서의 체크 주의점
- App Router의 Partial Rendering 때문에 Layout은 라우트 이동 시 매번 다시 렌더링되지 않을 수 있습니다.
- 따라서, Layout에서만 인증 체크를 하고 끝내면, 일부 상황에서 세션 변경이 반영되지 않는 문제가 생길 수 있습니다.
- 권장 방식:
- Layout에서는 공용 UI용 데이터(예: 현재 사용자 정보)를 가져오고,
- 실제 권한 체크는 DAL (
getUser()등) 내부에서 처리합니다.
5.3 Page / Leaf Components에서의 체크
- Page 컴포넌트: 해당 페이지 진입 시 세션을 검증하고 데이터 조회를 수행합니다.
- Leaf 컴포넌트: 특정 버튼이나 관리 기능을 감추기 위해, 내부에서
verifySession()을 호출하고 권한이 없으면null을 반환하는 패턴을 사용할 수 있습니다.
주의할 점은, UI만 숨긴다고 보안이 되지는 않기 때문에, 해당 컴포넌트에서 호출하는 Server Action이나 Route Handler도 반드시 자체적인 권한 체크를 해야 한다는 것입니다.
6. Server Actions / Route Handlers / Context Providers
6.1 Server Actions
- Server Actions는 공개 API 엔드포인트와 동일한 보안 기준을 가져야 합니다.
- 예:
verifySession()을 호출해 사용자가 admin인지 확인하고, 아니면 즉시 종료하는 패턴.
6.2 Route Handlers
app/api/.../route.ts같은 Route Handler도 일반 API와 동일하게 취급합니다.- 세션이 없는 경우
401, 권한이 부족한 경우403을 반환하는 2단계 체크 예제가 제공됩니다.
6.3 Context Providers
- 인증 정보를 Context Provider로 내려주는 패턴도 가능하지만, Context는 Client Component에서만 사용할 수 있습니다.
- 따라서 Server Components에서는 Context의 세션 정보를 사용할 수 없고,
- Client Component에서만
useSession()등으로 사용합니다. - 민감한 세션 데이터가 클라이언트로 노출되지 않도록, React의
taintUniqueValueAPI를 활용하는 방법도 언급합니다.
7. 권장 Auth / Session 라이브러리
문서에서 소개하는 Next.js 호환 인증 라이브러리들은 다음과 같습니다.
- Auth0
- Better Auth
- Clerk
- Descope
- Kinde
- Logto
- NextAuth.js (Auth.js)
- Ory
- Stack Auth
- Supabase
- Stytch
- WorkOS
세션 관리용 라이브러리 예시:
- Iron Session
- Jose
실제 서비스에서는 이들 라이브러리 + 문서에서 소개한 패턴(DAL, DTO, Proxy 등)을 조합하는 방식이 권장됩니다.