React - memoization

박요셉·2024년 5월 21일

React

목록 보기
5/15

01. memoization은 왜 등장했는가?

01. 최적화

리액트에서 리렌더링이 빈번하게, 자주 일어난다는 것은 비용이 더 발생한다는 것이고 이를 줄여야한다.
이런 작업을 우리는 최적화(Optimization)이라고 부른다.
리액트에서 불필요한 렌더링이 발생하지 않도록 최적화하는 대표적인 방법은 아래 3가지가 있다.

  • memo(React.memo) : 컴포넌트를 캐싱
  • useCallback : 함수를 캐싱
  • useMemo : 값을 캐싱

02. memo(React.memo)

01. memo란?

리-렌더링의 발생 조건 중 부모 컴포넌트가 리렌더링 되면 자식컴포넌트는 모두 리렌더링 된다는 것을 그림으로 보면 아래와 같다.

  • 1번 컴포넌트가 리렌더링 -> 2~7 리렌더링
  • 4번 컴포넌트 리렌더링 -> 6,7 리렌더링

자녀 컴포넌트 입장에서는 "난 바뀐게 없는데 왜 다시 렌더링 돼야하지?"라고 할 수 있다, 이 부분을 돕는 도구가 바로 React.memo이다.

02. 적용법

간단하게 React.memo를 이용해서 컴포넌트를 메모리에 저장해두고 필요할 때 가져다 쓰면 된다.
이렇게 하면 부모 컴포넌트의 state의 변경으로 인해 props가 변경이 일어나지 않는 한 컴포넌트는 리렌더링 되지 않는다.
이를 컴포넌트 memoization이라고 한다.

// Box들은 App의 자식 컴포넌트이다.
export default React.memo(Box1);
export default React.memo(Box2);
export default React.memo(Box3);

03. useCallback

01. useCallback이란?

React.memo는 컴포넌트를 메모이제이션 했다면, useCallback은 인자로 들어오는 함수 자체를 기억(메모이제이션)한다.

useCallback의 필요성

초기화 버튼이 있다고 가정해보자.

React.memo로 메모이제이션된 컴포넌트에 useState의 setter 함수가 인자로 내려왔다.
이 함수를 우리는 아래와 같이 순수함수로 만들어 사용할 것이고, 이를 자식 컴포넌트에서 실행 시 부모에서의 state가 변경되어 부모는 리렌더링이 될 것이지만 우리의 자식 컴포넌트는 리렌더링이 되선 안된다.
왜? 메모해놨으니까!

const onInitButtonClickHandler = () => {
  initCount();
};

하지만 생각과는 다르게 자식 또한 리렌더링이 될 것이다. 이유는?
우리가 함수형 컴포넌트를 사용하기 때문이고 부모가 리렌더링 되면서 위의 코드가 다시 만들어지기 때문이다.
자바스크립트에서는 함수도 객체의 한 종류이다.
따라서 모양은 같더라도 다시 만들어지면 구 주소값이 달라지고 이에 따라 하위 컴포넌트에서는 props가 변경되었다고 인시하는 것이다.

어떻게 사용을 해야하는가?

const onInitButtonClickHandler = () => {
  initCount();
};

위에서 사용한 함수가 자식 컴포넌트에 있다.
이 함수를 메모리 공간에 저장해놓고, 특정 조건이 아닌 경우엔 변경되지 않도록 해야한다.

그럼 위의 함수에 useCallback을 쓰면 되는가?
onInitButtonClickHandler는 저장되겠지, 그러나 initCount()가 실행되면 상위 컴포넌트가 리렌더링되며 해당 함수를 다시 그려내는 것이 문제이기에 올바른 방법이 아니다.

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

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

위와 같이 initCount를 메모리에 캐싱해주어야 부모가 리렌더링 될 때 함수를 다시 만들지 않고 임시 저장된 함수를 가져와 사용할 것이니 우리의 자식 컴포넌트는 리렌더링 되지 않을 것이다.

의존성 배열에 변하는 값에 따라 함수가 새로 할당되게 하여 불미스러운 오류를 피하도록 하자

04. useMemo

01. useMemo란?

동일한 값을 반환하는 함수를 계속 호출해야 하면 필요없는 렌더링을 한다고 볼 수 있다.
맨 처음 해당 값을 반환할 때 그 값을 메모리에 저장하면 필요할 때 마다 다시 함수를 호출해서 계산하는 것이 아니라 이미 저장한 값을 단순히 꺼내와서 쓸 수 있다.
보통 이러한 기법을 캐싱을 한다.라고 표현한다.

특히 복잡한 계산 결과값을 memoization할 때 많이 사용한다.

사용법

// as-is
const value = 반환할_함수();

// to-be
const value = useMemo(()=> {
	return 반환할_함수()
}, [dependencyArray]);
  const heavyWork = () => {
    for (let i = 0; i < 1000000000; i++) {}
    return 100;
  };

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

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

위의 상황같이 무거운 작업이 있다면 당연히 해줘야 겠지??

추가로 아래와 같이 변수로 사용할 예정이던 참조 데이터 또한 useMemo를 사용하여 캐싱해줄 수 있다.
왜 해야댐? 할 수 있는데 참조 데이터를 리렌더링할 때마다 다시 할당하게 되면 해당 주소의 값에 할당되어있는 객체 또는 배열 값에 대한 주소가 바껴있을 것이기 때문에 useEffect 입장에선 값이 바꼇네??싶다는 것이다.
즉 불변성을 잘 지켜주자.

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

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

--------

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

0개의 댓글