Next.js JWT 인증 간편하게 만들기: 설정부터 배포까지
Emily Parker
Product Engineer · Leapcell

Next.js에서 JWT 미들웨어를 사용한 인증 및 권한 부여 구현: 기본에서 실제 적용까지
I. 서론: 인증에 JWT를 선택하는 이유?
최신 웹 개발에서 사용자 인증 및 권한 부여는 안전한 애플리케이션을 구축하는 데 있어 핵심적인 부분입니다. JWT(JSON Web Token)는 상태 비저장, 플랫폼 간 호환성, 경량 특성으로 프런트엔드와 백엔드가 분리된 애플리케이션에서 가장 주류 인증 솔루션 중 하나가 되었습니다. React 생태계에서 가장 인기 있는 풀스택 프레임워크인 Next.js는 요청 가로채기 및 경로 보호를 효율적으로 구현할 수 있는 강력한 미들웨어 메커니즘을 제공합니다. 이 기사에서는 Next.js에서 JWT와 결합된 사용자 지정 미들웨어를 통해 사용자 인증을 달성하는 방법, 요청에 유효한 userid
및 username
이 포함되도록 하는 방법, 기본 원리에서 프로덕션 수준의 사례에 이르기까지 전체 프로세스를 다룹니다.
II. JWT 기본 사항: 핵심 개념과 작동 원리
2.1 JWT 구조 분석
JWT는 .
으로 구분된 세 부분으로 구성됩니다.
- 헤더: 토큰 유형(JWT) 및 서명 알고리즘(예: HMAC SHA256, RSA)을 포함합니다.
{ "alg": "HS256", "typ": "JWT" }
- 페이로드: 사용자 정보(민감하지 않은 데이터) 및 메타데이터(예: 만료 시간
exp
)를 저장합니다.{ "sub": "1234567890", // 사용자 고유 식별자 (userid) "name": "John Doe", // 사용자 이름 (username) "iat": 1687756800, // 발행 시간 "exp": 1687760400 // 만료 시간 (1시간 후) }
- 서명: 헤더에 지정된 알고리즘을 사용하여 헤더 및 페이로드에 서명하여 토큰이 변조되지 않았는지 확인합니다.
2.2 인증 프로세스
- 로그인 단계: 사용자가 자격 증명(사용자 이름/비밀번호)을 제출합니다. 서버가 성공적으로 확인한 후 JWT를 생성하여 클라이언트에 반환합니다.
- 토큰 저장: 클라이언트는 JWT를
HttpOnly
쿠키 또는 로컬 스토리지에 저장합니다(XSS 공격을 피하기 위해 쿠키를 권장합니다). - 요청 가로채기: 각 후속 요청은 JWT를 전달합니다(일반적으로
Authorization
헤더 또는 쿠키에). 서버는 미들웨어를 통해 토큰의 유효성을 검사합니다. - 권한 부여 결정: 성공적인 검증 후
userid
및username
을 파싱하여 권한 판단 또는 데이터 필터링을 수행합니다.
III. Next.js 미들웨어: 핵심 메커니즘 및 장점
3.1 미들웨어 유형
Next.js 13+는 두 가지 모드를 지원하는 새로운 미들웨어 시스템을 도입했습니다.
- 전역 미들웨어: 모든 경로에 적용됩니다(
middleware-exclude
패턴과 일치하는 경로는 제외). - 경로 미들웨어: 특정 경로 또는 경로 그룹에만 적용됩니다(파일 위치 또는 구성을 통해 정의).
3.2 핵심 기능
- 요청 가로채기: 요청이 페이지 또는 API 경로에 도달하기 전에 요청 헤더를 수정하고, 쿠키를 파싱하고, ID를 확인합니다.
- 응답 제어: 인증 결과에 따라 리디렉션 응답을 반환합니다(예: 인증되지 않은 경우 로그인 페이지로 리디렉션).
- Edge 실행: Vercel Edge Network에서 실행하여 낮은 지연 시간 인증을 달성할 수 있습니다(Node.js 특정 API 사용은 피하십시오).
3.3 미들웨어 파일 구조
프로젝트의 루트 디렉토리에 middleware.ts
(TypeScript) 또는 middleware.js
를 만들고 Request
및 NextRequest
객체를 수신하는 비동기 함수를 내보냅니다.
import { NextResponse } from 'next/server'; import type { NextRequest } from 'next/server'; export async function middleware(request: NextRequest) { // 인증 로직 const response = NextResponse.next(); return response; } // 미들웨어의 범위 구성 (예: 모든 페이지 경로와 일치) export const config = { matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'] };
IV. 프로젝트 설정: 초기화부터 종속성 설치까지
4.1 Next.js 프로젝트 생성
npx create-next-app@latest jwt-auth-demo # TypeScript, App Router (기본값), ESLint (선택 사항) 선택
4.2 종속성 설치
npm install jsonwebtoken cookie @types/cookie # JWT 처리 및 쿠키 파싱 npm install bcryptjs # 비밀번호 해싱 (서버 측에서만 사용)
4.3 환경 구성
루트 디렉토리에 .env.local
을 생성하여 JWT 서명 키와 만료 시간을 저장합니다.
JWT_SECRET=your-strong-secret-key-128-bit-or-longer JWT_EXPIRES_IN=3600 # 1시간 (초 단위)
V. 로그인 기능 구현: JWT 생성 및 반환
5.1 로그인 API 경로 생성
app/api/auth/login/route.ts
에서 로그인 로직을 구현합니다.
import { NextResponse } from 'next/server'; import jwt from 'jsonwebtoken'; import { compare } from 'bcryptjs'; import { User } from '@/types/user'; // 사용자 지정 사용자 유형 // 모의 데이터베이스 사용자 (실제로 데이터베이스에 연결해야 함) const mockUsers: User[] = [ { id: '1', username: 'admin', password: '$2a$10$H6pXZpZ...' } // 해시된 비밀번호 ]; export async function POST(request: Request) { const { username, password } = await request.json(); // 사용자 찾기 const user = mockUsers.find(u => u.username === username); if (!user) { return NextResponse.json({ error: '사용자가 존재하지 않습니다' }, { status: 401 }); } // 비밀번호 확인 const isPasswordValid = await compare(password, user.password); if (!isPasswordValid) { return NextResponse.json({ error: '비밀번호가 틀렸습니다' }, { status: 401 }); } // JWT 생성 const token = jwt.sign( { userId: user.id, username: user.username }, // 페이로드에 userid 및 username 포함 process.env.JWT_SECRET!, { expiresIn: process.env.JWT_EXPIRES_IN } ); // HttpOnly 쿠키 설정 (보안 속성) const response = NextResponse.json({ message: '로그인 성공' }); response.cookies.set('authToken', token, { httpOnly: true, secure: process.env.NODE_ENV === 'production', // 프로덕션 환경에서 HTTPS 활성화 sameSite: 'lax', // CSRF 공격 방지 maxAge: Number(process.env.JWT_EXPIRES_IN), path: '/' }); return response; }
5.2 로그인 페이지 컴포넌트
app/login/page.tsx
에서 로그인 양식을 만듭니다.
'use client'; // 클라이언트 컴포넌트 import { useState } from'react'; import { useRouter } from 'next/navigation'; export default function LoginPage() { const [username, setUsername] = useState(''); const [password, setPassword] = useState(''); const router = useRouter(); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); const response = await fetch('/api/auth/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username, password }) }); if (response.ok) { router.push('/dashboard'); // 로그인 성공 후 리디렉션 } else { const data = await response.json(); console.error(data.error); } }; return ( <form onSubmit={handleSubmit}> <input type="text" placeholder="Username" value={username} onChange={(e) => setUsername(e.target.value)} required /> <input type="password" placeholder="Password" value={password} onChange={(e) => setPassword(e.target.value)} required /> <button type="submit">Login</button> </form> ); }
VI. 핵심 미들웨어 구현: JWT 검증 및 사용자 정보 추출
6.1 쿠키에서 JWT 파싱
먼저 JWT 검증을 처리하기 위해 유틸리티 함수 utils/jwt.ts
를 만듭니다.
import jwt from 'jsonwebtoken'; import { Cookie } from 'cookie'; export type JwtPayload = { userId: string; username: string; iat?: number; exp?: number; }; export function parseAuthCookie(cookieHeader: string | undefined): string | null { if (!cookieHeader) return null; const cookies = Cookie.parse(cookieHeader); return cookies.authToken || null; } export function verifyJwt(token: string): JwtPayload | null { try { return jwt.verify(token, process.env.JWT_SECRET!) as JwtPayload; } catch (error) { console.error('JWT verification failed:', error); return null; } }
6.2 인증 미들웨어 작성
요청을 가로채고 JWT를 검증하기 위해 middleware.ts
에서 핵심 로직을 구현합니다.
import { NextResponse } from 'next/server'; import type { NextRequest } from 'next/server'; import { parseAuthCookie, verifyJwt } from './utils/jwt'; export async function middleware(request: NextRequest) { // 1. 쿠키에서 JWT 가져오기 const token = parseAuthCookie(request.headers.get('cookie')); // 2. 보호된 경로 정의 (예: 로그인 페이지를 제외한 모든 페이지) const isProtectedRoute = !request.nextUrl.pathname.startsWith('/login'); if (isProtectedRoute) { // 3. 토큰이 제공되지 않음: 로그인 페이지로 리디렉션 if (!token) { return NextResponse.redirect(new URL('/login', request.url)); } // 4. JWT 검증 const payload = verifyJwt(token); if (!payload) { // 토큰이 유효하지 않거나 만료됨: 유효하지 않은 쿠키 삭제 (선택 사항) const response = NextResponse.redirect(new URL('/login', request.url)); response.cookies.delete('authToken'); return response; } // 5. 검증 통과: 요청 컨텍스트에 사용자 정보 첨부 (선택 사항, 경로 매개변수와 함께 사용해야 함) // 또는 후속 처리에서 getToken 함수를 통해 가져오기 } else { // 6. 로그인 페이지: 인증된 경우 대시보드로 리디렉션 if (token && verifyJwt(token)) { return NextResponse.redirect(new URL('/dashboard', request.url)); } } // 7. 요청이 계속되도록 허용 return NextResponse.next(); } // 8. 미들웨어의 범위 구성 (API 경로 및 정적 리소스를 제외한 모든 페이지와 일치) export const config = { matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'] };
6.3 미들웨어의 핵심 로직 분석
- 경로 보호 전략:
/login
페이지를 제외하고isProtectedRoute
를 통해 인증이 필요한지 여부를 결정합니다. - 토큰 존재 확인: 유효한 쿠키가 전달되지 않으면 로그인 페이지로 리디렉션합니다.
- 서명 검증 및 만료 확인:
jsonwebtoken
라이브러리를 사용하여 토큰을 검증하고 만료(exp
필드)를 자동으로 처리합니다. - 보안 강화: 토큰이 유효하지 않을 때 클라이언트 쿠키를 삭제하여 만료된 토큰의 재사용을 방지합니다.
- 로그인 페이지 최적화: 인증된 사용자가 로그인 페이지를 방문하면 사용자 경험을 개선하기 위해 자동으로 대시보드로 리디렉션합니다.
VII. 경로 보호: 페이지에서 사용자 정보 가져오기
7.1 서버 컴포넌트에서 사용자 정보 가져오기
보호된 페이지(예: app/dashboard/page.tsx
)에서 JWT를 다시 검증하여 userid
및 username
을 가져옵니다.
import { NextResponse } from 'next/server'; import { parseAuthCookie, verifyJwt } from '@/utils/jwt'; export default async function Dashboard() { // 요청 헤더에서 쿠키 가져오기 (서버 컴포넌트는 요청 객체에 액세스할 수 있음) const token = parseAuthCookie(headers.get('cookie')); const payload = token? verifyJwt(token) : null; if (!payload) { // 이론적으로 미들웨어가 가로챘지만 에지 케이스를 처리해야 함 (예: 요청 중 토큰 만료) return NextResponse.redirect(new URL('/login', request.url)); } return ( <div> <h1>환영합니다, {payload.username}!</h1> <p>사용자 ID: {payload.userId}</p> </div> ); }
7.2 클라이언트 컴포넌트에서 인증 상태 가져오기
useEffect
또는 타사 라이브러리(예: SWR)를 통해 서버에서 사용자 상태를 가져옵니다.
'use client'; import { useEffect, useState } from'react'; import { useRouter } from 'next/navigation'; export default function UserProfile() { const [user, setUser] = useState<{ userId: string; username: string } | null>(null); const router = useRouter(); useEffect(() => { // 클라이언트는 API 경로로 요청을 보내 토큰을 확인하고 사용자 정보를 반환합니다. const fetchUser = async () => { const response = await fetch('/api/auth/user'); if (response.ok) { const data = await response.json(); setUser(data); } else { router.push('/login'); } }; fetchUser(); }, [router]); return ( user? ( <div> <h2>사용자 정보</h2> <p>사용자 이름: {user.username}</p> <p>사용자 ID: {user.userId}</p> </div> ) : ( <p>로딩 중...</p> ) ); }
7.3 사용자 정보 API 경로 생성
app/api/auth/user/route.ts
에서 사용자 정보 인터페이스를 제공합니다(미들웨어로 보호해야 함).
import { NextResponse } from 'next/server'; import { parseAuthCookie, verifyJwt } from '@/utils/jwt'; export async function GET(request: Request) { const token = parseAuthCookie(request.headers.get('cookie')); const payload = token? verifyJwt(token) : null; if (!payload) { return NextResponse.json({ error: '인증되지 않았습니다' }, { status: 401 }); } return NextResponse.json({ userId: payload.userId, username: payload.username }); }
VIII. 로그아웃: 클라이언트 쿠키 삭제
8.1 로그아웃 API 경로 구현
app/api/auth/logout/route.ts
에서 authToken
쿠키를 삭제합니다.
import { NextResponse } from 'next/server'; export async function POST(request: Request) { const response = NextResponse.redirect(new URL('/login', request.url)); response.cookies.delete('authToken', { path: '/' }); // 쿠키 삭제 return response; }
8.2 로그아웃 버튼 컴포넌트
클라이언트 컴포넌트에서 로그아웃 API를 호출합니다.
'use client'; import { useRouter } from 'next/navigation'; export default function LogoutButton() { const router = useRouter(); const handleLogout = async () => { await fetch('/api/auth/logout', { method: 'POST' }); router.push('/login'); }; return <button onClick={handleLogout}>로그아웃</button>; }
IX. 보안 강화: 프로덕션 환경의 모범 사례
9.1 쿠키 보안 속성 구성
HttpOnly
: XSS 공격 방지 (JavaScript를 통해 토큰에 액세스할 수 없음).Secure
: HTTPS 환경에서만 전송 (프로덕션 환경에서 필수).SameSite: 'lax'
또는'strict'
: CSRF 공격 방지 (lax
는 대부분의 시나리오에 적합하고strict
는 더 엄격함).Domain
및Path
: 쿠키의 범위를 제한 (예:yourdomain.com
만 액세스 허용).
9.2 JWT 서명 및 만료 전략
- 최소 256비트 키(
HS256
알고리즘) 또는 RSA 비대칭 암호화 사용 (분산 시스템에 적합). - 짧은 만료 시간 (예: 1시간)을 설정하고, 잦은 로그인을 피하기 위해 Refresh Token 메커니즘과 결합합니다.
9.3 미들웨어 성능 최적화
- Edge 미들웨어: 토큰 존재 확인과 같은 민감하지 않은 인증 로직을 에지 노드로 이동하여 서버 부하를 줄입니다.
- 캐시 제어: 정적 리소스(예: 이미지, CSS)에 대한 미들웨어 처리를 건너뜁니다 (
config.matcher
를 통해 제외).
9.4 오류 처리 및 로깅
- 미들웨어에서 JWT 파싱 오류를 포착하고 자세한 로그를 기록합니다 (프로덕션 환경에서는 Sentry와 같은 모니터링 도구를 사용하는 것이 좋습니다).
- 기술적 세부 정보를 노출하지 않도록 클라이언트에 일반적인 오류 메시지(예: "인증 실패")를 표시합니다.
9.5 Refresh Token 메커니즘 (확장)
- 사용자가 로그인할 때 JWT와 refresh token을 모두 반환합니다 (별도의
HttpOnly 쿠키
에 저장됨). - JWT가 만료되면 refresh token을 사용하여 다시 로그인하지 않고도 서버에서 새 JWT를 요청합니다.
// Refresh token API 예제 (app/api/auth/refresh/route.ts) export async function POST(request: Request) { const refreshToken = parseAuthCookie(request.headers.get('cookie')); // refresh token 확인 (서버 측 데이터베이스 또는 Redis에 저장해야 함) // 새 JWT 생성 및 반환 }
X. 일반적인 문제 및 해결 방법
10.1 미들웨어가 작동하지 않습니까?
middleware.ts
의 파일 위치를 확인합니다 (프로젝트의 루트 디렉토리에 있어야 함).config.matcher
일치 규칙이 올바른지 확인합니다 (절대 경로 또는 정규식 사용).
10.2 쿠키가 요청에 전달되지 않습니까?
- 로그인 중에 설정된 쿠키의
path
가'/'
인지 확인합니다. - 프로덕션 환경에서
Secure: true
가 활성화된 경우 HTTPS를 통해 액세스해야 합니다.
10.3 클라이언트 컴포넌트에서 사용자 정보를 어떻게 얻을 수 있습니까?
- 클라이언트 쿠키를 직접 파싱하지 말고 (XSS 방지) 서버 측 API 인터페이스 (예:
/api/auth/user
)를 통해 가져옵니다.
10.4 에지 미들웨어에서 토큰 확인이 실패합니까?
- 에지 환경은 Node.js 기본 모듈을 지원하지 않습니다.
jsonwebtoken
이 순수 JavaScript 구현 (예:jose
라이브러리의 ES6 버전)을 사용하는지 확인합니다.
XI: 보안 강화 조치
-
HttpOnly + Secure 쿠키 사용
-
합리적인 SameSite 정책 설정
-
JWT 유효 기간은 2시간을 초과하지 않아야 함
-
암호화 키를 정기적으로 교체
-
CSRF 보호 구현
XII. 결론: 안전하고 신뢰할 수 있는 인증 시스템 구축
Next.js 미들웨어를 JWT와 결합하여 다음과 같은 핵심 이점을 가진 완전한 인증 및 권한 부여 시스템을 구현했습니다.
- 상태 비저장 인증: 서버는 세션을 저장할 필요가 없으므로 높은 동시성 및 마이크로 서비스 아키텍처를 지원합니다.
- 세분화된 경로 보호:
config.matcher
를 통해 보호해야 하는 경로를 유연하게 구성합니다. - 보안 강화: 쿠키 보안 속성 및 짧은 토큰 만료 시간을 합리적으로 사용하여 공격 표면을 줄입니다.
- 우수한 확장성: 역할 기반 액세스 제어(RBAC) 및 다단계 인증(MFA)과 같은 기능을 추가로 지원합니다.
실제 프로젝트에서는 비즈니스 요구 사항(예: 권한 수준 판단)에 따라 미들웨어 로직을 조정해야 하며 보안 모범 사례를 엄격히 준수해야 합니다. Next.js의 미들웨어 메커니즘은 인증 프로세스를 간소화할 뿐만 아니라 풀스택 애플리케이션 구축을 위한 통합 요청 처리 계층을 제공하므로 최신 웹 개발에서 없어서는 안 될 도구입니다.
이 기사의 실습을 통해 개발자는 JWT 생성, 미들웨어 검증에서 사용자 정보 추출에 이르기까지 전체 프로세스를 마스터하여 향후 더 복잡한 인증 시스템을 개발하기 위한 견고한 기반을 마련할 수 있습니다. 항상 기억하십시오. 보안은 계층화된 시스템입니다. 코드 구현 외에도 인프라(예: HTTPS), 모니터링 시스템(예: 비정상적인 로그인 감지) 및 사용자 교육(예: 강력한 비밀번호 정책)과 결합하여 진정으로 신뢰할 수 있는 애플리케이션을 구축해야 합니다.
Leapcell: 최고의 서버리스 웹 호스팅
마지막으로 서비스 배포에 가장 적합한 플랫폼인 **Leapcell**을 추천합니다.
🚀 좋아하는 언어로 빌드
JavaScript, Python, Go 또는 Rust로 간편하게 개발하세요.
🌍 무료로 무제한 프로젝트 배포
사용한 만큼만 비용을 지불하세요. 요청도 없고, 요금도 없습니다.
⚡ 사용한 만큼 지불, 숨겨진 비용 없음
유휴 요금 없이 원활한 확장성만 제공합니다.
🔹 트위터 팔로우: @LeapcellHQ