[React] useCallback(), 함수형업데이트 (feat.일기장)

Hyun·2022년 1월 10일
0

React

목록 보기
18/22
post-thumbnail


크롬의 확장도구로 React Developer Tools를 사용하면 보다 더 편하다.
오늘은 components에서 highlight updates when components render를 이용해서 화면을 볼 예정이다.


📖예제(함수형업데이트 사용 전)

일기리스트에 있는 목록 중 하나를 삭제하면 DiaryEditor컴포넌트도 깜빡(render)되는것을 볼 수 있다.
근데, 일기리스트와 일기작성폼(DiaryEditor컴포넌트)는 연관이 없는데 불필요한 rendering이 일어난것을 useCallback을 사용하여 개선해보도록 하자!

<불필요한 rendering이 일어난 이유>

DiaryEditor.js컴포넌트의 DiaryEditor함수는 App.js컴포넌트로부터 onCreate함수를 prop으로 받고있다.
(onCreate함수는 저장하기버튼을 눌렀을때 data에 Item을 일기리스트에 추가해주는 역할)

DiaryEditor.js컴포넌트에 React.memo를 적용하고, (눈으로 확인할 용도)useEffect로 DiaryEditor가 언제 렌더링되는지 콘솔로 확인한다

render가 두번 발생한것을 확인할 수 있는데

<첫번째 render>
App.js컴포넌트에 App함수를보면 const [data, setData] = useState([]);data state의 useState값이 빈배열로 한번 rendering이 일어나고 DiaryEditor도 빈배열인 상태에서 rendering이 일어난다
<두번째 render>
useEffect(() => {getData();}, []);App컴포넌트가 mount되는시점에 호출한 getData함수에서 결과를 API에서 가져와 완성된 결과를 setData(initData);setData함수에 넣어 state값을 바꿔서 rendering이 일어난다

App컴포넌트 mount되자마자 2번 렌더링이 된것 그래서 DiaryEditor가 prop으로 전달받는 onCreate함수도 렌더링이 된것

전에 배웠던 비원시형 자료의 비교는 얕은비교를 하기때문에
DiaryEditor가 prop으로 전달받는 onCreate함수가 얕은비교하여 계속 렌더링되기에 이 함수가 재생성되지않아야 DiaryEditor함수가 최적화된다

그럼 App.js에 있는 onCreate에 useMemo를 쓰면 될거같지만
useMemo는 함수반환이 아닌 값을 반환하기때문에 적절하지않으므로 함수반환을 해주는 useCallback함수를 적용하면된다.

App.js코드

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

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

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

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

  const onDelete = (targetId) => {
    const newDiaryList = data.filter((it) => it.id !== targetId);
    setData(newDiaryList);
  };

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

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

  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 onEdit={onEdit} onDelete={onDelete} diaryList={data} />
    </div>
  );
};

export default App;

<코드 설명>

1) useCallback이 import되었는지 확인하기
import { useCallback, useMemo, useEffect, useRef, useState } from "react";

2) 우리가 원하는것은 onCreate함수가 다시 생성되지않게 하는것으로 useCallback함수를 사용하여
첫번째인자로 전달한 콜백함수에 DiaryEditor함수를
두번째인자로는 deps를 전달하는데, 빈배열로 전달을하여 mount되는 시점을 한번만 만들고 그다음부터는 첫번째만들어두었던 함수를 재사용할 수 있도록 만들었다.
const onCreate = useCallback((author, content, emotion) => {
const created_date = new Date().getTime();
const newItem = {
author,
content,
emotion,
created_date,
id: dataId.current,
};
dataId.current += 1;
setData([newItem, ...data]);
}, []);

정상적으로 작동되는지 확인해보자.
useCallback이 잘 작동했다면 일기리스트를 삭제했을때 DiartEditor render가 출력되지않을것이다

🖥결과화면(문제 발생_함수형업데이트 필요)


useCallback함수를 적용한 결과 20개 리스트일기 중 5개를 삭제했는데 DiaryEditor컴포넌트는 mount되는시점(빈배열상태)만 한번 render가 되고 이후 불필요한 render가 안되는것을 볼 수 있다.


근데 여기서 문제가 발생한다..

한번 render되는것으로 옳게 작동하는것같지만, onCreate함수가 잘 작동되는지 확인하기위해 일기를 작성하고 저장하기 버튼을 누르면
기존에 있던 일기리스트들은 다 삭제되고 현재 작성한 일기만 리스트에 뜨는것을 확인할 수 있다.

그 이유는 위에 말했듯이 DiaryEditor컴포넌트는 mount되는시점(빈배열상태)이다. DiaryEditor에는 빈배열로 전달되어 이후 계속 빈배열인 상태인것이다.

deps에 빈배열로 전달하면 mount되는 시점만 render가 되고 리스트는 빈배열로 전달되기때문에 새로 일기를 저장하면 있었던 일기들은 지워지고 한개의일기만 리스트에 뜨게된다.
그렇다고 deps에 data값을 넣어주면 또 리스트가 삭제될때마다 리렌더링이 일어나게되는 딜레마에 빠진다

이때 함수형업데이트(setState함수에 함수를 전달하는것)를 사용하면된다.

📖예제(함수형업데이트 사용 후)

<코드 설명>

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

setData에 화살표함수로 data를 받아서 아이템에 추가한 data를 리턴하는 콜백함수를 전달함

🖥결과화면


일기를 추가해도 리스트에 잘 추가가되고, DiaryEditor render도 한번만 뜨는것을 확인 할 수 있었다.

useCallback과 함수형업데이트를 적절하게 잘 사용하여 최적화하는방법을 배웠다.


🚀참고자료

React강의-이정환강사

profile
FrontEnd Developer (with 구글신)

0개의 댓글