키모스토리

#18 Redux-Toolkit (2) 본문

Web Devlopment/ReactJs

#18 Redux-Toolkit (2)

키모형 2025. 3. 26. 10:24
반응형

디렉토리 구조 (JavaScript 버전)

/src
├── app/
│   ├── store.js        	# Redux store 설정
│   ├── hooks.js        	# Custom hooks (useAppDispatch, useAppSelector)
│
├── features/			# 기능(도메인)별 상태 및 컴포넌트 관리
│   ├── auth/			# 인증 관련 기능
│   │   ├── authSlice.js	# Redux Slice
│   │   ├── authAPI.js		# API 요청 관리 (RTK Query 또는 fetch/axios)
│   │   ├── AuthPage.js		# 관련 페이지 컴포넌트
│   │   └── components/		# 관련 UI 컴포넌트
│   │       ├── LoginForm.js
│   │       └── SignupForm.js
│   │
│   ├── todos/			# Todo 리스트 관련 기능
│   │   ├── todosSlice.js
│   │   ├── todosAPI.js
│   │   ├── TodosPage.js
│   │   └── components/
│   │       ├── TodoItem.js
│   │       └── TodoList.js
│
├── components/			# 공용 UI 컴포넌트 (버튼, 모달 등)
│   ├── Button.js
│   ├── Modal.js
│   ├── Input.js
│
├── pages/			# 라우트 기반 페이지 관리
│   ├── HomePage.js
│   ├── AboutPage.js
│   ├── NotFoundPage.js
│   ├── Router.js		# React Router 설정
│
├── services/			# 공통 서비스 (예: API 클라이언트)
│   ├── axiosInstance.js	# Axios 글로벌 인스턴스 설정
│   ├── storage.js		# 로컬 스토리지 관련 유틸
│   ├── authService.js		# 인증 관련 로직
│
├── hooks/			# 공통 훅 (예: useAuth, useFetch)
│   ├── useAuth.js
│   ├── useTheme.js
│
├── utils/			# 유틸리티 함수
│   ├── formatDate.js
│   ├── debounce.js
│
├── assets/			# 이미지, SVG, 아이콘 등
├── styles/			# 글로벌 스타일 및 테마
│ 	├── global.css
│
├── index.js
├── App.js
└── vite.config.js

 

Redux 상태 관리 예제 (JS 버전)

1️⃣ Redux Slice ( /src/features/todos/ todosSlice.js)  

import { createSlice } from "@reduxjs/toolkit";

const initialState = { list: [] };

const todosSlice = createSlice({
  name: "todos",
  initialState,
  reducers: {
    addTodo: (state, action) => {
      state.list.push({ id: Date.now(), text: action.payload, completed: false });
    },
    toggleTodo: (state, action) => {
      const todo = state.list.find(todo => todo.id === action.payload);
      if (todo) {
        todo.completed = !todo.completed;
      }
    },
    removeTodo: (state, action) => {
      state.list = state.list.filter(todo => todo.id !== action.payload);
    },
  },
});

export const { addTodo, toggleTodo, removeTodo } = todosSlice.actions;
export default todosSlice.reducer;

 

2️⃣ Store 설정 (/src/app/store.js)

import { configureStore } from "@reduxjs/toolkit";
import todosReducer from "../features/todos/todosSlice";

export const store = configureStore({
  reducer: {
    todos: todosReducer,
  },
});

 

3️⃣ useAppDispatch & useAppSelector (/src/app/hooks.js) 

-  반복코드, 확장, 유지보수를 위해 분리해서 관리.

import { useDispatch, useSelector } from "react-redux";

export const useAppDispatch = () => useDispatch();
export const useAppSelector = useSelector;

 

4️⃣ Redux 상태 사용하는 컴포넌트 (/src/features/todos/componets/TodoList.js)

import { useAppSelector, useAppDispatch } from "../../app/hooks";
import { toggleTodo, removeTodo } from "./todosSlice";

const TodoList = () => {
  const todos = useAppSelector(state => state.todos.list);
  const dispatch = useAppDispatch();

  return (
    <ul>
      {todos.map(todo => (
        <li key={todo.id}>
          <span 
            onClick={() => dispatch(toggleTodo(todo.id))} 
            style={{ textDecoration: todo.completed ? "line-through" : "none" }}>
            {todo.text}
          </span>
          <button onClick={() => dispatch(removeTodo(todo.id))}>삭제</button>
        </li>
      ))}
    </ul>
  );
};

export default TodoList;

 

 

위 예제코드에서 useAppDispatch & useAppSelector를 hooks.js 로 분리하는 이유?

 

1️⃣ 반복되는 코드 최소화

Redux를 사용할 때 useDispatch와 useSelector는 직접 사용해도 되지만,

매번 useSelector(state => state.todos) 같은 패턴을 반복해야 합니다.

 

특히 TypeScript를 사용할 경우 useSelector의 타입을 명시해야 하는데,

이를 hooks.js에서 한 번만 정의하면 매번 타입을 지정할 필요가 없습니다.

 

예제: hooks.js를 분리하지 않은 경우 (반복되는 코드 많음)

import { useDispatch, useSelector } from "react-redux";
import { addTodo } from "../features/todos/todosSlice";

const TodoList = () => {
  const dispatch = useDispatch();
  const todos = useSelector(state => state.todos.list);

  return (
    <ul>
      {todos.map(todo => (
        <li key={todo.id}>
          <span>{todo.text}</span>
          <button onClick={() => dispatch(addTodo("New Task"))}>추가</button>
        </li>
      ))}
    </ul>
  );
};

 

  • useDispatch()와 useSelector()가 여러 컴포넌트에서 반복됨
  • 상태 구조가 변경될 경우, 여러 파일에서 수정해야 함

예제: hooks.js에서 분리한 경우 (중복 최소화)

// hooks.js
import { useDispatch, useSelector } from "react-redux";

export const useAppDispatch = () => useDispatch();
export const useAppSelector = useSelector;
// TodoList.js
import { useAppDispatch, useAppSelector } from "../../app/hooks";
import { addTodo } from "./todosSlice";

const TodoList = () => {
  const dispatch = useAppDispatch();
  const todos = useAppSelector(state => state.todos.list);

  return (
    <ul>
      {todos.map(todo => (
        <li key={todo.id}>
          <span>{todo.text}</span>
          <button onClick={() => dispatch(addTodo("New Task"))}>추가</button>
        </li>
      ))}
    </ul>
  );
};

 

  • useAppDispatch()와 useAppSelector()를 사용하여 코드를 단순화
  • 상태 구조가 변경되더라도 hooks.js만 수정하면 됨

2️⃣ Redux 미들웨어 확장 용이

Redux 미들웨어(예: redux-thunk, redux-saga)를 사용할 경우 dispatch를 확장해야 하는 경우가 있음.
이를 hooks.js에서 미리 분리해 놓으면 필요할 때 쉽게 확장 가능함.

 

예제: dispatch에 미들웨어 적용이 필요한 경우

import { useDispatch } from "react-redux";

// 커스텀 dispatch를 적용할 수도 있음
export const useAppDispatch = () => {
  const dispatch = useDispatch();
  return (action) => {
    console.log("Dispatching action:", action);
    return dispatch(action);
  };
};
  • 모든 dispatch 호출 전에 로그를 찍도록 수정 가능
  • 프로젝트 요구사항에 맞게 useAppDispatch를 한 곳에서 수정하면 전체 적용 가능

3️⃣ 코드 일관성 유지 & 유지보수성 증가

모든 Redux 관련 훅을 한 곳(hooks.js)에서 관리하면 다음과 같은 장점이 있음.

  • Redux 사용 방식이 프로젝트 전반에서 일관성 유지됨
  • 상태 구조 변경 시 한 곳에서 수정 가능 (state.todos.list → state.tasks.items 같은 변경)
  • 새 기능을 추가할 때 Redux 적용이 쉬움 (그냥 useAppSelector(state => state.XXX) 사용하면 됨)

✅ 결론

useAppDispatch와 useAppSelector를 hooks.js에서 분리하면
반복 코드가 줄어들고,
Redux 미들웨어 적용이 쉬워지며,
상태 변경 시 유지보수가 편리

그래서 대부분의 Redux Toolkit을 사용하는 프로젝트에서는 이 방식이 권장됨! 

 

 

 

⚠️ useEffect에서 dispatch를 의존성 배열에 포함해야 하는 이유

 

경고 메시지:

React Hook useEffect has a missing dependency: 'dispatch'.
Either include it or remove the dependency array  react-hooks/exhaustive-deps

 

App.js 

import { useEffect } from "react";
import Navbar from "./components/Navbar";
import { useAppDispatch, useAppSelector } from './app/hooks';
import { calculateTotals } from "./features/cars/carSlice";
import CarLists from "./features/cars/components/CarLists";

function App() {
  const {carModels} = useAppSelector((store)=>store.cars);
  const dispatch = useAppDispatch();
  useEffect(()=>{
    dispatch(calculateTotals())
  },[carModels]);  // dispatch를 뺀 경우, [carModels, dispatch]

  return (
    <>
      <Navbar/>
      <CarLists/>
    </>
  );
}

export default App;

 

경고 메시지:

React Hook useEffect has a missing dependency: 'dispatch'.
Either include it or remove the dependency array  react-hooks/exhaustive-deps

 

🔹 해결 방법

dispatch를 의존성 배열에 추가

  useEffect(() => {
    dispatch(calculateTotals());
  }, [carModels, dispatch]);  //  dispatch 추가
 

 

🔹 왜 dispatch를 의존성 배열에 추가해야 할까?

React의 useEffect는 의존성 배열에 있는 값이 변경될 때마다 다시 실행됩니다.

  1. useAppDispatch()는 내부적으로 불변성을 유지하는 함수이므로 값이 바뀌지 않지만,
  2. ESLint는 dispatch가 외부 스코프에서 가져온 값이므로 안전성을 위해 의존성 배열에 포함할 것을 권장합니다.

실제로 dispatch는 Redux의 store.dispatch를 참조하기 때문에 불변입니다.
하지만 exhaustive-deps 규칙을 따르려면 의존성 배열에 넣는 것이 좋습니다.


🔹 dispatch를 의존성 배열에 포함하면 발생할 문제는 없을까?

❌ 문제 없음

  • Redux의 dispatch는 불변 함수이므로, 의존성 배열에 추가해도 useEffect가 불필요하게 실행되지 않습니다.
  • ESLint 경고를 피하면서 안전한 코드 작성 가능.

✅ 정리

  • dispatch는 불변 함수지만, exhaustive-deps 규칙을 따르기 위해 의존성 배열에 추가하는 것이 권장됨
  • dispatch를 의존성 배열에 포함해도 불필요한 재렌더링은 발생하지 않음
  • useEffect는 carModels이 변경될 때만 실행됨 (기존 동작과 동일)

👉 결론: [carModels, dispatch]로 설정하는 것이 가장 안전하고 권장되는 방법! 🚀

반응형

'Web Devlopment > ReactJs' 카테고리의 다른 글

#19 Rest API - axios  (0) 2025.03.26
#17 Redux Toolkit  (0) 2025.03.25
#16 Flux패턴  (0) 2025.03.25
#15 React Bootstrap  (0) 2025.03.25
#14 react-router-dom  (0) 2025.03.24