[React]useMemo, useCallback, Automatic Batching, Windowing

김건휘·2024년 11월 12일
0

React

목록 보기
12/19
post-thumbnail

🧐들어가며

이번 시간에는 렌더링 최적화에 대해 공부한 내용을 작성해보는 시간을 가지도록 하겠습니다.

📌Memoization(메모이제이션)

useMemo에서 MomoMemoization을 의미하며, 동일한 값을 return하는 함수를 반복적으로 호출해야 된다면 맨 처음 값을 계산 할 때 해당 값을 메모리에 저장해서 필요할 때 마다(또 다시 계산하지 않고)메모리에서 꺼내서 재사용 하는 기법.

=> 즉, 이전에 계산한 값을 메모리에 저장하여 중복적인 계산을 제거하여 전체적인 실행속도를 빠르게 해주는 기법

✅Memoization(메모이제이션) 예시


위의 사진과 같이 동일한 값을 return하는 함수(calculate함수)를 반복적으로 호출해야 된다면, 맨처음 값을 계산할 때 해당값을 메모리에 저장해서 필요할 때 마다 메모리에서 꺼내서 재사용한다.

📌useMemo를 사용하는 이유

앞선 예시와 같이 간단한 일을 하는 함수가 아닌, 무거운 일을 하는 함수라고 한다면 컴포넌트가 렌더링이 될 때마다 반복적으로 호출되게 되면 매우 비효율적이고 성능에도 악영양을 끼칠것이다.
이를 useMemo를 활용하여 간단하게 해결이 가능하다.

✅useMemo 예시 코드 및 영상

🌱 어려운 계산기 코드

import { useState } from 'react'
import reactLogo from './assets/react.svg'
import viteLogo from '/vite.svg'
import './App.css'

const hardCalculate = (number) => {
  console.log('어려운 계산!');
  for (let i = 0; i < 999999999; i++) {} // 생각하는 시간
  return number + 10000;
}

function App() {
  const [hardNumber, setHardNumber] = useState(1);

  const hardSum = hardCalculate(hardNumber);

  return (
  <div>
    <h3>어려운 계산기</h3>
    <input
      type="number"
      value={hardNumber}
      onChange={(e) => setHardNumber(parseInt(e.target.value))}
    />
    <span> + 10000 = {hardSum}</span>
  </div>
  );
}

export default App


=>delay가 있는 것을 확인 할 수 있다.

🌱 어려운 계산기 코드에 쉬운 계산기 추가 코드

import { useState } from 'react'
import reactLogo from './assets/react.svg'
import viteLogo from '/vite.svg'
import './App.css'

const hardCalculate = (number) => {
  console.log('어려운 계산!');
  for (let i = 0; i < 999999999; i++) {} // 생각하는 시간
  return number + 10000;
}

const easyCalculate = (number) => {
  console.log("쉬운 계산!")
  return number + 1;
}

function App() {
  const [hardNumber, setHardNumber] = useState(1);
  const [easyNumber, setEasyNumber] = useState(1);

  const hardSum = hardCalculate(hardNumber);
  const easySum = easyCalculate(easyNumber);

  return (
  <div>
    <h3>어려운 계산기</h3>
    <input
      type="number"
      value={hardNumber}
      onChange={(e) => setHardNumber(parseInt(e.target.value))}
    />
    <span> + 10000 = {hardSum}</span>

    <h3>쉬운 계산기</h3>
    <input
      type="number"
      value={easyNumber}
      onChange={(e) => setEasyNumber(parseInt(e.target.value))}
    />
    <span> + 1 = {easySum}</span>
  </div>
  );
}

export default App


=>return number + 1;의 쉬운 계산만 하였을 뿐인데 첫번째와 동일한 delay가 발생

🧐도대체 왜⁉️ 나는 쉬운 계산(number+1)만 했을 뿐인데, 딜레이가 발생하는걸까?

App컴포넌트가 함수형 컴포넌트 이기 때문이다. 쉬운 계산기의 number를 증가시켜주면 easyNumber의 state가 변경된다.
state가 변경되었다는 말은 App컴포넌트가 다시 랜더링 된다는 것을 의미한다. 그래서, hardSum과 easySum 변수가 모두 초기화가 된다.
즉, hardNumber를 바꾸던 easyNumber를 바꾼던 상관없이 hardCalculate안에 있는 의미없는 for루프가 돌아가게되어서 delay가 발생한다. => 너무 비효율적이다.

🌱3. useMemo를 활용한 코드

import { useMemo, useState } from 'react'
import reactLogo from './assets/react.svg'
import viteLogo from '/vite.svg'
import './App.css'

const hardCalculate = (number) => {
  console.log('어려운 계산!');
  for (let i = 0; i < 999999999; i++) {} // 생각하는 시간
  return number + 10000;
}

const easyCalculate = (number) => {
  console.log("쉬운 계산!")
  return number + 1;
}

function App() {
  const [hardNumber, setHardNumber] = useState(1);
  const [easyNumber, setEasyNumber] = useState(1);

  //const hardSum = hardCalculate(hardNumber);
  const hardSum = useMemo(() => {              //useMemo 부분
    return hardCalculate(hardNumber);
  }, [hardNumber]);
  const easySum = easyCalculate(easyNumber);

  return (
  <div>
    <h3>어려운 계산기</h3>
    <input
      type="number"
      value={hardNumber}
      onChange={(e) => setHardNumber(parseInt(e.target.value))}
    />
    <span> + 10000 = {hardSum}</span>

    <h3>쉬운 계산기</h3>
    <input
      type="number"
      value={easyNumber}
      onChange={(e) => setEasyNumber(parseInt(e.target.value))}
    />
    <span> + 1 = {easySum}</span>
  </div>
  );
}

export default App


=> (useMemo를 사용하여)쉬운 계산을 할 때, delay이 없이 계산이 이루어지는 것을 확인할 수 있다.

🌱예시에 사용된 useMemo 구조 살펴보기

const hardSum = useMemo(() => {
    return hardCalculate(hardNumber);
  }, [hardNumber]);

콜백 함수: () => { return hardCalculate(hardNumber); }는 useMemo에 전달된 콜백 함수로, 실제로 메모이제이션하고자 하는 계산을 수행한다. 위의 예에서는 hardCalculate 함수를 호출하고 hardNumber를 인자로 전달하여 그 결과를 반환한다.
의존성 배열: [hardNumber]는 의존성 배열로, useMemo가 결과를 새로 계산해야 하는 조건을 정의합니다. 여기서는 hardNumber 값이 변경될 때마다 useMemo가 콜백 함수를 다시 실행하여 결과를 새로 계산하도록 설정되어 있다. 만약 hardNumber가 변경되지 않는다면, 이전에 계산한 hardSum 값을 재사용한다.

비슷한 역할을 하는 Hook이 또 있다고 하는데, 바로 useCallback이다.

📌useCallback이란?

함수를 메모이제이션(memoization)하는 데 사용된다.
즉, 함수의 참조를 재사용할 필요가 있을 때 사용한다.

✅useCallback 예시 코드 및 영상

🌱useCallback 사용 X

import { useEffect, useState } from 'react'
import './App.css'


function App() {
  const [Number, setNumber] = useState(0);
  const [toggle, setToggle] = useState(true);

  const someFunction = () => {
    console.log(`someFunc: number: ${number}`);
    return;
  };

  useEffect(() => {
    console.log('someFuction이 변경되었습니다.');
  }, [someFunction]);


  return (
  <div>
    <input
      type="number"
      value={Number}
      onChange={(e) => setNumber(parseInt(e.target.value))}
    />
    <button onClick={() => setToggle(!toggle)}>{toggle.toString()}</button>
    <br />
    <button onClick={someFunction}>Call someFunc</button>
  </div>
  );
}

export default App

=> 콘솔창을 확인해보니, 토글 버튼을 눌러 토글 스테이트가 변경될 때마다 useEffect가 실행되어 'someFuction이 변경되었습니다.'라는 로그가 콘솔에 출력되고 있다.
=> useEffect가 불필요하게 여러번 실행되고 있음.

🌱useCallback 사용 O 코드

import { useEffect, useCallback, useState } from 'react'
import './App.css'


function App() {
  const [number, setNumber] = useState(0);
  const [toggle, setToggle] = useState(true);

  const someFunction = useCallback(() => {
    console.log(`someFunc: number: ${number}`);
    return;
  }, [number]); //의존성 배열 number로 지정

  useEffect(() => {
    console.log('someFuction이 변경되었습니다.');
  }, [someFunction]);


  return (
  <div>
    <input
      type="number"
      value={number}
      onChange={(e) => setNumber(parseInt(e.target.value))}
    />
    <button onClick={() => setToggle(!toggle)}>{toggle.toString()}</button>
    <br />
    <button onClick={someFunction}>Call someFunc</button>
  </div>
  );
}

export default App

=> 이전 코드와 달리 토글 버튼을 클릭시 useEffect가 실행되지 않아 'someFuction이 변경되었습니다.'라는 로그가 콘솔창에 뜨지 않는 것을 확인 할 수 있다.
=> 대신 의존성 배열로 지정해준 number의 값이 변경 될 때만 콘솔창에 'someFuction이 변경되었습니다.'가 뜨는 것을 확인 할 수 있다.

🌱예시에 사용된 useCallback 구조 살펴보기

const someFunction = useCallback(() => {
    console.log(`someFunc: number: ${number}`);
    return;
  }, [number]);

useCallback 훅: useCallback은 첫 번째 인자로 전달된 함수를 메모리에 저장합니다. 저장된 함수는 의존성 배열([number])에 있는 값들의 변화에만 반응하여 업데이트됩니다.
함수 정의: () => { console.log(someFunc: number: ${number}); return; } 이 부분은 실제 저장되는 콜백 함수입니다. 이 함수는 콘솔에 number 변수의 현재 값을 출력합니다.
의존성 배열: [number] 이 배열은 useCallback에 의해 추적되는 의존성을 명시합니다. 여기서 number는 함수가 의존하는 변수입니다. 이 변수의 값이 변경될 때마다 useCallback은 새로운 함수를 메모리에 저장하여 이전 함수를 대체합니다.

📌useMemo, useCallBack 요약

useMemo

  • useMemo는 값의 계산을 메모이제이션하는 데 사용된다. 복잡한 계산 결과, 객체 리터럴, 배열 등 함수 호출 결과를 캐싱하여 재계산의 필요성을 줄이기 위해 사용.
  • useMemo는 계산된 값 자체를 반환한다.

useCallback

  • useCallback은 함수를 메모이제이션(memoization)하는 데 사용된다. 즉, 함수의 참조를 재사용할 필요가 있을 때 사용한다. 특히 함수를 자식 컴포넌트에 props로 전달할 때 유용하며, 자식 컴포넌트가 불필요하게 재렌더링되는 것을 방지할 수 있다.
  • useCallback은 함수 자체를 반환한다.

📌Automatic Batching

여러 상태 업데이트를 하나의 렌더링으로 묶어 처리하는 기능이다.

✅Automatic Batching 원리

React에서 상태가 변경되면 기본적으로 컴포넌트가 다시 렌더링된다. 하지만 여러 개의 상태 업데이트가 동시에 발생하는 경우, React는 각각의 상태 업데이트마다 리렌더링을 수행하지 않고, 이를 하나로 묶어서 한 번만 렌더링한다. 이를 배칭(Batching)이라고 하며, React 18부터는 비동기 작업에서도 자동으로 배칭을 수행하는 Auto Batching이 도입되었.

✅Auto Batching 동작 방식

기존의 React 17 이하에서는 onClick 같은 이벤트 핸들러 내부에서 상태를 여러 번 업데이트할 경우 배칭이 자동으로 이루어졌지만, 비동기 작업(예: setTimeout 또는 fetch 콜백 등)에서는 자동으로 배칭이 이루어지지 않았다. React 18에서는 이러한 비동기 작업에서도 배칭을 자동으로 수행하여, 여러 상태 업데이트가 한 번에 일어나도록 개선되었다.

📌Windowing

많은 양의 데이터가 있는 리스트나 테이블을 렌더링할 때 성능을 최적화하는 기법이다. 화면에 보이는 항목들만 렌더링하고, 스크롤에 따라 동적으로 필요한 항목을 추가적으로 렌더링해, 브라우저의 렌더링 부담을 줄여주는 방식이다.

✅Windowing의 동작 방식

  • 보이는 영역만 렌더링: 스크롤 위치를 기준으로 화면에 보이는 부분만 렌더링하고, 화면에 보이지 않는 항목은 렌더링하지 않는다.
  • 스크롤에 따른 동적 렌더링: 사용자가 스크롤할 때, 새로운 항목이 화면에 들어오면 해당 항목을 렌더링하고, 화면에서 벗어난 항목은 제거하여 DOM에 남아 있지 않게 한다.
  • 버퍼 영역: 사용자가 빠르게 스크롤할 경우를 대비해, 보이는 영역 위아래에 약간의 추가 항목(버퍼 영역)을 렌더링해 부드러운 사용자 경험을 제공한다.

✅Windowing 라이브러리

  • React Window: 가볍고 성능이 좋은 라이브러리로, 큰 데이터셋을 렌더링할 때 효과적이다.
  • React Virtualized: 다양한 컴포넌트(windowed list, grid, masonry 등)를 지원하며 기능이 풍부하다. React Window보다 많은 기능을 제공하지만, 다소 무거울 수 있다.

✅React Window 예제 코드

react-window 라이브러리를 사용하여 큰 리스트를 렌더링하는 예시

import React from 'react';
import { FixedSizeList as List } from 'react-window';

const items = Array.from({ length: 1000 }, (_, index) => `Item ${index + 1}`);

function Row({ index, style }) {
  return (
    <div style={style}>
      {items[index]}
    </div>
  );
}

function App() {
  return (
    <List
      height={400}       // 리스트의 높이
      itemCount={items.length}   // 항목 개수
      itemSize={35}      // 각 항목의 높이
      width={300}        // 리스트의 너비
    >
      {Row}
    </List>
  );
}

export default App;

height, itemCount, itemSize, width 등의 속성으로 리스트의 전체 크기와 아이템의 크기를 정의한다. react-window는 스크롤 시 화면에 보이는 항목만 동적으로 렌더링하여 메모리와 성능을 최적화할 수 있다.

profile
공유할 때 행복을 느끼는 프론트엔드 개발자

3개의 댓글

comment-user-thumbnail
2024년 11월 13일

안녕하세요! YB 유서연입니다.
아티클 너무 잘 읽었습니다! 개념을 쉽게 풀어서 설명해주셔서 이해하기 편했던 것 같아요.
특히, useMemo와 useCallback을 사용해 렌더링 최적화를 구현하는 예시를 직접 들어서 설명해주시고 콘솔이 찍히는 것까지 영상으로 담아주셔서 직관적인 이해가 가능했습니다. 수고하셨어요!

답글 달기
comment-user-thumbnail
2024년 11월 13일

아티클 잘 읽었습니다!
시각 자료들을 잘 활용해주셔서 너무 이해하기 편했던 것 같습니다.
특히 react의 함수형 컴포넌트의 렌더링 방식을 통해서 무슨 변수를 바꾸든 상관 없이 무겁고 오래 걸리는 작업이 무조건 실행되어야 한다는 것을 예시로 들어주신 게 흥미로웠던 주제 같아요. 저도 이번 아티클을 작성하면서 이렇게 불필요한 렌더링은 프로젝트를 설계하면서 무조건 마주하게 되는데 이걸 어떻게 설계해야 하는지, 컴포넌트를 어떻게 구성해야 하고 state 잘 쓰는 법은 무엇일지 많이 고민한 것 같습니다. 이런 측면에서 부모 > 자식 컴포넌트의 렌더링 관계성이 궁금해졌는데

https://velog.io/@mogulist/understanding-react-rerender-easily#3-2%EA%B0%80%EC%A7%80-%EB%A0%8C%EB%8D%94%EB%A7%81%EA%B3%BC-2%EA%B0%80%EC%A7%80-%EB%8B%A8%EA%B3%84phase

이 아티클을 참고하면서 생각보다 많은 정보를 얻었던 것 같아서 한번 읽어보셔도 좋을 것 같아요!
그리고 useCallback을 제대로 사용해 본 적이 없었는데 예시 코드를 통해서 감을 잡을 수 있었던 것 같아요. 좋은 아티클 감사합니다!

답글 달기
comment-user-thumbnail
2024년 11월 13일

아티클 너무 잘 읽었습니다! 읽으면서, 의문이 생기는 것들 바로 다음 줄에 설명이 적혀있어서 잘 이해하면서 봤습니다 ㅎㅎ 시각적인 자료들 첨부해주셔서 이해하는 것에 더 도움이 됐습니다!
memoization을 활용한 예시들을 보니, 무거운 연산이나 함수를 호출할 때는 메모이제이션을 활용한 최적화가 정말 필수일 것 같다는 것을 느꼈습니다..! 그러나 저도 아티클 작성을 위해 공부해보니, 무조건적인 도입은 오히려 성능 저하를 시킬 수 있다고 하네요ㅜㅜ 메모이제이션을 통해 성능이 개선될 수 있는 연산인지, 의존성 배열을 어떻게 설정할 것인지 등등 잘 고려하여 도입해야할 것 같습니다!
예시를 통한 자세한 설명 감사합니다^^

답글 달기