[Udemy] React 기본 - 일기장 만들기(13) 복잡한 상태 관리 로직 분리 : useReducer

productuidev·2022년 5월 7일
0

React Study

목록 보기
32/52
post-thumbnail

React 기본 (Project)

Udemy - 한입크기로 잘라 먹는 리액트


📌 일기장 만들기 (13)

☑️ As-Is : 상태변화 처리 함수

  • 만들었던 컴포넌트들 중에 가장 복잡하고 많은 상태 업데이트 로직을 가진 App 컴포넌트에는 많은 상태 변화 함수들이 존재하고 있음 (onCreate, onEdit, onRemove 등등)
const [data, setData] = useState([]);
  • 이 상태 변화 함수들은 컴포넌트 내에만 존재해야 했고 그 상태를 업데이트하기 위해서는 기존의 상태를 참조해야 했기 때문임
  • 그러나 이렇게 컴포넌트의 코드가 길어지고 무거워지는 것은 결코 좋은 일이 아님

☑️ To-Be : 복잡한 상태 변화 로직을 컴포넌트로부터 분리

  • useReducer라는 Hooks를 사용하여 이런 복잡하고 긴 상태 변화 로직을 컴포넌트 바깥으로 분리할 수 있게 되어서 컴포넌트를 더 가볍게 작성할 수 있도록 도와준다.

☑️ useReducer 왜 필요한가?

  • useReducer의 필요성
    1부터 10,000까지 각각 더 할 수 있는 카운터를 만들어 봄. 1, 10, 100, 1000, 10000 순서대로 더할 수 있는 버튼 5개와 5개의 상태 변화 함수까지 만들어봤는데, useState를 이용하면 이렇게 상태 변화 함수 5개를 각각 다 Counter 컴포넌트 안에 작성해야 함. (길고 복잡한 코드 작성)
    실제 개발 진행 시 state가 하나의 컴포넌트에 10~15개 있을 수도 있고 객체로 복잡한 state를 가지고 있을 수도 있음. 그래서 상태 변화 로직이 굉장히 많을 수도 있음. 그렇기 때문에 이렇게 컴포넌트를 무겁게 사용하는 것은 결코 좋지 않은 방법이 될 수 있다.

  • useReducer는 useState를 대체할 수 있는 기능으로, 왼쪽에 보이는 reducer라는 상태 변화 함수를 컴포넌트 바깥에 분리함으로써 다양한 상태 변화 로직을 컴포넌트 외부에 switch - case 문법을 통해 쉽게 처리할 수 있도록 바꿀 수 있음.

  • useState를 사용하듯이 배열을 반환하게 되고, 배열의 비구조화 할당을 통해서 사용한다고 보면 됨.

  • 첫 번째로 반환받게 되는 0번째 index인 count는 그냥 state고, 이 state를 useState에서 사용했던 것처럼 return 안에 {count} 이런 식으로 사용 가능함

  • 두 번째로 반환받게 되는 1번째 index인 dispatch상태를 변화시키는 action을 발생시키는 함수라고 생각하면 됨. (상태 변화 함수)

  • useReducer 함수를 호출할 때는 첫 번째로 꼭 reducer라는 함수를 전달해주어야 함. reducer는 상태 변화를 일으킨 dispatch를 처리해주는 역할을 하게 됨.

  • 두 번째로 전달하는 인자인 1의 경우 count state의 초기값.

  • dispatch 함수를 호출 시 type이라는 프로퍼티가 객체에 들어있고, dispatch와 함께 전달되는데 이것을 action 객체라고 부름. (action = 상태 변화, 다시 말해 상태 변화를 설명할 객체) dispatch가 호출되면서 전달된 이 action 객체는 reducer로 날아가게 된다.

  • reducer 함수는 dispatch가 일어나면 처리하기 위해 호출이 되는데 첫 번째 인자로는 가장 최신 state를 받고, 두 번째 인자로는 이 dispatch를 호출할 때 전달해줬던 action 객체를 전달받게 된다.

  • add 1 버튼 클릭 시 reducer 함수가 실행되고 이 reducer 함수가 받게 된 인자인 state는 1이 되고(현재), action 객체는 type: 1이라는 객체를 받게 됨

  • 그러면 상태 변화를 처리하는 reducer 함수는 switch - case 문을 이용해서 action type에 따라 각각 다르게 반환하여 새로운 state가 된다. (type : 1로 전달 > return state + 1에 더해져서 state는 2가 반영된다)

✔️ 정리
useReduce를 사용해서 count라는 state를 만들면, 초기 값은 1로 할당됨. 처음 1인 count state의 값을 변경하고 싶다면, 상태 변화 함수인 dispatch를 호출해서 상태 변화를 일으키면 상태 변화 처리 함수인 reducer가 그걸 처리하게 되는 것이라고 생각하면 됨. reducer로 전달한 action 객체는 dispatch가 일어나면 switch - case 문을 이용해 action type에 따라 새로운 state를 반환한다.


☑️ 상태 변화 함수로부터 독립시키기

  • useReducer와 함께 App 컴포넌트에서 일기 데이터인 data state의 상태 변화 로직 분리

src/App.js

// import useReducer

import { useCallback, useEffect, useMemo, useRef, useReducer } from "react";
import "./App.css";
import DiaryEditor from "./DiaryEditor";
import DiaryList from "./DiaryList";


// 상태변화를 밖으로 분리하는 reducer 함수 작성 (switch - case)
// reducer가 리턴하는 값이 새로운 상태의 값이 된다
// data에 어떤 action이 필요한지 알아보기
// action의 4가지 case (INIT, CREATE, REMOVE, EDIT)


const reducer = (state, action) => {
  switch(action.type){
    
    // action 객체에서 data 프로퍼티를 꺼내서 새로운 state가 된다
    case "INIT" : {
      return action.data; 
    }
    
    // create_date와 newItem은 여기에서 생성
    // 새로운 일기 작성 시 새 아이템을 원본 배열에 추가해서 리턴
    case "CREATE" : {
      const created_date = new Date().getTime();
      const newItem = {...action.data, created_date};
      return [newItem, ...state];
    }
    
    // action 객체의 targetId가 아닌 애들만 필터링해서 새로운 state 값으로 전달
    case "REMOVE" : {
      return state.filter((it) => it.id !== action.targetId);
    }
    
    // EDIT action 발생하면 map 함수를 사용
    // 전달받은 타겟아이디와 일치하는 요소를 찾아준 다음
    // 그 요소의 값은 content만 newContent로 수정해주고,
    // 나머지 요소는 그대로 돌려준 후 그 요소들을 합쳐서 새로운 배열을 만들고
    // 새로운 state로 보내줌
    case "EDIT" : {
      return state.map((it) => it.id === action.targetId
        ? {...it, content: action.newContent } : it
      );
    }
    
    // switch - case 문에는 반드시 default case를 작성해야 한다
    // reducer는 항상 새로운 state를 리턴해줘야 하는 의무가 있는데,
    // default일 경우 타입을 잘못 전달했구나라고 생각해서 상태 변화를 시키지 않도록 한다.
    // (state를 그대로 전달해서 새로운 값으로 사용하면 그냥 값이 안 바뀜)
    
    default : return state;
  }
};


function App() {

  // useReducer (상태변화처리하는 reducer와 data state의 초기값(빈배열)
  const [data, dispatch] = useReducer(reducer, []);

  // getData > API 콜해서 적절히 가공 후 한방에 data를 초기화하는 함수
  const getData = async() => {
    const res = await fetch("https://jsonplaceholder.typicode.com/comments")
      .then((res)=>res.json());

      const initData = res.slice(0, 20).map((it)=>{
        return {
          author : it.email,
          content : it.body,
          emotion : Math.floor(Math.random() * 5) + 1,
          created_date : new Date().getTime(),
          id : dataId.current++
        }
      });

	  // dispatch 호출
	  // reducer는 action 객체를 받는데 INIT type을 받고,
      // 그 action의 필요한 data는 initData가 된다
      // 기존의 setData가 할 일은 reducer가 대신 한다
      
      dispatch({type:"INIT", data:initData});
  };


  // 생성하는 함수
  // dispatch 호출 CREATE type일 때 기존의 newItem에 있던 프로퍼티를 그대로 전달
  
  const onCreate = useCallback(
    (author, content, emotion) => {
      dispatch({type:"CREATE", data:{author, content, emotion, id:dataId.current}});
      dataId.current += 1; 
    }, []);


  // 삭제하는 함수
  // dispatch 호출 REMOVE type일 때 타겟아이디를 가진 일기를 지우라고 전달
  
  const onRemove = useCallback((targetId) => {
    dispatch({type:"REMOVE", targetId});
  },[]);


  // 수정하는 함수
  // dispatch 호출 EDIT type일 때 타겟아이디와 새로운 컨텐츠를 전달
  
  const onEdit = useCallback((targetId, newContent) => {
    dispatch({type:"EDIT", targetId, newContent});
  }, []);

...

}

  • useReducer를 이용할 때 상태 변화를 발생시키는 함수로 dispatch를 사용하였는데, dispatch는 함수형 업데이트와 상관없이 그냥 호출을 하면 알아서 현재의 state를 reduce 함수가 참조해서 자동으로 업데이트하므로 useCallback을 사용하면서 dependency array를 걱정하지 않아도 된다.
profile
필요한 내용을 공부하고 저장합니다.

0개의 댓글