React

[React] 리액트 context API의 리렌더링 방지를 통한 성능 최적화 하기 / useMemo 사용하기

solfa 2024. 2. 18. 04:17

https://5ffthewall.tistory.com/67

 

[React] 리액트 context API로 상태 관리 하기

Context API란? Context API는 React에서 전역적인 상태를 관리하고 컴포넌트 간에 데이터를 전달하는 데 사용되는 기능이다. 이를 통해 props 전달이 깊은 컴포넌트 트리를 통해 이루어지는 것을 피할 수

5ffthewall.tistory.com

위 글과 같이 context API로 전역 상태를 관리하는 코드를 작성한 적이 있다.

하지만 이 코드에는 문제점이 있는데 바로 Provider의 상태가 변했을때 이를 구독하고 있는 컴포넌트의 경우 일제히 리렌더링이 일어난다는 문제이다. 하위 컴포넌트의 개수가 많아지거나 프로젝트의 규모가 커지면 불필요한 리렌더링이 많아지면서 성능도 당연히 떨어지게 되는 문제가 발생한다. 따라서 리렌더링 방지를 위해 useMemo를 사용하는 작업을 고려해 볼 수 있다.


useMemo란?

useMemo는 React의 훅 중 하나로, 함수형 컴포넌트 내에서 연산 비용이 높은 계산을 최적화하는 데 사용된다.

일반적으로 함수형 컴포넌트 내에서 변수나 값을 계산하는 경우, 그 값이 변경될 때마다 매번 다시 계산되어야 한다. 이 때 useMemo를 사용하면 특정 값이 변경되지 않는 한 이전에 계산된 값을 재사용할 수 있다. 이를 통해 다음 렌더링 사이에 이전에 계산된 값을 기억하고, 해당 값이 변경되지 않았을 때 재계산을 방지하여 성능을 향상시킬 수 있다.

  const count = useMemo(() => countActiveUsers(users), [users]);

 

useMemo 의 첫번째 파라미터에는 어떻게 연산할지 정의하는 함수를 넣어주고 두번째 파라미터에는 deps 배열을 넣어준다.

배열 안에 넣은 내용이 바뀌면 등록한 함수를 호출해서 값을 연산해주고 내용이 바뀌지 않았다면 이전에 연산한 값을 재사용하게 된다.

-> useMemo는 이전에 계산된 값을 기억하고 의존성 배열이 변경되지 않으면 다시 계산하지 않도록 한다는 뜻!

 

일반적으로 useMemo가 사용되는 상황

  1. 계산 비용이 높은 함수를 호출할 때
  2. 연산 결과를 재사용할 수 있는 경우
  3. 컴포넌트 렌더링 성능을 향상시키기 위해

 

결론

React에서 Context API를 사용할 때 useMemo를 사용하여 성능을 최적화할 수 있다.

보통 Context API를 사용할 때 성능을 고려해야 하는 경우는 해당 context 값을 사용하는 컴포넌트가 불필요하게 리렌더링되는 것을 방지하기 위함인데, 이를 위해 createContext로 생성한 context 객체를 useMemo를 사용하여 렌더링 최적화를 할 수 있다.

 

기존 코드

import React, { createContext, useContext, useState } from 'react';

const QuizContext = createContext();

export const useQuizContext = () => useContext(QuizContext);

export const QuizProvider = ({ children }) => {
  const [selectedItem, setSelectedItem] = useState({
    quiz1: '',
    quiz2: '',
    quiz3: '',
    quiz4: '',
  });

  const setQuizItem = (quizId, item) => {
    setSelectedItem(prevItems => ({
      ...prevItems,
      [quizId]: item
    }));
  };

  return (
    <QuizContext.Provider value={{ selectedItem, setQuizItem }}>
      {children}
    </QuizContext.Provider>
  );
};

 

리팩토링한 코드

import React, { createContext, useContext, useState, useMemo } from 'react';

const QuizContext = createContext();

export const useQuizContext = () => useContext(QuizContext);

export const QuizProvider = ({ children }) => {
  const [selectedItem, setSelectedItem] = useState({
    quiz1: '',
    quiz2: '',
    quiz3: '',
    quiz4: '',
  });

  const setQuizItem = (quizId, item) => {
    setSelectedItem(prevItems => ({
      ...prevItems,
      [quizId]: item
    }));
  };

  const contextValue = useMemo(() => ({
    selectedItem,
    setQuizItem
  }), [selectedItem, setQuizItem]);

  return (
    <QuizContext.Provider value={contextValue}>
      {children}
    </QuizContext.Provider>
  );
};

 

추가한 부분

  const contextValue = useMemo(() => ({
    selectedItem,
    setQuizItem
  }), [selectedItem, setQuizItem]);

 

useMemo를 사용하여 값을 렌더링할 때마다 새로운 객체가 생성되지 않도록 한다.

 

오류 발생

The 'setQuizItem' function makes the dependencies of useMemo Hook (at line 26) change on every render. Move it inside the useMemo callback. Alternatively, wrap the definition of 'setQuizItem' in its own useCallback() Hook react-hooks/exhaustive-deps

이런 오류가 발생했다. 이 오류는 React Hook의 규칙을 따르지 않아서 발생한다고 한다.

useMemo 훅의 의존성 배열에 있는 함수가 렌더링마다 새로운 함수로 간주되기 때문인데 이를 해결하기 위해서는 해당 함수를 useMemo의 콜백 내부에 정의하거나, useCallback 훅을 사용하여 해당 함수를 감싸주어야 한다!

 

최종 수정

import React, { createContext, useContext, useState, useMemo, useCallback } from 'react';

const QuizContext = createContext();

export const useQuizContext = () => useContext(QuizContext);

export const QuizProvider = ({ children }) => {
  const [selectedItem, setSelectedItem] = useState({
    quiz1: '',
    quiz2: '',
    quiz3: '',
    quiz4: '',
  });

  const setQuizItem = useCallback((quizId, item) => {
    setSelectedItem(prevItems => ({
      ...prevItems,
      [quizId]: item
    }));
  }, []);

  const contextValue = useMemo(() => ({
    selectedItem,
    setQuizItem
  }), [selectedItem, setQuizItem]);

  return (
    <QuizContext.Provider value={contextValue}>
      {children}
    </QuizContext.Provider>
  );
};

 

setQuizItem 함수를 useCallback 훅으로 감싸주는 방법을 사용하였다!

빈 배열은 함수가 컴포넌트가 마운트될 때만 생성되고 이후 변경되지 않음을 의미한다.

 

성능 비교

왼쪽이 성능 최적화 전, 오른쪽이 최적화 후 이다.

Render에 소요되는 시간이 줄어든 것을 확인할 수 있다.


 

다른 방법으로는 Dispatch provider와 Dispatch consumer 분리가 있다. 하지만 context API는 한계가 존재하기 때문에 다음에는 recoil을 사용해 전역 상태 관리를 해보려고 한다. 상태 관리를 제대로 공부해서 실전에도 써먹는게 올 해 목표이다!

 

참고한 글

https://itchallenger.tistory.com/285

 

리액트 성능 최적화 :contextAPI

컨텍스트 API는 props drilling을 막기 위한 수단일 뿐이며, 내부의 state가 갱신되면, 하위 컴포넌트들이 전부 렌더링되는, 리액트 컴포넌트의 룰을 벗어나지 않는다. 즉, 가 다시 렌더링될 때마다 하

itchallenger.tistory.com

 

728x90