[React] 상태관리 로직 분리하기 with useReducer()

27px·2022년 10월 19일
1

useReducer()

:useReducer를 사용하면, 컴포넌트에서 상태변화 로직을 분리 시킬 수 있다!
useState처럼 컴포넌트의 상태를 관리해주는 React hooks
(useState를 대체할 수 있는 훌륭한 hook입니다.)
useState와 같이 비구조화 할당을 통해 사용합니다.

문법

import React, {useReducer} from "react";

// 중략
const [count, dispatch] = useReducer(reducer, 1);

count는 state입니다. dispatch는 상태를 변화시키는 액션을 발생시키는 함수입니다.
우항에 useReducer를 호출할 때에는 꼭 인자로 reducer라는 함수를 전달해야 합니다.
dispatch는 상태변화를 일으키는 역할을 하는데 이 일어난 상태변화를 처리해주는 것이 reducer함수입니다. 2번째로 전달하는 인자는 count state의 초기값입니다.

ReducerEx.js

import React, { useReducer } from "react";

const reducer = (state, action) => {
  switch (action.type) {
    case 1:
      return state + 1;
    case 10:
      return state + 10;
    case 100:
      return state + 100;
    case 1000:
      return state + 1000;
    default:
      //기존값 리턴
      return state;
  }
};

function ReducerEx() {
  const [count, dispatch] = useReducer(reducer, 1);

  return (
    <div>
      {count}
      <button onClick={() => dispatch({ type: 1 })}>add 1</button>
      <button onClick={() => dispatch({ type: 10 })}>add 10</button>
      <button onClick={() => dispatch({ type: 100 })}>add 100</button>
      <button onClick={() => dispatch({ type: 1000 })}>add 1000</button>
    </div>
  );
}

export default DispatchEx;

예시 코드를 보면, dispatch에게 객체를 전달하고 있는데 이 객체를 action 객체라고 부릅니다. action이 곧 상태변화! 라고 생각하면 됩니다.

action = 상태변화를 설명할 객체 //"reducer야 너가 처리해야할 상태변화 이런 내용임 내가 들고옴~"

dispatch를 호출하면 상태변화가 일어나야 하고 그 상태변화에 대한 처리는 reducer에서 수행합니다.

reduce의 인자로는 state와 action이 있는 걸 확인했습니다.

  • 여기서 state란 현재 state를 가리키며 action은 dispatch를 통해 전달해준 액션 객체를 말합니다.

reducer함수는 switch case문을 통해서 액션 타입에 따른 다른 state값을 return해주고 있습니다. return으로 반환하는 것은 새로운 state입니다.

key point 👾 dispatch 함수를 useCallback 내부에서 사용할 때, useCallback의 deps값을 신경쓰지 않아도 된다. reducer가 알아서 현재의 state값을 기억하고 참조하기 때문임

App.js컴포넌트에서 분리된 상태로직 결과물

  • useState() -> useReducer()
  • setData() -> dispatch()
  • reducer 함수는 따로 만들어 줘야 합니다. 보면 App컴포넌트의 내부가 아니라 외부에 작성한 이유이기도 합니다.(원래 useReducer 사용한 이유가 컴포넌트와 상태관리 로직을 분리하는 거였으니까^ㅅ^*)
import React, {
  useRef,
  useEffect,
  useMemo,
  useCallback,
  useReducer,
} from "react";
import "./App.css";
import DiaryList from "./components/DiaryList";
import DiaryEditor from "./DiaryEditor";

const reducer = (state, action) => {
  switch (action.type) {
    case "INIT": {
      return action.data;
    }
    case "CREATE": {
      const created_date = new Date().getTime();
      const newItem = {
        ...action.data,
        created_date,
      };
      return [newItem, ...state];
    }
    case "REMOVE": {
      return state.filter((item) => item.id !== action.targetId);
    }
    case "EDIT": {
      return state.map((item) =>
        //기존 state의 item의 아이디가 수정하려는 item의 아이디(targetId) 와 같으면
        //해당 객체의 기존 프로퍼티는 그대로 두고, 콘텐츠의 내용만 새로운 아이로 업데이트
        //주의점: contents가 뒤로 가게 전개 연산자와 같이 사용
        //수정하려는 item이 아니라면 원래 item 그대로 return
        item.id === action.targetId
          ? { ...item, contents: action.newContents }
          : item
      );
    }
    default:
      return state;
  }
};

function App() {
  //useReducer를 통해 상태관리를 할 것이라서 useState는 주석처리
  // const [data, setData] = useState([]);

  //useReducer의 2번째 인자는 항상 dispatch로
  const [data, dispatch] = useReducer(reducer, []);

  const getData = async () => {
    const res = await fetch(
      "https://jsonplaceholder.typicode.com/comments"
    ).then((res) => res.json());
    console.log(res);

    const initData = res.slice(0, 20).map((item) => {
      return {
        author: item.email,
        contents: item.body,
        emotion: Math.floor(Math.random() * 5) + 1,
        created_date: new Date().getTime(),
        id: dataId.current++,
      };
    });
    //action 객체에 타입을 적어주고 어떤 데이터로 state를 변경할지도 넘겨줘야 하기 때문에
    //setData로 전달해준 새로운 state를 data라는 이름으로 액션객체에 같이 실어줍니다.
    dispatch({ type: "INIT", data: initData });
    // setData(initData);
  };
  useEffect(() => {
    getData();
  }, []);

  //useRef를 사용하여 고유 아이디를 만들 수 있다.
  const dataId = useRef(0);

  //리스트 아이템 추가 기능
  const onCreate = useCallback((author, contents, emotion) => {
    dispatch({
      type: "CREATE",
      data: { author, contents, emotion, id: dataId.current },
    });

    dataId.current += 1;
  }, []);

  //리스트 아이템 삭제 기능
  const onRemove = useCallback((targetId) => {
    dispatch({ type: "REMOVE", targetId });
  }, []);

  //리스트 아이템 수정 기능
  const onEdit = useCallback((targetId, newContents) => {
    dispatch({ type: "EDIT", targetId, newContents });
  }, []);

  //리액트에서는 return을 가지고 있는 함수를 메모이제이션할 수 있다.
  //메모이제이션하고 싶은 함수를 useMemo를 통해 감싸주면 된다.
  //useMemo()의 첫번째 인자로 콜백함수를 받고 이 콜백함수가 리턴하는 값(연산)을 최적화할 수 있도록 도와준다.
  //useMemo(콜백함수, [deps])-> 콜백함수를 유즈메모로 감싸주면 리턴해주는 아이는 더 이상 함수가 아니다.
  //그렇기 때문에 호출할 때에 ()는 없애줘야 한다. deps는 useEffect의 의존성 배열과 같은 역할을 한다.
  //해당 값에 어떤 값이 변했을 때에만 콜백 연산을 다시 수행할 지 알려준다.

  const getDiaryAnalysis = useMemo(() => {
    const goodCount = data.filter((item) => item.emotion >= 3).length;
    const badCount = data.length - goodCount;
    const goodRatio = Math.floor((goodCount / data.length) * 100);
    return { goodCount, badCount, goodRatio };
  }, [data.length]);

  //useMemo를 함수에 씌워주었으니
  // const { goodCount, badCount, goodRatio } = getDiaryAnalysis();
  const { goodCount, badCount, goodRatio } = getDiaryAnalysis;

  return (
    <div className="App">
      <DiaryEditor onCreate={onCreate} />
      <div>전체 일기: {data.length}</div>
      <div>기분 좋은 일기 개수 : {goodCount}</div>
      <div>기분 안좋은 일기 개수: {badCount}</div>
      <div>기분 좋은 일기 퍼센트: {goodRatio}%</div>
      <DiaryList diaryList={data} onRemove={onRemove} onEdit={onEdit} />
    </div>
  );
}

profile
안녕하세요?

0개의 댓글