[Udemy] React 기본 - 일기장 만들기(11) 최적화3 (컴포넌트&함수 재사용하기) : useCallback

productuidev·2022년 5월 3일
1

React Study

목록 보기
30/52
post-thumbnail

React 기본 (Project)

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


📌 일기장 만들기 (11) - 최적화3 (컴포넌트&함수 재사용하기) : useCallback

☑️ 컴포넌트 최적화 활용

  • 컴포넌트를 최적화하기 위해서는 어떤 컴포넌트가 최적화 대상인지 찾아낼 수 있어야 한다
  • 이전 시간에 배운 크롬 브라우저에 설치한 React Developers Tools 기능 활용 (Setting > General)

  • Highlight updates when components render : 어떤 컴포넌트가 지금의 행동에 의해서 리렌더링이 일어나고 있는지 색깔로 표시해주는 기능

  • 예 : 일기 하나를 삭제하면 나머지 일기 컴포넌트들도 깜빡이면서 리렌더링이 일어나고 있다고 알려주고 있음

  • useEffect와 console을 확인하면 다 확인할 수 있지만, 지금 실습처럼 컴포넌트를 적게 만든 게 아니라 실제 Product를 개발했을 때 10~20개 이상의 컴포넌트 생성/유지하는 상황이라면 특정 컴포넌트만 찾아 useEffect와 console을 일일이 작성하는 일은 굉장히 번거롭게 됨

  • 따라서, Highlight updates when components render 기능을 활용해 지금 어떤 컴포넌트가 연산 낭비되고 있는지 확인해보자.

☑️ 일기 삭제 시 작성 폼 리렌더링 문제

  • 일기 하나를 삭제했다고 작성 폼까지 리렌더링할 필요는 없음
  • 작성 폼인 상단의 DiaryEditor 컴포넌트를 최적화시켜보기

✔️ 컴포넌트는 언제 렌더링하는지 생각해보기

  • 본인이 가진 state에 변화가 생겼을 경우
  • 부모 컴포넌트가 리렌더링이 일어날 경우
  • 자신이 받은 prop이 변경될 경우

src/DiaryEditor.js

  • 현재 DiaryEditor는 onCreate 함수 하나만 prop으로 받고 있음
  • onCreate : 일기 저장하기 버튼을 눌렀을 때 데이터의 아이템을 추가하는 역할
import { useState, useRef } from "react";

const DiaryEditor = ({onCreate}) => {

  const [state, setState] = useState({
    author: "", content: "", emotion: 1,
  });
  
  ...
  
}

☑️ React.memo를 활용해 컴포넌트 최적화

src/DiaryEditor.js

// 최상단
import React, { useEffect, useState, useRef } from "react";

// 최하단
export default React.memo(DiaryEditor);
  • import React (useEffect도 확인해보기 위해 미리 import)
  • 코드가 길기 때문에 그냥 맨 마지막에 export default한 DiaryEditor 컴포넌트에 React.memo로 묶어주기
  • React.memo로 묶인 DiaryEditor를 밖으로 내보내겠다는 의미
  • useEffect와 console를 활용해 언제 진짜 렌더링이 일어나는지 확인
  useEffect(()=>{
    console.log("DiaryEditor Render");
  });

✔️ 왜 2번 콘솔에 출력됐을까?

src/App.js

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());
      console.log(res);

      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();
  },[]);
  • App 컴포넌트 렌더링 과정 생각해보기
    1) data state는 초기값이 빈 배열이라 1번 렌더링이 일어나서 diaryEditor도 1번 렌더링이 일어남
    2) 컴포넌트가 mount된 시점에 호출한 getData 함수에서 res API, initData를 연산
    3) 완성한 결과 setData에 전달하면서 data state가 1번 더 바뀌게 됨
  • 결론적으로 App 컴포넌트는 mount 되자마자 2번의 렌더링이 된 것
  • 그렇기 때문에 DiaryEditor가 전달받는 onCreate 함수도 App 컴포넌트가 렌더링이 되면서 계속 다시 생성되는 것
  • 그런데 onCreate 함수는 다시 생성이 되어도 똑같음

  • 이전 시간에 배운 비원시타입의 자료형의 비교는 React.memo에서 얉은 비교로 일어나기 때문에 결론적으로 prop으로 받고 있는 onCreate 함수가 App 컴포넌트가 렌더링될 때마다 계속 다시 만들어져서 onCreate를 prop으로 전달하는 것때문에 리렌더링이 발생하고 있는 문제

  • 일기를 삭제하면 DiaryEditor가 다시 렌더됨 (onCreate는 App 컴포넌트가 렌더링될 때마다 재생성되므로)

  • 📌 결론 : onCreate 함수가 재생성되지 않아야만 React.memo와 함께 최적화할 수 있음

src/App.js

  const onCreate = (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]); 
  };

☑️ Hooks API Reference

☑️ useCallback 활용해 최적화하기

src/App.js

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

function App() {

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

}
  • onCreate 함수에 useCallback을 활용해 렌더가 1번만 일어나게 되지만 새로운 문제 발생
  • 일기를 새롭게 저장하면 API로 불러온 일기 데이터(19개)가 삭제되고 새로 저장한 일기만 DiaryList 컴포넌트에 보여짐

  • 이유는 [] (Dependency Array)에 아무것도 안 넣어줘서 그럼
  • onCreate 함수는 App 컴포넌트가 mount 되는 시점에 1번만 생성되기 때문에 그 당시의 data state의 값이 빈 배열이므로 이런 현상이 발생

✔️ 함수는 컴포넌트가 재생성될 때 다시 생성되는 이유가 있다
왜냐하면 현재의 state 값을 참조할 수 있어야 하기 때문이다

  • 현재 onCreate 함수는 Callback 안에 갇혀서 Dependency Array를 빈 배열로 전달했기 때문에 이 onCreate 함수가 알고 있는 data의 값은 그대로 빈 배열임
setData([newItem, ...data]); 
  • 그러므로 빈 배열에 newItem을 추가해도 새로 저장한 일기 아이템만 보여지는 것

✔️ 정상적으로 작동시키려면?

  • Dependency Array에 data state를 넣어주기
  • 동작 에러 : 우리가 원하는 것은 data가 들어와도 onCreate 함수가 재생성되지 않는 건데 onCreate 함수가 재생성되지 않으면 최신의 data state를 참조할 수 없게 됨 (딜레마)

함수형 업데이트

  • setData에 값을 전달해서 새로운 state 값으로 바뀐다
  • 여기에 함수를 전달하는 것
  • 인자를 data를 받아서 newItem을 추가한 data를 리턴하는 콜백함수를 setData에 전달
  • 그러면 Dependency Array를 빈 배열로 두어도 항상 최신의 state를 setData의 인자를 통해서 참고할 수 있게 됨
  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]); 
    }, []);

☑️ 결과

  • 일기를 추가로 삭제해도 리렌더링되지 않음
profile
필요한 내용을 공부하고 저장합니다.

0개의 댓글