상세 컨텐츠

본문 제목

[리액트 스타터 3] 17장. 리덕스를 사용하여 리액트 애플리케이션 상태 관리하기

22-23/22-23 리액트 스타터 3

by 케이비이 2023. 1. 12. 10:01

본문

728x90

17.1 작업 환경 설정

$ yarn create react-app react-redux-tutorial
$ cd react-redux-tutorial
$ yarn add redux react-redux

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

<.prettierrc>

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

17.2 UI 준비하기

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

17.2.1 카운터 컴포넌트 만들기

숫자를 더하고 뺄 수 있는 카운터 컴포넌트를 만들어 봅시다. 

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.js

import React from 'react';
import Counter from './components/Counter';

const App = () => {
  return (
    <div>
      <Counter number={0} />
    </div>
  );
};

export default App;

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

이번에는 해야 할 일을 추가하고, 체크하고, 삭제할 수 있는 할 일 목록 컴포넌트를 만들어 보겠습니다.

components/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 컴포넌트를 파일 두 개로 분리해도 되고, 위 코드처럼 파일 하나에 작성해도 무방합니다.

 

App.js

import React from 'react';
import Counter from './components/Counter';
import Todos from './components/Todos';

const App = () => {
  return (
    <div>
      <Counter number={0} />
      <hr />
      <Todos />
    </div>
  );
};

export default App;

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

17.3.1 counter 모듈 작성하기

Ducks 패턴을 사용하여 액션 타입, 액션 생성 함수, 리듀서를 작성한 코드를 ‘모듈’이라고 합니다.

17.3.1.1 액션 타입 정의하기

modules/counter.js

const INCREASE = 'counter/INCREASE';
const DECREASE = 'counter/DECREASE';

가장 먼저 해야 할 작업은 액션 타입을 정의하는 것입니다. 액션 타입은 대문자로 정의하고, 문자열 내용은 '모듈 이름/액션 이름'과 같은 형태로 작성합니다.

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는 단 한 개만 내보낼 수 있습니다.

 

17.3.2 todos 모듈 만들기

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 연산자()를 잘 활용하여 작성해보면 다음과 같습니다.

modules/todos.js

(…)
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

import { combineReducers } from 'redux';
import counter from './counter';
import todos from './todos';

const rootReducer = combineReducers({
  counter,
  todos,
});



export default rootReducer;

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

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

17.4.1 스토어 만들기

src/index.js

import React from 'react';
import ReactDOM from 'react-dom';
import { createStore } from 'redux';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';
import rootReducer from './modules';

const store = createStore(rootReducer);

ReactDOM.render(<App />, document.getElementById('root'));

serviceWorker.unregister();

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

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

import React from 'react';
import ReactDOM from 'react-dom';
import { createStore } from 'redux';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';
import rootReducer from './modules';

const store = createStore(rootReducer);

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

serviceWorker.unregister();

17.4.3 Redux DevTools의 설치 및 적용

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

$ yarn add redux-devtools-extension

src/index.js

import React from 'react';
import ReactDOM from 'react-dom';
import { createStore } from 'redux';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';
import rootReducer from './modules';

const store = createStore(rootReducer, composeWithDevTools());

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

serviceWorker.unregister();

이제 브라우저에서 크롬 개발자 도구를 실행한 후 Redux 탭을 열어 보면 다음과 같습니다.

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

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

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

17.5.1 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로 넘겨주기 위해 사용하는 함수입니다.

containers/CounterContainer.js

import React from 'react';
import { connect } from 'react-redux';
import Counter from '../components/Counter';

const CounterContainer = ({ number, increase, decrease }) => {
  return (
    <Counter number={number} onIncrease={increase} onDecrease={decrease} />
  );
};



const mapStateToProps = state => ({
  number: state.counter.number,
});
const mapDispatchToProps = dispatch => ({
  // 임시 함수
  increase: () => {
    console.log(‘increase‘);
  },
  decrease: () => {
    console.log(‘decrease‘);
  },
});
export default connect(
  mapStateToProps,
  mapDispatchToProps,
)(CounterContainer);

mapStateToProps mapDispatchProps에서 반환하는 객체 내부의 값들은 컴포넌트의 props로 전달됩니다. mapStateToProps state를 파라미터로 받아 오며, 이 값은 현재 스토어가 지니고 있는 상태를 가리킵니다.

App.js

import React from 'react';
import Todos from './components/Todos';
import CounterContainer from './containers/CounterContainer';


const App = () => {
  return (
    <div>
      <CounterContainer />
      <hr />
      <Todos />
    </div>
  );
};



export default App;

브라우저를 열어서 +1, -1 버튼을 눌러 보세요. 콘솔에 increase decrease가 찍히는 것을 확인할 수 있습니다. 

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

containers/CounterContainer.js

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

const CounterContainer = ({ number, increase, decrease }) => {
  return (
    <Counter number={number} onIncrease={increase} onDecrease={decrease} />
  );
};



const mapStateToProps = state => ({
  number: state.counter.number,
});
const mapDispatchToProps = dispatch => ({
  increase: () => {
    dispatch(increase());
  },
  decrease: () => {
    dispatch(decrease());
  },
});
export default connect(
  mapStateToProps,
  mapDispatchToProps,
)(CounterContainer);

이제 다시 +1, -1 버튼을 눌러 보세요. 숫자가 바뀌는 것을 확인할 수 있습니다.

connect 함수를 사용할 때는 일반적으로 위 코드와 같이 mapStateToProps mapDispatchToProps를 미리 선언해 놓고 사용합니다. 하지만 connect 함수 내부에 익명 함수 형태로 선언해도 문제가 되지 않습니다. 

containers/CounterContainer.js

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

const CounterContainer = ({ number, increase, decrease }) => {
  return (
    <Counter number={number} onIncrease={increase} onDecrease={decrease} />
  );
};



export default connect(
  state => ({
    number: state.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 { connect } from 'react-redux';
import Counter from '../components/Counter';
import { increase, decrease } from '../modules/counter';

const CounterContainer = ({ number, increase, decrease }) => {
  return (
    <Counter number={number} onIncrease={increase} onDecrease={decrease} />
  );
};



export default connect(
  state => ({
    number: state.counter.number,
  }),
  dispatch =>
    bindActionCreators(
      {
        increase,
        decrease,
      },
      dispatch,
    ),
)(CounterContainer);

 

 

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

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

const CounterContainer = ({ number, increase, decrease }) => {
  return (
    <Counter number={number} onIncrease={increase} onDecrease={decrease} />
  );
};



export default connect(
  state => ({
    number: state.counter.number,
  }),
  {
    increase,
    decrease,
  },
)(CounterContainer);

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

17.5.2 TodosContainer 만들기

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

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);

컨테이너 컴포넌트를 다 만든 후에는 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 리덕스 더 편하게 사용하기

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을 사용하면 매번 객체를 직접 만들어 줄 필요 없이 더욱 간단하게 액션 생성 함수를 선언할 수 있습니다

17.6.1.2 todos 모듈에 적용하기

똑같은 작업을 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 객체가 반환되는 함수를 넣어 주었습니다.

17.6.2 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를 적용하지 않아도 됩니다. 

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

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

17.7.1 useSelector로 상태 조회하기

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

const 결과 = useSelector(상태 선택 함수);
상태 선택 함수는 mapStateToProps와 형태가 똑같습니다. 

CounterContainer에서 connect 함수 대신 useSelector를 사용하여 counter.number 값을 조회함으로써 Counter에게 props를 넘겨 줍시다.

containers/CounterContainer.js

import React from 'react';
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;

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

이 Hook은 컴포넌트 내부에서 스토어의 내장 함수 dispatch를 사용할 수 있게 해 줍니다. 컨테이너 컴포넌트에서 액션을 디스패치해야 한다면 이 Hook을 사용하면 됩니다.

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

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

containers/CounterContainer.js

import React 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();
  return (
    <Counter
      number={number}
      onIncrease={() => dispatch(increase())}
      onDecrease={() => dispatch(decrease())}
    />
  );
};



export default CounterContainer;

이렇게 코드를 작성하고 +1 -1 버튼을 눌러서 숫자가 잘 바뀌는지 확인해 보세요. 만약 컴포넌트 성능을 최적화해야 하는 상황이 온다면 useCallback으로 액션을 디스패치하는 함수를 감싸 주는 것이 좋습니다.

containers/CounterContainer.js

import React, { 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과 함께 사용하는 습관을 들일 것을 권합니다.

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

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

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

useStore는 컴포넌트에서 정말 어쩌다가 스토어에 직접 접근해야 하는 상황에만 사용해야 합니다.

17.7.4 TodosContainer를 Hooks로 전환하기

TodosContainer를 connect 함수 대신에 useSelector useDispatch 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를 사용할 때 비구조화 할당 문법을 활용했습니다.

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

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

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

 

Hooks | React Redux

API > Hooks: the `useSelector` and `useDispatch` hooks`

react-redux.js.org

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

17.7.6 connect 함수와의 주요 차이점

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

 

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

 

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

containers/CounterContainer.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);

Quiz

1. Ducks 패턴을 사용하여 액션 타입, 액션 생성 함수, 리듀서를 작성한 코드를 (모듈)이라고 합니다.

2. 리듀서를 하나로 합쳐 주어야 하는데요. 이 작업은 리덕스에서 제공하는 combineReducers라는 (유틸 함수)를 사용하면 쉽게 처리할 수 있습니다.

3. 리덕스 스토어와 연동된 컴포넌트를 (컨테이너 컴포넌트)라고 부릅니다.

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

5. 리듀서를 작성할 때도 switch/case 문이 아닌 (handleActions)라는 함수를 사용하여 각 액션마다 업데이트 함수를 설정하는 형식으로 작성해 줄 수 있습니다.

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

7. useDispatch를 사용할 때는 이렇게 (useCallback)과 함께 사용하는 습관을 들일 것을 권합니다.

8. 다음 코드를 useCallback으로 액션을 디스패치하는 함수를 감싸 주는 코드를 작성해보시오.

import React 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();
  return (
    <Counter
      number={number}
      onIncrease={() => dispatch(increase())}
      onDecrease={() => dispatch(decrease())}
    />
  );
};

export default CounterContainer;

답:

import React, { useCallback } from 'react';
import { useSelector, useDispatch } from 'react-
728x90

관련글 더보기