Step 04: 인증/세션 관리 — Express 수동 미들웨어 vs NextAuth + Middleware
Step 04: 인증/세션 관리 — Express 수동 미들웨어 vs NextAuth + Middleware
PM 요청 (김도연)
"준혁님, 로그인 기능을 추가하고 싶어요. 로그인한 사용자만 볼 수 있는 페이지도 만들어야 하고요. 세션은 어떻게 관리하고, SSR에서는 어떻게 사용자 정보를 가져오나요? 클라이언트에서도 로그인 상태를 알아야 할 것 같은데... 복잡해 보이네요."
시니어 멘토링 (이준혁)
"그래, 드디어 인증까지 왔구나. 솔직히 말하면 인증은 SSR의 핵심 유스케이스야. SEO가 중요한 개인화된 페이지를 만들 때 서버가 '누가' 요청했는지 알아야 올바른 HTML을 렌더링할 수 있거든. 그리고 클라이언트도 사용자 정보를 알아야 인터랙션이 동작하고."
"근데 여기서부터가 진짜 문제야. 인증을 추가하면 이전 Step들에서 만든 모든 배관이 한 곳에 모이기 시작해. Step 1의 renderToString, Step 2의 상태 직렬화, Step 3의 dehydrate... 여기에 세션 관리와 인증 미들웨어까지 더해지면 server.js가 급격히 복잡해지거든."
"먼저 SSR 인증의 흐름부터 보자."
SSR 인증의 흐름
Request
→ 쿠키 파싱
→ 세션 검증
→ 사용자 정보 조회
→ SSR 렌더링에 주입
→ 클라이언트 동기화"왜 인증이 SSR의 핵심이냐면:"
- SEO와 개인화의 충돌: 검색 엔진은 로그인 상태가 없는데, 로그인한 사용자에게는 개인화된 콘텐츠를 보여줘야 함
- 서버가 '누구'인지 알아야 함: 올바른 HTML을 렌더링하려면 요청한 사람이 누군지 서버에서 먼저 파악해야 함
- 클라이언트 동기화: 서버에서 렌더링한 사용자 정보와 클라이언트 상태가 정확히 일치해야 hydration 오류가 안 남
"자, 이제 Express부터 보자. 여기서 배관이 얼마나 누적되는지 집중해서 봐."
Express + Passport + express-session (수동 배관)
"Express에서 인증을 구현하려면 여러 라이브러리를 조합해야 해. 가장 많이 쓰는 조합이 express-session + Passport야."
1. 전체 인증 배관 구조
// server.js — 인증 배관 전체
import express from 'express';
import session from 'express-session';
import passport from 'passport';
import { Strategy as LocalStrategy } from 'passport-local';
import bcrypt from 'bcryptjs';
import React from 'react';
import { renderToString } from 'react-dom/server';
const app = express();
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// ========================================
// 1단계: 세션 미들웨어 설정
// ========================================
app.use(session({
secret: 'your-secret-key-change-in-production',
resave: false,
saveUninitialized: false,
cookie: {
secure: false, // production에서는 true (HTTPS 필요)
httpOnly: true, // XSS 방어
maxAge: 24 * 60 * 60 * 1000 // 24시간
},
}));
// ========================================
// 2단계: Passport 초기화
// ========================================
app.use(passport.initialize());
app.use(passport.session());
// ========================================
// 3단계: 인증 전략 구현
// ========================================
passport.use(new LocalStrategy(
async (username, password, done) => {
try {
// 데이터베이스에서 사용자 찾기
const user = await db.findUser(username);
if (!user) {
return done(null, false, { message: '사용자를 찾을 수 없습니다' });
}
// 비밀번호 검증
const isValid = await bcrypt.compare(password, user.passwordHash);
if (!isValid) {
return done(null, false, { message: '비밀번호가 틀렸습니다' });
}
// 성공
return done(null, user);
} catch (err) {
return done(err);
}
}
));
// ========================================
// 4단계: 세션 직렬화/역직렬화
// ========================================
// 세션에 저장할 때: user 객체 전체가 아니라 ID만 저장
passport.serializeUser((user, done) => {
done(null, user.id);
});
// 세션에서 복원할 때: ID로 사용자 정보 조회
passport.deserializeUser(async (id, done) => {
try {
const user = await db.findUserById(id);
done(null, user);
} catch (err) {
done(err);
}
});
// ========================================
// 5단계: 로그인 라우트
// ========================================
app.post('/login',
passport.authenticate('local', {
successRedirect: '/dashboard',
failureRedirect: '/login',
failureFlash: false
})
);
app.post('/logout', (req, res) => {
req.logout((err) => {
if (err) return res.status(500).json({ error: err.message });
res.redirect('/');
});
});
// ========================================
// 6단계: 보호 미들웨어 (수동 구현)
// ========================================
const requireAuth = (req, res, next) => {
if (!req.isAuthenticated()) {
return res.redirect('/login');
}
next();
};
// ========================================
// 7단계: SSR with 인증 — 모든 배관이 모이는 곳!
// ========================================
app.get('/dashboard', requireAuth, async (req, res) => {
// Passport가 req.user에 사용자 정보를 주입함
const user = req.user;
// Step 2의 상태 직렬화와 결합
const store = createUserStore({ user });
// Step 3의 TanStack Query와 결합 (여기서는 생략)
// const queryClient = new QueryClient();
// await queryClient.prefetchQuery(...);
// const dehydratedState = dehydrate(queryClient);
// Step 1의 SSR
const html = renderToString(
React.createElement(StoreProvider, { store },
React.createElement(DashboardPage)
)
);
// 모든 상태 직렬화
const initialState = JSON.stringify({
zustand: store.getState(),
user: user, // 인증 정보 추가
// tanstack: dehydratedState // Step 3에서 추가한 것
});
res.send(`
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Dashboard</title>
</head>
<body>
<div id="root">${html}</div>
<script>
window.__INITIAL_STATE__ = ${initialState};
</script>
<script src="/public/react-bundle.js"></script>
</body>
</html>
`);
});
app.listen(3000);"봤지? 이게 배관 누적의 실체야."
배관이 누적되는 과정
Step 1 배관: renderToString + hydration
↓
Step 2 배관: + state serialization (window.__INITIAL_STATE__)
↓
Step 3 배관: + dehydrate/hydrate pipeline (QueryClient)
↓
Step 4 배관: + session/cookie + auth middleware + 사용자 직렬화
↓
server.js의 한 라우트 핸들러에 이 모든 배관이 들어감!"한 라우트 핸들러(/dashboard)에서:"
- 세션 검증 (
requireAuth미들웨어) - 사용자 정보 조회 (
req.user) - Zustand 스토어 생성 및 초기화
- (Step 3이라면) QueryClient 생성 + prefetch + dehydrate
- React 컴포넌트 렌더링
- 모든 상태 직렬화 (
__INITIAL_STATE__) - HTML 템플릿 조립 및 응답
"이 7단계를 모두 수동으로 배관해야 해. 페이지가 10개면 이 패턴을 10번 반복하는 거야."
클라이언트 코드 (Express SSR)
// client.js
import { hydrateRoot } from 'react-dom/client';
import { StoreProvider } from './stores';
import DashboardPage from './pages/DashboardPage';
// 서버에서 직렬화한 상태 복원
const initialState = window.__INITIAL_STATE__;
// Step 2: Zustand 스토어 복원
const store = createUserStore(initialState.zustand);
// Step 3: TanStack Query 복원 (여기서는 생략)
// const queryClient = new QueryClient();
// hydrate(queryClient, initialState.tanstack);
hydrateRoot(
document.getElementById('root'),
<StoreProvider store={store}>
<DashboardPage />
</StoreProvider>
);"클라이언트에서도 서버와 동일한 순서로 상태를 복원해야 해. 순서가 하나라도 틀리면 hydration 오류가 나거든."
Express 인증의 문제점
"도연씨, 여기서 뭐가 문제인지 보여?"
- 배관 복잡도 급증: 인증 추가 시 이전 Step들의 배관이 모두 한 곳에 모임
- 수동 미들웨어:
requireAuth같은 보호 로직을 직접 구현해야 함 - 세션 저장소 관리: 메모리 세션은 서버 재시작하면 날아감 → Redis 같은 외부 저장소 필요
- CSRF 보호: 수동으로
csurf같은 라이브러리 추가해야 함 - OAuth 지원: Google/GitHub 로그인 추가하려면 각 전략을 일일이 설치 및 설정
- 상태 동기화: 서버의
req.user와 클라이언트의 상태를 수동으로 맞춰야 함
"가장 큰 문제는 관심사가 분리되지 않는다는 거야. 한 파일(server.js)에 세션, 인증, SSR, 상태 직렬화가 모두 섞여 있거든."
Next.js + NextAuth (자동 패턴)
"자, 이제 Next.js를 보자. NextAuth라는 라이브러리를 쓰면 대부분의 배관이 자동화돼."
1. NextAuth 설정
// app/api/auth/[...nextauth]/route.ts
import NextAuth from 'next-auth';
import CredentialsProvider from 'next-auth/providers/credentials';
import bcrypt from 'bcryptjs';
import { db } from '@/lib/db';
const handler = NextAuth({
providers: [
CredentialsProvider({
name: 'Credentials',
credentials: {
username: { label: "사용자명", type: "text" },
password: { label: "비밀번호", type: "password" },
},
async authorize(credentials) {
if (!credentials) return null;
// 사용자 조회 및 검증
const user = await db.findUser(credentials.username);
if (!user) return null;
const isValid = await bcrypt.compare(
credentials.password,
user.passwordHash
);
if (!isValid) return null;
// 반환한 객체가 세션에 저장됨
return {
id: user.id,
name: user.name,
email: user.email,
};
},
}),
],
session: {
strategy: 'jwt', // JWT 기반 세션 (stateless)
},
pages: {
signIn: '/login', // 커스텀 로그인 페이지
},
});
export { handler as GET, handler as POST };"이게 끝이야. Express의 80줄짜리 Passport 설정을 30줄로 압축했어."
2. 미들웨어로 페이지 보호 (10줄!)
// middleware.ts
import { withAuth } from 'next-auth/middleware';
export default withAuth({
callbacks: {
authorized: ({ token }) => {
// token이 있으면 인증됨
return !!token;
},
},
});
// 보호할 경로 지정
export const config = {
matcher: ['/dashboard/:path*', '/profile/:path*'],
};"Express에서 만든 requireAuth 미들웨어를 10줄로 선언적으로 구현했어. 게다가:"
/dashboard하위 모든 경로 자동 보호- 인증 실패 시 자동으로 로그인 페이지로 리다이렉트
- 토큰 검증 자동 처리
3. Server Component에서 세션 사용
// app/dashboard/page.tsx
import { getServerSession } from 'next-auth';
import { redirect } from 'next/navigation';
import DashboardClient from './DashboardClient';
export default async function DashboardPage() {
// 서버에서 세션 가져오기 (1줄!)
const session = await getServerSession();
// 추가 검증 (middleware 통과했지만 한번 더 체크 가능)
if (!session) {
redirect('/login');
}
// 사용자 정보로 데이터 페칭
const userData = await fetch(`/api/user/${session.user.id}`);
return (
<div>
<h1>{session.user.name}님의 대시보드</h1>
<DashboardClient user={session.user} data={userData} />
</div>
);
}"Express의 req.user를 가져오려고 했던 복잡한 과정이 getServerSession() 한 줄로 끝났어."
4. Client Component에서 세션 사용
// app/dashboard/DashboardClient.tsx
'use client';
import { useSession } from 'next-auth/react';
export default function DashboardClient({ user, data }) {
// 클라이언트에서 세션 가져오기 (자동 동기화!)
const { data: session, status } = useSession();
if (status === 'loading') {
return <div>로딩 중...</div>;
}
return (
<div>
<p>현재 사용자: {session?.user?.email}</p>
<button onClick={() => signOut()}>로그아웃</button>
</div>
);
}"클라이언트에서도 useSession() 한 줄로 세션을 가져와. Express처럼 window.__INITIAL_STATE__에서 수동으로 복원할 필요가 없어."
5. Provider 설정 (SessionProvider)
// app/layout.tsx
import { SessionProvider } from 'next-auth/react';
export default function RootLayout({ children }) {
return (
<html lang="ko">
<body>
<SessionProvider>
{children}
</SessionProvider>
</body>
</html>
);
}"SessionProvider로 감싸면 서버와 클라이언트의 세션이 자동으로 동기화돼. Express에서 수동으로 했던 상태 동기화 배관이 필요 없어."
인증 배관 비교표
| 단계 | Express + Passport | Next.js + NextAuth |
|---|---|---|
| 세션 저장소 | express-session (메모리/Redis) | JWT (stateless) 또는 DB |
| 인증 전략 구현 | 수동 Strategy 구현 (LocalStrategy 등) | Provider 선택 (CredentialsProvider 등) |
| 미들웨어 | 수동 requireAuth 함수 작성 | middleware.ts (선언적) |
| 서버에서 세션 | req.user (미들웨어 의존) | getServerSession() (독립적) |
| 클라이언트 세션 | window.__INITIAL_STATE__ → 수동 복원 | useSession() (자동 동기화) |
| CSRF 보호 | 수동 구현 (csurf 라이브러리) | 자동 내장 |
| OAuth 지원 | passport-google 등 각각 설치 | Provider 한 줄 추가 |
| 로그인 UI | 직접 구현 | 내장 UI (커스텀 가능) |
| 코드 복잡도 | ~100줄 (배관 포함 ~200줄) | ~40줄 |
| 관심사 분리 | 한 파일에 모든 배관 집중 | 파일별로 분리됨 |
배관이 겹치는 문제 (Express의 누적 복잡도)
"도연씨, 여기가 핵심이야. Express SSR에서는 배관이 누적돼."
Step 1: renderToString + hydration
→ server.js에 기본 배관
Step 2: + Zustand 상태 직렬화
→ server.js에 window.__INITIAL_STATE__ 추가
Step 3: + TanStack Query dehydrate/hydrate
→ server.js에 QueryClient 생성 + prefetch + dehydrate 추가
Step 4: + session/cookie + auth middleware
→ server.js에 express-session + Passport + requireAuth 추가
→ 사용자 정보도 __INITIAL_STATE__에 추가
결과: server.js의 한 라우트 핸들러가 이렇게 됨 ↓// Express SSR: /dashboard 라우트 핸들러의 최종 형태
app.get('/dashboard',
requireAuth, // Step 4: 인증 체크
async (req, res) => {
// Step 4: 사용자 정보
const user = req.user;
// Step 2: Zustand 스토어 생성
const store = createUserStore({ user });
// Step 3: TanStack Query 설정
const queryClient = new QueryClient();
await queryClient.prefetchQuery(['dashboard'], () =>
fetchDashboardData(user.id)
);
const dehydratedState = dehydrate(queryClient);
// Step 1: React 렌더링
const html = renderToString(
React.createElement(StoreProvider, { store },
React.createElement(QueryClientProvider, { client: queryClient },
React.createElement(DashboardPage)
)
)
);
// 모든 상태 직렬화
const initialState = JSON.stringify({
user: user, // Step 4
zustand: store.getState(), // Step 2
tanstack: dehydratedState, // Step 3
});
// HTML 응답
res.send(`
<!DOCTYPE html>
<html>
<body>
<div id="root">${html}</div>
<script>window.__INITIAL_STATE__ = ${initialState};</script>
<script src="/public/react-bundle.js"></script>
</body>
</html>
`);
}
);"이게 한 라우트 핸들러야. 페이지가 10개면 이 패턴을 10번 반복해야 하고, 각 페이지마다 필요한 배관이 조금씩 달라서 복사-붙여넣기 + 수정을 반복하게 돼."
Next.js는 관심사별로 파일이 분리됨
middleware.ts
→ 인증 체크 (Step 4)
app/dashboard/page.tsx
→ 서버 컴포넌트: 데이터 페칭 + SSR (Step 1, 3)
→ getServerSession()으로 사용자 정보 (Step 4)
app/dashboard/DashboardClient.tsx
→ 클라이언트 컴포넌트: 인터랙션 (Step 2)
→ useSession()으로 클라이언트 세션 (Step 4)
app/layout.tsx
→ Provider 설정 (Step 2, 3, 4)
stores/
→ 상태 관리 로직 (Step 2)"각 관심사가 독립된 파일로 분리돼 있어. 한 파일이 한 가지 역할만 하니까 코드 파악도 쉽고, 수정할 때도 해당 파일만 열면 돼."
OAuth 지원 비교 (Google 로그인 추가)
Express + Passport
// server.js
import { Strategy as GoogleStrategy } from 'passport-google-oauth20';
// Google 전략 추가
passport.use(new GoogleStrategy({
clientID: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
callbackURL: "http://localhost:3000/auth/google/callback"
},
async (accessToken, refreshToken, profile, done) => {
// 사용자 찾기 또는 생성
let user = await db.findUserByGoogleId(profile.id);
if (!user) {
user = await db.createUser({
googleId: profile.id,
email: profile.emails[0].value,
name: profile.displayName,
});
}
return done(null, user);
}
));
// 라우트 추가
app.get('/auth/google',
passport.authenticate('google', { scope: ['profile', 'email'] })
);
app.get('/auth/google/callback',
passport.authenticate('google', { failureRedirect: '/login' }),
(req, res) => {
res.redirect('/dashboard');
}
);"약 40줄 추가. 각 OAuth 제공자마다 이 패턴을 반복해야 해."
Next.js + NextAuth
// app/api/auth/[...nextauth]/route.ts
import GoogleProvider from 'next-auth/providers/google';
const handler = NextAuth({
providers: [
CredentialsProvider({ /* 기존 코드 */ }),
// Google 로그인 추가 (4줄!)
GoogleProvider({
clientId: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
}),
],
// ...
});"4줄 추가로 끝. 콜백 URL, 사용자 생성 로직 등이 자동으로 처리돼."
실전 예제: 보호된 대시보드 페이지
Express 버전 (전체 흐름)
// server.js
import express from 'express';
import session from 'express-session';
import passport from 'passport';
import { Strategy as LocalStrategy } from 'passport-local';
import React from 'react';
import { renderToString } from 'react-dom/server';
import bcrypt from 'bcryptjs';
const app = express();
// 1. 미들웨어 설정
app.use(express.json());
app.use(session({
secret: 'secret',
resave: false,
saveUninitialized: false,
}));
app.use(passport.initialize());
app.use(passport.session());
// 2. Passport 설정
passport.use(new LocalStrategy(async (username, password, done) => {
const user = await db.findUser(username);
if (!user || !(await bcrypt.compare(password, user.passwordHash))) {
return done(null, false);
}
return done(null, user);
}));
passport.serializeUser((user, done) => done(null, user.id));
passport.deserializeUser(async (id, done) => {
const user = await db.findUserById(id);
done(null, user);
});
// 3. 인증 라우트
app.post('/login', passport.authenticate('local', {
successRedirect: '/dashboard',
failureRedirect: '/login',
}));
// 4. 보호된 SSR 라우트
const requireAuth = (req, res, next) => {
if (!req.isAuthenticated()) return res.redirect('/login');
next();
};
app.get('/dashboard', requireAuth, async (req, res) => {
const user = req.user;
// SSR 렌더링 + 상태 직렬화
const store = createUserStore({ user });
const html = renderToString(
React.createElement(StoreProvider, { store },
React.createElement(DashboardPage)
)
);
const initialState = JSON.stringify({ user, zustand: store.getState() });
res.send(`
<!DOCTYPE html>
<html>
<body>
<div id="root">${html}</div>
<script>window.__INITIAL_STATE__ = ${initialState};</script>
<script src="/public/react-bundle.js"></script>
</body>
</html>
`);
});
app.listen(3000);"약 100줄. 여기에 Step 3의 TanStack Query까지 더하면 150줄 이상."
Next.js 버전 (전체 흐름)
// app/api/auth/[...nextauth]/route.ts (20줄)
import NextAuth from 'next-auth';
import CredentialsProvider from 'next-auth/providers/credentials';
const handler = NextAuth({
providers: [
CredentialsProvider({
credentials: {
username: { label: "Username", type: "text" },
password: { label: "Password", type: "password" },
},
async authorize(credentials) {
const user = await db.findUser(credentials.username);
if (user && await bcrypt.compare(credentials.password, user.passwordHash)) {
return { id: user.id, name: user.name, email: user.email };
}
return null;
},
}),
],
session: { strategy: 'jwt' },
});
export { handler as GET, handler as POST };
// middleware.ts (10줄)
import { withAuth } from 'next-auth/middleware';
export default withAuth({
callbacks: {
authorized: ({ token }) => !!token,
},
});
export const config = { matcher: ['/dashboard/:path*'] };
// app/dashboard/page.tsx (15줄)
import { getServerSession } from 'next-auth';
export default async function DashboardPage() {
const session = await getServerSession();
return (
<div>
<h1>{session?.user?.name}님의 대시보드</h1>
<DashboardClient user={session?.user} />
</div>
);
}
// app/dashboard/DashboardClient.tsx (20줄)
'use client';
import { useSession, signOut } from 'next-auth/react';
export default function DashboardClient({ user }) {
const { data: session } = useSession();
return (
<div>
<p>이메일: {session?.user?.email}</p>
<button onClick={() => signOut()}>로그아웃</button>
</div>
);
}"총 65줄. Express의 절반도 안 돼. 그리고 각 파일이 한 가지 역할만 해서 가독성도 훨씬 좋아."
세션 저장소 비교
Express: Stateful Session (express-session)
// 메모리 저장 (개발용)
app.use(session({
secret: 'secret',
resave: false,
saveUninitialized: false,
// 서버 재시작하면 세션 날아감!
}));
// Production: Redis 연결 필요
import RedisStore from 'connect-redis';
import { createClient } from 'redis';
const redisClient = createClient({ url: 'redis://localhost:6379' });
redisClient.connect();
app.use(session({
store: new RedisStore({ client: redisClient }),
secret: 'secret',
resave: false,
saveUninitialized: false,
}));"Express는 stateful 세션이라 서버에 세션 데이터를 저장해야 해. 메모리 저장은 개발용이고, production에서는 Redis 같은 외부 저장소가 필요해. 인프라 복잡도 증가."
Next.js: JWT (Stateless)
// app/api/auth/[...nextauth]/route.ts
const handler = NextAuth({
session: {
strategy: 'jwt', // 기본값
maxAge: 30 * 24 * 60 * 60, // 30일
},
// ...
});"Next.js는 기본적으로 JWT 기반 stateless 세션이야. 세션 데이터가 암호화된 쿠키에 저장되니까 외부 저장소가 필요 없어. 서버를 여러 대 띄워도 세션 공유 문제가 없고."
CSRF 보호
Express: 수동 구현
import csrf from 'csurf';
const csrfProtection = csrf({ cookie: true });
app.get('/login', csrfProtection, (req, res) => {
res.render('login', { csrfToken: req.csrfToken() });
});
app.post('/login', csrfProtection, passport.authenticate('local'), (req, res) => {
res.redirect('/dashboard');
});"CSRF 토큰을 수동으로 생성하고, 각 form에 넣고, 검증 미들웨어를 붙여야 해."
Next.js: 자동 내장
"NextAuth는 CSRF 보호가 자동으로 내장돼 있어. 별도 설정 없이 안전해."
깨달음 포인트
"자, 도연씨. 오늘의 핵심을 정리해볼게."
"Express SSR에서 인증을 추가하면, 이전 Step들의 배관(상태 직렬화, dehydrate)과 결합되면서 server.js가 급격히 복잡해진다. Next.js는 각 관심사(인증, 데이터, 상태)가 별도 파일로 분리되어 관리가 쉽다. 인증은 SSR에서 배관 누적이 가장 심한 영역이다."
"구체적으로 말하면:"
1. 배관 누적의 절정
"Step 1부터 4까지 배관이 누적되면서, Express의 한 라우트 핸들러에 이 모든 게 들어가:"
- Step 1: renderToString + hydration
- Step 2: Zustand 직렬화
- Step 3: TanStack Query dehydrate
- Step 4: 세션 검증 + 사용자 직렬화
"Next.js는 각 Step이 독립된 파일로 분리돼 있어서 복잡도가 선형적으로 증가하지 않아."
2. 관심사 분리의 차이
Express:
server.js에 모든 배관이 집중
→ 100줄짜리 라우트 핸들러
→ 페이지마다 복사-붙여넣기
Next.js:
middleware.ts → 인증
page.tsx → 데이터 + SSR
Client.tsx → 인터랙션
→ 각 파일 20줄 이하
→ 재사용 가능한 구조3. 개발자 경험 (DX) 차이
"Express에서 인증 추가하려면:"
- express-session 설치 및 설정
- Passport 설치 및 전략 구현
- serialize/deserialize 구현
- requireAuth 미들웨어 작성
- 각 라우트에 미들웨어 적용
- 상태 직렬화 로직 수정
- 클라이언트 복원 로직 수정
"Next.js에서 인증 추가하려면:"
- next-auth 설치
app/api/auth/[...nextauth]/route.ts작성- middleware.ts 작성 (선택)
- 끝!
4. 인증은 SSR의 핵심 유스케이스
"SEO가 중요한 개인화 페이지(대시보드, 마이페이지 등)를 만들 때 인증은 필수야. 그래서 인증 배관이 얼마나 복잡한지가 SSR 프레임워크의 생산성을 결정해."
실무 체크리스트
"도연씨 팀이 인증을 구현할 때 체크할 항목들:"
Express를 선택한다면:
- 세션 저장소는? (Redis 인프라 준비됐나?)
- CSRF 보호는? (csurf 설정했나?)
- OAuth는? (각 제공자 전략 설치 및 설정)
- 모든 보호 라우트에
requireAuth적용했나? - 클라이언트 상태 동기화 로직 테스트했나?
- 페이지마다 배관 복사-붙여넣기 계획은?
Next.js를 선택한다면:
- NextAuth Provider 설정 (5분)
- middleware.ts로 보호 경로 지정 (2분)
- 끝!
마무리
"오늘 본 것처럼, 인증은 SSR 배관이 가장 복잡해지는 영역이야. Express에서는 Step 1부터 4까지의 모든 배관이 한 곳에 모이면서 코드가 폭발적으로 복잡해지거든."
"Next.js는 각 관심사를 별도 파일로 분리하고, NextAuth 같은 도구로 대부분의 배관을 자동화해서 복잡도를 크게 낮췄어."
"근데 여기서 끝이 아니야. 다음 Step에서는 페이지가 10개, 20개로 늘어날 때 라우팅 구조가 어떻게 달라지는지 볼 거야. Express는 수동 라우팅, Next.js는 파일 시스템 기반 라우팅인데... 이것도 배관 복잡도에 큰 영향을 미쳐."
다음 단계
**Step 05: 라우팅 아키텍처**에서 페이지가 많아질 때 라우팅 구조의 차이를 비교합니다. Express의 수동 라우트 등록 vs Next.js의 파일 시스템 기반 라우팅, 그리고 중첩 레이아웃과 동적 라우트 처리 방식을 살펴봅니다.
작성: 2026-02-09 예상 읽기 시간: 25분