ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • React :: 17. 리덕스를 사용하여 리액트 애플리케이션 상태 관리하기
    Dev/프론트엔드 2021. 8. 3. 14:33
    이 글은 김민준(벨로퍼트)님의 - 리액트를 다루는 기술 도서를 참고하여 학습하는 글 입니다.

    이번 장에서는 리덕스를 사용하여 리액트 애플리케이션 상태를 관리하는 방법을 알아보겠습니다.

    소규모 프로젝트에서는 컴포넌트가 가진 state를 사용하는 것만으로도 충분하지만, 프로젝트의 규모가 커짐에 따라 상태관리가 번거로워질 수 있습니다.

    리액트 애플리케이션에서 리덕스를 사용하면, 상태 업데이트에 관한 로직을 모듈로 따로 분리하여 컴포넌트 파일과 별개로 관리할 수 있으므로 코드를 유지 보수하는데 도움이 됩니다. 또한, 여러 컴포넌트에서 동일한 상태를 공유해야 할 때 매우 유용하며, 실제 업데이트가 필요한 컴포넌트만 리렌더링 되도록 쉽게 최적화해 줄 수도 있습니다.

    앞에서 바닐라 자바스크립트 환경에서 리덕스를 사용할 때 스토어의 내장 함수인 store.dispatch와 store.subscribe 함수를 사용했지요? 리액트 애플리케이션에서 리덕스를 사용할 때는 store인스턴스를 직접 사용하기 보다는 주로 react-redux라는 라이브러리에서 제공하는 유틸 함수(connect)와 컴포넌트(Provider)를 사용하여 리덕스 관련 작업을 처리합니다.

     

    이번 실습은 다음과 같은 흐름으로 진행됩니다.

    1. 프로젝트 준비
    2. 프레젠테이셔널 컴포넌트 작성 
    3. 리덕스 관련 코드 작성 
    4. 컨테이너 컴포넌트 작성
    5. 더 편하게 사용하는 방법 알아보기
    6. connect대신 Hooks 사용하기 

    17.1 작업 환경 설정

    리액트 프로젝트를 생성하고, 해당 프로젝트에 리덕스를 적용해 봅시다.

    먼저 create-react-app를 사용하여 새로운 리액트 프로젝트를 생성하세요.

    $ yarn create react-app react-redux-tutorial

    생성한 프로젝트 디렉터리에 yarn 명령어를 사용하여 리덕스와 react-redux 라이브러리를 설치하세요.

    $ cd react-redux-tutorial
    $ tarn add redux react-redux

    Prettier를 적용하고 싶다면 디렉터리에 다음과 같이 .prettierrc 파일을 작성하세요.

    {
        "singleQuote": true,
        "semi": true,
        "useTabs": false,
        "tabWidth": 2,
        "trailingComma": "all",
        "printWidth": 80
    }

    17.2 UI 준비하기

    리액트 프로젝트에서 리덕스를 사용할 때 가장 많이 사용하는 패턴은 프레젠테이셔널 컴포넌트와 컨테이너 컴포넌트를 분리하는 것입니다. 여기서 프레젠테이셔널 컴포넌트란 주로 상태 관리가 이루어지지 않고, 그저 props를 받아 와서 화면에 UI를 보여주기만 하는 컴포넌트를 말합니다. 이와 달리 컨테이너 컴포넌트는 리덕스와 연동되어 있는 컴포넌트로, 리덕스로부터 상태를 받아오기도 하고 리덕스 스토어에 액션을 디스패치하기도 합니다.

    이러한 패턴은 리덕스를 사용하는 데 필수 사항은 아닙니다. 다만 이 패턴을 사용하면 코드의 재사용성도 높아지고, 관심사의 분리가 이루어져 UI를 작성할 대 좀 더 집중할 수 있습니다.

    프레젠테이셔널 컴포넌트와 컨테이너 컴포넌트

    이 장의 프로젝트에서는 이 패턴을 사용하여 코드를 작성해 보겠습니다. UI에 관련된 프레젠테이셔널 컴포넌트는 src/components 경로에 저장하고, 리덕스와 연동된 컨테이너 컴포넌트는 src/containers 컴포넌트에 작성합니다.

    17.2.1 카운터 컴포넌트 만들기

    숫자를 더하고 뺄 수 잇는 카운터 컴포넌트를 만들어 봅시다. components 디렉터리를 생성한 뒤, 그 안에 Counter 컴포넌트를 작성하세요.

    components/Counter.js
    import React from 'react';
    
    const Counter = ({ number, onIncrease, onDecrease }) => {
      return (
        <div>
          <h1>{number}</h1>
          <div>
            <button onClick={onIncrease}>+1</button>
            <button onClick={onDecrease}>-1</button>
          </div>
        </div>
      );
    };
    
    export default Counter;

    이제 이 컴포넌트를 App 컴포넌트에서 렌더링합니다.

    App.js
    import React from 'react';
    import Counter from './components/Counter';
    
    const App = () => {
      return (
        <div>
            <Counter number={0}/>
        </div>
      );
    };
    
    export default App;

    yarn start 명령어를 입력해서 개발 서버를 실행해 보세요. 다음과 같이 카운터 컴포넌트가 나타났나요?

    카운터 UI

    17.2.2 할 일 목록 컴포넌트 만들기

    이번에는 해야 할 일을 추가하고, 체크하고, 삭제할 수 있는 할 일 목록 컴포넌트를 만들어 보겠습니다. components 디렉터리에 Todos 컴포넌트를 다음과 같이 작성하세요.

    Todos.js
    import React from 'react';
    
    const TodoItem = ({ todo, onToggle, onRemove }) => {
      return (
        <div>
          <input type="checkbox" />
          <span>예제 테스트</span>
          <button>삭제</button>
        </div>
      );
    };
    
    const Todos = ({
      input, // 인풋에 입력되는 텍스트
      todos, // 할일 목록이 들어 있는 객체
      onChangeInput,
      onInsert,
      onToggle,
      onRemove,
    }) => {
      const onSubmit = (e) => {
        e.preventDefault();
      };
      return (
        <div>
          <form onSubmit={onsubmit}>
            <input />
            <button type="submit">등록</button>
          </form>
          <div>
            <TodoItem />
            <TodoItem />
            <TodoItem />
            <TodoItem />
            <TodoItem />
          </div>
        </div>
      );
    };
    export default Todos;

    파일 하나에 두 컴포넌트를 선언했습니다. 취향에 따라 Todos 컴포넌트와 TodoItem 컴포넌트를 파일 두 개로 분리해도 되고, 위 코드처럼 파일 하나에 작성해도 무방합니다.

    위 컴포넌트들이 받아오는 props는 나중에 사용하겠습니다.

     

    컴포넌트를 다 만들었다면 App 컴포넌트에서 카운터 아래에 렌더링해 주세요. hr 태그를 사용하여 사이에 구분선을 그려 주겠습니다.

    App.js
    import React from 'react';
    import Counter from './components/Counter';
    import Todos from './components/Todos'
    
    const App = () => {
      return (
        <div>
            <Counter number={0}/>
            <Todos/>
        </div>
      );
    };
    
    export default App;

    할 일 목록 UI

    17.3 리덕스 관련 코드 작성하기

    이제 프로젝트에 리덕스를 사용해 보겠습니다. 리덕스 관련 코드를 준비합니다. 리덕스를 사용할 때는 액션 타입, 액션 생성 함수, 리듀서 코드를 작성해야 하는데요. 이 코드들을 각각 다른 파일에 작성하는 방법도 있고, 기능별로 묶어서 파일 하나에 작성하는 방법도 있습니다.

     

    17-5 일반적인 구조

    그림 17-5는 가장 일반적인 구조로 actions, constants, reducers라는 세 개의 디렉터리를 만들고 그 안에 기능별로 파일을 하나씩 만드는 방식입니다. 코드를 종류에 따라 다른 파일에 작성하여 정리할 수 있어서 편리하지만, 새로운 액션을 만들 때마다 세 종류의 파일 모두 수정해야 하기 때문에 불편하기도 합니다. 이 방식은 리덕스 공식 문서에서도 사용되므로 가장 기본적이라 할 수 있지만, 사람에 따라서는 불편할 수도 있는 구조입니다.

    17-6 Ducks 패턴

    그림 17-6은 액션타입, 액션 생성 함수, 리듀서 함수를 기능별로 파일 하나에 몰아서 다 작성하는 방식입니다. 이러한 방식을 Ducks 패턴이라고 부러며, 앞서 설명한 일반적인 구조로 리덕스를 사용하다가 불편함을 느낀 개발자들이 자주 사용합니다.

    리덕스 관련 코드에 대한 디렉터리 구조는 정해진 방법이 없기 때문에 마음대로 작성해도 되지만, 위 두 가지 방법이 주로 사용됩니다. 이 책에서는 두 번째로 소개한 방식인 Ducks 패턴을 사용하여 코드를 작성하겠습니다.

    17.3.1 counter 모듈 작성하기

    Ducks 패턴을 사용하여 액션 타입, 액션 생성 함수, 리듀서를 작성한 코드를 '모듈'이라고 합니다. 먼저 counter 모듈을 작성해 봅시다.

     

    17.3.1.1 액션 타입 정의하기

    modules 디렉터리를 생성하고 그 안에 counter.js 파일을 다음과 같이 작성하세요.

    modules/counter.js
    const INCREASE = 'counter/INCREASE';
    const DECREASE = 'counter/DECREASE';

    가장 먼저 해야 할 작업은 액션 타입을 정의하는 것입니다. 액션 타입은 대문자로 정의하고, 문자열 내용은 '모듈 이름/액션 이름'과 같은 형태로 작성합니다.문자열 안에 모듈 이름을 넣음으로써, 나중에 프로젝트가 커졌을 때 액션의 이름이 충돌되지 않게 해 줍니다. 예를 들어 SHOW 혹은 INITIALIZE라는 이름을 가진 액션은 쉽게 중복될 수 있겠죠? 하지만 앞에 모듈 이름을 붙여 주면 액션 이름이 겹치는 것을 걱정하지 않아도 됩니다.

     

    17.3.1.2 액션 생성 함수 만들기

    액션 타입을 정의한 다음에는 액션 생성 함수를 만들어 주어야 합니다.

    modules/counter.js
    const INCREASE = 'counter/INCREASE';
    const DECREASE = 'counter/DECREASE';
    
    export const increase = () => ({ type: INCREASE });
    export const decrease = () => ({ type: DECREASE });

    더 필요하거나 추가할 값이 없으니 그냥 위와 같이 만들어 주면 됩니다. 꽤 간단하지요? 여기서 주의해야 할 점은 앞부분에 export라는 키워드가 들어간다는 것입니다. 이렇게 함으로써 추후 이 함수를 다른 파일에서 불러와 사용할 수 있습니다.

     

    17.3.1.3 초기 상태 및 리듀서 함수 만들기

    이제 counter 모듈의 초기 상태와 리듀서 함수를 만들어 줍시다.

    modules/counter.js
    const INCREASE = 'counter/INCREASE';
    const DECREASE = 'counter/DECREASE';
    
    export const increase = () => ({ type: INCREASE });
    export const decrease = () => ({ type: DECREASE });
    
    const initialState = {
      number: 0,
    };
    
    function counter(state = initialState, action) {
      switch (action.type) {
        case INCREASE:
          return {
            number: state.number + 1,
          };
        case DECREASE:
          return {
            number: state.number - 1,
          };
        default:
          return state;
      }
    }
    
    export default counter

    이 모듈의 초기 상태에는 number 값을 설정해 주었으며, 리듀서 함수에는 현재 상태를 참조하여 새로운 객체를 생성해서 반환하는 코드를 작성해 주었스니다. 마지막으로 export default 키워드를 사용하여 함수를 내보내 주었습니다.

    조금 전에 만들 액션 생성 함수는 export로 내보내 주었고, 이번에 만든 리듀서 함수는 export default로 내보내 주었죠? 두 방식의 차이점은 export는 여러 개를 내보낼 수 있지만 export default는 단 한 개만 내보낼 수 있다는 것입니다.

    불러오는 방식도 다릅니다. 다음과 같이 말이죠

    import counter from './counter';
    import { increase, decrease ] from './counter';
    // 한꺼번에 불러오고 싶을 때
    import counter, {increase, decrease} from './counter';

    17.3.2 todos 모듈 만들기

    이번에 만들 모듈은 좀 더 복잡합니다. modules 디렉터리에 todos.js파일을 생성하세요.

     

    17.3.2.1 액션 타입 정의하기

    이전과 마찬가지로 가장 먼저 해야 할 일은 액션 타입 정의입니다.

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

    17.3.2.2 액션 생성 함수 만들기

    다음으로 액션 생성 함수를 만듭니다. 조금 전과 달리 이번에는 액션 생성 함수에서 파라미터가 필요합니다. 전달받은 파라미터는 액션 객체 안에 추가 필드로 들어가게 됩니다.

    modules/todos.js
    const CHANGE_INPUT = 'todos/CHANGE_INPUT'; // 인풋 값을 변경함
    const INSERT = 'todos/INSERT'; // 새로운 todo를 등록함
    const TOGGLE = 'todos/TOGGLE'; // todo를 체크/체크 해제함
    const REMOVE = 'todos/REMOVE'; // todo를 제거함
    
    export const changeInput = (input) => ({
      type: CHANGE_INPUT,
      input,
    });
    
    let id = 3; // insert가 호출될 때마다 1씩 더해집니다.
    export const insert = (text) => ({
      type: INSERT,
      todo: {
        id: id++,
        text,
        done: false,
      },
    });
    
    export const toggle = (id) => ({
      type: TOGGLE,
      id,
    });
    
    export const remove = (id) => ({
      type: REMOVE,
      id,
    });

    위 액션 생성 함수 중에서 insert 함수는 액션 객체를 만들 때 파라미터 외에 사전에 이미 선언되어 있는 id라는 값에도 의존합니다. 이 액션 생성 함수는 호출될 때마다 id 값에 1씩 더해 줍니다.

    이 id 값은 각 todo 객체가 들고 있게 될 고윳값이죠.

    여기서 id 값이 3인 이유는 다음 절에서 초기 상태를 작성할 때 todo 객체 두 개를 사전에 미리 넣어 둘 것이므로 그 다음에 새로 추가될 항목의 id가 3이기 때문입니다.

     

    17.3.2.3 초기 상태 및 리듀서 함수 만들기

    이제 모듈의 초기 상태와 리듀서 함수를 작성합시다. 이번에는 업데이트 방식이 조금 까다로워 집니다. 객체에 한 개 이상의 값이 들어가므로 불변성을 유지해 주어야 하기 때문이죠. spread 연산자(...)를 잘 활용하여 작성해 보세요. 배열에 변화를 줄 때는 배열 내장 함수를 사용하여 구현하면 됩니다.

    (...)
    
    const initialState = {
      input: '',
      todos: [
        {
          id: 1,
          text: '리덕스 기초 배우기',
          done: true,
        },
        {
          id: 2,
          text: '리액트와 리덕스 사용하기',
          done: false,
        },
      ],
    };
    
    function 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;

    17.3.3 루트 리듀서 만들기

    이번 프로젝트에서는 리듀서를 여러개 만들었지요? 나중에 createStore 함수를 사용하여 스토어를 만들 때는 리듀서를 하나만 사용해야 합니다. 그렇기 때문에 기존에 만들었던 리듀서를 하나로 합쳐 주어야 하는데요. 이 작업은 리덕스에서 제공하는 combineReducers라는 유틸 함수를 사용하면 쉽게 처리할 수있습니다.

    modules 디렉터리에 index.js 파일을 만들고, 그 안에 다음과 같은 코드를 작성하세요.

    modules/index.js
    import { combineReducers } from 'redux';
    import counter from './counter';
    import todos from './todos';
    
    const rootReducer = combineReducers({
      counter,
      todos,
    });
    
    export default rootReducer;

    파일 이름을 이렇게 index.js로 설정해 주면 나중에 불러올 때 디렉터리 이름까지만 입력하여 붙러올수 있습니다.

    다음과 같이 말이죠.

    import rootReducer from './modules';

    17.4 리액트 애플리케이션에 리덕스 적용하기

    이제 드디어 리액트 애플리케이션에 리덕스를 적용할 차례입니다. 스토어를 만들고 리액트 애플리케이션에 리덕스를 적용하는 작업은 src 디렉터리의 index.js에서 이루어 집니다.

     

    가장 먼저 스토어를 생성합니다.

    index.js
    import React from 'react';
    import ReactDOM from 'react-dom';
    import { createStore } from 'redux';
    import './index.css';
    import App from './App';
    import rootReducer from './modules';
    
    const store = createStore(rootReducer);
    
    ReactDOM.render(
      <React.StrictMode>
        <App />
      </React.StrictMode>,
      document.getElementById('root'),
    );

    17.4.2 Provider 컴포넌트를 사용하여 프로젝트에 리덕스 적용하기

    리액트 컴포넌트에서 스토어를 사용할 수 있도록 App 컴포넌트를 react-redux에서 제공하는 Provider 컴포넌트로 감싸 줍니다. 이 컴포넌트를 사용할 때는 store를 props로 전달해 주어야 합니다.

    src/index.js
    import React from 'react';
    import ReactDOM from 'react-dom';
    import { createStore } from 'redux';
    import { Provider } from 'react-redux';
    import './index.css';
    import App from './App';
    import rootReducer from './modules';
    
    const store = createStore(rootReducer);
    
    ReactDOM.render(
      <Provider store={store}>
        <App />
      </Provider>,
      document.getElementById('root'),
    );

    17.4.3 Redux DevTools의 설치 및 적용

    Redux DevTools는 리덕스 개발자 도구이며, 크롬 확장 프로그램으로 설치하여 사용할 수 있습니다. 크롬 웹 스토어(https://chrome.google.com/webstore/)에서 Redux DevTools를 검색하여 설치해 주세요

    17-7 Redux DevTools 설치

    설치하고 나면 리덕스 스토어를 만드는 과정에서 다음과 같이 적용해 줄 수 있습니다.

    사용 예시
    const store = createStore(
    	rootReducer, /* preloadedState, */
        window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
    );

    하지만 패키지를 설치하여 적용하면 코드가 훨씬 깔끔해집니다. 우리는 패키지를 설치하는 형태로 적용해 보겠습니다.(패키지를 설치하여 사용한다고 해도 크롬 확장 프로그램은 설치해야 합니다).

    우선 redux-devtools-extension을 yarn을 사용하여 설치해 주세요.

    $ yarn add redux-devtools-extension

    그리고 다음과 같이 적용해 주면 됩니다.

    index.js
    import React from 'react';
    import ReactDOM from 'react-dom';
    import { createStore } from 'redux';
    import { Provider } from 'react-redux';
    import { composeWithDevTools } from 'redux-devtools-extension';
    import './index.css';
    import App from './App';
    import rootReducer from './modules';
    
    
    const store = createStore(rootReducer, composeWithDevTools());
    
    ReactDOM.render(
      <Provider store={store}>
        <App />
      </Provider>,
      document.getElementById('root'),
    );

    이제 브라우저에서 크롬 개발자 도구를 실행한 후 Redux 탭을 열어보세요. 리덕스 개발자 도구가 잘 나타났나요?

    Redux Devtools

    리덕스 개발자 도구 안의 State 버튼을 눌러 현재 리덕스 스토어 내부의 상태가 잘 보이는지 확인 해 보세요.

    17.5 컨테이너 컴포넌트 만들기

    이제는 컴포넌트에서 리덕스 스토어에 접근하여 원하는 상태를 받아오고, 또 액션도 디스패치해 줄 차례입니다. 리덕스 스토어와 연동된 컴포넌트를 컨테이너 컴포넌트라고 부릅니다.

    17.5.1 CounterContainer 만들기

    src 디렉터리에 containers 디렉터리를 생성하고, 그 안에 CounterContainer 컴포넌트를 만드세요.

    containers/CounterContainer.js
    import React from 'react';
    import Counter from '../components/Counter';
    
    const CounterContainer = () => {
      return <Counter />;
    };
    
    export default CounterContainer;

    위 컴포넌트를 리덕스와 연동하려면 react-redux에서 제공하는 connect 함수를 사용해야 합니다. 이 함수는 다음과 같이 사용합니다.

    connect(mapStateToProps, mapDispatchToProps)(연동할 컴포넌트)

    여기서 mapStateToProps는 리덕스 스토어 안의 상태를 컴포넌트의 props로 넘겨주기 위해 설정하는 함수이고, mapDispatchToProps는 액션 생성 함수를 컴포넌트의 props로 넘겨주기 위해 사용하는 함수입니다.

    이렇게 connect 함수를 호출하고 나면 또 다른 함수를 반환합니다. 반환된 함수에 컴포넌트를 파라미터로 넣어 주면 리덕스와 연동된 컴포넌트가 만들어 집니다.

    위 코드를 더 쉽게 풀면 다음과 같은 형태입니다.

    const makeContainer = connect(mapStateToProps, mapDispatchToProps)
    makeContainer(타깃 컴포넌트)

    자, 이제 CounterContainer 컴포넌트에서 connect를 사용해 볼까요?

    containers/CounterContainer.js
    import React from 'react';
    import Counter from '../components/Counter';
    import { connect } from 'react-redux';
    
    const CounterContainer = ({ number, increase, decrease }) => {
      return <Counter number={number} increase={increase} decrease={decrease} />;
    };
    
    const mapStateToProps = (status) => ({
      number: status.counter.number,
    });
    
    const mapDispatchToProps = (dispatch) => ({
      // 임시 함수
      increase:()=>{
          console.log('increase');
      },
      decrease:()=>{
          console.log('decrease');
      },
    });
    export default connect(
        mapStateToProps,
        mapDispatchToProps,
    )(CounterContainer)

    mapStateToProps와 mapDispatchProps에서 반환하는 객체-내부의 값들은 컴포넌트의 props로 전달됩니다. mapStateToProps는 state를 파라미터로 받아오며, 이 값은 현재 스토어가 지니고 있는 상태를 가리킵니다. mapDispatchToProps의 경우 store의 내장 함수 dispatch를 파라미터로 받아 옵니다. 현재 mapDispatchToProps에서는 진행 절차를 설명하기 위해 임시로 console.log를 사용하고 있습니다.

    다음으로 App에서 Counter를 CounterContainer로 교체하세요.

    App.js
    import React from 'react';
    import CounterContainer from './containers/CounterContainer';
    import Todos from './components/Todos'
    
    const App = () => {
      return (
        <div>
            <CounterContainer/>
            <Todos/>
        </div>
      );
    };
    
    export default App;

    브라우저를 열어서 +1, -1 버튼을 눌러보세요. 콘솔에 increase와 decrease가 찍히나요?

    connect를 통해 함수 전달

    자, 이번에는 console.log 대신 액션 생성 함수를 불러와서 액션 객체를 만들고 디스패치해 주겠습니다.

    containers/CounterContainer.js
    import React from 'react';
    import Counter from '../components/Counter';
    import { connect } from 'react-redux';
    import { increase, decrease } from '../modules/counter';
    
    const CounterContainer = ({ number, increase, decrease }) => {
      return (
        <Counter number={number} onIncrease={increase} onDecrease={decrease} />
      );
    };
    
    const mapStateToProps = (status) => ({
      number: status.counter.number,
    });
    
    const mapDispatchToProps = (dispatch) => ({
      increase: () => {
        dispatch(increase());
      },
      decrease: () => {
        dispatch(decrease());
      },
    });
    export default connect(mapStateToProps, mapDispatchToProps)(CounterContainer);

    이제 다시 +1, -1 버튼을 눌러 보세요. 숫자가 바뀌나요? 리덕스 개발자 도구도 한번 확인해 보세요.

    CounterContainer 연동 완료

    connect 함수를 사용할 때는 일반적으로 위 코드와 같이 mapStateToProps와 mapDispatchToProps를 미리 선언해 놓고 사용합니다. 하지만 connect 함수 내부에 익명 함수 형태로 선언해도 문제가 되지 않습니다. 어떻게 보면 코드가 더 깔끔해지기도 하는데요. 취향에 따라 다음과 같이 작성해도 됩니다.

    containers/CounterContainer.js
    import React from 'react';
    import Counter from '../components/Counter';
    import { connect } from 'react-redux';
    import { increase, decrease } from '../modules/counter';
    
    const CounterContainer = ({ number, increase, decrease }) => {
      return (
        <Counter number={number} onIncrease={increase} onDecrease={decrease} />
      );
    };
    
    export default connect(
      (status) => ({
        number: status.counter.number,
      }),
      (dispatch) => ({
        increase: () => dispatch(increase()),
        decrease: () => dispatch(decrease()),
      }),
    )(CounterContainer);

    위 코드에서는 액션 생성 함수를 호출하여 디스패치 하는 코드가 한줄이기 때문에 불피요한 코드 블록을 생략해 주었습니다. 다음 두 줄의 코드는 작동 방식이 완전히 같습니다.

    increase : () => dispatch(increase()),
    increase : { return dispatch(increase()) },

    컴포넌트에서 액션을 디스패치하기 위해 각 액션 생성 함수를 호출하고 dispatch로 감싸는 작업이 조금 번거로울 수도 있습니다. 특히 액션 생성 함수의 개수가 많아진다면 더더욱 그럴 것입니다.

    이와 같은 경우에는 리덕스에서 제공하는 bindActionCreators 유틸 함수를 사용하면 간편합니다. 한번 사용해 볼까요?

    containers/CounterContainer.js
    import React from 'react';
    import Counter from '../components/Counter';
    import { connect } from 'react-redux';
    import { bindActionCreators } from 'redux';
    import { increase, decrease } from '../modules/counter';
    
    const CounterContainer = ({ number, increase, decrease }) => {
      return (
        <Counter number={number} onIncrease={increase} onDecrease={decrease} />
      );
    };
    
    export default connect(
      (status) => ({
        number: status.counter.number,
      }),
      (dispatch) =>
        bindActionCreators(
          {
            increase,
            decrease,
          },
          dispatch,
        ),
    )(CounterContainer);

    브라우저를 열어서 조금 전과 똑같이 작동하는지 확인해 보세요.

    방금 작성한 방법보다 한 가지 더 편한 방법이 있습니다. 바로 mapDispatchToProps에 해당하는 파라미터를 함수 형태가 아닌 액션 생성 함수로 이루어진 객체 형태로 넣어 주는 것입니다. 다음과 같이 말이죠.

    containers/CounterContainer.js
    import React from 'react';
    import Counter from '../components/Counter';
    import { connect } from 'react-redux';
    import { bindActionCreators } from 'redux';
    import { increase, decrease } from '../modules/counter';
    
    const CounterContainer = ({ number, increase, decrease }) => {
      return (
        <Counter number={number} onIncrease={increase} onDecrease={decrease} />
      );
    };
    
    export default connect(
      (status) => ({
        number: status.counter.number,
      }),
      {
          increase,
          decrease
      }
    )(CounterContainer);

    위와 같이 두 번째 파라미터를 아예 객체 형태로 넣어 주면 connect 함수가 내부적으로 bindActionCreators 작업을 대신해 줍니다.

    17.5.2 TodosContainer 만들기

    이번에는 Todos 컴포넌트를 위한 컨테이너인 TodosContainer를 작성해 보겠습니다.

    CounterContainer를 만들 때 배웠던 connect 함수를 사용하고, mapDispatchToProps를 짧고 간단하게 쓰는 방법을 적용해서 코드를 작성해 보세요.

    containers/TodosContainer.js
    import React from 'react';
    import { connect } from 'react-redux';
    import { changeInput, insert, toggle, remove } from '../modules/todos';
    import Todos from '../components/Todos';
    
    const TodosContainer = ({
      input,
      todos,
      changeInput,
      insert,
      toggle,
      remove,
    }) => {
      return (
        <Todos
          input={input}
          todos={todos}
          onChangeInput={changeInput}
          onInsert={insert}
          onToggle={toggle}
          onRemove={remove}
        />
      );
    };
    
    export default connect(
      // 비구조화 할당을 통해 todos를 분리하여
      // state.todos.input 대신 todos.input 사용
      ({ todos }) => ({
        input: todos.input,
        todos: todos.todos,
      }),
      {
        changeInput,
        insert,
        toggle,
        remove,
      },
    )(TodosContainer);

    이전에 todos 모듈에서 작성했던 액션 생성 함수와 상태 안에 있던 값을 컴포넌트의 props로 전달해 주었습니다.

    컨테이너 컴포넌트를 다 만든 후에는 App 컴포넌트에서 보여 주던 Todos 컴포넌트를 TodosContainer 컴포넌트로 교체하세요.

    App.js
    import React from 'react';
    import CounterContainer from './containers/CounterContainer';
    import TodosContainer from './containers/TodosContainer';
    
    const App = () => {
      return (
        <div>
            <CounterContainer/>
            <hr/>
            <TodosContainer/>
        </div>
      );
    };
    
    export default App;

    그 다음에는 Todos 컴포넌트에서 받아 온 props를 사용하도록 구현해 보세요.

    components/Todos.js
    import React from 'react';
    
    const TodoItem = ({ todo, onToggle, onRemove }) => {
      return (
        <div>
          <input
            type="checkbox"
            onClick={() => onToggle(todo.id)}
            checked={todo.done}
            readOnly={true}
          />
          <span style={{ textDecoration: todo.done ? 'line-through' : 'none' }}>
            {todo.text}
          </span>
          <button onClick={() => onRemove(todo.id)}>삭제</button>
        </div>
      );
    };
    
    const Todos = ({
      input, // 인풋에 입력되는 텍스트
      todos, // 할일 목록이 들어 있는 객체
      onChangeInput,
      onInsert,
      onToggle,
      onRemove,
    }) => {
      const onSubmit = (e) => {
        e.preventDefault();
        onInsert(input);
        onChangeInput(''); // 등록 후 인풋 초기화
      };
      const onChange = (e) => onChangeInput(e.target.value);
      return (
        <div>
          <form onSubmit={onSubmit}>
            <input value={input} onChange={onChange} />
            <button type="submit">등록</button>
          </form>
          <div>
            {todos.map((todo) => (
              <TodoItem
                todo={todo}
                key={todo.id}
                onToggle={onToggle}
                onRemove={onRemove}
              />
            ))}
          </div>
        </div>
      );
    };
    export default Todos;

    이제 모든 작업이 끝났습니다. 브라우저에서 할 일 목록 기능이 잘 작동하는지 확인해 보세요. 일정을 새로 추가해 보고, 체크 박스도 눌러 보고, 삭제도 해보세요.

    모든 기능 구현 완료 

    모든 기능이 잘 작동하나요?... (기능은 잘 작동하지만 이해를 못함...ㅠㅠ)

    17.6 리덕스 더 편하게 사용하기

    이번에는 리덕스를 좀 더 편하게 사용하는 방법을 알아보겠스니다. 액션 생성 함수, 리듀서를 작성할 때 redux-actions라는 라이브러리와 이전에 배웟던 immer 라이브러리를 활용하면 리덕스를 훨씬 편하게 사용할 수 있습니다.

    17.6.1 redux-actions

    redux-actions를 사용하면 액션 생성 함수를 더 짧은 코드로 작성할 수 있습니다. 그리고 리듀서를 작성할 때도 switch/case 문이 아닌 handleActions라는 함수를 사용하여 각 액션마다 업데이트 함수를 설정하는 형식으로 작성해 줄 수 있습니다.

    우선 라이브러리를 설치해 주세요.

    $ yarn add redux-actions

    17.6.1.1 counter 모듈에 적용하기

    counter 모듈에 작성된 액션 생성 함수를 createAction이란 함수를 사용하여 만들어 주겠습니다.

    modules/counter.js
    import { createAction } from 'redux-actions';
    
    const INCREASE = 'counter/INCREASE';
    const DECREASE = 'counter/DECREASE';
    
    export const increase = createAction(INCREASE);
    export const decrease = createAction(DECREASE);
    
    (...)

    createAction을 사용하면 매번 객체를 직접 만들어 줄 필요 없이 더욱 간단하게 액션 생성 함수를 선언할 수 있습니다.

    이번에는 리듀서 함수도 더 간단하고 가독성 높게 작성해 보겠습니다. handleActions라는 함수를 사용합니다.

    modules/counter.js
    import { createAction, handleActions } from 'redux-actions';
    
    const INCREASE = 'counter/INCREASE';
    const DECREASE = 'counter/DECREASE';
    
    export const increase = createAction(INCREASE);
    export const decrease = createAction(DECREASE);
    
    const initialState = {
      number: 0,
    };
    
    const counter = handleActions({
    [INCREASE] : (state, action) => ({number: state.number +1}),
    [DECREASE] : (state, action) => ({number: state.number -1}),
    },initialState)
    
    export default counter;

    handleActions 함수의 첫 번째 파라미터는 각 액션에 대한 업데이트 함수를 넣어 주고, 두 번째 파라미터에는 초기 상태를 넣어 줍니다.

    어떤가요? 코드가 훨씬 짧아지고 가독성이 높아졌죠?

     

    17.6.1.2 todos 모듈에 적용하기

    똑같은 작업을 todos 모듈에도 적용해 봅시다. 먼저 액션 생성 함수를 교체해 줄 텐데, 조금 다른 점이 있습니다. 바로 각 액션 생성 함수에서 파라미터를 필요로 한다는 점입니다.

    createAction으로 액션을 만들면 액션에 필요한 추가 데이터는 payload라는 이름을 사용합니다.

    예를 들면 다음과 같습니다.

    const MY_ACTION = 'sample/MY_ACTION';
    const myAction = createAction(MY_ACTION);
    const action = myAction('hello world');
    /*
    	결과:
    	{ type: MY_ACTION, payload: 'hello world' }
    */

    액션 생성 함수에서 받아 온 파라미터를 그대로 payload에 넣는 것이 아니라 변형을 주어서 넣고 싶다면, createAction의 두 번째 함수에 payload를 정의하는 함수를 따로 선언해서 넣어 주면 됩니다.

    const MY_ACTION = 'sample/MY_ACTION';
    const myAction = createAction(MY_ACTION, text => `${text}!`);
    const action = myAction('hello world');
    /*
        결과:
        { type: MY_ACTION, payload: 'hello world' }
    */

    자, 그럼 이제 todos 모듈의 액션 생섬 함수를 다음과 같이 새로 작성해 주세요.

    modules/todos.js
    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와 같은 형태로 파라미터를 그대로 반환하는 함수를 넣었습니다. 이 작업이 필수는 아닙니다. 생략해도 똑같이 동작하지만, 여기서 이

     

    수를 넣어 줌으로써 코드를 보았을 때 이 액션 생성 함수의 파라미터로 어떤 값이 필요한지 쉽게 파악할 수 있습니다.

    액션 생성 함수를 다 작성했으면 handleActions로 리듀서를 재작성 해보겠습니다. createAction으로 만든 액션 생성 함수는 파라미터로 받아 온 값을 객체 안에 넣을 때 원하는 이름으로 넣는 것이 아니라 action.id, action.todo와 같이 action.payload라는 이름을 공통적으로 넣어 주게 됩니다. 그렇게 때문에, 기존의 업데이트 로직에서도 모두 action.payload 값을 조회하여 업데이트 하도록 구현해 주어야 합니다.

    액션 생성 함수는 액션에 필요한 추가 데이터를 모두 payload라는 이름으로 사용하기 때문에 action.id, action.todo를 조회하는 대신, 모두 공통적으로 action.payload 값을 조회하도록 리듀서를 구현해 주어야 합니다.

    modules/todos.js
    (...)
    
    const todos = handleActions(
      {
        [CHANGE_INPUT]: (state, action) => ({ ...state, input: action.payload }),
        [INSERT]: (state, action) => ({
          ...state,
          todos: state.todos.concat(action.payload),
        }),
        [TOGGLE]: (state, action) => ({
          ...state,
          todos: state.map((todo) =>
            todo.id === action.payload ? { ...todo, done: !todo.done } : todo,
          ),
        }),
        [REMOVE]: (state, action) => ({
          ...state,
          todos: state.filter((todo) => todo.id !== action.payload),
        }),
      },
      initialState,
    );

    모든 추가 데이터 값을 action.payload로 사용하기 때문에 나중에 리듀서 코드를 다시 볼 때 헷갈릴 수 있습니다. 객체 비구조화 할당 문법으로 action 값이 payload 이름을 새로 설정해 주면 action.payload가 정확히 어떤 값을 의미하는지 더 쉽게 파악할 수 있습니다.

    modules/todos.js
    (...)
    
    const todos = handleActions(
      {
        [CHANGE_INPUT]: (state, { payload: input }) => ({ ...state, input }),
        [INSERT]: (state, { payload: todo }) => ({
          ...state,
          todos: state.todos.concat(todo),
        }),
        [TOGGLE]: (state, { payload: id }) => ({
          ...state,
          todos: state.map((todo) =>
            todo.id === id ? { ...todo, done: !todo.done } : todo,
          ),
        }),
        [REMOVE]: (state, { payload: id }) => ({
          ...state,
          todos: state.filter((todo) => todo.id !== id),
        }),
      },
      initialState,
    );
    
    export default todos;

    어떤가요? 코드의 가독성이 더 높아졌지요?

    17.6.2 immer

    리듀서에서 상태를 업데이트 할 때는 불변성을 지켜야 하기 때문에 앞에서는 spread 연산자(...)와 배열의 내장 함수를 활용했습니다. 그러나 모듈의 상태가 복잡해질수록 불변성을 지키기가 까다로워 집니다.

    따라서 모듈의 상태를 설계할 때는 객체의 깊이가 너무 깊어지지 않도록 주의해야 합니다. 깊은 객체와 깊지 않은 개체를 한번 비교해 볼까요?

    const deepObject = {
        modal : {
            open:false,
            content : {
                title : '알림',
                body:'성공',
                buttons:{
                    confirm:'확인',
                    cancel:'취소'
                },
            },
        },
        waiting :false,
        settings : {
            theme: 'dark',
            zoomLevel:5,
        }
    };
    
    const shallowObject = {
        modal:{
            open :false,
            title: '알림',
            body : '성공',
            confirm:'확인',
            cancel:'취소',
        },
        waiting:false,
        theme:'dark',
        zoomLevel:5
    }

    객체의 깊이가 깊지 않을수록 추후 불변성을 지켜 가면서 값을 업데이트할 때 수월합니다. 하지만 상황에 따라 상태값들을 하나의 객체 안에 묶어서 넣는 것이 코의 가독성을 높이는데 유리하며, 나중에 컴포넌트에 리덕스를 연동할 때도 더욱 편합니다.

    객체의 구조가 복잡해지거나 객체로 이루어진 배열을 다룰경우, immer를 사용하면 훨씬 편리하게 상태를 관리할 수 있습니다.

    우선 immer를 현재 프로젝트에 설치해 주세요.

    $ yarn add immer

    counter 모듈처럼 간단한 리듀서에 immer를 사용하면 오히려 코드가 더 길어지기 때문에 todos 모듈에 적용해 보겠습니다.

    modules/todos.js
    import { createAction, handleActions } from 'redux-actions';
    import produce from 'immer';
    
    (...)
    
    
    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,
    );
    
    export default todos;

    immer를 사용한다고 해서 모든 업데이트 함수에 immer를 적용할 필요는 없습니다. 일반 자바스크립트로 처리하는 것이 더 편할 때는 immer를 적용하지 않아도 됩니다. 예를 들어 위 코드에서 TOGGLE을 제외한 업데이트 함수들은 immer를 쓰지 않는 코드가 오히려 더 짧기 때문에 이전 형태를 유지하는 것도 무방합니다.

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

    리덕스 스토어와 연동된 컨테이너 컴포넌트를 만들 때 connect 함수를 사용하는 대신 react-redx에서 제공하는 Hooks를 사용할 수도 있습니다.

    17.7.1 useSelector로 상태 조회하기

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

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

    여기서 상태 선택 함수는 mapStateToProps와 형태가 똑같습니다. 이제 CounterContainer에서 connect 함수 대신 useSelector를 사용하여 counter.number 값을 조회함으로써 Counter에게 props를 넘겨 줍시다.

    containers/CounterContainer.js
    import React from 'react';
    import Counter from '../components/Counter';
    import { useSelector } from 'react-redux';
    import { increase, decrease } from '../modules/counter';
    
    const CounterContainer = () => {
      const number = useSelector((status) => status.counter.number);
      return <Counter number={number} />;
    };
    
    export default CounterContainer;

    꽤 간단하지요?

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

    이번에는 useDispatch라는 Hook에 대해 알아봅시다. 이 Hook은 컴포넌트 내부에서 스토어의 내장 함수 dispatch를 사용할 수 있게 해줍니다. 컨테이너 컴포넌트에서 액션을 디스패치해야 한다면 이 Hook을 사용하면 됩니다. 사용법은 다음과 같습니다.

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

    이제 CounterContainer에서도 이 Hook을 사용하여 INCREASE와 DECREASE 액션을 발생시켜 봅시다.

    import React from 'react';
    import Counter from '../components/Counter';
    import { useDispatch, useSelector } from 'react-redux';
    import { increase, decrease } from '../modules/counter';
    
    const CounterContainer = () => {
      const number = useSelector((status) => status.counter.number);
      const dispatch = useDispatch();
      return (
        <Counter
          number={number}
          onIncrease={() => dispatch(increase())}
          onDecrease={() => dispatch(decrease())}
        />
      );
    };
    
    export default CounterContainer;

    이렇게 코드를 작성하고 +1과 -1 버튼을 눌러서 숫자가 잘 바뀌는지 확인해 보세요.

    지금은 숫자가 바귀어서 컴포넌트가 리렌더링될 때마다 onIncrease 함수와 onDecrease 함수가 새롭게 만들어지고 있스니다.

    만약 컴포넌트 성능을 최적화해야 하는 상황이 온다면 useCallback으로 액션을 디스패치하는 함수를 감사 주는 것이 좋습니다.

    다음과 같이 코드를 한번 수정해 보세요.

    containers/COunterContainer.js
    import React {useCallback} from 'react';
    import Counter from '../components/Counter';
    import { useDispatch, useSelector } from 'react-redux';
    import { increase, decrease } from '../modules/counter';
    import { useCallback } from 'react';
    
    const CounterContainer = () => {
      const number = useSelector((status) => status.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과 함께 사용하는 습관을 들일 것을 권합니다.

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

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

    사요법은 다음과 같습니다.

    const store = useStore();
    store.dispatch({ type : 'SAMPE_ACTION' });
    store.getState();

    useStore는 컴포넌트에서 정말 어쩌다가 스토어에 직접 접근해야 하는 상황에만 사용해야 합니다. 이를 사용해야 하는 상황은 흔치 않을 것입니다.

     

    17.7.4 TodosContainer를 Hooks로 전환하기

    이제 TodosContainer를 connect 함수 대신에 useSelector와 useDospatch Hooks를 사용하는 형태로 전환해 봅시다.

    containers/TodosContainer.js
    import React, { useCallback } from 'react';
    import { useSelector, useDispatch } from 'react-redux';
    import { changeInput, insert, toggle, remove } from '../modules/todos';
    import Todos from '../components/Todos';
    
    const TodosContainer = () => {
      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 TodosContainer;

    이번에는 useSelector를 사용할 때 비 구조화 할당 문법을 활용했습니다.

    또한, useDispatch를 사용할 때 각 액션을 디스패치하는 함수를 만들엇는데요. 위 코드의 경우 액션의 종류가 많은데 어떤 값이 액션 생성 함수의 파라미터로 사용되어야 하는지 일일이 명시해 주어야 하므로 조금 번거롭습니다. 이 부분은 우선 컴포넌트가 잘 작동하는 것을 확인하고 나서 한번 개선해 보겠습니다. 코드를 저장하고 TodosContainer가 잘 작동하는지 확인해 보세요.

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

    useActions는 원래 react-redux에 내장된 상태로 릴리즈될 계획이었으나 리덕스 개발 팀에서 꼭 필요하지 않다고 판단하여 제외된 Hook입니다. 그 대신 공식 문서에서 그대로 복사하여 사용할 수 있도록 제공하고 있습니다.

    • 참고링크 : https://react-redux.js.org/next/api/hooks#recipe-useactions

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

    src 디렉터리에 lib 디렉터리를 만들고, 그 안에 useActions.js 파일을 다음과 같이 작성해 보세요.

    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);
        },
        deps ? [dispatch, ...deps] : deps
      );
    }

    방금 작성한 useActions Hook은 액션 생성 함수를 액션을 디스패치하는 함수로 변환해 줍니다. 액션 생성 함수를 사용하여 액션 객체를 만들고, 이를 스토어에 디스패치하는 작업을 해주는 함수를 자동으로 만들어 주는 것이죠.

    useActions는 두 가지 파라미터가 필요합니다. 첫 번째 파라미터는 액션 생성 함수로 이루어진 배열입니다. 두 번째 파라미터는 deps 배열이며, 이 배열 안에 들어 있는 원소가 바뀌면 액션을 디스패치하는 함수를 새로 만들게 됩니다. 한번 TodoContainer에서 useActions를 불러와 사용해 봅시다.

    containers/TodoContainer.js
    import React from 'react';
    import { useSelector } from 'react-redux';
    import { changeInput, insert, toggle, remove } from '../modules/todos';
    import Todos from '../components/Todos';
    import useActions from '../lib/useActions';
    
    const TodosContainer = () => {
      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 TodosContainer;

    코드를 저장한 뒤, TodoListContainer가 잘 작동하는지 다시 확인해 보세요.

    17.7.6 connect 함수와의 주요 차이점

    앞으로 컨테이너 컴포넌트를 만들 때 connect 함수를 사용해도 좋고, useSelector와  useDispatch를 사용해도 좋습니다. 리덕스 관련 Hook이 있다고 해서 기존 connect 함수가 사라지는 것은 아니므로, 더 편한 것을 사용하면 됩니다.

    하지만 Hooks를 사용하여 컨테이너 컴포넌트를 만들 때 잘 알아 두어야 할 차이점이 있습니다.

     

    connect 함수를 사용하여 컨테이너 컴포넌트를 만들었을 경우, 해당 컨테이너 컴포넌트의 부모 컴포넌트가 리렌더링 될 때 해당 컨테이너 컴포넌트의 props가 바뀌지 않았다면 리렌더링이 자동으로 방지되어 성능이 최적화 됩니다.

    반명 useSelector를 사용하여 리덕스 상태를 조회했을 때는 이 최적화 작업이 자동으로 이루어지지 않으므로, 성능 최적화를 위해서는 React.memo를 컨테이너 컴포넌트에 사용해 주어야 합니다. 다음과 같이 말이죠.

    containers/TodosContainer.js
    import React from 'react';
    import { useSelector } from 'react-redux';
    import { changeInput, insert, toggle, remove } from '../modules/todos';
    import Todos from '../components/Todos';
    import useActions from '../lib/useActions';
    
    const TodosContainer = () => {
      (...)
    };
    
    export default React.memo(TodosContainer);

    물론 지금과 같은 경우에는 TodoContainer의 부모 컴포넌트인 App 컴포넌트가 리렌더링되는 일이 없으므로 불필요한 성능 최적화 입니다.

    17.8 정리

    이 장에서는 리액트 프로젝트에 리덕스를 적용하여 사용하는 방법을 배워 보았습니다. 리액트 프로젝트에서 리덕스를 사용하면 업데이트에 관련된 로직을 리액트 컴포넌트에서 완벽하게 분리시킬 수 있으므로 유지 보수성이 높은 코드를 작성해 낼 수 있습니다. 사실 이번에 만든 프로젝트처럼 정말 작은 프로젝트에 리덕스를 적용하면 오히려 프로젝트의 복잡도가 높아질 수 있습니다. 하지만 규모가 큰 프로젝트에 리덕스를 적용하면 상태를 더 체계적으로 관리할 수 있고, 개발자 경험도 향상시켜 줍니다.

    댓글

Designed by Tistory.