ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 리액트 렌더링 최적화
    CS지식 2023. 11. 14. 21:43

    리액트 랜더링 최적화에 대해 알아보기 전에 알아야할 사전지식을 먼저 정리하고 시작한다.

    - 리액트에서의 렌더링 = 함수를 호출하는 것

    App 컴포넌트가 호출되고 -> 내부 로직들이 싱행되고 -> return문의 리액트 element 반환하는 것
    이것이 리액트에서의 렌더링이다.

    - 리렌더링 되는 조건 

    1. state가 바뀌었을 때

    2. props가 바뀌었을 때

    3. 부모컴포넌트가 바뀌었을 때

    - 불필요한 렌더링 발생 확인 지표

    1. console.log

    2. React Developer Tools → Profiler 

    : 웹사이트의 컴포넌트들이 어떻게 렌더링되고 있는지를 보여주는 도구
    : 어디 부분에서 렌더링이 오래 걸리고 불필요한 렌더링이 걸리는지 확인해야 한다면 용이!

    - 리액트에서의 렌더링 과정

    1. render phase = 재조정

    컴포넌트를 호출하여 React Element를 반환하고 새로운 Virtual DOM을 생성한다.

    만약, 이번이 첫번째 렌더링이 아니라면!!
    이전 Virtual DOM과 현재 Virtual DOM을 비교하는 재조정 과정을 거친 후,
    Real DOM에 변경이 필요한 목록들을 체크해둔다.

     

    2. commit phase

    render phase에서 체크해놓았던 변경이 필요한 부분들을 Real DOM에 반영해주는 단계!!!
    만약, 변경이 필요한 부분들이 없다면 commit phase는 skip 된다.


    불필요한 렌더링 방지하기

     

    여기 세개의 컴포넌트가 있다.

    부모요소인 Parent 컴포넌트, 자식요소인 FirstChild 컴포넌트, SecondChild 컴포넌트이다.

     

    valueForFirstChild state에게 변경이 생기면
    Parent 컴포넌트와 FirstChild 컴포넌트는 본인들이 사용하는 값(state, props)에 변경이 생겨서
    변경이 된 값을 보여줘야하므로 리렌더링이 되는 것이 당연하다!

     

    그런데 SecondChild 컴포넌트 입장에서는 변경된 값(state, props)도 없는데
    똑같은 정보를 보여주기 위해 리렌더링 되는 게 불필요해 보인다.
    더불어서 SecondChild 컴포넌트가 리렌더링이 되면 약 1000개의 GrandChild 컴포넌트들도 리렌더링이 될 것이다. 

     

    SecondChild 컴포넌트는 Parent 컴포넌트의 변경된 state가 아닌 함수만을 props로 받고 있기 때문에
    리렌더링이 안될 것도 같지만 리렌더링이 되고있다.
    왜냐하면 Parent 컴포넌트가 매번 리렌더링 될 때마다 handleClick이라는 함수도 재생성이 되고
    이전 handleClick과 현재 handleClick 함수는 서로 다른 참조값을 가지게 되며 이 둘은 서로 다른 함수이다.
    즉, handleClick도 props이니 달라지면서 컴포넌트가 리렌더링 되는 것이다.

     

    handleClick 함수의 참조값이 바뀌지 않도록 해주면 불필요한 리렌더링을 방지해 줄 수 있지 않을까?
    어떻게 매번 리렌더링 될 때마다 함수의 참조값을 유지할 수 있을까?

    함수를 메모이제이션 해주는 useCallback 사용하자!!

     

    근데.. console.log를 찍어보니 여전히 리렌더링이 되고있다.
    그렇다면 효과가 전혀 없는 것일까? 아니다. 그래도 효과는 있다. 
    render phase는 실행되지만 commit phase는 실행되지 않는다.

     

    더 자세히 설명하자면
    리액트에서 렌더링이 일어날 때마다
    render phase와 commit phase의 렌더링 프로세스를 거치게 되는데
    render phase는 실행되지만
    useCallback을 활용해서 props 값을 이전과 같게 유지해주었기 때문에
    render phase의 재조정과정에서 Real DOM에 변경이 필요하지 않다고 판단을 해준다.
    그래서 commit phase는 실행되지 않는다.

     

    render phase도 막아줄 수는 없을까? 아직도 SecondChild 컴포넌트가 리렌더링 되고있는데..

    React.memo도 사용하자!!!!

     

    React.memo는 
    전달받은 props가 이전 props와 비교했을 때 같으면 컴포넌트의 리렌더링을 막아주고
    마지막으로 렌더링된 결과를 재사용하는 고차 컴포넌트이다.

     

    이 과정에서 props를 비교할 때 기본적으로 얕은 비교를 통해서 진행한다.
    얕은 비교란 원시타입은 값이 다른지 비교하고 참조타입은 참조값이 같은지 비교하는 것이다.
    (콜스택만 비교한다. 메모리힙은 비교하지 않는다.)

    SecondChild 컴포넌트에 React.memo를 적용하면
    SecondChild 컴포넌트가 렌더링 과정에 진입하기 전에
    props 값인 onClick에 대해서 이전 값과 현재 값이 다른지를 비교해준다.
    서로 같은 값이면 SecondChild 컴포넌트의 렌더링 과정은 진행이되지 않는다.

     

    그렇다면.. 함수가 아닌 객체를 props로 넘겨주게 된다면 리렌더링이 발생할까?

     

    리렌더링이 발생한다..!! 왜 그럴까..?

    SecondChild 컴포넌트 입장에서는 Parent 컴포넌트로부터 매번 다른 item을 전달받기 때문이다.

     

    Parent 컴포넌트가 리렌더링 될 때마다
    item 객체가 새로 생성이 되고 매번 다른 참조값을 가지게 되는 것이다.
    그래서 React.memo가 정상적으로 작동되지 않는다.

     

    객체 props에 React.memo가 정상적으로 작동되게 하기 위해서는 

    ⇒ 값을 메모이제이션해주는 useMemo를 사용하자!


    이렇게 useCallback, React.memo, useMemo로 렌더링 최적화하는 방법에 대해 알아보았다.

    그렇지만.. 이를 남발하는 것은 좋지 않다...!!!!

    이 또한 하나하나가 메모리를 차지하고 비용이 드는 것이다.

    잘못 사용한다면 최적화가 아니라 오히려 더 성능을 망칠 수가 있다.

     

    리렌더링이 자주 되는 컴포넌트라고 해서 컴포넌트 내부의 함수를 무작정 useCallback으로 감싸주는 경우,
    부모로부터 전달받은 props가 매번 바뀔 수 밖에 없는 상황에서 자식 컴포넌트에 React.memo를 적용해주는 경우에서는
    최적화를 시도하기 전보다 웹 사이트 성능이 더 안 좋아질 수도 있다.

     

    제대로 이해를 하고 써야하는데 사실 복잡한 로직 속에서 이를 제대로 쓰기란 쉽지 않을 것이다.

     

    그래서 useCallback, React.memo, useMemo를 사용하기 전에,

    코드적으로 개선할 수 있는 방법은 리팩토링을 해서 개선하도록 해야한다.

     

    예를들어, 불필요하게 리렌더링이 되는 자식요소는 children으로 주입하자!
    이렇게 리팩토링 해주면 리렌더링 되지 않는다.
    왜냐하면 부모컴포넌트 return문에는 children에 대한 React.createElement가 존재하지 않기 때문에
    부모 컴포넌트가 렌더링되더라도 자식 컴포넌트는 호출되지 않기 때문이다.

     

    가장 중요한 것은..
    미리 성급하게 최적화를 하지않고 정말 최적화가 필요할 때 최적화를 해주는 것이다. 


    너무나도 많은 도움이 된 테코톡 앨버님의 영상!! 너무 감사합니다🌟👍🏻🌟

    https://youtu.be/1YAWshEGU6g?si=sWHdyM1A4YhVvKjT

    댓글

Designed by Tistory.