1. 프로젝트 생성 및 세팅
npx create-next-app@latest my-auth-app --typescript
cd my-auth-app
npm install @reduxjs/toolkit react-redux next-redux-wrapper js-cookie cookie
- --typescript 옵션은 필수! 타입 안정성을 보장합니다.
2. 폴더 구조
my-auth-app/
├── app/ # App Router 디렉토리
│ ├── page.tsx # 홈 페이지
│ ├── login/ # 로그인 페이지
│ │ └── page.tsx
├── pages/
│ └── api/
│ ├── login.ts # 로그인 API
│ └── logout.ts # 로그아웃 API
├── store/
│ ├── index.ts # Redux store 설정
│ └── slices/
│ └── authSlice.ts
├── components/
│ └── LoginGuard.tsx # 로그인 확인용 클라이언트 컴포넌트
3. Redux 구성
//store/slices/authSlice.ts
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
interface User {
id: string;
email: string;
name: string;
}
interface AuthState {
user: User | null;
token: string | null;
}
const initialState: AuthState = {
user: null,
token: null,
};
const authSlice = createSlice({
name: 'auth',
initialState,
reducers: {
setAuth: (state, action: PayloadAction<AuthState>) => {
state.user = action.payload.user;
state.token = action.payload.token;
},
logout: (state) => {
state.user = null;
state.token = null;
},
},
});
export const { setAuth, logout } = authSlice.actions;
export default authSlice.reducer;
// store/index.ts
'use client';
import { configureStore } from '@reduxjs/toolkit';
import authReducer from './slices/authSlice';
import { Provider } from 'react-redux';
export const store = configureStore({
reducer: {
auth: authReducer,
},
});
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
export function ReduxProvider({ children }: { children: React.ReactNode }) {
return <Provider store={store}>{children}</Provider>;
}
4. App에 Redux 연결
// app/layout.tsx
import './globals.css';
import { ReduxProvider } from '@/store';
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="ko">
<body>
<ReduxProvider>{children}</ReduxProvider>
</body>
</html>
);
}
5. 로그인 API
// pages/api/login.ts
import { serialize } from 'cookie';
import { NextApiRequest, NextApiResponse } from 'next';
export default function handler(req: NextApiRequest, res: NextApiResponse) {
const { email = '', password = '' } = req.body;
if (email === 'test@test.com' && password === '1234') {
const token = 'mock-token-123';
res.setHeader(
'Set-Cookie',
serialize('token', token, {
path: '/',
httpOnly: true,
maxAge: 60 * 60 * 24,
})
);
return res.status(200).json({
token,
user: { id: '1', name: '테스트유저', email },
});
}
return res.status(401).json({ message: '인증 실패' });
}
6. 로그아웃 API
// pages/api/logout.ts
import { serialize } from 'cookie';
import { NextApiRequest, NextApiResponse } from 'next';
export default function handler(req: NextApiRequest, res: NextApiResponse) {
res.setHeader('Set-Cookie', serialize('token', '', {
path: '/',
httpOnly: true,
expires: new Date(0),
}));
return res.status(200).json({ message: '로그아웃 완료' });
}
7. 로그인 페이지 (입력 + 처리)
// app/login/page.tsx
'use client';
import { useState } from 'react';
import { useDispatch } from 'react-redux';
import { useRouter } from 'next/navigation';
import { setAuth } from '@/store/slices/authSlice';
export default function LoginPage() {
const dispatch = useDispatch();
const router = useRouter();
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const handleLogin = async () => {
const res = await fetch('/api/login', {
method: 'POST',
body: JSON.stringify({ email, password }),
headers: { 'Content-Type': 'application/json' },
});
if (res.ok) {
const data = await res.json();
dispatch(setAuth(data));
router.push('/');
} else {
alert('로그인 실패');
}
};
return (
<div className="max-w-md mx-auto mt-20 p-6 border rounded shadow">
<h1 className="text-2xl font-semibold mb-6">로그인</h1>
<input
type="email"
placeholder="이메일"
value={email}
onChange={(e) => setEmail(e.target.value)}
className="w-full p-2 border mb-4 rounded"
/>
<input
type="password"
placeholder="비밀번호"
value={password}
onChange={(e) => setPassword(e.target.value)}
className="w-full p-2 border mb-6 rounded"
/>
<button
onClick={handleLogin}
className="w-full bg-blue-600 hover:bg-blue-700 text-white p-2 rounded"
>
로그인
</button>
</div>
);
}
8. 메인 페이지 + 로그아웃 버튼
// app/page.tsx
'use client';
import { useDispatch, useSelector } from 'react-redux';
import { useRouter } from 'next/navigation';
import { RootState } from '@/store';
import { logout } from '@/store/slices/authSlice';
export default function HomePage() {
const user = useSelector((state: RootState) => state.auth.user);
const dispatch = useDispatch();
const router = useRouter();
const handleLogout = async () => {
await fetch('/api/logout');
dispatch(logout());
router.push('/login');
};
return (
<main className="p-10">
<h1 className="text-2xl font-bold">홈페이지</h1>
{user ? (
<>
<p className="mt-4">환영합니다, {user.name}님!</p>
<button
onClick={handleLogout}
className="mt-4 bg-red-500 text-white px-4 py-2 rounded"
>
로그아웃
</button>
</>
) : (
<div className="mt-4 text-red-500">
<p>로그인하지 않았습니다.</p>
<button
onClick={() => router.push('/login')}
className="mt-2 bg-blue-500 text-white px-4 py-2 rounded"
>
로그인하러 가기
</button>
</div>
)}
</main>
);
}
주요 구현 포인트
- POST /api/login → 쿠키 저장 + Redux 저장
- POST /api/logout → 쿠키 삭제 + Redux 초기화
- 클라이언트에서 상태 기반 로그인/로그아웃 처리
'개발 공부 > React' 카테고리의 다른 글
| Next.js - layout.tsx, children (0) | 2025.05.11 |
|---|---|
| Next.js- cookies().get(), middleware 로그인 유지 (0) | 2025.05.09 |
| Redux login 예시 (0) | 2025.05.05 |
| getStaticProps, getStaticPaths (0) | 2025.04.26 |
| Next.js 페이지 로딩 속도 최적화와 SEO 영향 (0) | 2025.04.24 |