[React] 불필요한 렌더링 방지하기

김재훈·2023년 4월 20일
0

React에서 컴포넌트는 다음과 같은 상황에 리렌더링(re-rendering)된다.

  • 자신의 state가 변경될 때
  • 부모 컴포넌트로부터 받아오는 props가 변경될 때
  • 부모 컴포넌트가 리렌더링(re-rendering)될 때
  • foreceUpdate 함수가 실행될 때

불필요한 렌더링을 방지하는 방법은 다음과 같다.

적절한 위치에 state 선언

리액트는 특정 state가 변경되면 그 state가 선언된 컴포넌트와 그 하위 컴포넌트들을 모두 리렌더링 시킨다. 따라서 state가 선언되는 위치를 잘 설계하는 것은 리렌더링 횟수에 엄청난 영향을 끼친다. 기본적으로 state의 선언 위치는 해당 state를 사용하는 컴포넌트들을 잘 구분해 놓은 뒤 가장 최상위 컴포넌트에 선언한다. 만약 그 state를 사용하는 최상위 컴포넌트보다 더 상위 컴포넌트에 state를 선언하면 state를 사용하지 않는 더 많은 컴포넌트가 state 변경에 의해 불필요한 렌더링을 겪게 된다.

Index
ㄴGroup
ㄴUserList
 ㄴUserItem

위와 같은 컴포넌트 구조가 있다고 볼 때 UserList와 UserItem에서만 사용되는 users state가 있다. 이 users state는 UserItem에서 보여줘야 할 데이터들을 가지고 있다. 이 데이터는 두 컴포넌트에서만 사용하기 때문에 그 중 가장 상위 컴포넌트인 UserList에 선언해야 한다.

그런데 만약 이 users state를 UserList보다 상위 컴포넌트인 Index에 선언하게 되면 users state가 변경되면 users 데이터를 사용하지 않는 Index 컴포넌트와 Group 컴포넌트까지 리렌더링이 발생하게 된다.

React.memo()

컴포넌트가 React.memo()로 래핑될 때, React는 컴포넌트를 렌더링하고 결과를 메모이징(Memoizing)한다. 그리고 다음 렌더링이 일어날 때 props가 같다면 React는 메모이징(Memoizing)된 내용을 재사용한다.

const Child = (...) => {
}
export default React.memo(CHild);

또는

const Child = React.memo((...) => {
  ....
})
export default Child;

React.memo는 컴포넌트가 같은 props로 자주 렌더링되거나, 무겁고 비용이 큰 연산이 있는 경우에 사용한다. 그 외에는 사용하지 않는것이 좋다.

컴포넌트 매핑 시 key값

React에서는 컴포넌트들을 매핑할 때, 고유의 key값을 부여할 것을 강제하고 있다. 이 때, key값에 index를 사용하는것을 지양해야 한다.

배열 중간에 어떤 요소가 삽입되면 그 중간보다 이후에 위치한 요소들은 전부 인덱스가 변경된다. 이로 인해 key값이 변경되고 리렌더링이 된다. 또한 데이터가 key와 매치가 안되어 서로 꼬이는 부작용도 발생한다.

가급적이면 데이터의 ID 등 고유값을 key에 넣기를 권장한다. 다음과 같은 경우에는 index를 써도 무방하다.

  • 배열과 각 요소가 수정, 삭제, 추가 등의 기능이 없는 단순 렌더링만 담당하는 경우
  • id로 쓸만한 unique 값이 없을 경우
  • 정렬 혹은 필터 요소가 없는 경우

useMemo()

컴포넌트 내 어떤 함수가 값을 리턴하는데 많은 시간이 소요된다면, 이 컴포넌트가 리렌더링 될 때마다 함수호출에 많은 시간이 소요될 것이다. 또 그 함수의 리턴값을 자식 컴포넌트가 참조한다면, 해당값이 변경될 때마다 리렌더링이 발생될 것이다.

useMemo()는 이런 경우 사용되는 Hook으로, CPU 소모가 심한 함수들을 캐싱하기 위해 사용된다.

const memoizedValue = useMemo(() => func, [depsList]);

첫 번째 인자는 캐싱하는 함수이며, 두 번째 인자는 의존 배열 객체로 여기에 포함된 값이 바뀌어야 해당 함수를 재호출한다.

useCallback()

useCallback() 역시 useMemo()와 같은 매커니즘으로 렌더링 최적화에 활용된다.
useMemo()가 특정 리턴값을 메모이징했다면, useCallback()은 props로 넘겨주는 함수 자체를 메모이징한다.

useCallback()React.memo()와 함께 사용하여 자식 컴포넌트의 불필요한 렌더링을 줄일 수 있다.

다음은 침실, 주방, 욕실의 불을 켜고 끄는 예제다.

import React from "react";

function Light({room, on, toggle}) {
  console.log({room, on});
  return (
    <button onClick={toggle}>
      {room} {on ? "💡" : "⬛"}
   </button>
  );
};
   

그리고 React.memo()로 이 컴포넌트를 감싸준다.

Light = React.memo(Light);

다음으로 3개의 방의 스위치를 중앙 제어해주는 SmartHome 컴포넌트다.

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

function SmartHome() {
  const [masterOn, setMasterOn] = useState(false);
  const [kitchenOn, setKitchenOn] = useState(false);
  const [bathOn, setBathOn] = useState(false);
  
  const toggleMaster = () => setMasterOn(!masterOn);
  const toggleKitchen = () => setKitchenOn(!kitchenOn);
  const toggleBath = () => setBathOn(!bathOn);
  
  return (
    <>
      <Light room"침실" on={masterOn} toggle={toggleMaster} />
      <Light room"주방" on={kitchenOn} toggle={toggleKitchen} />
      <Light room"욕실" on={bathOn} toggle={toggleBath} />
    </>
  );
};

이 컴포넌트를 이용해서 침실의 조명을 켜보면 침실 뿐만 아니라 다른 모든 방에 대한 Light 컴포넌트가 호출되는것을 콘솔로그로 확인할 수 있다.

{room: "침실", on: true}
{room: "주방", on: false}
{room: "욕실", on: false}

조명을 키거나 끄는 방에 대한 Light 컴포넌트만 호출되게 하고 싶어서 React.memo()를 사용한 것인데 해결되지 않았다. 이유는 조명을 제어할 때 쓰이는 toggleMaster(), toggleKitchen(), toggleBath() 함수의 참조값이 SmartHome 컴포넌트가 렌더링 될 때마다 모두 바뀌어버리기 때문이다.

이 문제를 해결하려면 모든 조명 제어 함수를 useCallback()으로 감싸고 두 번째 인자로 각 함수가 의존하고 있는 상태를 배열로 넘겨야 한다.

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

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} />
    </>
  );
};

이제는 침실에 조명을 켜 보면 침실에 대한 Light 컴포넌트만 호출되는 것을 확인할 수 있다.


참고

profile
김재훈

0개의 댓글