Link Search Menu Expand Document

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

작업 환경 설정

  • yarn add redux react-redux

UI 준비하기

  • Presentational Component: 상태 관리가 이루어지지 않고 props를 받아 와서 화면에 UI를 보여주기만 함
  • Container Component: 리덕스와 연동되어 있는 컴포넌트로 리덕스로부터 상태를 받아오거나 리덕스 스토어에 액션을 디스패치

리덕스 관련 코드 작성하기

리덕스 관련 컴포넌트 구조

  1. 일반적인 구조
    • actions: 액션 생성 함수
    • constants: 액션 타입
    • reducers: 리듀서 코드
    • 코드를 종류에 따라 다른 파일에 작성
    • 그러나 새로운 액션을 만들 때마다 세 종류의 파일 수정 필요
  2. Ducks 패턴
    • 액션 타입, 액션 생성 함수, 리듀서 함수를 기능별로 하나의 파일에 작성

리덕스 관련 코드 작성

  1. 액션 타입 정의하기
    • 모듈 이름/액션 이름 형태로 작성
  2. 액션 생성 함수 만들기
    • export 키워드 추가하여 다른 파일에서 사용 가능
    • 액션 생성 함수의 파라미터는 액션 객체 안에 추가 필드로 들어감
  3. 초기 상태 및 리듀서 함수 만들기
    • export default 키워드는 파일 당 한 개만 사용 가능
    • 객체의 불변성을 유지하기 위하여 spread 연산자(...) 사용

참고: spread 연산자의 역할

  1. 함수 호출
  function myFunction(v, w, x, y, z) { }
  var args = [0, 1];
  myFunction(-1, ...args, 2, ...[3]); // myFunction(-1, 0, 1, 2, 3);
  1. 배열 리터럴
  var parts = [shoulders, knees]; 
  var lyrics = [head, parts, and, toes]; 
  // [“head”, “shoulders”, “knees”, “and”, “toes”]

  var arr = [1, 2, 3]; 
  var arr2 = [arr]; // arr.slice() 와 유사
  arr2.push(4); 

  // arr2 은 [1, 2, 3, 4] 이 됨
  // arr 은 영향을 받지 않고 남아 있음
  • 나머지 매개변수(...)와 유사하나 용도가 다름
  1. 객체 리터럴
  var obj1 = { foo: 'bar', x: 42 };
  var obj2 = { foo: 'baz', y: 13 };

  var clonedObj = { ...obj1 };
  // Object { foo: "bar", x: 42 }

  var mergedObj = { ...obj1, ...obj2 };
  // Object { foo: "baz", x: 42, y: 13 }
  • 코드 작성 예제
    // 액션 타입 정의하기
    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
    });
    
    // 초기 상태 및 리듀서 함수 만들기
    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;
    

루트 리듀서 만들기

  • createStore 함수로 스토어를 만들 때는 하나의 리듀서만을 사용해야 함
  • combineReducers라는 유틸 함수로 여러 개의 리듀서를 하나로 합침
const rootReducer = combineReducers({
  counter,
  todos,
});

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

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

// 스토어 만들기
const store = createStore(rootReducer, composeWithDevTools());

ReactDOM.render(
  // 프로젝트에 리덕스 적용하기
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root'),
);
  • composeWithDevTools(): redux-devtools-extension 설치하고 적용

컨테이너 컴포넌트 만들기

  • connect(mapStateToProps, mapDispatchToProps)(연동할 컴포넌트)
    • mapStateToProps: 리덕스 스토어 안의 상태를 props로 넘겨주는 함수
    • mapDispatchToProps: 액션 생성 함수를 컴포넌트의 props로 넘겨주는 함수

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={onChangeInput}
      onInsert={onInsert}
      onToggle={onToggle}
      onRemove={onRemove}
    />
  );
};

export default connect(
  // 비구조화 할당을 통해 todos를 분리하여
  // state.todos.input 대신 todos.input을 사용
  ({ todos }) => ({
    input: todos.input,
    todos: todos.todos,
  }),
  {
    changeInput,
    insert,
    toggle,
    remove,
  },
)(TodosContainer);
  • mapDispatchToProps를 함수 형태로 넣지 않고 액션 생성 함수의 객체 형태로 넣을 수 있음

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

리덕스 더 편하게 사용하기

redux-actions

  • createAction으로 액션 생성 함수 선언 가능
    • 액션에 필요한 추가 데이터는 payload라는 이름을 사용
  • switch/case 문이 아닌 hadleActions 함수를 사용

immer

  • 객체의 불변성을 쉽게 유지할 수 있음
  • produce(originalState, draft => function) 형태로 사용
    • originalState: 수정하고 싶은 상태
    • draft => function: 상태를 어떻게 업데이트할 지 정의하는 함수

modules/todos.js

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

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

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

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;

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

useSelector로 상태 조회하기

  • useSelector를 사용하여 connect 함수를 사용하지 않고 리덕스의 상태를 조회할 수 있음
  • const result = useSelector(상태 선택 함수)

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

  • useDispatch를 사용하여 컴포넌트 내부에서 스토어의 내장 함수인 dispatch를 사용할 수 있음
  • useCallback으로 액션을 디스패치하는 함수를 감싸 컴포넌트 성능을 최적화

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

  • useStore를 사용하여 컴포넌트 내부에서 리덕스 스토어 객체를 직접 사용할 수 있음
  • 스토어 객체에 직접 접근하는 대신 useSelector를 사용하는 것을 권장

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

  • useActions를 사용하여 여러 개의 액션을 사용할 때 코드를 깔끔하게 정리할 수 있음
  • 리덕스에서 제공하지는 않으나 공식 문서에서 복사하여 사용할 수 있음
    • 참고 링크: https://react-redux.js.org/api/hooks#recipe-useactions
  • useActions(actions, deps) 형태로 사용
    • actions: 액션 생성 함수로 이루어진 배열
    • deps: 이 배열 내에 들어있는 원소가 바뀌면 액션을 디스패치하는 함수를 생성

TodosContainer를 Hook으로 전환하기

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 = () => {
  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 React.memo(TodosContainer);
  • connect를 사용할 경우 해당 컨테이너 컴포넌트의 부모 컴포넌트가 리렌더링될 때 해당 컨테이너 컴포넌트의 props가 바뀌지 않을 경우 리렌더링이 되지 않음
  • 위의 예제처럼 connect를 사용하지 않을 경우 React.memo()를 사용하여 컴포넌트 성능을 최적화해야 함