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

PYOUNANI·2023년 1월 8일
0

React

목록 보기
9/9
post-thumbnail

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

리덕스를 사용할 때 주로 react-redux라는 라이브러리에서 제공하는 유틸 함수(connect)와 컴포넌트(Provider)를 사용하여 처리합니다.


I. 작업 환경 설정

1. create-react-app으로 새로운 프로젝트 생성

2. 리덕스와 react-redux 라이브러리 설치

   $ yarn add redux react-redux

3. .prettierrc 파일 작성

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

II. UI 준비하기

리액트 프로젝트에서 리덕스를 사용할 때 가장 많이 사용하는 패턴은 프레젠테이셔널 컴포넌트와 컨테이너 컴포넌트를 분리하는 것입니다.

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

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


1. 카운터 컴포넌트 만들기

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

//components/Counter.js

const Counter = React.memo(({ 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 Counter from './components/Counter';

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

export default App;

2. 할 일 목록 컴포넌트 만들기

//components/Todos.js 

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

III. 리덕스 관련 코드 작성하기

리덕스를 사용할 때는 액션 타입, 액션 생성 함수, 리듀서 코드를 작성해야 합니다.

일반적인 구조로는 actions, constants, red니cers라는 세 개의 디렉터리를 만들고 그 안에 기능별로 파일을 하나씩 만드는 방식입니다. 코드를 종류에 따라 다른 파일에 작성할 수 있어 편하지만 새로운 액션을 만들 때마다 모든 파일을 수정해야한다는 불편한 점도 있습니다.


다른 방식으로는 액션 타입, 액션 생성 함수, 리듀서 함수를 기능별로 파일 하나에 몰아서 작성하는 방식입니다.


1. counter 모듈 작성하기

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

(1) 액션 타입 정의하기

//modules/counterjs

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

액션 타입은 대문자로 정의하고, 문자열 내용은 '모듈 이름/액션 이름'과 같은 형태로 작성합니다.

(2) 액션 생성 함수 만들기

//modules/counterjs

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

export const increase = () => ({ type: INCREASE });
export const decrease = () => ({ type: DECREASE });

주의해야 할 점은 앞부분에 export라는 키워드가 들어간다는 것입니다. 이렇게 함으로 추후에 이 함수를 다른 파일에서 불러와 사용할 수 있습니다.

(3) 초기 상태 및 리듀서 함수 만들기

//modules/counterjs

...

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는 단 한 개만 내보낼 수 있다는 것을 의미합니다.

2. todos 모듈 작성하기

(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 를 제거함

(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객체가 들고 있게 될 고윳값입니다.

(3) 초기 상태 및 리듀서 함수 만들기
객체에 한 개 이상의 값이 들어가므로 불변성을 유지해 주어야 합니다.

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

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;

파일 이름을 index.js로 설정하면 디렉터리 이름까지만 입력하여 불러올 수 있습니다.

import rootReducer from './modules';

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

1. 스토어 만들기

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

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);

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';
import { Provider } from 'react-redux';

const store = createStore(rootReducer);
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <Provider store={store}>
    <App />
  </Provider>
);

3. Redux DevTools의 설치 및 적용

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

(2) 패키지를 설치하여 적용하면 코드가 훨씬 깔끔해집니다. 패키지를 설치해줍니다.

 $ yarn add redux-devtools-extension

(3)

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';
import { Provider } from 'react-redux';
import { devToolsEnhancer } from '©redux-devtools/extension';


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

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

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

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

1. CounterContainer 만들기

// containers/CounterContainers.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(타깃 컴포넌트)
// containers/CounterContainers.js

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와 mapDispatchToProps에서 반환하는 객체 내부의 값들은 컴포넌트의 props로 전달됩니다. mapStateToProps는 state를 파라미터로 받아 오며, 이 값은 현재 스토어가 지니고 있는 상태를 가리킵니다. mapDispatchToProps는 store의 내장 함수 dispatch를 파라미터로 받아옵니다.

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

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

export default App;

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

// containers/CounterContainers.js

import { connect } from 'react-redux';
import Counter from '../components/Counter';
import { decrease, increase } 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);

connect 함수를 사용할 때 일반적으로 mapStateToProps와 mapDispatchToProps를 미리 선언해 놓고 사용합니다. 하지만 아래 코드와 같이 connect 함수 내부에 익명 함수 형태로 선언해도 됩니다.

// containers/CounterContainers.js

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

컴포넌트에서 액션을 디스패치하기 위해 각 액션 생성 함수를 호출하고 dispatch로 감싸는 작업이
조금 번거로울 수도 있습니다. 이와 같은 경우에는 리덕스에서 제공하는 bindActionCreators 유틸 함수를 사용하면 간편합니다.

// containers/CounterContainers.js

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

(...)

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

방금 작성한 방법보다 한 가지 더 편한 방법이 있습니다. 바로 mapDispatchToProps에 해당하는 파라미터를 함수 형태가 아닌 액션 생성 함수로 이루어진 객체 형태로 넣어 주는 것입니다. 두 번째 파라미터를 아예 객체 형태로 넣어 주면 connect 함수가 내부적으로bindActionCreators 작업을 대신해 줍니다.

// containers/CounterContainers.js

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

(...)

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

2. TodosContainer 만들기

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

//containers/TodosContainer.js

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

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;

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

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

1. redux-actions

redux-actions를 사용하면 액션 생성 함수를 더 짧은 코드로 작성할 수 있습니다.

$ yarn add redux-actions

(1) counter 모듈에 적용하기

// 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라는 함수를 통해 더 간단하고 가독성 높게 작성해볼 수 있습니다.
handleActions 함수의 첫 번째 파라미터로 각 액션에 대한 업데이트 함수를, 두 번째 파라미터로 초기 상태를 넣습니다.

// modules/counter.js

import { createAction, handleActions } from 'redux-actions';

(...)

const counter = handleActions(
  {
    [INCREASE]: (state, action) => ({ number: state.number + 1 }),
    [DECREASE]: (state, action) => ({ number: state.number - 1 }),
  },
  initialState,
);

export default counter;

(2) 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 => </span><span class="co49">${</span><span class="co33">text</span><span class="co49">}</span><span class="co31">!);
const action = myAction(‘hello world‘);
/*
결과: 
  { type: MY_ACTION, payload: ‘hello world!‘ }
*/
// 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 객체가 반환되는 함수를 넣어 주었습니다. 나머지 파라미터에도 그대로 반환하는 함수를 넣었습니다.

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

// modules/todos.js

import { createAction, handleActions } from 'redux-actions';

(...)

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.todos.map(todo =>
        todo.id = = = id ? { ...todo, done: !todo.done } : todo,
      ),
    }),
    [REMOVE]: (state, { payload: id }) => ({
      ...state,
      todos: state.todos.filter(todo => todo.id != = id),
    }),
  },
  initialState,
);

export default todos;

2. immer

객체의 깊이가 너무 깊어질 경우 불변성을 지키기가 까다롭습니다. 이럴땐 immer 라이브러리를 활용해볼 수 있습니다.

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

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

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

1. useSelector로 상태 조회하기

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

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

useSelector를 사용해서 CounterContainer를 다시 작성해보겠습니다.

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

2. useDispatch로 액션 디스패치하기

컨테이너 컴포넌트에서 액션을 디스패치해야 한다면 이 Hook을 사용하면 됩니다.

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

useDispatch를 사용해서 CounterContainer를 다시 작성해보겠습니다.

//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();
  const onIncrease = useCallback(() => dispatch(increase()), [dispatch]);
  const onDecrease = useCallback(() => dispatch(decrease()), [dispatch]);

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

export default CounterContainer;

3. useStore로 리덕스 스토어 사용하기

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

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

4. TodosContainer를 Hooks로 전환하기

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;

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

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

6. connect 함수와의 주요 차이점

  • 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/Todos1;
import useActions from '../lib/useActions1;

const TodosContainer =()=>{
(...)
);
  
export default React.memo(TodosContainer);

문제
1. 리덕스를 사용할 때에는 (), (), ()를 작성해야 한다.
2. 액션 타입은 대문자로 정의하고, 문자열 내용은 '() 이름/() 이름'과 같은 형태로 작성합니다.
3. createStore 함수를 사용하여 스토어를 만들 때는 리듀서를 하나만 사용해야 합니다. 이때 ()라는 유틸함수를 사용해볼 수 있습니다.
4. 리액트 컴포넌트에서 스토어를 사용할 수 있도록 App 컴포넌트를 react-redux에서 제공하는 () 컴포넌트로 감싸 줍니다.
5. 리덕스 스토어 안의 상태를 컴포넌트의 props로 넘겨주기 위해 설정하는 함수는 ()이고, 액션 생성 함수를 컴포넌트의 props로 넘겨주기 위해 사용하는 함수는 ()입니다.
6. dispatch로 감싸는 작업이 조금 번거로울 수도 있습니다. 이와 같은 경우에는 리덕스에서 제공하는 () 유틸 함수를 사용하면 간편합니다.
7. connect 함수와 useSelector의 큰 차이점으로는 ()에 있다.
8.
스토어를 작성해놓은 상태입니다. 리액트 컴포넌트에서 스토어를 사용할 수 있도록 코드를 수정하고 깔끔한 코드가 되기 위해 Redux DevTools를 사용해줍니다.

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

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);
  1. 아래 코드를 useSelector를 이용해서 수정하십시오.
// containers/CounterContainers.js

import { connect } from 'react-redux';
import Counter from '../components/Counter';
import { decrease, increase } 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. 액션 타입, 액션 생성 함수, 리듀서 코드
2. 모듈, 액션
3. combineReducers
4. Provider
5. mapStateToProps, mapDispatchToProps
6. bindActionCreators
7. 성능 최적화
8.

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

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);

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

0개의 댓글