[React] 복잡한 상태 관리 로직 분리하기 - useReducer

이재훈·2023년 6월 13일
0

React

목록 보기
19/27

App.js

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

function App() {
  const [data, setData] = useState([]);

  const dataId = useRef(0);

  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 {
        name: it.email,
        content: it.body,
        hungry: (Math.floor(Math.random() * 9) + 1) * 10,
        created_date: new Date().getTime(),
        id: dataId.current++,
      };
    });
    setData(initData);
  };

  useEffect(() => {
    getData();
  }, []);

  const onCreate = useCallback((name, content, hungry) => {
    const created_date = new Date().getTime();
    const newItem = {
      name,
      content,
      hungry,
      created_date,
      id: dataId.current,
    };
    dataId.current += 1;
    setData((data) => [newItem, ...data]);
  }, []);

  const onRemove = useCallback((targetId) => {
    setData((data) => data.filter((it) => it.id !== targetId));
  }, []);

  const onEdit = useCallback((targetId, newContent) => {
    setData((data) =>
      data.map((it) =>
        it.id === targetId ? { ...it, content: newContent } : it
      )
    );
  }, []);

  const getDiaryAnalysis = useMemo(() => {
    const reallyHungy = data.filter((it) => it.hungry >= 80).length;
    const notHungry = data.filter((it) => it.hungry < 40).length;
    const reallyHungryRatio = (reallyHungy / data.length) * 100;
    const notHungryRatio = (notHungry / data.length) * 100;
    return { reallyHungy, notHungry, reallyHungryRatio, notHungryRatio };
  }, [data.length]);

  const { reallyHungy, notHungry, reallyHungryRatio, notHungryRatio } =
    getDiaryAnalysis;

  return (
    <div className="App">
      <DiaryEditor onCreate={onCreate} />
      <div>전체 일기 : {data.length}</div>
      <div>배부른 일기 개수 : {notHungry}</div>
      <div>배고픈 일기 개수 : {reallyHungy}</div>
      <div>배부른 일기 비율 : {notHungryRatio}%</div>
      <div>배고픈 일기 비율 : {reallyHungryRatio}%</div>
      <DiaryList onEdit={onEdit} onRemove={onRemove} diaryList={data} />
    </div>
  );
}

export default App;

현재 제 컴포넌트 최상단의 App 컴포넌트입니다. onCreate, onEdit, onRemove 상태 변화 처리함수를 가지고 있습니다. 컴포넌트가 길어지고 무거워지는 것은 좋은 것이 아닙니다. 그래서 복잡하고 긴 상태 변화 처리 함수를 컴포넌트 바깥으로 분리해보도록 하겠습니다.

useReducer

useState 처럼 react의 상태관리를 돕는 react hooks 입니다. useReducer를 이용하면 상태 관리 로직들을 컴포넌트에서 분리할 수 있습니다. 결과적으로 컴포넌트를 가볍게 사용할 수 있습니다.

import { useCallback, useEffect, useMemo, useReducer, useRef } from "react";

// const [data, setData] = useState([]);

const [data, dispatch] = useReducer(reducer, []);

기존에 사용하던 useState를 주석처리 해주고, useReducer hooks를 선언해주도록 합니다.

  • data : state
  • dispatch : 항상 dispatch로 선언
  • useReducer : react hooks
  • reducer : 상태변화를 처리할 함수
  • [] : 초기값

이제 컴포넌트 바깥에 reducer를 선언해주도록 합니다.

const reducer = (state, action) => {
  switch (action.type) {
    case "INIT": 
    case "CREATE":
    case "REMOVE":
    case "EDIT":
    default:
      return state;
  }
};

이런 형태로 작성을 할 것입니다. 주의할 점은 default는 필수입니다.

  • state : 상태변화 직전의 상태
  • action : 어떤 상태 변화를 일으켜야하는지에 대한 정보

getData 변경

  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 {
        name: it.email,
        content: it.body,
        hungry: (Math.floor(Math.random() * 9) + 1) * 10,
        created_date: new Date().getTime(),
        id: dataId.current++,
      };
    });
    dispatch({ type: "INIT", data: initData });
    // setData(initData); 주석처리
  };

setData를 주석처리하고 대신 dispatch를 사용해줍니다. type을 INIT으로 보내고, init action에 필요한 데이터인 initData를 보냅니다.

const reducer = (state, action) => {
  switch (action.type) {
    case "INIT": {
      return action.data;
    }
    case "CREATE":
    case "REMOVE":
    case "EDIT":
    default:
      return state;
  }
};

reducer 함수에서는 return 값은 새로은 state 객체를 의미합니다. init case일 시 그 값으로 리턴해주면 새로운 state가 됩니다.

onCreate 변경

  const onCreate = useCallback((name, content, hungry) => {
    dispatch({
      type: "CREATE",
      data: { name, content, hungry, id: dataId.current },
    });

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

useCallback 함수 안에 dispatch를 선언해줍니다. 기존에 setData함수도 지워줍니다.

const reducer = (state, action) => {
  switch (action.type) {
    case "INIT": {
      return action.data;
    }
    case "CREATE": {
      const created_date = new Date().getTime();
      const newItem = {
        ...action,
        created_date,
      };
      return [newItem, ...state];
    }
    case "REMOVE": 
    case "EDIT": 
    default:
      return state;
  }
};

reducer 함수 안에 있는 create case에 로직을 작성해줍니다.

onRemove 변경

  const onRemove = useCallback((targetId) => {
    dispatch({ type: "REMOVE", targetId });
  }, []);

targetId를 넘겨 reducer함수에서 제거해주도록 하겠습니다.

const reducer = (state, action) => {
  switch (action.type) {
    case "INIT": {
      return action.data;
    }
    case "CREATE": {
      const created_date = new Date().getTime();
      const newItem = {
        ...state,
        created_date,
      };
      return [newItem, ...state];
    }
    case "REMOVE": {
      return state.filter((it) => it.id !== action.targetId);
    }
    case "EDIT":
    default:
      return state;
  }
}

filter 함수를 사용하여 remove를 수행합니다.

onEdit 변경

  const onEdit = useCallback((targetId, newContent) => {
    dispatch({ type: "EDIT", targetId, newContent });
  }, []);

targetId와 newContent를 넘겨줍니다.

const reducer = (state, action) => {
  switch (action.type) {
    case "INIT": {
      return action.data;
    }
    case "CREATE": {
      const created_date = new Date().getTime();
      const newItem = {
        ...action,
        created_date,
      };
      return [newItem, ...state];
    }
    case "REMOVE": {
      return state.filter((it) => it.id !== action.targetId);
    }
    case "EDIT": {
      return state.map((it) =>
        it.id === action.targetId ? { ...it, content: action.newContent } : it
      );
    }
    default:
      return state;
  }
};

map 함수를 사용하여 targetId가 같으면 컨텐츠를 변경합니다.

useState에서 useReducer로 모두 변경해보았습니다.
최종 코드입니다.

App.js

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

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((it) => it.id !== action.targetId);
    }
    case "EDIT": {
      return state.map((it) =>
        it.id === action.targetId ? { ...it, content: action.newContent } : it
      );
    }
    default:
      return state;
  }
};

function App() {
  // const [data, setData] = useState([]);

  const [data, dispatch] = useReducer(reducer, []);

  const dataId = useRef(0);

  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 {
        name: it.email,
        content: it.body,
        hungry: (Math.floor(Math.random() * 9) + 1) * 10,
        created_date: new Date().getTime(),
        id: dataId.current++,
      };
    });
    dispatch({ type: "INIT", data: initData });
    // setData(initData);
  };

  useEffect(() => {
    getData();
  }, []);

  const onCreate = useCallback((name, content, hungry) => {
    dispatch({
      type: "CREATE",
      data: { name, content, hungry, id: dataId.current },
    });

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

  const onRemove = useCallback((targetId) => {
    dispatch({ type: "REMOVE", targetId });
  }, []);

  const onEdit = useCallback((targetId, newContent) => {
    dispatch({ type: "EDIT", targetId, newContent });
  }, []);

  const getDiaryAnalysis = useMemo(() => {
    const reallyHungy = data.filter((it) => it.hungry >= 80).length;
    const notHungry = data.filter((it) => it.hungry < 40).length;
    const reallyHungryRatio = (reallyHungy / data.length) * 100;
    const notHungryRatio = (notHungry / data.length) * 100;
    return { reallyHungy, notHungry, reallyHungryRatio, notHungryRatio };
  }, [data.length]);

  const { reallyHungy, notHungry, reallyHungryRatio, notHungryRatio } =
    getDiaryAnalysis;

  return (
    <div className="App">
      <DiaryEditor onCreate={onCreate} />
      <div>전체 일기 : {data.length}</div>
      <div>배부른 일기 개수 : {notHungry}</div>
      <div>배고픈 일기 개수 : {reallyHungy}</div>
      <div>배부른 일기 비율 : {notHungryRatio}%</div>
      <div>배고픈 일기 비율 : {reallyHungryRatio}%</div>
      <DiaryList onEdit={onEdit} onRemove={onRemove} diaryList={data} />
    </div>
  );
}

export default App;

useReducer를 사용하여 복잡한 상태관리 로직을 컴포넌트로부터 분리를 해보았습니다.


리액트 공식 홈페이지
https://ko.legacy.reactjs.org/docs/react-api.html#reactmemo
해당 게시글은 인프런 강의
"한입 크기로 잘라 먹는 리액트(React.js) : 기초부터 실전까지(이정환)"
를 정리한 내용입니다. 쉽게 잘 설명해주시니 여러분도 강의를 듣는 것을 추천드립니다.

profile
부족함을 인정하고 노력하자

0개의 댓글