[React 숙련] React Hooks(3) - 최적화

Habin Lee·2023년 11월 10일
1

요약

  1. 최적화가 필요한 이유를 알 수 있다.
  2. React.memo, useCallback, useMemo의 개념을 알고 사용할 수 있다.

React Hooks 최적화

  • 리렌더링의 발생 조건
  1. 컴포넌트에서 state가 바뀌었을 때
  2. 컴포넌트가 내려받은 props가 변경되었을 때
  3. 부모 컴포넌트가 리렌더링 된 경우 자식 컴포넌트 모두 리렌더링 발생
  • 불필요한 렌더링이 발생하지 않도록 최적화(Optimization)해야한다.
  1. React.memo : 컴포넌트를 캐싱
  2. useCallback : 함수를 캐싱
  3. useMemo : 값을 캐싱

최적화 종류

memo(React.memo)

  • memo를 import하는 방법도 있지만 React.memo를 더 많이 사용함
  • 부모 컴포넌트가 리렌더링 된 경우 자식 컴포넌트 모두 리렌더링이 발생하는데 이 현상을 막기 위해 사용함
  • memo를 이용해서 컴포넌트를 메모리에 저장(임시적으로 저장: 캐싱)해두고 필요할 때 가져다 쓰게 됨
    : 부모 컴포넌트의 state의 변경으로 인해 props가 변경이 일어나지 않는 한 컴포넌트는 리렌더링 되지 않는다. -> 컴포넌트 memoizstion

예시 코드

  • 카운터가 되는 App 컴포넌트와 Box1,2,3 이라는 각각의 box 컴포넌트가 자식 컴포넌트로 있다.
// App.jsx
import React, { useState } from 'react'
import Box1 from './components/Box1';
import Box2 from './components/Box2';
import Box3 from './components/Box3';

function App() {
  console.log("App 컴포넌트가 렌더링 되었습니다.")
  // 카운터 useState 만들기
  const [count, setCount] = useState(0);
  // 증가 버튼 함수
  const onPlusButton = () => {
    setCount(count + 1);
  }
  // 감소 버튼 함수
  const onMinusButton = () => {
    setCount(count - 1);
  }

  return (
    <div style={{marginLeft: '110px', marginTop: '110px'}}>
     <h3>카운트 예제입니다!</h3>
     <p>현재 카운트 : {count}</p>
      // 버튼에 증감 버튼 먹여주기
     <button onClick={onPlusButton}>+</button>
     <button onClick={onMinusButton}>-</button>
     <div style={{display: 'flex', marginTop: '10px'}}>
      <Box1 />
      <Box2 />
      <Box3 />
     </div>
    </div>    
  )
}

export default App

// Box1.jsx
import React from 'react'

const style = {
  width: '100px',
  height: '100px',
  backgroundColor: '#01c49f',
  color: 'white',
    }

function Box1() {
  console.log("Box1 컴포넌트가 렌더링 되었습니다.")
  return (
    <div style={style}>Box1</div>
  )
}

export default Box1;

// Box2.jsx
import React from 'react'

const style = {
  width: '100px',
  height: '100px',
  backgroundColor: '#4e93ed',
  color: 'white',
    }

function Box2() {
  console.log("Box2 컴포넌트가 렌더링 되었습니다.")
  return (
    <div style={style}>Box2</div>
  )
}

export default Box2;

// Box3.jsx
import React from 'react'

const style = {
  width: '100px',
  height: '100px',
  backgroundColor: '#c491be',
  color: 'white',
    }

function Box3() {
  console.log("Box3 컴포넌트가 렌더링 되었습니다.")
  return (
    <div style={style}>Box3</div>
  )
}

export default Box3;
  • 카운트를 할 때마다 자식 컴포넌트인 Box1,2,3도 App과 함께 렌더링이 되는 것을 볼 수있다.
    -> 박스 모양은 카운트와 상관없이 항상 그대로인데, 부모 컴포넌트가 바뀐다고 해서 자식 컴포넌트까지 항상 바뀔(렌더링이 될) 필요가 없다.

React.memo 사용하기

  • 해당 문제를 해결하기 위해 사용하는 React.memo 기능은 간단하다.
    -> export default 자식컴포넌트 에 export default React.memo(자식컴포넌트)를 더해주는 것!
  • Box.jsx 파일들 아래에 있는 부분을 전부 아래처럼 바꿔주기만 하면 된다.
export default React.memo(Box1);
export default React.memo(Box2);
export default React.memo(Box3);

  • 이렇게 React.memo를 사용하면 카운트가 아무리 바뀌어도 박스 컴포넌트들은 처음 렌더링될 때만 빼면 렌더링이 되지 않는다.

useCallback

  • useCallback은 인자로 들어오는 함수 자체를 기억(memoizstion)한다.

예시 코드

  • 위의 코드에서 Box1을 초기화 버튼으로 바꿔보자.
// App.jsx
import React, { useCallback, useState } from 'react'
import Box1 from './components/Box1';
import Box2 from './components/Box2';
import Box3 from './components/Box3';

function App() {
  console.log("App 컴포넌트가 렌더링 되었습니다.")

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

  const onPlusButton = () => {
    setCount(count + 1);
  };
  const onMinusButton = () => {
    setCount(count - 1);
  };
  // 초기화 버튼 함수 만들기
  const initCount = () => {
    setCount(0);
  };

  return (
    <div style={{marginLeft: '110px', marginTop: '130px'}}>
     <h3>카운트 예제입니다!</h3>
     <p>현재 카운트 : {count}</p>
     <button onClick={onPlusButton}>+</button>
     <button onClick={onMinusButton}>-</button>
     <div style={{display: 'flex', marginTop: '10px'}}>
       // Box1 컴포넌트로 데이터 넘겨주기
      <Box1 initCount={initCount}/>
      <Box2 />
      <Box3 />
     </div>
    </div>    
  )
}

export default App

// Box1.jsx
import React from 'react'

const style = {
  width: '100px',
  height: '100px',
  backgroundColor: '#01c49f',
  color: 'white',
    }
// initCount 데이터 가져오기
function Box1({initCount}) {
  console.log("Box1 컴포넌트가 렌더링 되었습니다.")
  return (
    <div style={style}>
      // onClick에 initCount 함수 먹여주기
      <button onClick={()=>{
        initCount();
      }}>초기화</button>
    </div>
  )
}
// 렌더링이 되지 않도록 React.memo는 유지
export default React.memo(Box1);

???

  • 렌더링이 되지 않도록 React.memo는 유지했는데 증감버튼을 누르면 Box1 컴포넌트도 함께 렌더링 되는 것을 볼 수 있다.

    함수형 컴포넌트를 사용했기 때문에 Box1 컴포넌트에 React.memo가 되어있음에도 불구하고 카운터가 올라갈때마다 렌더링이 되는 것을 알 수 있다.

  • 이게 무슨 말이냐 하면..

  1. initCount 라는 함수도 App이라는 함수가 리렌더링 될때마다 다시 만들어진다.
  2. Box1 컴포넌트에 있는 onClick 부분의 initCount가 다시 만들어졌기 때문에 props로 새로운 값을 전달받았다고 여겨지게 된다.
    -> 하지만 결과적으로 initCount 함수는 바뀐게 없다.
  • 이 부분은 리액트에서의 불변성을 생각하면 이해하기가 쉬운데, 차례로 살펴보자.
  1. 객체나 함수는 불변성을 유지하기 위해 어떠한 방법으로 처리를 해주어야한다.
  2. 그 이유는 함수도 변수보다 크기가 조금 더 크다고 여겨져 변수처럼 데이터 자체가 저장되는 것이 아니라 별도의 공간에 저장이 된다.
    -> 즉, 별도의 공간을 바라보고 있는 주소값을 저장하게 된다는 뜻이다.
  3. 그래서 다시 리렌더링이 되면서 함수가 다시 만들어질때 그 전에 만들어진 함수는 그대로 있고, 다시 새로운 주소를 가진 initCount가 생기게 된다.
  4. 이 문제를 해결하기 위해 함수 자체를 기억시키는 useCallback을 사용한다.
    -> initCount 함수를 별도 공간에 저장을 해놓고 특수한 조건이 아닌 경우 변경되지 않도록 막아야하기 때문이다.

useCallback 사용하기

  • initCount함수를 useCallback으로 감싸고 의존성 배열도 뒤에 넣어준다.
    -> 특정 state가 변경될 때 callback함수가 실행되도록 해야하므로 의존성 배열에 해당 state를 넣어줘야 한다.
const initCount = useCallback(() => {
    setCount(0);
}, []);

  • useCallback 함수를 사용해주면 Box1 컴포넌트는 렌더링되지 않는 것을 볼 수 있다.
  • App.jsx가 처음 렌더링 될때 initCount함수를 메모리 공간에 그대로 저장하는데 useCallback의 의존성 배열(빈 배열)에 의해 state는 항상 초기값인 0으로 기억되어 있기 때문에 App 컴포넌트가 렌더링 되더라도 Box1 컴포넌트는 리렌더링이 되지 않는다.

한 단계 더 나아가기

  • 자 이번에는 initCount함수 안에 콘솔을 찍어보자.
const initCount = useCallback(() => {
  console.log(`${count}에서 0으로 변경되었습니다.`)
  setCount(0);
}, []);
  • 우리가 생각했을 때 카운트를 5번 누르고 초기화 버튼을 누르면 '5에서 0으로 변경되었습니다.' 라고 콘솔에 찍혀야하는 것이 맞다.
  • 하지만 결과적으로는 그렇게 나오지 않는다.
    -> useCallback으로 인해 이미 처음 렌더링될 때의 값인 0으로 고정되어 있기 때문이다.
  • 이럴 때 쓰는 것이 의존성 배열(Dependency Array)인 것이다.
    -> Dependency Array에 count를 써준다.
const initCount = useCallback(() => {
  console.log(`${count}에서 0으로 변경되었습니다.`)
  setCount(0);
}, [count]);

  • 이제서야 count가 콘솔에 잘 찍히게 되었다. 하지만 count가 바뀔 때마다 렌더링을 해달라고 했으니 Box1 컴포넌트는 다시 찍힐 수 밖에 없다.

useMemo

  • memo : memoizstion(기억한다는 뜻)
  • 위에서 소개한 방법들과 조금씩 다른데, useMemo는 value를 기억한다.
  • useMemo로 감싼 함수는 Dependency Array의 값이 변경될 때만 호출이 된다.
    -> 그 외의 경우는 memoizstion 해놨던 값을 가져오기만 함

예시 코드

// App.jsx
import React from 'react'
import HeavyComponent from './components/HeavyComponent'

// heavy work -> 엄청 무거운 작업
function App() {
  return (
    <>
      <nav style={{
        backgroundColor: 'yellow',
        marginBottom: '30px',
      }}>네비게이션 바</nav>
      <HeavyComponent />
      <footer style={{
        backgroundColor: 'green',
        marginBottom: '30px',
      }}>푸터 영역이에요.</footer>
    </>
  )
}

export default App

// HeavyComponent.jsx
import React, { useState } from 'react'

function HeavyComponent() {

  const [count, setCount] = useState(0)

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

  const value = heavyWork();

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

export default HeavyComponent

  • 무거운 함수의 값을 리렌더링 될 때마다 계속 불러오면 너무 비효율적이다.
  • 추가적으로 버튼을 누를 때마다 HeavyComponent가 리렌더링되면서 카운터가 바로 올라가지 않고 조금씩 늦게 반응한다.

useMemo 사용하기

  • useMemo를 사용하는 방법도 어렵지 않다.
  • heavyWork()를 useMemo로 감싸고 , 뒤에 Dependency Array까지 써주면 된다.
// useMemo import 잊지말기!
import React, { useState, useMemo } from 'react'

function HeavyComponent() {

  const [count, setCount] = useState(0)

  const heavyWork = () => {
    for(let i=0; i<200000000000000; i++)
    return 100;
  }
  // heavyWork()를 useMemo로 감싸고 , 뒤에 의존성 배열까지 넣어주면 된다.
  const value = useMemo(() => heavyWork(), [])

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

export default HeavyComponent
  • 이렇게 하면 heavyWork의 값이 계속 리렌더링 되지 않아 로딩이 빨라지는 것을 확인할 수 있다.

느낀 점

🙄😵😫😖🤯
갈 길이 먼데 머릿 속에 새로운 것을 하나 집어 넣는 것도 몇 시간동안 붙잡고 있어야 하니 조급해지지만.. 멘탈 관리 잘하자!😆

0개의 댓글