useEffect로 관리자 페이지 막지 마세요 (Next.js Middleware 완벽 가이드)
1. "깜박임"의 공포
관리자 페이지(/admin)를 만들고 있었습니다.
일반 유저가 들어오면 안 되니까, 당연히 리다이렉트를 시켜야겠죠.
저는 아주 자연스럽게 useEffect를 썼습니다.
/* AdminPage.tsx (BAD EXAMPLE) */
'use client';
export default function AdminPage() {
const { user, loading } = useAuth();
const router = useRouter();
useEffect(() => {
if (!loading && !user?.isAdmin) {
router.push('/'); // "나가!"
}
}, [user, loading, router]);
if (loading) return <p>Loading...</p>;
return <div>매출 데이터: 100억 원... (1급 기밀)</div>;
}
완벽해 보였습니다. 로그인 안 한 유저가 들어오면 메인으로 튕겨 나갔거든요. 그런데 인터넷 속도가 느린 카페에서 테스트하다가 등골이 서늘해졌습니다.
"매출 데이터: 100억 원..."이 0.5초 동안 보였다가 사라지는 겁니다! (FOUC)
게다가 JavaScript를 끄고(NoScript) 접속해봤더니? 리다이렉트가 작동하지 않고, 기밀 데이터가 그대로 노출되었습니다.
2. useEffect는 "사후 약방문"이다
이게 바로 클라이언트 사이드 보호(Client-side Protection)의 한계입니다.
- 브라우저: HTML을 다운로드하고 화면에 그립니다. (이미 데이터 노출됨)
- JS: React를 로딩하고,
useEffect를 실행합니다. - JS: "어? 너 관리자 아니네?" 하고 페이지를 이동시킵니다. (너무 늦음)
교훈: 보안 검사는 문지기(서버)가 해야지, 방 안의 손님(브라우저)이 하는 게 아니다.
3. 문지기(Middleware)를 고용하자
Next.js의 Middleware는 서버와 페이지 사이의 문지기입니다. 유저가 페이지에 도달하기 전에 요청을 가로채서 검사합니다.
유저 요청 -> [미들웨어] -> (통과?) -> 페이지 렌더링
(거절!) -> 로그인 페이지로 납치
이제 middleware.ts를 작성해 봅시다. (파일 위치는 src/middleware.ts 또는 루트)
/* middleware.ts */
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
// 1. 쿠키에서 세션 토큰 확인 (가장 확실함)
const token = request.cookies.get('auth_token')?.value;
// 2. 토큰이 없으면 로그인 페이지로 강제 이동
if (!token) {
const loginUrl = new URL('/login', request.url);
// 원래 가려던 곳으로 돌아오기 위해 callbackUrl 추가
loginUrl.searchParams.set('callbackUrl', request.nextUrl.pathname);
return NextResponse.redirect(loginUrl);
}
// 3. 통과!
return NextResponse.next();
}
// 중요: 어디에서 검문할지 정하기 (Matcher)
export const config = {
matcher: ['/admin/:path*', '/dashboard/:path*'],
};
이렇게 하면:
- 페이지가 렌더링되기 전에 검사합니다.
- 토큰이 없으면 아예 페이지 내용(HTML)을 보내지 않고, HTTP 307 Redirect 응답을 줍니다.
- 깜박임(Flash)이 0.1초도 없습니다.
- 자바스크립트를 꺼도 못 들어옵니다. (서버에서 막으니까)
Edge Runtime의 제약 제대로 파보기
미들웨어를 작성하다 보면 가끔 이상한 에러를 만납니다.
Error: The edge runtime does not support Node.js 'crypto' module.
Next.js 미들웨어는 Node.js가 아니라 Edge Runtime에서 돌아갑니다. Edge Runtime은 V8 엔진의 경량화 버전으로, 가볍고 빠르지만 Node.js API를 전부 지원하지는 않습니다.
못 쓰는 것 (Node.js API)
fs(파일 시스템 접근 불가)path,os모듈 등- 무거운 라이브러리 (일부 암호화 라이브러리 등)
- 데이터베이스 직접 연결 (Prisma 등은 Edge Client 써야 함)
할 수 있는 것 (Web Standard API)
fetch,Request,Responsecookies,headersWeb Crypto API(표준 암호화)
그래서 JWT 검증을 할 때 jsonwebtoken 라이브러리 대신 jose 같은 Edge 호환 라이브러리를 써야 합니다.
또는, 미들웨어에서는 간단히 쿠키 유무만 체크하고, 상세한 권한 검증은 페이지(layout.tsx이나 page.tsx) 서버 컴포넌트에서 하는 "이중 보안" 전략을 추천합니다.
5. 고급 패턴 - 미들웨어 체이닝 (Chaining)
미들웨어 하나에서 모든 로직(인증, 로깅, A/B 테스트, I18n)을 다 처리하면 코드가 스파게티가 됩니다.
하지만 Next.js는 공식적으로 여러 미들웨어 파일을 지원하지 않습니다. (오직 하나의 middleware.ts만 가능)
이럴 땐 직접 체이닝(Chaining) 함수를 만들어야 합니다.
/* middlewares/chain.ts */
export function chain(middlewares, index = 0) {
const current = middlewares[index];
if (current) {
const next = chain(middlewares, index + 1);
return current(next);
}
return () => NextResponse.next();
}
이렇게 함수형 프로그래밍 스타일로 여러 미들웨어를 엮어서 사용하면 유지보수가 훨씬 쉬워집니다.
6. 마무리 - 보안은 "입구컷"이어야 한다
useEffect로 리다이렉트 시키는 건 UX(사용자 경험)를 위한 것이지, 보안이 아닙니다.
진짜 보안은 데이터가 유저에게 전달되기 전에, 서버(Middleware)에서 이루어져야 합니다.
- 클라이언트(useEffect): "들어오셨네요? 어 근데 나가주세요." (이미 늦음, 데이터 유출 가능)
- 서버(Middleware): "신분증 없으시네요. 문도 못 열어드립니다." (입구컷, 안전함)
여러분의 소중한 데이터를 "0.5초의 깜박임"으로 유출하지 마세요. 미들웨어, 선택이 아니라 필수입니다.
Stop Protecting Admin Pages with useEffect (Zero-Flash Security with Middleware)
1. The Horror of the "Flash"
I was building a corporate Admin Dashboard (/admin).
Regular users shouldn't access it, so naturally, I added a redirect.
I innocently used useEffect, which is what every React tutorial teaches.
/* AdminPage.tsx (BAD EXAMPLE) */
'use client';
export default function AdminPage() {
const { user, loading } = useAuth();
const router = useRouter();
useEffect(() => {
if (!loading && !user?.isAdmin) {
router.push('/'); // Kick them out
}
}, [user, loading, router]);
if (loading) return <p>Loading...</p>;
return <div>Revenue Data: $10 Million... (TOP SECRET)</div>;
}
It looked perfect. Non-logged-in users were bounced to the main page. But while testing in a cafe with slow Wi-Fi, I saw something terrifying.
"Revenue Data: $10 Million..." flashed on the screen for 0.5 seconds before disappearing!
I then tried disabling JavaScript via Chrome DevTools. The redirect didn't work at all, and the dashboard stayed open.
2. useEffect is "Too Little, Too Late"
This is a classic FOUC (Flash of Unstyled Content), or in this case, a "Flash of Secure Content".
- Browser: Downloads HTML and paints it. (Data is already leaked here).
- JS: React loads, hydraes, and runs
useEffect. - JS: "Oh, you're not an admin?" ->
router.push(). (Too late).
Lesson: Security checks must happen at the Gate (Server), not inside the Living Room (Browser).
3. Hire a Bouncer (Middleware)
Next.js Middleware is a Bouncer sitting between the Request and the Response. It intercepts the user BEFORE they reach the page.
User Request -> [Middleware] -> (Pass?) -> Render Page
(Reject!) -> Kidnap to Login Page
Let's write middleware.ts. (Place it in src/middleware.ts or root).
/* middleware.ts */
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
// 1. Check token in Cookie (Most reliable method)
const token = request.cookies.get('auth_token')?.value;
// 2. If no token, redirect immediately
if (!token) {
const loginUrl = new URL('/login', request.url);
loginUrl.searchParams.set('callbackUrl', request.nextUrl.pathname);
return NextResponse.redirect(loginUrl);
}
// 3. Pass!
return NextResponse.next();
}
// IMPORTANT: Define where to set up the checkpoint (Matcher)
export const config = {
matcher: ['/admin/:path*', '/dashboard/:path*'],
};
This ensures:
- Check happens before rendering.
- If no token, the page content (HTML) is never sent; instant HTTP 307 Redirect.
- Zero Flash.
- Even with JavaScript disabled, they can't get in. (Blocked at server level).
4. Deep Dive: Edge Runtime Limitations
You might encounter this error while writing middleware:
Error: The edge runtime does not support Node.js 'crypto' module.
Next.js Middleware runs on the Edge Runtime, NOT standard Node.js. Edge Runtime is a lightweight version of V8. It's fast and lacks cold starts, but it doesn't support all Node.js APIs.
What you CANNOT use:
fs(No file system access)path,osmodules.- Direct Database Connections (e.g., standard MongoDB/Postgres drivers). You must use HTTP-based clients (like Supabase client or Prisma Data Proxy).
- Heavy encryption libraries relying on C++ bindings.
What you CAN use:
- Standard Web APIs:
fetch,Request,Response. cookies,headers.Web Crypto API(Standard encryption).
Pro Tip for JWT
Do not use jsonwebtoken (it relies on Node streams). Use jose instead. It's specifically built for the Web/Edge environment and works perfectly in middleware.
5. Advanced Pattern: Middleware Chaining
If you try to jam Authentication, Logging, Geo-blocking, and I18n all into one function, your middleware.ts will become unreadable.
Since Next.js only allows one middleware.ts file, you need to implement Chaining manually.
Basic idea:
Create a "Stack" of middlewares where each function calls the next() function in the chain.
// Example structure
export default stackMiddlewares([
withAuth,
withLogging,
withI18n
]);
This keeps your code modular and testable.
6. Conclusion: Security Must Be "Pre-emptive"
Redirecting with useEffect is a UX feature, not a Security feature.
Real security implies that unauthorized users never even receive the HTML bytes.
- Client (useEffect): "Oh, you're inside? Please leave." (Too late, data leaked).
- Server (Middleware): "No ID? You cannot pass." (Blocked at door, Safe).
Don't risk your sensitive data for a "0.5-second flash". Middleware is not optional for protected routes; it's mandatory.
7. FAQ: Middleware Gotchas
Q. Why is console.log not showing up?
Middleware runs on the Server (Edge). The logs appear in your Terminal, not the Browser Console.
Q. Can I use Mongoose/TypeORM in Middleware?
No. Most database ORMs rely on Node.js specific APIs (TCP sockets, FS) which are not available in the Edge Runtime.
You must use an HTTP-based client (like Supabase supabase-js, PlanetScale, or Upstash Redis).
Q. Why is my Middleware running on static files?
Check your matcher config. If you don't exclude _next/static, middleware runs for every CSS/JS file request, slowing down your site.
Always use the standard exclusion pattern:
'/((?!api|_next/static|_next/image|favicon.ico).*)'
Q. Can I modify the response body in Middleware?
No. Middleware typically modifies Headers, Cookies, or Redirects.
You cannot change the HTML content (Response Body) easily. If you need to inject data, do it in layout.tsx or page.tsx after the middleware passes the request.