useCallback, React.memo, useMemo는 잘 사용하면 리렌더링을 줄이는데 유용

김현준·2024년 8월 26일
0

리액트 이모저모

목록 보기
18/27

기본 설명

리액트는 props와 state에 변경이 있을 때 리렌더링이 발생

useCallback, React.memo, useMemo 모두 렌더 페이즈에 관여하여 불필요한 리렌더링이 발생하지 않도록 하는 것이 주요 목적

렌더 페이즈란?

useMemo - 값(연산 결과)을 캐싱

  • 연산량이 많거나 비용이 큰 작업을 매번 실행하는 것을 방지하고 싶을 때
  • 불필요한 리렌더링으로 인해 같은 값이 다시 계산되는 것을 피하고 싶을 때

예제: 연산 최적화 (useMemo 사용 전 vs 후)

import { useState, useMemo } from "react";

function ExpensiveComponent() {
  const [count, setCount] = useState(0);
  const [text, setText] = useState("");

  // 연산량이 큰 함수 (useMemo 사용 전)
  const expensiveCalculation = () => {
    console.log("연산 수행...");
    return count * 1000;
  };

  const result = expensiveCalculation(); // 매번 렌더링마다 실행됨

  return (
    <div>
      <p>결과: {result}</p>
      <button onClick={() => setCount(count + 1)}>카운트 증가</button>
      <input onChange={(e) => setText(e.target.value)} placeholder="입력하세요" />
    </div>
  );
}
  • 문제점: text 값이 변경될 때도 expensiveCalculation()이 실행된다.

useMemo로 연산 결과를 캐싱

const result = useMemo(() => {
  console.log("연산 수행...");
  return count * 1000;
}, [count]); // count가 변경될 때만 연산 실행

useMemo 적용 후 최적화된 코드

import { useState, useMemo } from "react";

function ExpensiveComponent() {
  const [count, setCount] = useState(0);
  const [text, setText] = useState("");

  // count가 변경될 때만 실행됨
  const result = useMemo(() => {
    console.log("연산 수행...");
    return count * 1000;
  }, [count]);

  return (
    <div>
      <p>결과: {result}</p>
      <button onClick={() => setCount(count + 1)}>카운트 증가</button>
      <input onChange={(e) => setText(e.target.value)} placeholder="입력하세요" />
    </div>
  );
}
  • 최적화 결과:
    • text가 변경되어도 expensiveCalculation()은 실행되지 않음.
    • count가 변경될 때만 useMemo의 콜백 함수가 실행됨.

useCallback - 함수를 캐싱

  • 자식 컴포넌트에 함수를 전달할 때, 불필요한 함수 재생성을 방지하고 싶을 때
  • 컴포넌트가 자주 리렌더링되는 상황에서 함수가 새로 생성되지 않도록 하고 싶을 때

예제: useCallback 사용 전 (불필요한 함수 재생성 발생)

import { useState } from "react";

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

  // 매번 렌더링될 때마다 새로운 함수가 생성됨
  const handleClick = () => {
    console.log("클릭!");
  };

  return <Child onClick={handleClick} />;
}

function Child({ onClick }) {
  console.log("🔄 자식 컴포넌트 렌더링");
  return <button onClick={onClick}>클릭</button>;
}

export default Parent;
  • 문제점:
    • Parent가 리렌더링될 때마다 handleClick 함수가 새롭게 생성됨.
    • Child 컴포넌트는 onClick이 바뀌었다고 판단해 불필요한 리렌더링 발생.

useCallback을 사용하여 함수 캐싱

import { useState, useCallback } from "react";

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

  // count가 변경되지 않는 한, 같은 함수가 유지됨
  const handleClick = useCallback(() => {
    console.log("클릭!");
  }, []);

  return <Child onClick={handleClick} />;
}

function Child({ onClick }) {
  console.log("🔄 자식 컴포넌트 렌더링");
  return <button onClick={onClick}>클릭</button>;
}

export default Parent;
  • 최적화 결과
    • useCallback을 사용하면 handleClick 함수가 재생성되지 않음.
    • Child는 props가 변경되지 않으므로 리렌더링되지 않음.

React.memo – 컴포넌트 캐싱 (불필요한 리렌더링 방지)

  • 컴포넌트가 같은 props를 받을 때 불필요한 리렌더링을 방지하고 싶을 때
  • 특히, 부모 컴포넌트가 자주 리렌더링될 때 성능 최적화가 필요할 때

예제: React.memo 사용 전 (불필요한 리렌더링 발생)

import { useState } from "react";

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

  return (
    <div>
      <button onClick={() => setCount(count + 1)}>카운트 증가</button>
      <Child name="React" />
    </div>
  );
}

function Child({ name }) {
  console.log("🔄 자식 컴포넌트 렌더링");
  return <p>안녕하세요, {name}!</p>;
}

export default Parent;
  • 문제점:
    • count가 바뀌면 Parent가 리렌더링됨.
    • Child의 props(name="React")는 바뀌지 않았지만 불필요한 리렌더링 발생.

React.memo를 사용하여 최적화

import { memo, useState } from "react";

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

  return (
    <div>
      <button onClick={() => setCount(count + 1)}>카운트 증가</button>
      <MemoizedChild name="React" />
    </div>
  );
}

// React.memo를 사용하여 같은 props면 리렌더링 방지
const MemoizedChild = memo(function Child({ name }) {
  console.log("🔄 자식 컴포넌트 렌더링");
  return <p>안녕하세요, {name}!</p>;
});

export default Parent;
  • 최적화 결과
    • React.memo를 사용하면 같은 props가 전달될 경우, 자식 컴포넌트의 리렌더링이 방지됨.

좀 더 다양한 예시

useCallback, React.memo를 사용한 경우

Hello, world!를 100번 출력하는 코드

//렌더링을 여러 번 하는 컴포넌트
const ManyRendering = ({onClick}) => {
	return (
    	<>
      		{Array.from({length: 100}, (_, i) => (
            	if(i === 99) {
      				console.log('렌더링 마지막 아이템: ', i)
      			}
				return (
                	<div key={i} onClick={onClick}>
                    	Hello, world!
                    </div>
                )
            ))}
      	</>
    )
}

export default ManyRendering

function App() {
	const [state, setState] = useState(0)
    
    //함수나 값은 매번 App()이 실행될 때마다 새로 생성된다,
    const onClick = () => {} 
    
    useEffect(() => {
    	setTimeout(() => {
        	setTimeout(1)
      		console.log('업데이트')
        }, [1000])
    }, [])
}

return (
	<div>
  		<ManyRendering onClick={onClick} />{/*그러므로 props가 변경*/}
  	</div>
)

해당 코드는 먼저 ManyRendering를 100번 렌더링을 한다.
그리고 App 컴포넌트의 state가 변경될 때마다 ManyRendering가 100번 또 리렌더링이 된다.
그러므로 불필요한 렌더링이 많아진다.

문제점1

  • props가 일정하지 않다.
    App 컴포넌트의 const onClick = () => {}을 보면 변경된게 없지만 훅이 사용되지 않고 선언된 함수나 값은 컴포넌트가 다시 실행될 때마다 새로 할당된다.
    즉, 매번 렌더링이 될 때마다 다른 친구다.
    그러므로 <ManyRendering onClick={onClick} />의 props가 변경된 것으로 된다.

  • 해결법: props를 변경해지 않아야 한다.
    메모이제이션한 useCallback을 쓴다.

    //const onClick = () => {} //이전코드
    const memoizationCallback = useCallback(() => {
    	onClick()
    }, [])
    
    return (
    		<div>
     		<ManyRendering onClick={onClick} />{/*그러므로 props가 변경*/}
     	</div>
    )
    • useCallback이란?
      리액트 내에 어떤 메모리에 할당을 해놓는 것(캐싱)
      이제 리렌더링이 돼도 memoizationCallback은 이전과 달라지지 않는다.

문제점2

1번 해결법으로도 렌더링이 발생한다.

리액트는 렌더링 과정에서 2가지 페이즈를 거친다.
1. 렌더 페이즈
2. 커밋 페이즈: 최적화를 할 수 있음

현재 렌더 페이즈는 반영되지 않은 상태다.

  • 해결법
    //export default ManyRendering 이전 코드
    export default React.memo(ManyRendering)
    이제 ManyRendering의 props로 전달받은 값이 바뀌지 않는 한 ManyRendering도 렌더링을 할 필요가 없이 캐싱이 된다.

useMemo를 사용한 경우

function App() {
	const [state, setState] = useState(0)
    
    /*함수나 값은 매번 App()이 실행될 때마다 새로 생성된다.
    그러므로 props가 변경되는데 때론 이게 성능에 좋지 않다.*/
    const onClick = () => {} 
    
    const value = {a:1}
    const memoizationValue = useMemo(() => {
    	return value
    }, [])
    
}

return (
	<div>
  		<ManyRendering onClick={memoizationValue} />
  	</div>
)

props로 전달하는 value가 있다고 했을 때 이전과 마찬가지로 재선언이 여러 번 되면서 렌더링 횟수가 기하급수적으로 늘어난다.

  • 해결법
    위 코드처럼 useMemo를 사용한다.
    이제 메모이제이션된 값들을 전달해주면 불필요한 렌더링 페이즈들을 줄일 수 있다.

언제 사용하나?

상황에 따라 다르므로 정답이 없다. 그리고 메모리가 사용되는 것이기 때문에 공짜가 아니다.
무분별하게 훅으로 걸어놓으면 너무 오버한 개발이 될 수도 있다.

  • 정답이 정해진 경우

    • 이전까지의 경우처럼 렌더링이 100번, 1000번 되는 경우는 당연히 메모이제이션을 해주는게 좋다.

    • 자식 컴포넌트에게 props로 함수를 전달하고자 할 때 써야한다.

      • 함수 자체의 불변성을 유지하기 위함이다.
        함수는 일급객체이기 때문에 매번 "새로운 함수가 전달되었다"고 인식하게 되어, 불필요한 리렌더링이 발생할 수 있다.

하지만 갯수가 적으면 굳이 메모이제이션을 해줄 필요가 없다.
실제로 리액트의 dev tool인 profiler를 통해서 속도 등을 측정해보면 도리어 useCallback을 잡아줬을 때 성능이 하락하는 경우도 분명히 있다.

참고 자료

profile
기록하자

0개의 댓글

관련 채용 정보