[React] 성능 최적화 3 - useCallback

이재훈·2023년 6월 9일
0

React

목록 보기
17/27

목표

기존 프로젝트 컴포넌트 최적화

컴포넌트 렌더링 되는 시점

  • 본인이 가진 state의 변화
  • 부모 컴포넌트의 리렌더링
  • 자신이 가진 props의 변경

이 점을 생각하면서 컴포넌트 최적화를 해보도록 하겠습니다.

DiaryEditor.js

import React, { useEffect, useRef, useState } from "react";

const DiaryEditor = ({ onCreate }) => {
  useEffect(() => {
    console.log("DiaryEditor 렌더");
  });

  const nameInput = useRef();
  const contentInput = useRef();

  const [state, setState] = useState({
    name: "",
    content: "",
    hungry: 10,
  });

  const handleChangeState = (e) => {
    setState({
      ...state,
      [e.target.name]: e.target.value,
    });
  };

  const handleSubmit = () => {
    if (state.name.length < 1) {
      nameInput.current.focus();
      return;
    }

    if (state.content.length < 5) {
      contentInput.current.focus();
      return;
    }

    onCreate(state.name, state.content, state.hungry);

    setState({
      name: "",
      content: "",
      hungry: 10,
    });
  };

  return (
    <div className="DiaryEditor">
      <h2>오늘의 먹방 일기</h2>
      <div>
        이름
        <input
          ref={nameInput}
          name="name"
          value={state.name}
          onChange={handleChangeState}
        />
      </div>
      <div>
        내용
        <textarea
          ref={contentInput}
          name="content"
          value={state.content}
          onChange={handleChangeState}
        />
      </div>
      <div>
        배고픔 정도
        <select name="hungry" value={state.hungry} onChange={handleChangeState}>
          <option value={10}>10</option>
          <option value={20}>20</option>
          <option value={30}>30</option>
          <option value={40}>40</option>
          <option value={50}>50</option>
          <option value={60}>60</option>
          <option value={70}>70</option>
          <option value={80}>80</option>
          <option value={90}>90</option>
          <option value={100}>100</option>
        </select>
      </div>
      <div>
        <button onClick={handleSubmit}>일기 저장하기</button>
      </div>
    </div>
  );
};

export default React.memo(DiaryEditor);
export default React.memo(DiaryEditor);

React.memo를 맨 아래 달았습니다.

useEffect(() => {
  console.log("DiaryEditor 렌더");
});

useEffect를 사용하여 렌더링 될 때 로그를 남겨보겠습니다.

페이지를 새로고침을 하니 로그에 2번 출력되는 것을 확인 할 수 있습니다.

  • 첫번째 렌더링 : 빈배열로 렌더링
  • 두번째 렌더링 : useEffect 안에 Mount 될 때 getData() 호출로 인해 완성된 결과를 setData를 하면서 Data state가 변경이 되면서 렌더링
const onCreate = (name, content, hungry) => {
  const created_date = new Date().getTime();
  const newItem = {
    name,
    content,
    hungry,
    created_date,
    id: dataId.current,
  };
  dataId.current += 1;
  setData([newItem, ...data]);
};

현재 props로 onCreate를 넘기고 있습니다. 이것을 React.memo로 현재는 얕은 비교를 하고 있기 때문에 불필요한 렌더링이 계속됩니다. 이것을 최적화 해보도록 하겠습니다.

useCallback

const memoizedCallback = useCallback(
  () => {
    doSomething(a, b);
  },
  [a, b],
);

메모이제이션된 콜백을 반환합니다.
dependency array가 변경되게 되면 콜백 함수 자체를 반환합니다. 불필요한 렌더링을 방지하기 위해 참조의 동일성에 의존적인 최적화된 자식 컴포넌트에 콜백으로 전달될 때 유용합니다.

설명으로는 어려우니 실습을 해보도록 하겠습니다.

현재 onCreate 함수가 불필요하게 실행되고 있기 때문에 onCreate 함수에 useCallback으로 감싸주도록 합니다.

App.js

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

  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([newItem, ...data]);
  }, []);


useCallback을 추가하니 처음 렌더링 될 때만 "DiaryEditor" 로그가 남는 것을 확인할 수 있습니다. 게시글을 삭제해도 렌더링이 새로되지 않습니다. 그럼 잘 된 것일까요? 게시글 하나를 저장해보도록 하겠습니다.


기존의 20개의 일기가 삭제되고 직전에 저장했던 데이터만 남아있는 것을 확인할 수 있습니다.

이 이유는 dependency array가 빈 배열이기 때문입니다. onCreate 함수는 컴포넌트가 마운트 되는 순간에만 생성되기 때문에 그 당시 data state는 빈 배열입니다. 그래서 onCreate함수가 마지막으로 생성되었을 때는 빈 배열의 state를 가지고 있습니다. 그래서 이런 현상이 생기게 된 것입니다.

그렇다면 dependency array에 data를 넣어주어야 합니다.

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([newItem, ...data]);
  }, [data]);

이렇게 하면 data가 변경되게 되면 안에 함수를 생성하게 됩니다. 하지만 이것은 원하는 동작이 아닙니다. 최신의 data를 적용시켜야 하지만 data가 변화된다고 onCreate 함수를 실행시키면 안되는 딜레마에 빠진 것입니다... ㅇ0ㅇ

결론 : 함수형 업데이트를 활용하자

setData((data) => [newItem, ...data]);

setData(상태변화 함수)에 함수를 전달하여 최신 데이터가 업데이트 되게 할 수 있습니다.

  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]);
  }, []);

dependency array는 다시 빈 배열로 바꿔줍니다.

useCallback hooks을 사용하여 DiaryEditor 컴포넌트는 한번만 생성이 되고 최신 데이터는 항상 유지될 수 있게 적용해 보았습니다.


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

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

0개의 댓글