[이제와서 시작하는 Next.js 마스터하기 #8] Proxy.ts로 네트워크 제어하기
[이제와서 시작하는 Next.js 마스터하기 #8] Proxy.ts로 네트워크 제어하기
“모든 요청의 문지기!” - Proxy.ts로 인증, 리다이렉트, 헤더 수정을 한 곳에서!
🎯 이 글에서 배울 내용
- Proxy.ts가 무엇인지
- 인증 체크하기
- 리다이렉트와 리라이트
- 국제화(i18n) 구현
예상 소요 시간: 40분
🚪 Proxy.ts란?
Next.js 16의 새 기능으로 middleware.ts를 대체합니다.
역할: 모든 요청이 페이지에 도달하기 전에 거치는 “문지기”
1
2
3
4
5
사용자 요청 → Proxy.ts → 페이지
↑
인증 체크
리다이렉트
헤더 수정
🔐 기본 인증 체크
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// proxy.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
// 쿠키에서 토큰 확인
const token = request.cookies.get('auth_token');
// 보호된 경로
if (request.nextUrl.pathname.startsWith('/dashboard')) {
if (!token) {
// 로그인 페이지로 리다이렉트
return NextResponse.redirect(new URL('/login', request.url));
}
}
return NextResponse.next();
}
export const config = {
matcher: ['/dashboard/:path*', '/profile/:path*']
};
🌐 국제화 (i18n)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// proxy.ts
import { NextResponse } from 'next/server';
const locales = ['en', 'ko', 'ja'];
const defaultLocale = 'en';
export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
// 이미 로케일이 있으면 통과
const pathnameHasLocale = locales.some(
(locale) => pathname.startsWith(`/${locale}/`) || pathname === `/${locale}`
);
if (pathnameHasLocale) {
return NextResponse.next();
}
// Accept-Language 헤더에서 선호 언어 가져오기
const locale = request.headers
.get('accept-language')
?.split(',')[0]
?.split('-')[0] || defaultLocale;
// 적절한 로케일로 리다이렉트
request.nextUrl.pathname = `/${locale}${pathname}`;
return NextResponse.redirect(request.nextUrl);
}
🔄 리다이렉트 vs 리라이트
Redirect (URL 변경됨)
1
2
3
4
5
6
7
export function middleware(request: NextRequest) {
if (request.nextUrl.pathname === '/old-blog') {
return NextResponse.redirect(new URL('/blog', request.url));
}
return NextResponse.next();
}
Rewrite (URL 유지)
1
2
3
4
5
6
7
8
export function middleware(request: NextRequest) {
// /blog를 /posts로 내부적으로 처리 (URL은 /blog 유지)
if (request.nextUrl.pathname.startsWith('/blog')) {
return NextResponse.rewrite(new URL('/posts', request.url));
}
return NextResponse.next();
}
📊 헤더 수정
1
2
3
4
5
6
7
8
9
10
11
12
13
export function middleware(request: NextRequest) {
const response = NextResponse.next();
// 보안 헤더 추가
response.headers.set('X-Frame-Options', 'DENY');
response.headers.set('X-Content-Type-Options', 'nosniff');
response.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin');
// 커스텀 헤더
response.headers.set('X-Custom-Header', 'my-value');
return response;
}
🎯 실전 예제: 완전한 인증 시스템
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
// proxy.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import { verifyAuth } from '@/lib/auth';
export async function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
// 공개 경로
const publicPaths = ['/', '/login', '/signup', '/api/auth'];
const isPublicPath = publicPaths.some(path => pathname.startsWith(path));
if (isPublicPath) {
return NextResponse.next();
}
// 인증 확인
const token = request.cookies.get('token')?.value;
if (!token) {
const url = new URL('/login', request.url);
url.searchParams.set('from', pathname);
return NextResponse.redirect(url);
}
try {
// 토큰 검증
const user = await verifyAuth(token);
// 관리자 전용 경로
if (pathname.startsWith('/admin') && user.role !== 'admin') {
return NextResponse.redirect(new URL('/unauthorized', request.url));
}
// 사용자 정보를 헤더에 추가
const response = NextResponse.next();
response.headers.set('x-user-id', user.id);
response.headers.set('x-user-role', user.role);
return response;
} catch (error) {
// 유효하지 않은 토큰
return NextResponse.redirect(new URL('/login', request.url));
}
}
export const config = {
matcher: [
/*
* Match all request paths except:
* - _next/static (static files)
* - _next/image (image optimization files)
* - favicon.ico (favicon file)
* - public folder
*/
'/((?!_next/static|_next/image|favicon.ico|public).*)',
],
};
verifyAuth 함수 구현
위 예제에서 사용한 verifyAuth 함수를 직접 만들어보겠습니다!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
// lib/auth.ts
import { jwtVerify, SignJWT } from 'jose';
// 환경 변수에서 시크릿 키 가져오기
const secret = new TextEncoder().encode(
process.env.JWT_SECRET || 'your-secret-key-min-32-characters-long'
);
// 사용자 타입 정의
export interface User {
id: string;
email: string;
role: 'user' | 'admin';
}
// JWT 토큰 생성
export async function createToken(user: User): Promise<string> {
const token = await new SignJWT({
userId: user.id,
email: user.email,
role: user.role
})
.setProtectedHeader({ alg: 'HS256' })
.setIssuedAt()
.setExpirationTime('7d') // 7일 후 만료
.sign(secret);
return token;
}
// JWT 토큰 검증
export async function verifyAuth(token: string): Promise<User> {
try {
const verified = await jwtVerify(token, secret);
const payload = verified.payload;
return {
id: payload.userId as string,
email: payload.email as string,
role: payload.role as 'user' | 'admin'
};
} catch (error) {
throw new Error('Invalid token');
}
}
// 쿠키에서 사용자 정보 가져오기
export async function getCurrentUser(request: Request): Promise<User | null> {
const cookieHeader = request.headers.get('cookie');
if (!cookieHeader) return null;
// 쿠키에서 토큰 추출
const tokenMatch = cookieHeader.match(/token=([^;]+)/);
if (!tokenMatch) return null;
const token = tokenMatch[1];
try {
return await verifyAuth(token);
} catch {
return null;
}
}
패키지 설치:
1
npm install jose
코드 설명 (초보자용):
- JWT (JSON Web Token):
- 사용자 정보를 암호화해서 저장하는 토큰
- 서버가 발급하고, 클라이언트가 쿠키에 저장
- 매 요청마다 서버가 검증
createToken(): 로그인 성공 시 토큰 생성1 2 3 4 5 6
const token = await createToken({ id: '123', email: '[email protected]', role: 'user' }); // 쿠키에 저장
verifyAuth(): 토큰이 유효한지 확인- 유효하면: 사용자 정보 반환
- 무효하면: 에러 던짐 → 로그인 페이지로 리다이렉트
- 환경 변수 설정 (.env.local):
1
JWT_SECRET=my-super-secret-key-at-least-32-characters-long-for-security
실전 사용 예시:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// app/api/auth/login/route.ts
import { createToken } from '@/lib/auth';
import { cookies } from 'next/headers';
export async function POST(request: Request) {
const { email, password } = await request.json();
// 사용자 인증 (실제로는 데이터베이스에서 확인)
// 예: const user = await prisma.user.findUnique({ where: { email } });
// 간단한 시뮬레이션
if (email === '[email protected]' && password === 'password123') {
const user = {
id: '1',
email: '[email protected]',
role: 'user' as const
};
// 토큰 생성
const token = await createToken(user);
// 쿠키에 저장
const cookieStore = await cookies();
cookieStore.set('token', token, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
maxAge: 60 * 60 * 24 * 7 // 7일
});
return Response.json({ success: true, user });
}
return Response.json(
{ error: 'Invalid credentials' },
{ status: 401 }
);
}
💡 보안 팁:
- ✅
JWT_SECRET은 최소 32자 이상 - ✅ 프로덕션에서는
secure: true사용 - ✅
httpOnly: true로 JavaScript에서 접근 차단 - ✅ 토큰 만료 시간 설정
🔍 자주 묻는 질문 (FAQ)
Q1: proxy.ts vs middleware.ts?
Proxy.ts (Next.js 16 신기능):
- 더 명확한 네트워크 경계
- Node.js 런타임
- 더 강력한 기능
Middleware.ts (기존):
- Edge 런타임
- 제한적이지만 빠름
권장: 새 프로젝트는 proxy.ts 사용!
🎯 오늘 배운 내용 정리
- Proxy.ts
- 모든 요청 제어
- 인증, 리다이렉트, 헤더 수정
- 인증
- 쿠키 기반 인증
- 역할 기반 접근 제어
- 국제화
- 자동 언어 감지
- URL 기반 로케일
📚 시리즈 네비게이션
- #7 동적 라우팅과 고급 패턴
- #9 인증과 권한 관리 (다음 편)
“Proxy.ts로 보안과 UX를 동시에!” 🔐
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.