React re-rendering

Y·2022년 10월 10일
1
post-thumbnail

React에서 re-render 이란?

React의 performance에 대해서 이야기할 때, 우리는 크게 2가지를 신경쓴다.

  • initial render (초기 렌더링) : 컴포넌트가 화면에 처음 등장할때
  • re-render : 두 번째, 그리고 연이은 렌더링 (이미 화면에 있는 컴포넌트)

Re-render는 React가 새로운 데이터로 앱을 업데이트 해야할 때 일어난다.
보통 유저 인터랙션이나 비동기적으로 처리되는 외부 데이터 등의 결과로 일어난다.

non-interactive 앱은 비동기적인 데이터 업데이트가 없으므로 절대 re-render 하지 않을 것이므로 re-render performance 최적화에 대해서 신경쓸 필요가 없다.

필요한 vs. 필요하지 않은 re-render가 있을까?

  • 필요한 리렌더링 : 컴포넌트가 직접접으로 변화된 새로운 정보를 사용할 때
    (예를 들어, 유저가 input 필드를 작성할 때, 해당 state를 관리하는 컴포넌트는 매 keystroke마다 자신을 업데이트해야할 것이다)

  • 불필요한 리렌더링 : 컴포넌트의 리렌더링은 실수로 혹은 비효율적인 앱 아키텍처에 의해서 앱 내에서 전파된다.
    (예를 들어, 유저가 input 필드를 입력할 때, 전체 페이지가 매 keystroke마다 리렌더링된다고 하면 매우 불필요할 것이다)

불필요한 리렌더링 그 자체는 문제가 되지 않지만, 리렌더링이 너무 자주 또는 무거운 컴포넌트 내에서 일어나면 이는 사용자에게 "게으른" 경험을 제공할 수 있다.

React 컴포넌트는 언제 스스로 리렌더링될까?

컴포넌트가 자기 자체로 리렌더링되는 이유는 크게 4가지이다.
state 변화 / parent(또는 children)이 리렌더링 / context 변화 / hook 변화

추가적으로, props 변화에 따라 리렌더링된다는 이야기도 있지만 이는 사실이 아니다. 아래에 설명이 있다.

1. state 변화로 인한 리렌더링

컴포넌트의 state가 변경될 때 컴포넌트는 리렌더링된다.
보통 callback이나 useEffect hook 내에서 일어난다.
state 변화가 리렌더링의 주요 원인이다.

2.부모의 리렌더링으로 인한 리렌더링

부모 컴포넌트가 리렌더링되면 컴포넌트가 리렌더링된다.
이는 즉 컴포넌트가 리렌더링 되면 그 자식 children도 리렌더링된다는 의미다.

항상 트리의 "아래" 방향으로 진행된다. 자식의 리렌더링은 부모의 리렌더링을 일으키지는 않는다.

3. context 변화로 인한 리렌더링

Context Provider의 value가 변화될 때, 이 Context를 사용하고 있는 모든 컴포넌트는 리렌더링된다. 해당 데이터의 정확한 portion을 직접적으로 바꾸지 않아도 리렌더링된다.
이러한 리렌더링은 memoization으로 예방할 수 있다.

4. hooks 변화

hook 안에서 발생하는 것들은 해당 hook을 사용하는 컴포넌트에 속한다.

  • hook 내의 state 변화는 어쩔 수 없는 “host” 컴포넌트의 re-render를 야기할 것이다
  • hook이 Context를 사용하고, Context의 value가 변하면 이 또한 “host” 컴포넌트의 re-render를 야기할 것이다

hook은 chain 될 수 있다.

5. props 변화 ?

memoization 되지 않은 컴포넌트에 대해서는 리렌더링에 있어 prop의 변화는 상관이 없다.

prop이 변하기 위해서는 부모 컴포넌트에서 업데이트가 되어야한다.

즉, 부모 컴포넌트는 리렌더링될 것이고 prop과 무관하게 어차피 자식 컴포넌트는 리렌더링이 될 것이다.

React.memo나 useMemo와 같은 memoization 기술이 사용될때에만 prop 변화는 중요해진다.


composition을 통해 리렌더링 방지

🚫 anti-pattern : render 함수에서 컴포넌트를 생성하는 것

컴포넌트의 render 함수 내에 컴포넌트를 생성하는 것은 안티 패턴이고, 성능을 악화시키는 주요 원인이 될 수 있다.

모든 리렌더링마다 해당 컴포넌트를 re-mount 할 것이고(destroy it and re-create it from scratch), 일반적인 리렌더링보다 훨씬 느릴 것이다. 또한 아래와 같은 버그를 유발할 것이다.

  • 리렌더링 동안 컨텐트의 “번쩍임” 가능성
  • 매 리렌더링마다 state가 리셋
  • 의존성 배열이 없는 useEffect가 매 리렌더링마다 실행
  • 컴포넌트가 포커스되어 있었다면, 포커스를 잃을 것

🟢 state를 아래로 전달하는 것

이 패턴은 무거운 컴포넌트가 state를 관리하고, state가 render tree 에서 작은 portion에 사용될 때 유용하다.

대표적인 예시는 버튼 클릭으로 모달을 열고/닫을 때가 있다.

이 경우, 모달의 형태와 열고닫을 수 있는 버튼에 대한 state는 더 작은 컴포넌트에 캡슐화할 수 있다.

결론적으로, 더 크고 무거운 컴포넌트는 이러한 state 변화에 리렌더링하지 않을 것이다.

🟢 children을 prop으로

children을 state로 감싸는 방식으로도 불리는데, state를 아래로 전달하는 위의 패턴과 유사하다.

이 경우, 상태 관리와 해당 상태를 사용하는 컴포넌트는 작은 컴포넌트로 추출될 수 있고, slow 컴포넌트는 children으로 넘겨줄 수 있다.

작은 컴포넌트 입장에서, children은 그저 prop이므로 state 변화에 영향을 받지 않아 리렌더링되지 않을 것이다.

🟢 component를 prop으로

위의 패턴과 유사하다. state를 작은 컴포넌트 내에 캡슐화하고, 무거운 컴포넌트들은 prop으로 전달된다.

state 변화에 있어서 해당 prop은 영향을 받지 않으므로, 무거운 컴포넌트들은 리렌더링되지 않을 것이다.


React.memo로 리렌더링 방지하기

React.memo로 컴포넌트를 감싸는 것은 리렌더링의 downstream chain을 막을 것이다. (prop의 변화가 있지 않은 이상)

이는 state와 같이 리렌더링의 원인이 되는 요소에 의존적이지 않은 무거운 컴포넌트를 렌더링할 때 유용하게 사용된다.

🟢 React.memo : component with props

React.memo가 동작하기 위해서는, 원시값이 아닌 모든 prop은 memoized 되어야한다.

🟢 React.memo : component as props or children

React.memo는 children/prop으로 전달되는 엘리먼트에 적용되어야한다.

단순히 부모 컴포넌트를 메모이징하는 것만으로는 동작하지 않는다. children과 prop은 객체일테니, 모든 리렌더링때마다 변할 것이다.

Improving re-renders performance with useMemo / useCallback

🚫 anti-pattern : prop에 불필요하게 useMemo / useCallback 적용

그냥 단순히 prop을 메모이징하는 것은 자식 컴포넌트의 리렌더링을 방지하지 않는다.

부모 컴포넌트가 리렌더링하면, 이는 prop과 관계없이 자식 컴포넌트를 리렌더링할 것이다.

🟢 Necessary useMemo / useCallback

자식 컴포넌트가 React.memo로 감싸져있다면, 원시값이 아닌 모든 prop은 메모이징되어야한다.

컴포넌트가 원시값이 아닌 값을(non-primitive) useEffect, useMemo, useCallback의 의존성(dependency) 으로 사용된다면, 이는 메모이징되어야한다.

🟢 useMemo for expensive calculations

useMemo를 사용하는 또 다른 경우는, 모든 리렌더링때마다의 무거운 연산을 방지하기 위함이다.

useMemo는 그 자체로도 cost가 존재하기 때문에(메모리를 소요하고, 초기 렌더링을 살짝 느리게 만들 수 있다), 모든 연산에 사용하지는 말아야한다.

React에서, 컴포넌트를 마운트하고 업데이트하는 것이 가장 expensive 연산일 것이다.

결론적으로, useMemo는 React 엘리먼트를 메모이징할 때 주로 사용된다.

(이미 존재하는 render tree의 부분이나, generate된 render tree의 결과 / 예를 들면 새로운 엘리먼트를 반환하는 map function..)

Improving re-render performance of lists

key 속성은 React에서 list의 성능에 영향을 줄 수 있다.

중요한 것은, 단순히 key 속성을 부여하는 것이 list의 성능을 향상시키는 것은 아니다.

list 엘리먼트들의 리렌더링을 방지하기 위해서는, React.memo로 감싸야하고 best practice를 따라야한다.

key의 값으로는 string이어야하고, list의 엘리먼트들이 리렌더링됨에 있어서 일관되어야한다.

보통은 아이템의 id나 index가 key로 사용된다.

엘리먼트들이 추가 / 삭제 / 순서 재정비가 되지 않는 이상, 만약 리스트가 static하다면, 배열의 index를 key로 사용하는 것도 가능하다.

만약 동적으로 변하는 list에 배열 index를 사용하게 되면 아래와 같은 상황을 야기할 수 있다.

  • state나 uncontrolled element (ex. form inputs)를 가지는 경우 버그 유발
  • item들이 React.memo로 감싸져 있는 경우 저하된 성능

🚫 anti-pattern : list에서 random value를 key로 사용

랜덤으로 만들어진 값은 절대 key 속성으로 사용하면 안된다.

이는 매 리렌더링마다 React의 re-mounting을 유발할 것이며, 이는

  • 리스트의 매우 저하된 성능

  • state나 uncontrolled element (ex. form inputs)를 가지는 경우 버그 유발

Preventing re-renders caused by Context

🟢 memoizing Provider value

Context Provider가 앱의 최상단 root에 위치하지 않고, ancestor의 변화로 인해 리렌더링 가능성이 있다면, 해당 value는 메모이징되어야한다.

🟢 splitting data and API

data와 API(getters, setters)로 구성되어있는 Context의 경우, 이들은 동일한 컴포넌트 내에서 각각 다른 Provider로 분리될 수 있다.

이 경우, API만을 사용하는 컴포넌트는 data 변화에 있어서 리렌더링되지 않을 것이다.

🟢 splitting data into chunks

Context가 몇몇 독립적인 data 덩어리들을 다루고 있다면, 이들은 동일한 Provider 내에서 더 작은 provider로 쪼갤 수 있다. 이 경우 변화된 chunk의 consumer만 리렌더링될 것이다.

🟢 Context selectors

Context value의 한 부분을 사용하는 컴포넌트의 리렌더링을 방지 할 수 없을것이다. (사용하는 data의 조각이 변하지 않았어도 / useMemo 훅을 사용해도)

그러나 Context selector를 통해 가짜로 hoc 컴포넌트와 React.memo를 사용할 수 있다.

좌측 예제와 같이, something이 변경되지 않아도 useSomething이 리렌더링되는 상황이 발생한다.

이때 hoc(고차 컴포넌트)를 사용하여 특정 상태(something)가 변경될때만 리렌더링되도록 구현할 수 있다.

다른 예시 참고

HOC (Higher Order Components)
고차 컴포넌트는 컴포넌트 로직을 재사용하기 위한 React의 고급 기술로,
인자로 받은 컴포넌트를 새로운 별도의 컴포넌트로 다시 리턴해주는 함수다.


무작정 useCallback, useMemo, React.memo를 사용하면 좋을까?

useCallback, useMemo, React.memo도 결국엔 하나의 cost인 연산이므로,

무작정 모두 이것들로 감싸주는 경우 최적화 시도 전보다 성능이 저하될 수도 있다.

=> 최적화 도구를 사용하기 전에 근본적인 코드를 먼저 개선하자!

Don’t optimize rendering prematurely, do it when needed

참고 자료

React re-renders guide: everything, all at once

One simple trick to optimize React re-renders

profile
기록중

0개의 댓글