Redux 학습 가이드 - 단계별 실습

odada·2025년 1월 6일
0

next.js

목록 보기
9/12

🎯 학습 목표

이 가이드를 통해 다음을 학습합니다:
1. Redux의 기본 개념과 사용법
2. Redux Toolkit을 활용한 상태 관리
3. 비동기 작업 처리
4. TypeScript와 함께 사용하기

📚 준비사항

프로젝트 세팅

# Next.js 프로젝트 생성
npx create-next-app@latest redux-practice --typescript --tailwind --eslint

# 필요한 패키지 설치
npm install @reduxjs/toolkit react-redux @tanstack/react-query axios

🎓 단계별 학습

1단계: 기본적인 인증 상태 관리

먼저 가장 기본적인 로그인/로그아웃 기능을 구현해봅니다.

// src/types/auth.ts
export interface User {
  id: string;
  username: string;
  role: 'admin' | 'user';
}

export interface AuthState {
  user: User | null;
  token: string | null;
  isAuthenticated: boolean;
}
// src/store/slices/authSlice.ts
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { AuthState, User } from '@/types/auth';

const initialState: AuthState = {
  user: null,
  token: null,
  isAuthenticated: false
};

export const authSlice = createSlice({
  name: 'auth',
  initialState,
  reducers: {
    setCredentials: (
      state,
      action: PayloadAction<{ user: User; token: string }>
    ) => {
      state.user = action.payload.user;
      state.token = action.payload.token;
      state.isAuthenticated = true;
    },
    logout: (state) => {
      state.user = null;
      state.token = null;
      state.isAuthenticated = false;
    }
  }
});

export const { setCredentials, logout } = authSlice.actions;
export default authSlice.reducer;
// src/store/index.ts
import { configureStore } from '@reduxjs/toolkit';
import authReducer from './slices/authSlice';

export const store = configureStore({
  reducer: {
    auth: authReducer
  }
});

export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
// src/components/LoginForm.tsx
import { useState } from 'react';
import { useDispatch } from 'react-redux';
import { setCredentials } from '@/store/slices/authSlice';

export const LoginForm = () => {
  const dispatch = useDispatch();
  const [username, setUsername] = useState('');
  const [password, setPassword] = useState('');

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    // 실제로는 API 호출을 하겠지만, 예시를 위해 직접 데이터를 생성
    dispatch(
      setCredentials({
        user: {
          id: '1',
          username,
          role: 'user'
        },
        token: 'dummy-token'
      })
    );
  };

  return (
    <form onSubmit={handleSubmit} className="space-y-4">
      <div>
        <label htmlFor="username">Username</label>
        <input
          type="text"
          id="username"
          value={username}
          onChange={(e) => setUsername(e.target.value)}
          className="border p-2 w-full"
        />
      </div>
      <div>
        <label htmlFor="password">Password</label>
        <input
          type="password"
          id="password"
          value={password}
          onChange={(e) => setPassword(e.target.value)}
          className="border p-2 w-full"
        />
      </div>
      <button
        type="submit"
        className="bg-blue-500 text-white px-4 py-2 rounded"
      >
        Login
      </button>
    </form>
  );
};

2단계: 주문 관리 상태 추가

// src/types/order.ts
export interface Order {
  id: string;
  productName: string;
  quantity: number;
  status: 'pending' | 'approved' | 'rejected';
  createdAt: string;
}

export interface OrderState {
  orders: Order[];
  selectedOrder: Order | null;
  loading: boolean;
  error: string | null;
}
// src/store/slices/orderSlice.ts
import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit';
import { Order, OrderState } from '@/types/order';

// 비동기 작업을 위한 thunk
export const fetchOrders = createAsyncThunk(
  'orders/fetchOrders',
  async () => {
    // 실제로는 API 호출
    const mockOrders: Order[] = [
      {
        id: '1',
        productName: 'Product 1',
        quantity: 2,
        status: 'pending',
        createdAt: new Date().toISOString()
      }
    ];
    return mockOrders;
  }
);

const initialState: OrderState = {
  orders: [],
  selectedOrder: null,
  loading: false,
  error: null
};

export const orderSlice = createSlice({
  name: 'orders',
  initialState,
  reducers: {
    selectOrder: (state, action: PayloadAction<string>) => {
      state.selectedOrder = state.orders.find(
        order => order.id === action.payload
      ) || null;
    },
    updateOrderStatus: (
      state,
      action: PayloadAction<{ orderId: string; status: Order['status'] }>
    ) => {
      const order = state.orders.find(o => o.id === action.payload.orderId);
      if (order) {
        order.status = action.payload.status;
      }
    }
  },
  extraReducers: (builder) => {
    builder
      .addCase(fetchOrders.pending, (state) => {
        state.loading = true;
      })
      .addCase(fetchOrders.fulfilled, (state, action) => {
        state.loading = false;
        state.orders = action.payload;
      })
      .addCase(fetchOrders.rejected, (state, action) => {
        state.loading = false;
        state.error = action.error.message || 'Failed to fetch orders';
      });
  }
});

export const { selectOrder, updateOrderStatus } = orderSlice.actions;
export default orderSlice.reducer;
// src/components/OrderList.tsx
import { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { RootState } from '@/store';
import { fetchOrders, selectOrder } from '@/store/slices/orderSlice';

export const OrderList = () => {
  const dispatch = useDispatch();
  const { orders, loading, error } = useSelector(
    (state: RootState) => state.orders
  );

  useEffect(() => {
    dispatch(fetchOrders());
  }, [dispatch]);

  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error}</div>;

  return (
    <div className="space-y-4">
      {orders.map((order) => (
        <div
          key={order.id}
          className="border p-4 rounded"
          onClick={() => dispatch(selectOrder(order.id))}
        >
          <h3 className="font-bold">{order.productName}</h3>
          <p>Quantity: {order.quantity}</p>
          <p>Status: {order.status}</p>
        </div>
      ))}
    </div>
  );
};

3단계: 문서 관리 기능 추가

// src/types/document.ts
export interface Document {
  id: string;
  title: string;
  type: 'manual' | 'ecn';
  url: string;
  approvalStatus: 'pending' | 'approved' | 'rejected';
}

export interface DocumentState {
  documents: Document[];
  filters: {
    type: Document['type'] | 'all';
    searchTerm: string;
  };
  loading: boolean;
}
// src/store/slices/documentSlice.ts
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { Document, DocumentState } from '@/types/document';

const initialState: DocumentState = {
  documents: [],
  filters: {
    type: 'all',
    searchTerm: ''
  },
  loading: false
};

export const documentSlice = createSlice({
  name: 'documents',
  initialState,
  reducers: {
    setDocuments: (state, action: PayloadAction<Document[]>) => {
      state.documents = action.payload;
    },
    setFilter: (
      state,
      action: PayloadAction<{
        type: DocumentState['filters']['type'];
        searchTerm: string;
      }>
    ) => {
      state.filters = action.payload;
    },
    updateApprovalStatus: (
      state,
      action: PayloadAction<{
        documentId: string;
        status: Document['approvalStatus'];
      }>
    ) => {
      const document = state.documents.find(
        doc => doc.id === action.payload.documentId
      );
      if (document) {
        document.approvalStatus = action.payload.status;
      }
    }
  }
});

export const { setDocuments, setFilter, updateApprovalStatus } =
  documentSlice.actions;
export default documentSlice.reducer;

🔄 실습 과제

위 코드를 기반으로 다음 기능들을 직접 구현해보세요:

  1. 인증 기능 확장

    • 로그인 상태 유지 (localStorage 활용)
    • 권한별 접근 제어 구현
  2. 주문 관리 기능 확장

    • 주문 필터링 기능 추가
    • 주문 상태 변경 히스토리 관리
    • 페이지네이션 구현
  3. 문서 관리 기능 확장

    • 문서 업로드 기능
    • 문서 승인 워크플로우
    • 문서 버전 관리

🎯 다음 단계

  1. 성능 최적화

    • Redux Selector 최적화
    • 메모이제이션 활용
  2. 테스트 작성

    • Redux 리듀서 테스트
    • 비동기 액션 테스트
  3. 미들웨어 활용

    • 커스텀 미들웨어 작성
    • 로깅 미들웨어 구현

💡 팁

  1. Redux DevTools를 활용하여 상태 변화를 모니터링하세요.
  2. TypeScript의 타입 추론을 최대한 활용하세요.
  3. 컴포넌트와 비즈니스 로직을 명확히 분리하세요.
  4. 재사용 가능한 커스텀 훅을 만들어 사용하세요.

0개의 댓글