본문 바로가기
JavaScript/React

React) 리덕스를 사용하여 리액트 애플리케이션 상태 관리하기 ④ - Hooks를 사용하여 컨테이너 컴포넌트 만들기

by 박채니 2022. 12. 16.
안녕하세요, 코린이의 코딩 학습기 채니 입니다.
[리액트를 다루는 기술]의 책을 참고하여 포스팅한 개인 공부 내용입니다.

 

리덕스를 사용하여 리액트 애플리케이션 상태 관리하기

 

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

 

리덕스 스토어와 연동된 컨테이너 컴포넌트 생성 시, connect 대신 react-redux에서 제공하는 Hook을 이용할 수 있습니다.

 

useSelector로 상태 조회하기

 

useSelector Hook은 connect 함수를 사용하지 않고도 리덕스의 상태를 조회할 수 있습니다.

(connect 함수 - 컴포넌트를 리덕스와 연결시켜주는 역할)

 

예시

const 결과 = useSelector(상태 선택 함수);

상태 선택 함수는 mapStateToProps와 형태가 같습니다.

CounterContainer에서 useSelector를 이용해 counter.number 값을 조회함으로써 Counter에게 props로 넘겨주겠습니다.

 

containers/CounterContainer.js

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

const CounterContainer = () => {
    const number = useSelector(state => state.counter.number);
    return (
        <Counter number={number} />
    );
};

export default CounterContainer;

코드가 간결해진 것을 확인할 수 있습니다.

 

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

 

useDispatch Hook컴포넌트 내부에서 스토어의 내장 함수 dispatch를 사용할 수 있게 해줍니다.

 

예시

const dispatch = useDispatch();
dispatch({ type: 'SAMPLE_ACTION' });

 

CounterContainer에서 useDispatch Hook을 이용해 액션을 발생시켜 보겠습니다.

 

containers/CounterContainer.js

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

const CounterContainer = () => {
    const number = useSelector(state => state.counter.number);
    const dispatch = useDispatch();
    return (
        <Counter 
            number={number} 
            onIncrease={() => dispatch(increase())} 
            onDecrease={() => dispatch(decrease())} 
        />
    );
};

export default CounterContainer;

잘 작동하는 것을 보니 액션이 정상적으로 발생되었다는 것을 알 수 있습니다.

하지만 이 경우 숫자가 바뀌어서 리렌더링될 때마다 onIncrease 함수와 onDecrease 함수가 새롭게 생성되고 있습니다.

따라서 useCallback으로 컴포넌트 성능을 최적화 시켜주는 것이 좋습니다.

 

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

const CounterContainer = () => {
    const number = useSelector(state => state.counter.number);
    const dispatch = useDispatch();
    const onIncrease = useCallback(() => dispatch(increase()), [dispatch]);
    const onDecrease = useCallback(() => dispatch(decrease()), [dispatch]);

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

export default CounterContainer;

useDispatch는 useCallback 함수와 같이 사용하는 것을 권장!

 

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

 

useStore Hook컴포넌트 내부에서 리덕스 스토어 객체를 직접 사용할 수 있습니다.

 

예시

const store = useStore();
store.dispatch({ type: 'SAMPLE_ACTION' });
store.getStore();

해당 Hook은 스토어에 직접 접근해야 하는 상황에만 사용하며, 이 경우는 흔치 않다고 합니다.

 

TodosContainer를 Hooks로 전환하기

 

TodoContainer를 useSelector와 useDispatch Hooks를 사용하는 형태로 변경해보겠습니다.

 

containers/TodoContainer.js

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

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;

useDispatch를 이용해 액션을 디스패치할 때 일일이 명시해줘야 하므로 번거로울 수도 있습니다.

 

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

 

useActions는 원래 react-redux에 내장되려고 하였다가 꼭 필요하지 않다 판단되어 제외된 Hook이라고 합니다.

useActions를 사용하면, 여러 개의 액션을 사용해야 하는 경우 코드를 훨씬 깔끔하게 정리하여 작성할 수 있습니다.

 

src/lib/useActions.js

import { bindActionCreators } from "redux";
import { useDispatch } from "react-redux";
import { useMemo } from "react";

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);
        },
        // eslint-disable-next-line react-hooks/exhaustive-deps
        deps ? [dispatch, ...deps] : deps
    );
}

useActions Hook액션 생성 함수를 액션을 디스패치하는 함수로 변환해줍니다.

즉, 액션 생성 함수를 사용해 액션 객체를 만들고, 이를 스토어에 디스패치하는 작업을 해주는 함수를 자동으로 만들어줌!

 

useActions의 첫 번째 파라미터액션 생성 함수로 이루어진 배열

두 번째 파라미터deps 배열입니다. (해당 배열 안에 들어 있는 원소가 바뀌면 액션을 디스패치하는 함수를 재생성)

 

containers/TodoContainer.js

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

const TodoContainer = () => {
    const { input, todos } = useSelector(({ todos }) => ({
        input: todos.input,
        todos: todos.todos
    }));

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

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

export default TodoContainer;

첫 번째 파라미터에 액션 생성 함수로 이루어진 배열을 넣어주었고, 두 번째 파라미터는 빈 배열을 넣어주었습니다.

이를 받아 스토어에 디스패치하는 작업을 해주는 함수를 만들어 반환해주었고, 이를 적용하였습니다.

위에서의 문제점을 말끔하게 해결할 수 있습니다.

 

connect 함수와의 주요 차이점

 

connect 함수를 사용해 컨테이너 컴포넌트를 만들었을 경우

  • 해당 컨테이너 컴포넌트의 부모 컴포넌트가 리렌더링될 때 해당 컨테이너 컴포넌트의 props가 바뀌지 않았다면 리렌더링이 자동으로 방지되어 성능이 최적화됨

 

useSelector를 사용해 리덕스 상태를 조회했을 경우

  • 최적화 작업이 자동으로 이루어지지 않으므로 React.memo를 컨테이너 컴포넌트에 사용해줘야 함

containers/TodosContainer.js

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

...
...

export default React.memo(TodoContainer);

현재 같은 경우는 App 컴포넌트가 리렌더링되는 일이 없으므로 필요하지 않습니다.