리액트 렌더링 최적화

junamee·2023년 1월 19일
0

프론트엔드

목록 보기
12/16

리렌더링을 제어하는 방법

*render-and-commit
https://www.geeksforgeeks.org/reactjs-reconciliation/
리렌더링을 줄여야하는게 성능최적화의 한가지 방법이라 중요한데, 정확히 리렌더링이 무엇인지 알고있을까?
render Phase와commit Phase로 렌더링과정을 나누어 볼 수 있는데 render Phase는 가상돔끼리의 비교, diff 알고리즘으로 계산해 어디를 바꿀지 파악하는 과정이라고 이해하면 되고, commit phase에서 실제 dom을 변경하는 렌더링이 발생한다.

렌더링이 발생되는 시점 컴포넌트에 전달되는 state, props들이 변경될때이다. props는 어떤 데이터이기도, onClick과 같은 함수가 될 수도 있다. 컴포넌트에 전해지는 모든 props들은 특별한 조치를 취하지 않으면 참조형 데이터로서 부모 컴포넌트가 실행될 때, 새로운 데이터 참조를 갖게되어 재렌더링을 일으킨다.

이를 해결하기 위해서 참조값을 변하지 않도록하는 메모이제이션을 생각할 수 있다.

1.메모이제이션

props로 사용되는 데이터, 함수, 렌더링결과가 크게 변경될 일이 없을 때만 사용하는 것이 성능상 이점을 얻을 수 있다. 이 역시 기능이기 때문에 오히려 남용하면 성능이 더 악화될 수 있다.

  • useMemo
    : props가 변하지 않는 참조형 데이터인 경우
  • useCallback
    : props가 함수인 경우

캐시로 참조값을 유지시켜서 새로운 데이터로 인식하지 않도록한다.
렌더링을 확인하기 위해 콘솔을 찍어보면 콘솔이 확인되어서 리렌더링이 된 것으로 생각할 수 있는데, 이 메모이제이션은 render phase까지 진행되고 commit phase는 실행되지 않은 것이다.

  • React.memo

: 렌더링 결과를 메모이징한 후, 이전 이후 props를 얕은 비교를 통해 비교하고 렌더링 여부를 결정하여 불필요한 렌더링을 막는다. 비교 조건을 변경하고 싶다면 두번째 인자로 넣어주면 된다.
위의 useMemo, useCallback과 달리 아예 renderPhase 조차 막는 것이다.

function MyComponent(props) {
  /* props를 사용하여 렌더링 */
}
function areEqual(prevProps, nextProps) {
  /*
  nextProps가 prevProps와 동일한 값을 가지면 true를 반환하고, 그렇지 않다면 false를 반환
  */
}
export default React.memo(MyComponent, areEqual);

2.상태변경 최소화

상태는 props가 변경되거나 setState로 값이 변경될 때 발생하기 때문에 이런 경우를 줄이는 것이 도움이 된다.
props로 값을 내려주기보다 상태관리 라이브러리를 사용하는 것도 도움이 된다. 함수 역시 props로 넘기기 보다 useHooks와 같이 뷰에서 분리하여 필요한 곳에서 호출하는 방법도 있다.

3.Children 활용

props로 전해지는 값이 변경이 없을 때 렌더링이 되지 않았던것과 같이 children도 동일하다.리액트는 children에 대해 얕은 비교가 일어나기 때문이다.

before> children이 아닌 형제요소로 위치하고 있는 <Child/>컴포넌트.

const Parents= () =>{
  console.log('Parents')
  const [count, setCount] = useState(0)
  const addCount = () => setCount(prev => prev+1)
  return (
    <div>
      <button onClick={addCount}>{'plus'}</button>
      <div>{count}</div>
      <Child/>
    </div>
  )
}

const Child = () =>{
  console.log('Child')
  return <div>{'Child'}</div>
}

export default Parents;

plus버튼을 5번 눌렀을 때의 모습을 profiler로 확인해보았을 때이다. Parents 컴포넌트의 상태변화로 Parents와 Child 컴포넌트 둘다 렌더링 된 것을 확인할 수 있다. 이 구조에서 리렌더링을 막으려면 Child컴포넌트에 React.memo로 감싸주는 것이 도움이 된다.

after > children 컴포넌트로 수정

import React, {useState} from 'react';

function Children_test() {
  return (
    <Parents>
      <Child/>
    </Parents>
  );
}

interface Parents{
  children: React.ReactElement
}

const Parents= ({children}:Parents) =>{
  console.log('Parents')
  const [count, setCount] = useState(0)
  const addCount = () => setCount(prev => prev+1)
  return (
    <div>
      <button onClick={addCount}>{'plus'}</button>
      <div>{count}</div>
      <div>{children}</div>
    </div>
  )
}

const Child = () =>{
  console.log('Child')
  return <div>{'Child'}</div>
}

export default Children_test;

profiler에서 Parents컴포넌트만 렌더링되고 있음을 확인할 수 있다.

만약 부모의 상태변경으로 children 역시 상태 변경이 일어나는 상황이라면 렌더링 방지는 일어나게 된다.

때문에 children하나로 요소들을 넘기는 방법 말고도, props로 component를 넘기는 방법을 권장할 수 있다. 리액트 문서에서 이를 '구멍'에 비유하기도 했는데 children을 사용할때에는 하나의 구멍이고, props를 사용할 때에는 여러개의 구멍을 갖게되어 더 유연성있고 렌더링될 요소와 유지될 요소를 더 세밀하게 관리할 수 있게 된다.
(*리액트 합성)

결론적으로는 최상단 앱으로 부터 컴포넌트 depth와 props drilling을 줄일 수 있다.
(*합성을 통한 depth 축소)

profile
아티클리스트 - bit.ly/3wjIlZJ

0개의 댓글