react를 다루는 기술

17. 리덕스를 사용하여 리액트 애플리케이션 상태 관리하기(2)

crab. 2022. 6. 21. 08:32

리덕스 더 편하게 사용하기

  • 액션 생성 함수, 리듀서를 작성할 때 redux-actions라는 라이브러리와 이전에 배웠던 immer 라이브러리를 활용하면 리덕스를 훨씬 편하게 사용할 수 있습니다.

redux-actions

  • 액션 생성 함수를 더 짧게 작성할 수 있다.
  • switch/case 문이 아닌 handleActions 함수 사용 가능
  • 업데이트 함수를 설정하는 형식으로 작성

counter 모듈에 적용하기

// createAction을 이용해 액션 생성 함수 설정
export const increase = createAction(INCREASE);
export const decrease = createAction(DECREASE);

// handleActions를 이용한 리듀서 함수 설정
const counter = handleActions(
  {
    [INCREASE]: (state, action) => ({ number: state.number + 1 }),
    [DECREASE]: (state, action) => ({ number: state.number - 1 }),
  },
  initialState,
);
  • 첫 번째 인자로 업데이트 함수, 두 번째 함수로 초기 상태 설정

todos 모듈에 적용하기

  • 먼저 액션 생성 함수를 교체해 줄 텐데, 조금 다른 점이 있습니다.
  • 바로 각 액션 생성 함수에서 파라미터를 필요로 한다는 점입니다.
  • createAction으로 액션을 만들면 액션에 필요한 추가 데이터는 payload라는 이름을 사용합니다.
import { createAction } from 'redux-actions';

const CHANGE_INPUT = 'todos/CHANGE_INPUT'; // 인풋 값을 변경함
const INSERT = 'todos/INSERT'; // 새로운 todo를 등록함
const TOGGLE = 'todos/TOGGLE'; // todo를 체크/체크 해제함
const REMOVE = 'todos/REMOVE'; // todo를 제거함

export const changeInput = createAction(CHANGE_INPUT, input => input);

let id = 3; // insert가 호출될 때마다 1씩 더해집니다.
export const insert = createAction(INSERT, text => ({
  id: id++,
  text,
  done: false,
}));

export const toggle = createAction(TOGGLE, id => id);
export const remove = createAction(REMOVE, id => id);

(...)
  • insert의 경우 todo 객체를 액션 객체 안에 넣어 주어야 하기 때문에 두 번째 파라미터에 text를 넣으면 todo 객체가 반환되는 함수를 넣어 주었습니다.
  • 나머지 함수에는 text => text 혹은 id => id와 같은 형태로 파라미터를 그대로 반환하는 함수를 넣었습니다.

immer

  • yarn add immer
  • 불변성 유지를 위해 깊어지는 spread 연산자 방지
  • Counter 처럼 간단한 경우 immer을 사용하면 더 복잡. Todos모듈에 적용.
  • Todos.js
// immer 사용한 reducer
const todos = handleActions(
  {
      [CHANGE_INPUT]: (state, { payload: input }) =>
      produce(state, (draft) => {
          draft.input = input;
      }),
      [INSERT]: (state, { payload: todo }) =>
      produce(state, (draft) => {
          draft.todos.push(todo);
      }),
      [TOGGLE]: (state, { payload: id }) =>
      produce(state, (draft) => {
          const todo = draft.todos.find((todo) => todo.id === id);
          todo.done = !todo.done;
      }),
      [REMOVE]: (state, { payload: id }) =>
      produce(state, (draft) => {
          const index = draft.todos.findIndex((todo) => todo.id === id);
          draft.todos.splice(index, 1);
      }),
  },
  initialState,
);

Hooks를 사용하여 컨테이너 컴포넌트 만들기

useSelector로 상태 조회하기

  • useSelector Hook을 사용하면 connect 함수를 사용하지 않고도 리덕스의 상태를 조회할 수 있습니다.
const 결과 = useSelector(상태 선택 함수);
  • 여기서 상태 선택 함수는 mapStateToProps와 형태가 똑같습니다.

useDispatch를 사용하여 액션 디스패치하기

  • 이 Hook은 컴포넌트 내부에서 스토어의 내장 함수 dispatch를 사용할 수 있게 해 줍니다.
  • 컨테이너 컴포넌트에서 액션을 디스패치해야 한다면 이 Hook을 사용하면 됩니다.
const dispatch = useDispatch();
dispatch({ type: ‘SAMPLE_ACTION‘ });

useStore를 사용하여 리덕스 스토어 사용하기

  • useStore Hooks를 사용하면 컴포넌트 내부에서 리덕스 스토어 객체를 직접 사용할 수 있습니다.
  • 사용법은 다음과 같습니다.
const store = useStore();
store.dispatch({ type: 'SAMPLE_ACTION '});
store.getState();
  • useStore는 컴포넌트에서 정말 어쩌다가 스토어에 직접 접근해야 하는 상황에만 사용해야 합니다.
  • 이를 사용해야 하는 상황은 흔치 않을 것입니다.

TodosContainer를 Hooks로 전환하기

  • 이제 TodosContainer를 connect 함수 대신에 useSelector와 useDispatch Hooks를 사용하는 형태로 전환해 봅시다.
const TodoContainer = () => {
const { input, todos } = useSelector(({ todos }) => ({
    input: todos.input,
    todos: 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 (
        <Todos
            input={input}
            todos={todos}
            onChangeInput={onChangeInput}
            onInsert={onInsert}
            onToggle={onToggle}
            onRemove={onRemove}
        />
    );
};

export default TodoContainer;
  • useSelector를 사용할 때 비구조화 할당 문법을 활용했습니다.
  • 또한, useDispatch를 사용할 때 각 액션을 디스패치하는 함수를 만들었는데요.

useActions 유틸 Hook을 만들어서 사용하기

export default function 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
  );
}
  • 방금 작성한 useActions Hook은 액션 생성 함수를 액션을 디스패치하는 함수로 변환해 줍니다.
  • 액션 생성 함수를 사용하여 액션 객체를 만들고, 이를 스토어에 디스패치하는 작업을 해 주는 함수를 자동으로 만들어 주는 것이죠.
  • useActions는 두 가지 파라미터가 필요합니다. 첫 번째 파라미터는 액션 생성 함수로 이루어진 배열입니다.
  • 두 번째 파라미터는 deps 배열이며, 이 배열 안에 들어 있는 원소가 바뀌면 액션을 디스패치하는 함수를 새로 만들게 됩니다.

connect 함수와의 차이점

  • connect : props가 바뀌지 않았다면 리렌더링이 자동으로 방지되어 성능이 최적화
  • useSelector : 리덕스 상태를 조회했을 때 최적화 작업이 자동으로 이루어지지 않는다.
    • React.memo를 컨테이너 컴포넌트에 사용해 준다.
    • TodosContainer.js
    • export default React.memo(TodosContainer);