공부블로그

17장. 리덕스를 사용하여 리액트 애플리케이션 상태 관리하기 본문

리액트/리액트를 다루는 기술

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

떠어영 2021. 2. 9. 23:58

< 리덕스를 사용한 상태 관리 >

  • 상태 업데이트에 관한 로직을 모듈로 따로 분리하여 컴포넌트 파일과 별개로 관리할 수 있어서 코드를 유지 보수하는데 편리 

  • react-redux 라이브러리에서 제공하는 유틸함수와 컴포넌트를 사용

  • props를 받아와서 화면에 UI를 보여주기만 하는 프레젠테이셔널 컴포넌트와 리덕스와 연동되어 있는 컨테이너 컴포넌트를 분리

  • UI에 관련된 프레젠테이셔널 컴포넌트는 src/components 에 작성, 리덕스와 연동된 컨테이너 컴포넌트는 src/containers 컴포넌트에 작성

 

1. UI 준비하기 ( 프레젠테이셔널 컴포넌트 )

components 디렉터리 안에 Counter. js 와 Todos. js 를 작성해주고 App 컴포넌트에서 렌더링해준다.

 

2. 모듈 작성하기 ( 리덕스 관련 코드 )

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

modules 디렉터리 안에 counter, todos 모듈을 만든다. ( counter. js 와 todos. js )

스토어를 만들 때는 리듀서를 하나만 사용해야 하므로 counter, todos 리듀서를 combineReducers를 통해 하나로 합쳐 루트 리듀서를 만들어준다. ( modules/ index. js 에서 작성 )

 

modules/ counter. js

//액션 타입 정의 (대문자)
//문자열: 모듈 이름/ 액션 이름
const INCREASE = 'counter/INCREASE';
const DECREASE = 'counter/DECREASE';

//액션 생성 함수 만들기
export const increase = () => ({ type: INCREASE });
export const decrease = () => ({ type: DECREASE });

//counter모듈의 초기 상태
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; //함수 내보내기 (default는 한개만 내보낼 수 있다.)

 

 

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: {
    //todo객체
    id: id++, //파라미터 외에 사전에 이미 선언되어 있는 id라는 값에 의존
    text,
    done: false,
  },
});
export const toggle = (id) => ({
  type: TOGGLE,
  id,
});
export const remove = (id) => ({
  type: REMOVE,
  id,
});

//초기 상태
const initialState = {
  input: '',
  todos: [
    {
      id: 1,
      text: '리덕스 기초 배우기',
      done: true,
    },
    {
      id: 2,
      text: '리액트와 리덕스 사용하기',
      done: false,
    },
  ],
};
// 리듀서 함수 만들기 (배열에 변화를 줄 때는 배열 내장 함수 사용)
function todos(state = initialState, action) {
  //state: { input: string; todos: { id: number; text: string; done: boolean; } [ ] ; }
  switch (action.type) {
    case CHANGE_INPUT:
      return {
        ...state,
        input: action.input,
      };
    case INSERT:
      return {
        ...state,
        todos: state.todos.concat(action.todo), //todos 배열에 추가
      };
    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;

 

modules/ index. js ( 루트 리듀서 만들기 )

//기존에 만들었던 리듀서를 하나로 합치기 위해 combineReducers라는 유틸함수 사용
import { combineReducers } from 'redux';
import counter from './counter';
import todos from './todos';

const rootReducer = combineReducers({
  counter,
  todos,
}); //작성했던 리듀서 함수들을 합쳐줌

export default rootReducer;

 

3. 리덕스 적용하기

src안의 index. js에서 스토어를 만들고, 이를 사용할 수 있도록 react-redux에서 제공하는 Provider 컴포넌트로 감싸준다.

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 reportWebVitals from './reportWebVitals';
import rootReducer from './modules';

const store = createStore(rootReducer, composeWithDevTools()); // 스토어 생성!

//react-redux에서 제공하는 Provider 컴포넌트로 App 컴포넌트를 감싸준다, store을 props로 전달
ReactDOM.render(
  <Provider store={store} >
    <App />
  </Provider>,
  document.getElementById('root'),
);

//ServiceWorker.unregister();

 

4. 컨테이너 컴포넌트 만들기

리덕스 스토어에 접근하여 원하는 상태를 받아오고, 액션도 디스패치 해준다.

src 디렉터리에 containers 디렉터리를 생성하고, 그 안에 CounterContainer 컴포넌트와 TodosContainer 컴포넌트를 만든다.

 

containers/ CounterContainer. js

import React from 'react';
// connect함수 사용
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} />
  );
};
/* 아래처럼 mapStateToProps와 mapDispatchToProps를 미리 선언해서 사용할 수도 있고

//리덕스 스토어 안의 상태를 컴포넌트의 props로 넘겨줌
const mapStateToProps = (state) => ({
  number: state.counter.number,
});

//store의 내장함수인 dispatch를 컴포넌트의 props로 넘겨줌
const mapDispatchToProps = (dispatch) => ({
  //임시함수
  increase: () => {
    //액션 생성함수를 불러와 액션 개체를 만들어 디스패치
    dispatch(increase());
  },
  decrease: () => {
    dispatch(decrease());
  },
});

//connect함수의 타깃 컴포넌트 = CounterContainer를 파라미터로 넣어주면 리덕스와 연동된 컴포넌트 생성
export default connect(mapStateToProps, mapDispatchToProps)(CounterContainer);

*/

//connect함수 내부에 익명 함수 형태로 선언할 수도 있다.
export default connect(
  (state) => ({
    number: state.counter.number,
  }),
  //mapDispatchToProps에 해당하는 파라미터를 액션생성함수로 이루어진 객체 형태로 넣어주면
  //connect함수가 내부적으로 bindActionCreators작업을 대신해줌
  { increase, decrease },
)(CounterContainer);

 

containers/ TodosContainer. js

import React from 'react';
import { connect } from 'react-redux';
//리덕스 스토어에서 불러옴 (todos 상태안에 있는 값들과 액션 생성 함수)
import { changeInput, insert, toggle, remove } from '../modules/todos';
//프레젠테이셔널 컴포넌트에서 불러옴
import Todos from '../components/Todos';

const TodosContainer = ({
  input,
  todos,
  changeInput,
  insert,
  toggle,
  remove,
}) => {
  return (
    <Todos //Todos 컴포넌트에 props로 전달
      input={input}
      todos={todos}
      onChangeInput={changeInput}
      onInsert={insert}
      onToggle={toggle}
      onRemove={remove}
    />
  );
};
export default connect(
  //비구조화 할당을 통해 todos를 분리하여
  //state.todos.input 대신 todos.input을 사용
  ({ todos }) => ({
    //받아온 todos( todos.js )를 분리해서 각각 input과 todos로 만들어서 파라미터로 받을 수 있게 해줌
    input: todos.input,
    todos: todos.todos,
  }),
  {
    changeInput,
    insert,
    toggle,
    remove,
  },
)(TodosContainer);

TodosContainer. js 코드를 작성하고 Todos 컴포넌트에 넘겨준 props값을 사용하기 위해 Todos. js 코드를 수정한다.

마지막으로 App. js에 두개의 컨테이너 컴포넌트를 렌더링해준다.

 

정리하자면!!!

상태 업데이트에 관한 로직을 '모듈'로 따로 분리하여 컴포넌트 파일과 별개로 관리하는 것!!!인데

모듈안에서 액션 타입, 액션 생성 함수, 리듀서를 작성하고 루트리듀서를 만들어준다.

루트리듀서를 파라미터로 받아 스토어를 생성하기 위해 src의 index. js에서 코드를 작성하고 리덕스를 적용하기 위해서App컴포넌트를 Provider컴포넌트로 감싸준다.

컨테이너 컴포넌트에서는 connect 함수를 통해 스토어의 상태를 받아오기도 하고 액션을 디스패치하기도 한다. 또한 프레젠테이셔널 컴포넌트에 props를 넘겨준다.

 

5. 리덕스 더 편하게 사용하기 

엑션 생성 함수, 리듀서를 작성할 때 redux-actions라는 라이브러리와 이전에 배운 immer라이브러리를 활용한다.

 

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

connect 함수를 사용하는 대신 react-redux 에서 제공하는 Hooks를 사용할 수 있다.

useSelector로 상태 조회, useDispatch로 액션 디스패치, useStore로 리덕스 스토어를 사용할 수 있다.

 

connect 함수와의 차이점: connect 함수를 사용하여 컨테이너 컴포넌트를 만들었을 경우에는 해당 컨데이너 컴포넌트의 부모 컴포넌트 컴포넌트가 리렌더링될 때 해당 컨테이너 컴포넌트의 props가 바뀌지 않았다면 리렌더링이 자동으로 방지되어 성능이 최적화된다. 하지만 Hooks를 사용하면 이러한 최적화가 되지 않으므로 React. memo를 사용해 주어야 한다.