본문 바로가기
Programming/React.js

14. Redux로 React app 상태 관리

by _S0_H2_ 2022. 10. 26.
728x90
반응형

 

 

 

Counter와 Todo component를 만들자.

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;

 Todo.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;

 

Ducks 패턴(액션 타입, 액션 생성 함수, 리듀서 함수를 기능별로 파일 하나에 몰아서 다 작성하는 방식)을 사용하여 액션 타입, 액션 생성 함수, 리듀서를 작성한 코드를 ‘모듈’이라고 한다. 각각의 컴포넌트를 모듈화하자.

 

 

# modules 폴더!!!

counter.js

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

// actionType = moduleName/actionName
const INCREASE = 'counter/INCREASE';
const DECREASE = 'counter/DECREASE';

// action create function
// createAction을 사용하면 매번 객체를 직접 만들 필요 없이 간단히 액션 생성 함수 선언
export const increase = createAction(INCREASE);
export const decrease = createAction(DECREASE);

// 초기상태
const initialState = {
    number: 0
};

// 리듀서 함수
// handleActions(update함수, 초기상태)
const counter = handleActions(
    {
        [INCREASE]: (state, action) => ({number : state.number + 1}),
        [DECREASE]: (state, action) => ({number : state.number - 1}),
    },
    initialState
)

export default counter;

 

todos.js

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

// action type
const CHANGE_INPUT = 'todos/CHANGE_INPUT';
const INSERT = 'todos/INSERT';
const TOGGLE = 'todos/TOGGLE';
const REMOVE = 'todos/REMOVE';

// action create func
export const changeInput = createAction(CHANGE_INPUT, input => input);

    // insert는 todo객체를 액션 객체에 넣어줘야하므로, text를 넣으면 todo객체가 반환된다
let id = 3;
export const insert = createAction(INSERT, text =>({
        id: id++,
        text,
        done: false
}));

export const toggle = createAction(TOGGLE, id => id);
export const remove = createAction(TOGGLE, id => id);

// 초기값
 const initialState = {
    input: '',
    todos:[
        {
            id: 1,
            text: '리덕스 기초 배우기',
            done: true
        },
        {
            id: 2,
            text: '리액트와 리덕스 사용하기',
            done: false
        }
    ]
 };

 // reducer 함수
 // 액션에 필요한 추가 데이터를 모두 payload라는 이름으로 사용하도록 구현
 const todos = handleActions(
    {
        [CHANGE_INPUT]: (state, {playload: input}) => produce(state, draft => {draft.input = input}),
        [INSERT]: (state, {playload: todo}) => produce(state, draft => {draft.todos.push(todo)}),
        [TOGGLE]: (state, {playload: id}) => produce(state, draft => {
            const todo = draft.todos.find(todo => todo.id === id); 
            todo.done = !todo.done;
        }),
        [REMOVE]: (state, {playload: id}) => produce(state, draft => {
            const index = draft.todos.findIndex(todo => todo.id === id);
            draft.todos.splice(index, 1);
        })
    },
    initialState
 )
 export default todos;

- 객체의 구조가 복잡해지거나 객체로 이루어진 배열을 다룰 경우, immer를 사용하면 코드의 가독성 및 컴포넌트와 리덕스 연동에 편리하다.

 

 

 

store를 사용하기 위해서는 reducer를 하나만 사용해야하므로, index.js 파일에서 reducer를 합쳐 주자.

index.js

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

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

export default rootReducer;

 

# src 폴더!!!

redux를 적용하기 위해 src/index.js에 store를 생성한다.

index.js

import ReactDOM from 'react-dom';
import {createStore} from 'redux';
import {Provider} from 'react-redux';
import { composeWithDevTools } from 'redux-devtools-extension';
import App from './App';
import rootReducer from './modules';

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

react component에서 store를 사용할 수 있도록 app component를 react-redux에서 제공하는 Provider component로 감싸준다.

 

이제 component에서 redux store에 접근하여 원하는 상태를 받아오고, action을 dispatch하자. redux store와 연동된 컴포넌트를 container component라고 한다.

 

# src / containers 폴더!!!

CounterContainer.js

import { 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;

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;

 

위 코드에서 액션의 종류가 많은데 어떤 값이 액션 생성 함수의 파라미터로 사용되어야 하는지 명시해주어야 하므로 번거롭다. 이를 개선하기 위해 useActions를 사용해보자. 해당 lib은 공식 문서에서 제공하고 있다. (https://react-redux.js.org/api/hooks#recipe-useactions)

 

# src / 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
    );
}

이 hook은 액션 생성 함수를 액션을 dispatch하는 함수로 변환해준다. 액션 생성 함수를 사용하여 액션 객체를 만들고, 이를 store에 dispatch하는 작업을 해주는 함수를 자동으로 만들어준다.

 

useActions(액션 생성 함수로 이루어진 배열, deps 배열) : deps 배열 안에 있는 원소가 바뀌면 action을 dispatch하는 함수를 새로 만든다.

 

TodoContainer.js에 적용

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;

 

컨테이너 컴포넌트를 만들 때 connect를 사용하면 해당 컨테이너 컴포넌트의 부모 컴포넌트가 리렌더링 될 때 해당 컨테이너 컴포넌트의 props가 바뀌지 않았다면 리렌더링이 자동으로 방지되어 성능이 최적화된다. 하지만, useSelector는 자동으로 최적화가 이루어지지 않으므로 React.memo를 컨테이너 컴포넌트에 사용해주어야 한다.

 

 

 

 

 

 

 

 

728x90
반응형

'Programming > React.js' 카테고리의 다른 글

13. Redux  (0) 2022.09.17
12. ContextApi  (0) 2022.09.14
11. 라우터로 SPA 개발하기  (0) 2022.09.08
10. immer사용하여 불변성 유지하기  (0) 2022.08.31
9. 컴포넌트 성능 최적화  (0) 2022.08.24