티스토리 뷰

반응형

 

리액트에서는 

  1. 컴포넌트에서 state가 바뀌었을 때
  2. 컴포넌트가 내려받은 props가 변경되었을 때
  3. 부모 컴포넌트가 리-렌더링 된 경우 자식 컴포넌트는 모두

위경우 , 렌더링이 되는데 이러한 과정을 줄이는것을 최적화 라고 한다.

 

그리고 최적화를 도와주는 리액트 훅이 있는데,

React.memo, useCallback, useMemo 에 대해 정리해보고자 한다.

이녀석들은 다음과같은 기능을 가지고 있다.

  • memo(React.memo) : 컴포넌트 재사용
  • useCallback : 함수 재사용
  • useMemo : 재사용

 

 

1.memo(React.memo) => 컴포넌트 캐싱

컴포넌트를 여러트리로 설계함에 있어 리렌더링이 발생한다.

  • 1번 컴포넌트가 리렌더링 된 경우, 2~7번이 모두 리렌더링 된다.
  • 4번 컴포넌트가 리렌더링 된 경우, 6, 7번이 모두 리렌더링 된다.

1번에서 만약 state, props 값이 변경된경우 하위 컴포넌트는 모두 리렌더링되는데

불필요한 리렌더링을 막기위해 사용하는것이 컴포넌트를 재사용하는 React.memo 다.

 

1-1. React.memo 사용법

export default React.memo(컴포넌트1);
export default React.memo(컴포넌트2);
export default React.memo(컴포넌트3);

React.memo를 사용하여 각각 페이지에서 export하게 되면 , 컴포넌트1,2,3를 사용하는 상위 컴포넌트에서,

렌더링할경우 상위 컴포넌트만 호출이되고, 컴포넌트 1,2,3을 재호출하지 않음 

 

 

2.useCallback => 함수를 캐싱

예제를 먼저 보도록하자.

// count를 초기화해주는 함수
  const initCount = () => {
    setCount(0);
  };

  return (
    <>
      <h3>카운트 예제입니다!</h3>
      <p>현재 카운트 : {count}</p>
      <button onClick={onPlusButtonClickHandler}>+</button>
      <button onClick={onMinusButtonClickHandler}>-</button>
      <div style={boxesStyle}>
        <Box1 initCount={initCount} />
        <Box2 />
        <Box3 />
      </div>
    </>
  );
}

Box1 컴포넌트에 값을 리셋하는 함수를 전달하였다. 

 

클릭하게 되면?

그렇다. Box1을 React.memo를 사용하더라도, 리렌더링이 발생된다. 함수를  사용하게 되면 함수도 객체의종류라서,

컴포넌트에 모양은 같더라도, 다시 만들면 그 주솟값이 달라지고 이에 따라 하위 컴포넌트인 Box1.jsx는 props가 변경됐다고 인식한다.

 

2-1.  useCallback 사용법

-App.jsx

// 변경 전
const initCount = () => {
  setCount(0);
};

// 변경 후
const initCount = useCallback(() => {
  setCount(0);
}, []);

함수를 useCallback으로 감싸면 , 함수를 할당받은 컴포넌트는 리렌더링이 되지 않는다. 

 

 

2-2 useCallback Dependency Array 사용법

...

// count를 초기화해주는 함수
const initCount = useCallback(() => {
  console.log(`[COUNT 변경] ${count}에서 0으로 변경되었습니다.`);
  setCount(0);
}, [count]);

...

디펜던시를 사용하지 않게 되면 무조건 [ Count 변경 ] 0 에서 0으로 변경

[count] 사용하면,  [ Count 변경 ] ${누른횟수} 에서 0으로 변경

 

useCallback이 count가 0일 때의 시점을 기준으로

메모리에 함수를 저장했기 때문에 dependency array가 필요!

 

한가지 예제를 더 살펴보자.

fetchUser 함수가 변경될 때만 호출 (의존성배열)

function Profile({ userId }) {
  const [user, setUser] = useState(null);

  const fetchUser = () =>
    fetch(`https://your-api.com/users/${userId}`)
      .then((response) => response.json())
      .then(({ user }) => user);

  useEffect(() => {
    fetchUser().then((user) => setUser(user));
  }, [fetchUser]);

  // ...
}

fetchUser는 함수( =객체 , = 메모리 주솟값 참조 , = 메모리변경시 리랜더링 )이기 때문에,

props에 있는 userId 값이 바뀌든 말든 컴포넌트가 랜더링될 때 마다 새로운 참조값으로 변경되어,

실행이 된다. 

 

그러면 useEffect() 함수가 호출되어 user 상태값이 바뀌고 그러면 다시 랜더링

(변경되면 리렌더링 => 무한루프

 

그러면 useEffect() 함수가 호출되어 user 상태값이 바뀌고 그러면 다시 랜더링이 되고 그럼 또 다시 useEffect() 함수가 호출되는 악순환이 반복

 

이와 같은 상황에서 useCallback() hook 함수를 이용하면

function Profile({ userId }) {
  const [user, setUser] = useState(null);

  const fetchUser = useCallback(
    () =>
      fetch(`https://your-api.com/users/${userId}`)
        .then((response) => response.json())
        .then(({ user }) => user),
    [userId]
  );

  useEffect(() => {
    fetchUser().then((user) => setUser(user));
  }, [fetchUser]);

  // ...
}

 

 

컴포넌트가 다시 랜더링되더라도 fetchUser 함수의 참조값을 동일하게 유지시킬 수 있다

따라서 의도했던 대로, useEffect()에 넘어온 함수는 userId 값이 변경되지 않는 한 재호출 되지 않게 된다.

 

useCallback 사용법 또다른 예제 (독립컴포넌트에 함수를넘길때 )

import React, { useState, useCallback } from "react";

export default function SmartHome() {
  const [masterOn, setMasterOn] = useState(false);
  const [kitchenOn, setKitchenOn] = useState(false);
  const [bathOn, setBathOn] = useState(false);

  const toggleMaster = useCallback(() => setMasterOn(!masterOn), [masterOn]);
  const toggleKitchen = useCallback(
    () => setKitchenOn(!kitchenOn),
    [kitchenOn]
  );
  const toggleBath = useCallback(() => setBathOn(!bathOn), [bathOn]);

  return (
    <>
      <Light room="침실" on={masterOn} toggle={toggleMaster} />
      <Light room="주방" on={kitchenOn} toggle={toggleKitchen} />
      <Light room="욕실" on={bathOn} toggle={toggleBath} />
    </>
  );
}

 

3.useMemo

예제 먼저 볼까요?

  return (
    <>
      <nav style={navStyleObj}>네비게이션 바</nav>
      <HeavyComponent />
      <footer style={footerStyleObj}>푸터 영역이에요</footer>
    </>
  );

 

 

HeavyComponent 에서 연산이 오래걸리거나 랜더링하기 무거운 친구라면 다른 state가 바뀔 때 마다 계속해서 호출이 되는데,

이를 최적화 하기 위한 훅이 useMemo 입니다.

 

3-1.useMemo 사용법

function HeavyButton() {
  const [count, setCount] = useState(0);

  const heavyWork = () => {
    for (let i = 0; i < 1000000000; i++) {}
    return 100;
  };

	// CASE 1 : useMemo를 사용하지 않았을 때
  const value = heavyWork();

	// CASE 2 : useMemo를 사용했을 때
  // const value = useMemo(() => heavyWork(), []);

  return (
    <>
      <p>나는 {value}을 가져오는 엄청 무거운 작업을 하는 컴포넌트야!</p>
      <button
        onClick={() => {
          setCount(count + 1);
        }}
      >
        누르면 아래 count가 올라가요!
      </button>
      <br />
      {count}
    </>
  );
}

 

 

이런 예제도 있습니다.

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

function ObjectComponent() {
  const [isAlive, setIsAlive] = useState(true);
  const [uselessCount, setUselessCount] = useState(0);

  const me = {
    name: "Ted Chang",
    age: 21,
    isAlive: isAlive ? "생존" : "사망",
  };

  useEffect(() => {
    console.log("생존여부가 바뀔 때만 호출해주세요!");
  }, [me]);

  return (
    <>
      <div>
        내 이름은 {me.name}이구, 나이는 {me.age}야!
      </div>
      <br />
      <div>
        <button
          onClick={() => {
            setIsAlive(!isAlive);
          }}
        >
          누르면 살았다가 죽었다가 해요
        </button>
        <br />
        생존여부 : {me.isAlive}
      </div>
      <hr />
      필요없는 숫자 영역이에요!
      <br />
      {uselessCount}
      <br />
      <button
        onClick={() => {
          setUselessCount(uselessCount + 1);
        }}
      >
        누르면 숫자가 올라가요
      </button>
    </>
  );
}

export default ObjectComponent;

unlessCount 버튼을 눌러 숫자가 올라가는 버튼을 눌렀는데,

useEffectt가 작동되어서 콘솔이 찍힙니다. 왜그럴까?

 

uselessCount에 state가 바뀌면

리렌더링되고,

컴포넌트 함수가 새로 호출,

me 객체도 다시 할당(이 때, 다른 메모리 주소값을 할당받기때문),

useEffect의 dependency array에 의해 me 객체가바뀌었는지 확인,

me의 메모리 주소가 바뀐줄알고 console이 출력.

 

이런 상황을 해결하기 위해서 또한 useMemo를 활용할 수 있다.

const me = useMemo(() => {
  return {
    name: "Ted Chang",
    age: 21,
    isAlive: isAlive ? "생존" : "사망",
  };
}, [isAlive]);

 

프트웨어의 성능 최적화에는 그에 상응하는 대가(코드가 복잡해지거나 유지보수가 어려워짐)가 따르기 마련

따라서, useCallback()를 사용하시기 전에 실질적으로 얻을 수 있는 성능 이점이 어느 정도인지 반드시 직접 측정을 해보시고 사용하여야한다.

반응형
댓글
공지사항
반응형