[React] 렌더링 성능 최적화

dee·2023년 1월 4일
1

react

목록 보기
5/6
post-thumbnail

🤔 렌더링 성능 최적화가 왜 필요할까?

React는 state가 변경되면 컴포넌트 리렌더링이 발생하고 리렌더링의 결과를 모아 Virtual DOM을 만든다. 생성된 Virtual DOM을 이전의 Virtual DOM과 비교하여 변경된 부분만을 실제 DOM에 적용시키는데 이 때 브라우저에 의해 수행된다. 그럼 여기서 컴포넌트를 반복적으로 렌더링 시키면 브라우저가 렌더링을 자주 수행하게 되고 성능이 저하되게 될 것이다. 그렇기 때문에 state가 이전 값과 다를 경우에만 렌더링이 발생되게 하는 렌더링 성능 최적화가 필요하다.


State Scheduling & Batching

렌더링 성능 최적화 방법을 알기 전에 state가 어떤식으로 갱신되는 지를 알 필요가 있다. state 반영하는 방법은 아래와 같다.

  • State Scheduling
    같은 state 값을 갱신할 때 바로 반영하는 것이 아니고 Scheduled State Change를 순서대로 유지하고 있다가 순차적으로 계산한 후에 컴포넌트에 반영하는 것을 말한다. 그러다보니 Scheduled State Change가 일어날 때 state가 최신값이 아닐 수가 있다. 그렇기에 최신값이 snapshot이 필요할 경우 이전 state를 인자로 넘겨주어 상태를 업데이트 해야한다.
const [count, setCount] = useState(0);

const add = () => setCount(prev => prev + 1);

Scheduled State Change
setState가 호출되고 상태 업데이트 함수를 호출하면 데이터로 상태가 업데이트 하게끔 예약하는 것을 말한다.

  • State Batching
    • 여러 state를 동시에 갱신할 때 동시에 갱신된 state을 값을 가지고 한 번에 컴포넌트에 반영하는 것을 말한다.
const [count, setCount] = useState(0);
const [isTrue, setIsTrue] = useState(false);
const [total, setTotal] = useState(0);

const add = () => {
  	// 동시에 업데이트를 한다.
    setCount(prev => prev + 1);
    setIsTrue(prev => !prev);
    setTotal(prev => prev + count);
}

React.memo

  • 고차 컴포넌트(Higher Order Component)
  • 컴포넌트가 불필요한 재평가가 이루어지는 것을 막기 위해 사용.
  • React.memo로 랩핑된 컴포넌트는 결과를 메모이징(Memoizing)하여 성능을 향상시킴.
  • 컴포넌트를 렌더링하지 않고 마지막으로 렌더링된 결과를 재사용.
  • props 변화에만 영향. state와 context가 변할 때는 렌더링됨.
  • 함수형 컴포넌트에서도 메모제이션의 장점을 얻게 해주는 컴포넌트.
  • 같은 props로 렌더링이 자주 일어나는 컴포넌트에 사용함.
const App = (props) => {/* 렌더링 */};
export default React.memo(App); // 얕은 비교 수행

  • React.memo가 다른 비교 동작을 하기 원한다면 아래와 같이 두번째 인자로 비교함수를 넘겨주면 된다.
const App = (props) => {/* 렌더링 */};
function areEqual(prevProps, nextProps){
	/* nextProps가 이전값과 같다면 true 반환 아니면 false */
}
export default React.memo(App, areEqual); // 얕은 비교 수행

🧐 모든 컴포넌트에 사용하지 않는걸까?
컴포넌트를 재평가하는 데 필요한 성능 비용과 PROPS를 비교하는 성능 비용을 맞바꾸는것과 다름이 없기 때문에 모든 컴포넌트에 사용하지 않고 필요한 부분에만 사용하는 것이 알맞다.


useMemo

  • 메모리제이션된 값을 반환.
  • useMemo를 사용해 데이터를 저장하면 이는 메모리를 사용하는 것
  • 의존성 배열에 따라 다시 실행.

📍아래 예시를 살펴보자
1. setPage되는 버튼을 클릭
2. 의존성 배열에 따라 useMemo의 console이 출력
3. setCount되는 버튼을 클릭
4. page의 값이 변하지 않았기 때문에 console이 출력되지 않음.

이에 따라 의존성 배열에 해당되는 값이 변할때 함수를 실행한다.

const App = () => {
    const [page, setPage] = useState(0);
    const [count, setCount] = useState(0);
  	
  	useMemo(() => {
      console.log(page);
    },[page])
  
	return (
    	<div>
      		<button onClick={() => setPage(prePage => prePage + 1)}>add page</button>
      		<button onClick={() => setCount(preCount => preCount + 1)}>add count</button>
      	</div>
    )
}

export default App

useCallback

  • 메모리제이션된 함수를 반환.
  • 의존성 배열이 변해야 새로운 상태값이 반영된 함수를 반환.
  • 컴포넌트 실행 전반에 걸쳐 함수를 저장할 수 있게 하는 훅.
  • 동일한 함수 객체가 메모리의 동일한 위치에 저장되므로 비교할 수 있음.
  • 함수는 정의되는 시점의 환경을 기억함.
  • 참조하고 있는 state값의 변경을 알려주어야 함. (최신 state값 참조)

📍왔썹 프로젝트에서 사용한 코드로 살펴보자.

  • 상황 : 실시간 랭킹과 최신 목록을 조회하는 두 개의 탭이 있으며 꿀조합 리스트를 서버에서 요청하여 state에 저장하고 있다. 이 때 꿀조합 리스트는 10개씩 요청하여 가져오고 있다.
  • 같은 조건의 리스트를 10개씩 요청하고 있으므로 같은 로직으로 계속 요청하면 되므로 useCallback으로 감싸주어 컴포넌트가 다시 렌더될때 다시 생성하지 않도록 해주었다.
  • 현재탭이 변경될 때마다 정렬_조건이 바뀌게 된다. 이에 의존성 배열에 현재탭을 넣어 탭이 변경될때마다 새로운 함수를 만들어 정렬 조건이 새로운 값으로 반영될 수 있도록 해주었다.
const 꿀조합_컬렉션_정렬해서_가져오기 = useCallback(async () => {
    const 랭킹리스트_정보 = await getRankingList(key.current, 정렬_조건, 10);

    if (랭킹리스트_정보) {
      key.current = 랭킹리스트_정보.마지막_키;
      setTimeout(() => {
        랭킹리스트_수정(prev => [...prev, ...랭킹리스트_정보.랭킹리스트]);
      }, 100);
    }
  }, [현재탭]);

useMemo vs useCallback의 차이

  • 반환하는 값이 다르다는 점.
  • useMemo는 값을 useCallback은 함수를 반환.

🍑 오늘의 공부 일기

최적화는 컴포넌트를 재평가하는 데 필요한 성능 비용을 props를 비교하는 성능 비용을 맞바꾼 것과 다름없다고 한다라는 이 말이 가장 기억에 남는다. 이 말로 또다시 개발하면서 가장 적절한 곳에 적절한 방법을 적용해야하는 것이 최고의 개발 방법이라는 것을 깨닫게 되었다.


참고자료
https://leehwarang.github.io/2020/05/02/useMemo&useCallback.html
https://ui.toast.com/weekly-pick/ko_20190731
https://yceffort.kr/2022/04/best-practice-useCallback-useMemo

profile
웹 프론트엔드 개발자

0개의 댓글