본문 바로가기

Computer Programming/[리액트를 다루는 기술]

[리액트를 다루는 기술] 17. 리덕스로 리액트 앱 상태 관리하기

 

이 책에서는 Ducks 패턴을 사용해 액션타입, 액션 생성함수, 리듀서 함수를 기능별로 하나의 파일에 몰아서 작성하는 방법을 사용하고 있다. 

 

 

이렇게 카운터 기능 파일 1개, todo 기능 파일 1개를 만들어 각각 액션 타입, 액션 함수, 리듀서를 몰아넣는다.

 

 

todos 모듈 만들기!

// modules/todos.js

//액션 타입 정의
const CHANGE_INPUT = "todos/CHANGE_INPUT";
const INSERT = "todos/INSERT";
const TOGGLE = "todos/TOGGLE";
const REMOVE = "todos/REMOVE";


//액션 생성 함수
export const changeInput = (input) => {
  return { type: CHANGE_INPUT, input };
};

let id = 3;
export const insert = (text) => {
  return {
    type: INSERT,
    todo: {
      id: id++,
      text,
      done: false,
    },
  };
};

export const toggle = (id) => {
  return { type: TOGGLE, id };
};

export const remove = (id) => {
  return { type: REMOVE, id };
};


//초깃값
const initialState = {
  input: "",
  todos: [
    {
      id: 1,
      text: "투두리스트 만들기",
      done: true,
    },
    {
      id: 2,
      text: "리덕스 연습하기",
      done: false,
    },
  ],
};


//리듀서
const todos = (state = initialState, action) => {
  switch (action.type) {
    case CHANGE_INPUT:
      return {
        ...state,
        input: action.input,
      };
    case INSERT:
      return {
        ...state,
        todos: state.todos.concat(action.todo),
      };
    case TOGGLE:
      return {
        ...state,
        todos: state.todos.map((todo) =>
          todo.id === action.id ? { ...todo, done: !todo.done } : todo,
        ),
      };
    case REMOVE:
      return {
        ...state,
        todos: state.todos.filter((todo) => todo.id === action.id),
      };
    default:
      return state;
  }
};

export default todos;

 

 

 

 

 

index.js 에서 리덕스 적용하면 된다. (rootReducer은 modules에 index.js에 정의)

 

 

import React from "react";
import ReactDOM from "react-dom/client";
import "./index.css";
import App from "./App";
import { createStore } from "redux";
import rootReducer from "./modules";
import { Provider } from "react-redux";
import { devToolsEnhancer } from "redux-devtools-extension";

// chrome 의 Redux devtools 사용
const store = createStore(rootReducer, devToolsEnhancer());

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

chrome 확장 도구의 Redux devtools를 사용할 것이기 때문에 추가로 devToolsEnhancer() 코드를 작성해준다.

 

state을 누르면 현재 리덕스 스토어 내부 상태를 확인할 수 있다.

 

이제 이 스토어와 연관된 '컨테이너' 컴포넌트를 만들어보자. 이 컨테이너 컴포넌트에서는 리덕스 스토어에 접근하고, 액션도 디스패치할 수 있다.

 

컴포넌트 분리 참고 : https://nayoungkim00.tistory.com/88

 

리덕스 프로젝트 구조 - Dumb & Smart (Presentational 컴포넌트와 Container 컴포넌트의 분리)

리액트는 컴포넌트를 통해 UI를 이리저리 조합하여 잘 동작하게 하기 위해 만들어졌다. 항상 컴포넌트의 재사용을 고려하고, 이러한 패턴들에 대해 공부하고 효율적이면서 가독성 좋은 프로젝

nayoungkim00.tistory.com

 

 

 

 

CounterContainer 에 Counter.js 컴포넌트를 렌더링하고 store의 state의 값을 가져온 후, Counter 이라는 프레젠테이셔널 함수에 값을 전달한다. 

 

action 객체를 던질 때는 useDIspatch(), store 의 값을 가져올 때는 useSelector()을 사용하면 된다.

 

 

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

const CounterContainer = () => {
  const number = useSelector((state) => state.counter.number);
  const dispatch = useDispatch();

  return (
    <Counter
      number={number}
      onIncrease={useCallback(() => dispatch(increase()), [dispatch])}
      onDecrease={useCallback(() => dispatch(decrease()), [dispatch])}
    />
  );
};

export default CounterContainer;

 

함수가 렌더링 될 때 마다 onIncrease의 함수와 onDecrease의 함수가 재생성되고 있다. useCallback으로 렌더링 최적화를 해주자

즉 useDispatch를 이용할 때는 항상 useCallback과 같이 사용해서 함수가 새롭게 생성되는 것을 막는다. 이렇게 기억하면 된다

 

 

 

 

 

이번엔 투두리스트의 Container에서 store에 접근하고, dispatch를 호출해서 Todo component에 넘겨줬다.

 

 

import React, { useCallback } from "react";
import Todos from "../components/Todos";
import { useDispatch, useSelector } from "react-redux";
import { changeInput, insert, remove, toggle } from "../modules/todos";

const TodosContainer = () => {
  const { input, todos } = useSelector(({ todos }) => ({
    input: todos.input, //store의 state 중 todos의 input
    todos: todos.todos, //store의 state 중 todos의 todos
  }));
  const dispatch = useDispatch();

  const onChangeInput = useCallback(
    (input) => {
      dispatch(changeInput(input));
    },
    [dispatch],
  );

  const onInsert = useCallback(
    (text) => {
      dispatch(insert(text));
    },
    [dispatch],
  );

  const onToggle = useCallback(
    (id) => {
      dispatch(toggle(id));
    },
    [dispatch],
  );

  const onRemove = useCallback(
    (id) => {
      dispatch(remove(id));
    },
    [dispatch],
  );

  return (
    <div>
      <Todos
        input={input}
        todos={todos}
        onChangeInput={onChangeInput}
        onInsert={onInsert}
        onToggle={onToggle}
        onRemove={onRemove}
      />
    </div>
  );
};

export default TodosContainer;

 

문제가 되지는 않지만 지금 dispatch해주는 함수가 4개이고 코드가 중복된다. 딱봐도 재사용할 수 있을거같이 생겼음

 

그래서 useActions라는 hook을 사용해서 따로 뺄 수도 있다. (공식 hook은 아니라서 따로 만들어줘야 함)

 

lib/useActions.js 를 만들어서 아래와 같이 작성하면 된다. 여기서는 react-redux에 내장된 bindActionCreators 라는 함수를 사용한다.

 

 

lib/useActions.js

// lib/useActions.js

/* eslint-disable react-hooks/exhaustive-deps */
import { useMemo } from "react";
import { useDispatch } from "react-redux";
import { bindActionCreators } from "redux";

const useActions = (actions, deps) => {
  const dispatch = useDispatch();
  return useMemo(
    () => {
      if (Array.isArray(actions)) {
        return actions.map((a) => bindActionCreators(a, dispatch));
      }
      return bindActionCreators(actions, dispatch);
    },
    deps ? [dispatch, ...deps] : deps,
  );
};

export default useActions;

 

그러면 Container에서의 dispatch 사용은 한결 간단해진다. 

import Todos from "../components/Todos";
import { useSelector } from "react-redux";
import { changeInput, insert, remove, toggle } from "../modules/todos";
import useActions from "../lib/useActions";

const TodosContainer = () => {
  const { input, todos } = useSelector(({ todos }) => ({
    input: todos.input, //store의 state 중 todos의 input
    todos: todos.todos, //store의 state 중 todos의 todos
  }));


//useActions.js 사용

  const [onChangeInput, onInsert, onToggle, onRemove] = useActions([
    changeInput,
    insert,
    toggle,
    remove,
  ],[]);

  return (
    <div>
      <Todos
        input={input}
        todos={todos}
        onChangeInput={onChangeInput}
        onInsert={onInsert}
        onToggle={onToggle}
        onRemove={onRemove}
      />
    </div>
  );
};

export default TodosContainer;