Redux-Toolkit (With TodoList)

Seju·2024년 2월 29일
1

React

목록 보기
8/9
post-thumbnail

개요


  • 리덕스에 개념과 사용이유에 대해 알아보고 이를 기반으로 redux 내에서 현재 많이 사용되고 권고되고 있는 redux-toolkit라이브러리를 통해 간단한 react todoList 컴포넌트를 구현해보고자 합니다.

리덕스란?


Redux는 action이라는 이벤트를 통해 애플리케이션의 상태를 관리하고 업데이트하기 위한 패턴이자 라이브러리입니다.

  • 전역상태로 집약적으로 관리되어야 할 상태를 핸들링하는 중앙 저장소 역할을 하게되며, 예측가능한 방식으로만 상태를 업데이트 할 수 있도록하는 엄격한 규칙을 개발자가 지정합니다.
  • 현재 Redux 팀내에서도 Redux 대신 Redux-toolkit의 사용을 권고하고 있습니다.


리덕스를 사용해야하는 이유는?


Redux를 사용하면 애플리케이션의 여러 부분에 필요한 "전역적인 상태"를 관리할 수 있습니다.

  • Redux에서 제공하는 도구를 사용하면 애플리케이션의 상태가 언제,어디서,왜,어떻게 업데이트 되는지
  • 그리고 이러한 변경이 발생할 때 로직이 어떻게 작동되는지 더 쉽게 이해할 수 있습니다.
  • 최종적으로 예측 가능하고 테스트가 용이한 로직을 작성하도록 안내합니다.

리덕스 사용시 알아야 할 키워드들


1. Action

Action은 단순히 type과 paload 프로퍼티가 존재하는자바스크립트 객체로 생각하시면 됩니다.

  • 액션은 애플리케이션에서 발생하는 일을 설명하는 일종의 이벤트입니다.
  • type : 작업에 대한 이름을 부여하는 문자열입니다.
    • 일반적으로 도메인/이벤트명과 같은 유형의 문자열로 작성합니다.
  • payload : 액션 객체에 어떤일이 일어났는지에 대한 추가적인 정보가 담긴 필드입니다.
  • 최종적으로 Action 객체는 다음과 같이 상태로 전달되게 됩니다.
const addTodoAction = {
  type: "todos/todoAdd",
  payload: "Redux 공부하기",
};

2. Reducer

리듀서현재 상태와 액션 객체를 구독하고, 필요한 경우에 따라 상태를 업데이트하는 로직을 작성 후 새로운 상태를 반환하는 하나의 함수입니다.

  • 리듀서는 수신된 액션의 타입에 따라 이벤트를 처리하는 이벤트 리스너와 같이 동작한다고 생각하면 됩니다.

const initialState = {value: 0};

function counterReducer(state = initialState, action) {
  switch (action.type) {
    case "counter/increment":
      return {
        ...state,
        value: state.value + 1,
      };

    /* 리듀서에서 type이 존재하지 않을 경우 기존 상태를 변경하지 않고 반환합니다*/
    default:
      return state;
  }
}

📑 Reducer 함수 작성 시 지켜야 할 규칙

  1. 상태와 액션을 기반으로 새로운 상태 값만을 작성해야 합니다.
  2. 기존 상태를 수정할 수 없습니다. (따라서 복사를 통해 복사된 값을 변경하는 방식으로 업데이트를 수행합니다. setState와 유사하죠?)
  3. 로직 이외에 로직과 관련없는 부수효과를 일으키는 로직은 작성하면 안됩니다(=리듀서는 순수함수로 구현해야 합니다.)

3. Dispatch

rtk의 configureStore로 만든 store 내부에는 dispatch라는 함수가 존재합니다.

  • 상태를 업데이트 하는 유일한 방법store.dispatch()를 호출하고 액션객체를 전달하는 것입니다.
  • 스토어는 리듀서 함수를 실행하고 새로운 상태값을 저장하게 되며, getState()를 통해 업데이트된 값을 참조할 수 있게됩니다.

Redux-Toolkit을 통한 간단한 TodoList 구현


  • 이제 실제 React에서 Redux-Toolkit으로 todoList를 구현해보겠습니다.
  • 기존 리액트 프로젝트는 기본적으로 구성되어있다는 가정하에 진행하겠습니다.

0. 사전 준비

  • 리덕스 툴킷과 리액트 리덕스 패키지를 프로젝트 내 추가합니다.
pnpm i -D @reduxjs/toolkit react-redux

1. 스토어 생성 후 초기상태 정의하기

Redux 폴더 생성(=개인취향) 폴더 내부에 store.ts 파일을 정의하고 configureStore함수를 통해 만든 스토어를 내보냅니다.

  • 아직 reducer 함수는 정의되지 않았으므로 빈객체로 지정하겠습니다.

/* redux/store.ts */

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

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

export default store;

2. 만든 store를 Provider에 공급하기

만든 store의 상태는 전역적으로 관리되어야 하기 때문에, main.tsx에서 Provider 컴포넌트로 App을 감쌉니다.

  • Provider 컴포넌트는 react-redux에서 제공하는 컴포넌트로 해당 Provider를 통해 전역 상태를 공급할 수 있게됩니다.
/* main.tsx */

import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App.tsx";
import {Provider} from "react-redux";
import store from "./store/store.ts";

ReactDOM.createRoot(document.getElementById("root")!).render(
  <React.StrictMode>
    <Provider store={store}>
      <App />
    </Provider>
  </React.StrictMode>
);

3. TodoSlice 생성하기

마찬가지로 redux 폴더 내부에서 공통적으로 관리하기 위해 redux 폴더 내부에 todoSlice 파일을 생성합니다.

  • 여기서 slice는 Redux의 상태의 한부분을 나타냅니다. 이는 Redux를 사용하는데 있어서 개발자들끼리의 관행이라고 볼 수 있습니다.
  • redux-toolkit은 createSlice라는 함수를 제공하는데, 해당 함수 내에서 액션 생성함수와 리듀서를 한번에 생성할 수 있게해줍니다.
    • 이는 기존 redux에서 발생하는 보일러플레이트 코드를 줄여주며, 코드의 가독성을 높일 수 있습니다.
  • 여기서 실제 리듀서 함수가 정의되고 초기상태를 설정하는 역할을 하게됩니다.

/* redux/todoSlice.ts */

import {createSlice, nanoid, PayloadAction} from "@reduxjs/toolkit";

interface Todo {
  id: string;
  text: string;
  done: boolean;
}

const initialState: Todo[] = [
  {id: nanoid(), text: "Redux 학습하기", done: false},
];

export const todoSlice = createSlice({
  name: "redux/todos",
  initialState,
  reducers: {
    addTodo: (state, action: PayloadAction<string>) => {
      const newTodo: Todo = {
        id: nanoid(),
        text: action.payload,
        done: false,
      };
      state.push(newTodo);
    },
    toggleTodo: (state, action: PayloadAction<string>) => {
      const todo = state.find((todo) => todo.id === action.payload);
      if (todo) {
        todo.done = !todo.done;
      }
    },
    removeTodo: (state, action: PayloadAction<string>) => {
      const index = state.findIndex((todo) => todo.id === action.payload);
      if (index !== -1) {
        state.splice(index, 1);
      }
    },
  },
});

export const {addTodo, toggleTodo, removeTodo} = todoSlice.actions;

export default todoSlice.reducer;

3-1 코드를 자세히 살펴봅시다!

  • 먼저, 코드 윗줄에서 initalState라는 배열을 설정했고, slice에 넘겨줄 기본값 변수의 역할을 하게 됩니다.

const initialState: Todo[] = [
  {id: nanoid(), text: "Redux 학습하기", done: false},
];

createSlice

createSlice API는 기존 Redux 작성시, 리듀서,액션, 불변성을 지키는 업데이트를 작성하는데 작성해야하는 상용구들을 모두 제거하도록 설계되었습니다.

  • 기존 action.textaction.id와 같이 고유한 이름의 모든 필드는 해당 필드들이 포함된 객체인 action.payload로 대체할 수 있습니다.
  • 위에서 언급되었듯이 상태를 직접적으로 업데이트 하는 방식이아닌 복사를 해 복사한 상태를 업데이트 하는 방식으로 리듀서를 작성해야한다고 했습니다.
    • 기존 redux에서 spread-syntax등으로 값을 복사 해 복사한 값을 업데이트 하는 방식이 아닌, 자체적으로 immer 라이브러리가 탑재되어 있으므로 상태를 직접적으로 업데이트하는 로직을 작성할 수 있게 합니다.

reducers 내부에서 직접적인 기능 설정하기

  • 이제 우리가 할일은 slice의 reducers 내부에 구현 로직을 설정하면 됩니다.
  • 간단한 todoApp을 구현하기로 했으니 기능은 할일 추가, 할일 내부에서 완료 여부 토글링, 할일 삭제 이 세가지 기능을 다루겠습니다.
    • 여기서 nanoid는 리덕스 툴킷에서 제공하는 함수로, 암호화 되지 않은 임의의 문자열 ID를 추가해줍니다.
console.log(nanoid()); // 'dgPXxUz_6fWIQBD8XmiSy'

1. addTodo (todos 배열에 새로운 todoItem 추가하기)

  • 할일을 추가해주는 기능을 하게 됩니다.
  • 여기서 textaction.payload로 설정하게 되는데, 최종적으로 사용자의 input을 상태로 받고 해당 input이 text로 가게될 것입니다.
  • 이후 push를 통해 기존 state 배열에 새로운 TodoItem을 추가합니다.
 addTodo: (state, action: PayloadAction<string>) => {
      const newTodo: Todo = {
        id: nanoid(),
        text: action.payload,
        done: false,
      };
      state.push(newTodo);
    }

2. toggleTodo (todoItem의 done 프로퍼티 토글 하기)

  • toggleTodo 메서드는 Array.find 메서드를 통해 todo.idaction.payload와 같으면, 즉, 사용자가 해당 todoItem을 눌렀을 때 발생하며, 최종적으로 사용자가 클릭한 todoItem의 done 상태가 토글링됩니다.
 toggleTodo: (state, action: PayloadAction<string>) => {
      const todo = state.find((todo) => todo.id === action.payload);
      if (todo) {
        todo.done = !todo.done;
      }
    }

3. removeTodo (todoItem 삭제하기)

  • todoItem을 삭제하는 로직입니다.

  • findIndex 메서드를 통해 index가 -1과 같지않으면 즉, 유효한 인덱스라면 todoItem을 삭제합니다.

removeTodo: (state, action: PayloadAction<string>) => {
      const index = state.findIndex((todo) => todo.id === action.payload);
      if (index !== -1) {
        state.splice(index, 1);
      }
    }

4. 최종적으로 만든 액션들을 구조분해 할당(todoSlice.actions) 후 내보냅니다.

  • todoSlice의 Reducer 또한 내보냅니다.
export const {addTodo, toggleTodo, removeTodo} = todoSlice.actions;

export default todoSlice.reducer;

4. 만든 slice 기반으로 기존 store 재정의하기

기존 store의 reducer 내부에서 빈 객체로 남겨두었던 todos를 만든 slice로 재정의합니다.

import {configureStore} from "@reduxjs/toolkit";
import todosReducer from "./todoSlice";

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

export default store;

5. 정의한 전역상태 사용 및 이벤트 적용하기

이제 준비는 끝났습니다. 만든 전역상태를 실제 컴포넌트 단에서 사용할 수 있습니다!

useSelector를 통해 정의한 상태를 참조할 수 있습니다. useDispacth를 통해 액션을 디스페치 즉, 이벤트 핸들링을 할 수 있습니다.

이제 handleAddTodo와 같은 함수들을 추가해 실질적인 UI가 업데이트 될 수 있도록 로직을 작성합니다.

  • 이미 slice 파일 내부에서 기능들을 전부 정의해서 필요한건 find나, findIndex에서 필요한 action.payload, 즉 id값만 매개변수로 전달해주면 됩니다.

/* App/TodoList.tsx */

import {useState} from "react";
import {useDispatch, useSelector} from "react-redux";
import {addTodo, removeTodo, toggleTodo} from "./redux/todoSlice";

interface Todo {
  id: string;
  text: string;
  done: boolean;
}

function App() {
  const [inputTodo, setInputTodo] = useState("");
  const todos = useSelector((state) => state.todos);
  const dispatch = useDispatch();

  const handleAddTodo = () => {
    dispatch(addTodo(inputTodo));
    setInputTodo("");
  };

  const handleToggleTodo = (id: string) => () => {
    dispatch(toggleTodo(id));
  };

  const handleRemoveTodo = (id: string) => () => {
    dispatch(removeTodo(id));
  };

  return (
    <>
      <div>
        <input
          type="text"
          value={inputTodo}
          onChange={(e) => setInputTodo(e.target.value)}
        />
        <button onClick={handleAddTodo}>할일 추가</button>
        {todos.map((todo: Todo) => (
          <li key={todo.id}>
            <span
              style={{textDecoration: todo.done ? "line-through" : "none"}}
              onClick={handleToggleTodo(todo.id)}>
              {todo.text}
            </span>
            <button onClick={handleRemoveTodo(todo.id)}>할일 삭제</button>
          </li>
        ))}
      </div>
    </>
  );
}

export default App;

6. 완성

  • 이제 todos의 전역상태에 따라 동적으로 상태가 변하는 기본적인 기능을 가지고 있는 TodoList를 구현완료 했습니다.

우리 TodoList 귀엽죠?.. 😀

profile
Talk is cheap. Show me the code.

0개의 댓글