상세 컨텐츠

본문 제목

[리액트를 다루는 기술] 17장 리덕스를 사용하여 리액트 애플리케이션 상태 관리하기

21-22/21-22 리액트 스타터 -1

by Kimpeep 2022. 1. 31. 13:01

본문

728x90

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

 

리액트 애플리케이션에서 리덕스를 사용할 때는 주로 react-redux라는 라이브러리에서 제공하는 유틸 함수(connext) 컴포넌트(Provider)를 사용하여 리덕스 관련 작업을 처리합니다.


17-1 작업 환경 설정

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
}

 

17-2 UI 준비하기

 

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

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

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

필수 사항은 아니지만 이 패턴을 사용하면 코드의 재사용성이 높아지고, UI를 작성할 떄 좀 더 집중할 수 있습니다. 아래 실습에서는 UI에 관련된 프레젠테이션 컴포넌트는 src/components 경로에, 리덕스와 연동된 컨테이너 컴포넌트는 src/containers 경로에 저장했습니다.

 

(1) 카운터 컴포넌트 만들기

- components/Counter.js (숫자를 더하고 뺄 수 있는 카운터 컴포넌트)

import React from 'react';

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 React from 'react';
import Counter from './components/Counter';

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

export default App;

- 결과 화면

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

- 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 리덕스 관련 코드 작성하기

리덕스를 사용할 때는 액션 타입, 액션 생성 함수, 리듀서 코드를 작성해야 합니다. 각각 다른 파일에 작성해도 되고, 기능별로 묶어서 파일 하나에 작성할 수도 있습니다. 디렉터리 구조는 정해진 방법이 없어 마음대로 작성해도 되지만, 아래 두 가지 방법이 주로 사용됩니다.

 

- 일반적인 구조

actions, constants, reducers라는 세 개의 디렉터리를 만들고 그 안에 기능별로 파일을 하나씩 만드는 방법입니다. 코드를 종류에 따라 다른 파일에 정리할 수 있어 편리하지만, 새로운 액션을 만들 때마다 세 종류의 파일을 모두 수정해야 한다는 불편함이 있습니다.

 

- Ducks 패턴

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

 

(1) counter 모듈 작성하기

모듈: Ducks 패턴을 사용하여 액션 타입, 액션 생성 함수, 리듀서를 작성한 코드

 

1-1 액션 타입 정의하기

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

액션 타입은 대문자로 정의하고, 문자열 내용은 '모듈 이름/액션 이름'과 같은 형태로 작성합니다. 문자열 안에 모듈 이름을 넣어 나중에 액션 이름이 충돌되지 않게 해 줍니다.

 

1-2 액션 생성 함수 만들기

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

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

앞부분에 export 키워드가 들어간다는 점을 주의해야 합니다. 추후 이 함수를 다른 파일에서 불러와 사용할 수 있습니다.

 

1-3 초기 상태 및 리듀서 함수 만들기

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

 

(2) todos 모듈 작성하기

2-1. 액션 타입 정의하기

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

 

2-2. 액션 생성 함수 만들기

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

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

(초기 상태를 작성할 때 todo 객체 두 개를 미리 넣어둘 것이므로 새로 추가될 항목의 id 값은 3입니다)

 

2-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';

 

17-4 리액트 애플리케이션에 리덕스 적용하기

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

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

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

serviceWorker.unregister();

 

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

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

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

const store = createStore(rootReducer);

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

serviceWorker.unregister();

 

(3) Redux DevTools의 설치 및 적용

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

 

2. yarn을 사용하여 redux-devtools-extension을 설치합니다.

   $ yarn add redux-devtools-extension

 

3. 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 * as serviceWorker from './serviceWorker';
import rootReducer from './modules';
import { composeWithDevTools } from 'redux-devtools-extension';

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

 

4. 크롬 개발자 도구의 Redux 탭을 클릭하고, 리덕스 개발자 도구 안의 State 버튼을 눌러 현재 리덕스 스토어 내부의 상태가 잘 보이는지 확인합니다.

 

17-5 컨테이너 컴포넌트 만들기

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

 

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

 

- 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를 누르면 increase가, -1를 누르면 decrease가 콘솔 창에 출력됩니다.

 

- containers/CounterContainers.js

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

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

 

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

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

 

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

- containers/CounterContainers.js

import React from 'react';
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 React from 'react';
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 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.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 라이브러리를 활용하면 리덕스를 더 편하게 사용할 수 있습니다.

 

(1) redux-actions

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

 

$ yarn add redux-actions

 

1-1 counter 모듈에 적용하기

import { createAction } from 'redux-actions';

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

export const increase = createAction(INCREASE);
export const decrease = createAction(DECREASE);

(...)

counter 모듈에 작성된 액션 생성 함수를 createAction 함수를 사용하여 만듭니다.  createAction을 사용하면 매번 객체를 직접 만들어 줄 필요 없이 액션 생성 함수를 선언할 수 있습니다.

 

리듀서 함수는 handleActions 함수를 사용해 가독성을 높일 수 있습니다. handleActions 함수의 첫 번째 파라미터로 각 액션에 대한 업데이트 함수를, 두 번째 파라미터로 초기 상태를 넣습니다.

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;

 

1-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' }
*/

액션 생성 함수에서 받아 온 파라미터를 변형을 주어서 넣고 싶으면, 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의 두 번떄 파라미터에 text를 넣으면 todo 객체가 반환되는 함수를 넣어 주었습니다. 나머지 함수에는 파라미터를 그대로 반환하는 함수를 넣었습니다. (생략해도 상관은 없음)

 

handleActions으로 리듀서 재작성. createAction으로 만든 액션 생성 함수는 파라미터로 받아 온 값을 객체 안에 넣을 때 action.payload라는 이름을 공통적으로 넣어 줍니다. 따라서, 기존 업데이트 로직에서도 action.payload 값을 조회하여 업데이트하도록 구현해야 합니다. 객체 비구조화 할당 문법으로 payload 이름을 새로 설정해 주면 action.payload가 의미하는 값을 쉽게 파악할 수 있다.

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를 사용하면 편하게 상태 관리가 가능합니다.

 

$ yarn add 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;

 

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

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

(1) useSelector로 상태 조회하기

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

 

- useSelector 사용법

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

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

 

- useSelector를 사용하여 재작성한 CounterContainer

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;

 

(2) useDispatch로 액션 디스패치하기

useDispatch Hook은 컴포넌트 내부에서 dispatch를 사용할 수 있게 합니다. 

 

- useDispatch 사용법

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

- useDispatch를 사용하여 재작성한 CounterContainer

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;

 

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

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

- useStore 사용법

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

 

(4) TodosContainer를 Hooks로 전환하기

- connect 함수 대신 useSelector useDispatch Hooks로 재작성한 TodosContainer

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을 만들어서 사용하기

useActions 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

 

(6) connect 함수와의 주요 차이점

  • connect 함수로 컨테이너 컴포넌트를 만든 경우

부모 컴포넌트가 리렌더링될 때 해당 컨테이너 컴포넌트의 props가 바뀌지 않으면, 리렌더링이 방지되어 성능이 최적화됩니다.

  • useSelector로 리덕스 상태를 조회했을 때

최적화 작업이 자동으로 이루어지지 않아, 성능 최적화를 위해 React.memo를 컨테이너 컴포넌트에 사용해 주어야 합니다.

 

Quiz

1. ○○○를 사용하면, 상태 업데이트에 관한 로직을 ○○로 따로 분리하여 컴포넌트 파일과 별개로 관리할 수 있어 코드 ○○ ○○에 도움됩니다.

2. 리덕스 관련 코드를 작성할 때 액션 타입, 액션 생성 함수, 리듀서 함수를 기능별로 파일 하나에 몰아서 작성하는 방식은? 

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

(ex) 'counter/INCREASE'

4. ○○○○ ○○○○: 리덕스와 연동되어 있는 컴포넌트로, 리덕스로부터 상태를 받아 오기도 하고 리덕스 스토어에 액션을 디스패치하기도 합니다.

 

더보기

1. 리덕스, 모듈, 유지 보수

2. Ducks 패턴

3. 모듈, 액션

4. 컨테이너 컴포넌트

 


Corner React Starter #1
Editor dori

728x90

관련글 더보기