본문 바로가기
react

[React 상태관리]리덕스 이해하기 + todoList 만들기

by HomieKim 2022. 3. 27.

 

✔ 리덕스란?

리액트를 사용하다 보면 상태 관리에 대한 고민을 하게 됩니다. 

기본적인 react-app의 경우 컴포넌트 내부에서 상태를 정의 하고 setState() 함수를 사용하여 불변성을 유지하며 상태를 업데이트 합니다.

이때, 다른 컴포넌트에서 특정 state를 사용하거나 update하고 싶다면 어떻게 해야할까요.

저는 보통 하위 컴포넌트로 props를 전달하여 사용하거나 상위 컴포넌트에서 필요한 경우? 는 상위 컴포넌트에서 상태를 정의하여 props로 전달해 줍니다. (기존 react-app은 단방향의 데이터 흐름을 가지고 있기 때문)

이러한 방식의 경우 규모가 커지면 커질 수록 상태 관리가 복잡해 지며 상태를 전달하는 과정에서 불필요한 props를 전달하게 되는 경우도 발생하게 됩니다.

즉, 자주 재사용되는 상태의 경우 전역적으로 상태를 사용할 수 있게 만들어 주는 라이브러리가 리덕스 라고 할 수 있습니다.

 

✔ 리덕스 왜 쓰는가

전역 상태 관리를 위해 사용하는 라이브러리라고 하였지만 조금 더 정확힌 이해를 위해 공식문서의 소개를 가지고 왔습니다.

Redux helps you manage "global" state - state that is needed across many parts of your application.
The patterns and tools provided by Redux make it easier to understand when, where, why, and how the state in your application is being updated, and how your application logic will behave when those changes occur.

 

정리 하자면 리덕스는 어플리케이션에서 사용되는 state의 중앙 저장소 같은 개념입니다.

전역 상태관리 라이브러리 이며, 어플리케이션에서 상태들이 어디서 어떻게 변화가 일어날지 쉽게 로직을 작성하고 디버깅하는데 도움을 줍니다.

 

✔ 리덕스 동작에 대한 이해

리덕스가 처음에 어려운 이유 중 하나는 리덕스만의 특유한 패턴을 가지고 있기 때문이라고 생각합니다.

이 패턴을 이해하기 위해 flux 구조에 대해서 알 필요가 있습니다.

 

flux 구조

flux 구조란 기존의 단방향 데이터 흐름을 가지고 있는 react를 보완하기 위해 facebook에서 제안한 패턴 입니다.

위 구조를 이해하기 위해 사용되는 용어를 하나씩 알아보겠습니다.

 

◼️ Store

    - 스토어는 말그대로 저장소 입니다. 우리가 작성한 상태와 리듀서를 가지고 있습니다.

    - 프로젝트당 단 하나의 store를 가질 수 있습니다. 

◼️ Action

   - 액션은 update할 상태를 나타내는 개념입니다.

   - 객체로 표현되며 스토어에 있는 특정 상태를 update하고 싶을 때 이 액션 객체를 사용합니다.

   - 액션 객체는 type을 프로퍼티로 가지고 있어야 합니다. type은 어떤 Action을 발생시켜 상태를 update할지 구분하는 개념 입니다.

◼️ Dispatch

   - dispatch는 쉽게 말해 action을 발생 시키는 함수라고 생각하면 됩니다.

   - dispatch함수의 인자로 Action 객체를 전달하면 액션이 발생됩니다.

   - 보통 Action객체를 생성하는 함수를 따로 만들어서 dispatch함수의 인자로 전달하기도 합니다.

◼️ Reducer

   - 리듀서는 action이 dispatch될 때 실행되는 로직입니다.

   - 리듀서 역시 함수 입니다. 이때 작성원칙을 따라야합니다. 

   - action객체는 타입을 가지고 있으므로 타입별로 switch 문을 통해 구분하여 상태를 변화시킬 로직을 작성 합니다. 

참고) 리듀서 작성원칙
- 리듀서는 순수함수이어야 합니다. 즉, 전달 받은 파라미터 외에 외부에 어떠한 상태도 변경되어선 안됩니다.
- 리듀서 함수는 이전 상태와 액션 객체를 파라미터로 받습니다.
- 불변성을 유지하여 새로운 객체를 만들어서 return 해야합니다.

✔ 리액트에 리덕스 적용하기 (TodoList 만들기)

먼저 프로젝트에 리덕스를 설치해 줍니다. 

yarn add redux react-redux

그 다음 리듀서 함수를 만들어 줍니다.

이때 먼저 액션 타입을 정의 해주었습니다. 

재사용시 오타가 나지 않게 상수형태로 액션타입만 문저열로 먼저 정의 후

액션생성 함수를 만들었습니다.

이후 리듀서를 정의 해서 export해주었습니다.

이때, 리듀서의 인자로 state와 action을 받는데 구조 분해 할당으로 {type, payload}를 꺼내 준 것으로

(state=initialState, action) 과 동일한 의미 입니다.

// todo.js
// 리듀서 함수 만들기

/* 액션 타입 정의 */
const TODO_INSERT = "TODO/INSERT";
const TODO_UPDATE = "TODO/UPDATE";
const TODO_REMOVE = "TODO/REMOVE";
const TODO_TOGGLE = "TODO/TOGGLE";
const TODO_SET = "TODO/SET";

/* 액션 생성 함수 */
export const todoInsert = (id, text) => {
    return {
        type : TODO_INSERT,
        payload : {
            id : id,
            text : text,
            checked : false,
        },
    };
};

export const todoRemove = (id) => {
    return {
        type: TODO_REMOVE,
        payload : {id : id},
    };
};

export const todoUpdate = (id, text) => {
    return {
        type: TODO_UPDATE,
        payload : {id :id, text : text}
    };
};
export const todoToggle = (id) => {
    return {
        type : TODO_TOGGLE,
        payload : {id: id}
    };
};

export const todoSetUpdate = (id) => {
    return {
        type : TODO_SET,
        payload : {id : id}
    };
};

/* 초기상태 */

const initState= {
    todos : [
        {
            id:1,
            text : 'todo-list With Redux!',
            checked : false,
            updated : false,
        },
    ]
};

/* 리듀서 생성 */
const todoReducer = (state = initState, {type, payload}) => {
    switch (type) {
        case TODO_INSERT :
            return {
                ...state,
                todos: state.todos.concat({
                    id : payload.id,
                    text : payload.text,
                    checked : false,
                }),
            };
        case TODO_REMOVE:
            return {
                ...state,
                todos : state.todos.filter((todo) => todo.id !== payload.id),
            };
        case TODO_UPDATE:
            return {
                ...state,
                todos: state.todos.map((todo) => todo.id === payload.id ? {...todo, text: payload.text} : todo),
            };
        case TODO_TOGGLE:   
            return {
                ...state,
                todos : state.todos.map((todo) => todo.id === payload.id ? {...todo, checked : !todo.checked} : todo),
            };
        case TODO_SET :
            return {
                ...state,
                todos : state.todos.map((todo) => todo.id === payload.id ? {...todo, updated : !todo.updated} : todo),
            } 
        default :
            return {...state}
    }
}

export default todoReducer;

추가적인 tip이 있다면 액션 생성함수는 꼭 만들지 않아도 됩니다. 컴포넌트에서 dispatch 하여 사용하는데 이때 액션 객체를 인자로 넣어주면 상관 없습니다. 예를 들어

// dispatch할 컴포넌트에서
dispatch({
  type: 'TODO_SET',
  id: id,
})
// 이런식으로 사용해도 됩니다.

그리고, 주의 해야 할 점은 리듀서에서 상태의 불변성을 지키기위해 새로운 객체를 return해줘야 됩니다.

이를 쉽게 처리하기 위해 immer라는 라이브러리를 사용하기도 합니다.

 

그 다음 store를 설정해 줍니다.

// index.js
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import reportWebVitals from './reportWebVitals';
import { createStore } from 'redux';
import todoReducer from './modules/todos';
import { Provider } from 'react-redux';
import { Global } from "@emotion/react";
import { GlobalStyles } from "./index.style";

const store = createStore(todoReducer);

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

redux에서 제공하는 createStore() 함수를 사용하여 스토어를 생성할 수 있습니다.

createStore() 함수는 작성한 리듀서를 넣어 주면 됩니다.

Provider 컴포넌트로 <App/>을 한번 감싸주고 props로 생성한 store를 전달해 줍니다.

 

이렇게 하면 스토어에 리듀서 설정까지 마쳤고 dispatch를 통해 상태를 변화 시키면 store의 상태가 reducer의 로직에 맞춰 업데이트 됩니다.

 

컴포넌트에서 사용 리듀서를 상태값을 사용하고 dispatch하여 상태값을 업데이트 하는 예시를 살펴보겠습니다.

// todoList.jsx
import React from "react";
import { useSelector } from "react-redux";
import TodoItem from "./todoItem";
import styled from "@emotion/styled/macro";
import TodoUpdateForm from './todoUpdateForm';

const StyleList = styled.div`
  max-width: 600px;
  max-height: 300px;
  overflow-y: auto;
  background : #fff;
  border: 1.4px solid rgba(0, 0, 0, 0.1);
  border-radius : 3px;

`;

const TodoList = () => {
  const todos = useSelector((state) => state.todos);
  console.log(todos);
  return (
    <StyleList>
      {todos.map((todo) => (
        todo.updated ?
        (<TodoUpdateForm key={todo.id} todo={todo} />):( <TodoItem key={todo.id} todo={todo} />)
      ))}
    </StyleList>
  );
};

export default TodoList;

useSelector함수를 통해 store에 저장된 state를 참조 할 수 있습니다. 현재 store에는 todoReducer하나만 정의 되어있고 todo.js에 initialState를 보면 todo객체가 있는 것을 확인할 수 있습니다.

 

dispatch 사용 예시

import React from "react";
import {
  MdCheckBoxOutlineBlank,
  MdCheckBox,
  MdRemoveCircleOutline,
} from "react-icons/md";
import { BiPencil } from "react-icons/bi";
import { useDispatch } from "react-redux";
import { todoRemove, todoSetUpdate, todoToggle } from "../modules/todos";
import {
  StyleCheckBox,
  StyleItemBox,
  StyleRemoveButton,
  StyleTextBox,
  StyleUpdateButton,
} from "./todoItem.style";

const TodoItem = ({ todo }) => {
  const { id, text, checked } = todo;
  const dispatch = useDispatch();

  

  return (
    <StyleItemBox>
      <StyleCheckBox onClick={() => dispatch(todoToggle(id))} checked={checked}>
        {checked ? <MdCheckBox /> : <MdCheckBoxOutlineBlank />}
      </StyleCheckBox>
      <StyleTextBox readOnly={true} checked={checked}>
        {text}
      </StyleTextBox>
      <StyleUpdateButton onClick={()=> dispatch(todoSetUpdate(id))}>
        <BiPencil />
      </StyleUpdateButton>
      <StyleRemoveButton onClick={() => dispatch(todoRemove(id))}>
        <MdRemoveCircleOutline />
      </StyleRemoveButton>
    </StyleItemBox>
  );
};

export default TodoItem;

 

useDispatch 함수를 통해 dispatch를 사용할 수 있습니다. dispatch함수에 액션 생성함수를 import해와서 인자로 전달해 주면 reducer에서 해당 액션 타입에 맞는 로직을 실행 해 줍니다. 컴포넌트에서 액션 생성함수를 사용해야 하기 때문에  액션 생성함수는 todo.js에서 export로 작성합니다.

 

✔ 전체 코드

https://github.com/HomieKim/React_PlayGround/tree/main/redux_todolist

 

GitHub - HomieKim/React_PlayGround: 리액트 연습하는 레포

리액트 연습하는 레포. Contribute to HomieKim/React_PlayGround development by creating an account on GitHub.

github.com

 

댓글