본문 바로가기

개발 공부/React

Next.js, Redux - 로그인/로그아웃

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 초기화
  • 클라이언트에서 상태 기반 로그인/로그아웃 처리