[React] Redux-toolkit

DaYoung·2023년 12월 12일
0

React

목록 보기
3/6
post-thumbnail

redux-toolkit 공식사이트를 참고하면

  • Redux 스토어 구성이 너무 복잡하다.
  • Redux가 유용한 작업을 수행하도록 하려면 많은 패키지를 추가해야 한다.
  • Redux에는 너무 많은 상용구 코드가 필요하다.
    이러한 이유로 Redux-toolkit 사용을 권장하고 있다.

Redux-toolkit을 사용해보기 위해 간단하게 카운터와 to-do-list를 만들어 보았다!

폴더구조는 아래와 같이 만들었다!


Store.tsx
configureStore가 reducere를 감싸고 있는데 여기서 모든 state를 관리하고 있다.

const Store = configureStore({
	reducer: {
		counter: counterReducer,
		todos: todosReducer,
	},
});

export default Store;

index.tsx
index에 store.tsx를 연결시켜준다!
Provider로 감싸주는데 store가 리액트 앱 전체를 감싸주도록 하는 것이다.
그리고 store 라는 파라미터를 전달해준다.

import React from "react";
import ReactDOM from 'react-dom';
import { Provider } from "react-redux";
import App from "./App.tsx";
import store from "./redux/Store.tsx";
import reportWebVitals from "./reportWebVitals";

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

reportWebVitals();

App.tsx
크게 CounterContainer과 TodoContainer이 있다.

import React from 'react';
import './App.css';
import CounterContainer from './containers/CounterContainer.tsx';
import TodosContainer from './containers/TodosContainer.tsx';

function App() {
  return (
    <div>
      <CounterContainer />
      <hr />
      <TodosContainer />
    </div>
  );
}

export default App;

Reducer 만들기


modules/counter.tsx

기존에는 createReducer, createAction이 하던 일을 createSlice가 한번에 해준다.
name은 리듀서에 들어갈 이름을 정하고,
initialState는 초기값,
reducers는 상태가 변하면 어떻게 실행될지 정하는 부분이다.

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

// 인터페이스 정의
export interface ICounterState {
  number: number;
  diff: number;
}

// 초기 상태 선언
const initialState: ICounterState = {
  number: 0,
  diff: 1, // 차이 값
};

//reducer
const counterSlice = createSlice({
  name: "counter",
  initialState,
  reducers: {
    increase(state, action: PayloadAction<void>) {
      state.number = state.number + state.diff;
    },
    decrease(state, action: PayloadAction<void>) {
      state.number = state.number - state.diff;
    },
    setDiff(state, action: PayloadAction<number>) {
      state.diff = action.payload;
    },
  },
});

// 액션 생성 함수와 리듀서 내보내기
export const { increase, decrease, setDiff } = counterSlice.actions;
export default counterSlice.reducer;

modules/todos.tsx

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

// 인터페이스 정의
export interface Todo {
  id: number;
  text: string;
  done: boolean;
}

//초기 상태 선언
const initialState: Todo[] = [];
let nextId = 1;

const todoSlice = createSlice({
  name: "todos",
  initialState,
  reducers: {
    ADD_TODO: (state, action) => {
     return [...state, action.payload.todo];
    },
    TOGGLE_TODO: (state, action) => {
      return state.map((todo) =>
        todo.id === action.payload.id ? { ...todo, done: !todo.done } : todo
      );
    },
  },
});

export const todoActions = todoSlice.actions;
export default todoSlice.reducer;

container/CounterContainer.tsx

-> useSelector를 이용하면 위에서 만든 리듀서에 접근할 수 있다.
store.getState() 함수를 호출했을 때 나타나는 결과물과 동일하다.

-> name: "counter" 라고 지어준 리듀서에 있는 state에 접근하는데,
counter 리듀서에 초기값을 number, diff로 설정해놨기 때문에
counter에 있는 number, diff를 가져온다.

-> useDispatch객체를 dispatch로 재선언한 후,
dispatch 변수를 활용하여 액션을 호출하였다.

  const result = useSelector((state: IRootState) => state.counter);

console.log를 찍어서 확인해보니 아래와 같이 초기값이 나왔다.

import React, { useCallback } from "react";
import { useSelector, useDispatch } from "react-redux";
import Counter from "../components/Counter.tsx";
import {
  increase,
  decrease,
  setDiff,
  ICounterState,
} from "../modules/counter.tsx";

interface IRootState {
  counter: ICounterState;
}

const CounterContainer: React.FC = () => {
  const { number, diff } = useSelector((state: IRootState) => state.counter);

  const dispatch = useDispatch();
  // 각 액션들을 디스패치하는 함수들 만듬
  const onIncrease = useCallback(() => dispatch(increase()), [dispatch]);
  const onDecrease = useCallback(() => dispatch(decrease()), [dispatch]);
  const onSetDiff = useCallback(
    (diff: number) => dispatch(setDiff(diff)),
    [dispatch]
  );

  return (
    <Counter
      number={number}
      diff={diff}
      onIncrease={onIncrease}
      onDecrease={onDecrease}
      onSetDiff={onSetDiff}
    />
  );
};

export default CounterContainer;

container/TodoContainer.tsx TodoContainer도 위와 마찬가지이다.
import React, { useCallback } from "react";
import { useDispatch, useSelector } from "react-redux";
import Todos from "../components/Todos.tsx";
import { Todo, todoActions } from "../modules/todos.tsx";

interface IRootState {
  todos: Todo[];
}

const TodosContainer: React.FC = () => {
  const todos = useSelector((state: IRootState) => state.todos);
  const dispatch = useDispatch();

  const onCreate = (text: string) =>
    dispatch(
      todoActions.ADD_TODO({ todo: { id: Date.now(), text, done: false } })
    );

  const onToggle = useCallback(
    (id: number) => dispatch(todoActions.TOGGLE_TODO({ id })),
    [dispatch]
  );

  return <Todos todos={todos} onCreate={onCreate} onToggle={onToggle} />;
};

export default TodosContainer;


components/Counter.tsx
import React, { ChangeEvent } from "react";

interface ICounterProps {
  number: number;
  diff: number;
  onIncrease: () => void;
  onDecrease: () => void;
  onSetDiff: (diff: number) => void;
}

const Counter: React.FC<ICounterProps> = ({
  number,
  diff,
  onIncrease,
  onDecrease,
  onSetDiff,
}) => {
  const onChange = (e: ChangeEvent<HTMLInputElement>) => {
    onSetDiff(parseInt(e.target.value, 10));
  };
  return (
    <div>
      <h1>{number}</h1>
      <div>
        <input type="number" value={diff} min="1" onChange={onChange} />
        <button onClick={onIncrease}>+</button>
        <button onClick={onDecrease}>-</button>
      </div>
    </div>
  );
};

export default Counter;

components/Todos.tsx
import React, { ChangeEvent, FormEvent, useState } from "react";
import { Todo } from "../modules/todos";

interface TodosProps {
  todos: Todo[];
  onCreate: (text: string) => void;
  onToggle: (id: number) => void;
}

const Todos: React.FC<TodosProps> = ({ todos, onCreate, onToggle }) => {
  const [text, setText] = useState<string>("");
  const onChange = (e: ChangeEvent<HTMLInputElement>) =>
    setText(e.target.value);
  const onSubmit = (e: FormEvent<HTMLFormElement>) => {
    e.preventDefault(); // Submit 이벤트 발생했을 때 새로고침 방지
    onCreate(text);
    setText(""); // 인풋 초기화
  };

  // 컴포넌트 최적화를 위하여 React.memo를 사용
  const TodoItem: React.FC<{ todo: Todo }> = React.memo(({ todo }) => (
    <li
      style={{ textDecoration: todo.done ? "line-through" : "none" }}
      onClick={() => onToggle(todo.id)}
    >
      {todo.text}
    </li>
  ));

  const TodoList: React.FC<{ todos:Todo[] }> = React.memo(({ todos }) => (
    <ul>
      {todos.map((todo: Todo) => (
        <TodoItem key={todo.id} todo={todo} />
      ))}
    </ul>
  ));

  return (
    <div>
      <form onSubmit={onSubmit}>
        <input
          value={text}
          placeholder="할 일을 입력하세요.."
          onChange={onChange}
        />
        <button type="submit">등록</button>
      </form>
      <TodoList todos={todos} />
    </div>
  );
};

export default Todos;

<링크>

https://github.com/carrotdy/react-redux-toolkit

<참고>

https://react.vlpt.us/redux/01-keywords.html

profile
안녕하세요. 프론트앤드 개발자 홍다영입니다.

0개의 댓글