[React] Rendering 과정 및 Re-rendering 방지 기술 정리

amjong2·2022년 12월 24일
0

참고 및 출처

[10분 테코톡] 리액트 렌더링 최적화 - 우아한Tech 유투브 채널, 2022.08

[번역] useEffect 완벽 가이드 - 안도형(Dohyung Ahn)님, 2019.10


렌더링 과정 및 조건

브라우저에서의 렌더링 과정

브라우저에서 렌더링 과정은 위와 같은 과정을 거친다.
HTML과 CSS, JS를 활용해 DOM과 CSSOM 요소들이 Render Tree로 만들어지고 이들이 Layout -> Paint 과정을 거쳐 화면에 뿌려지게 된다.

리액트에서의 렌더링

함수(컴포넌트)를 호출하는 것.
내부적으로 Render Phase, Commit Phase를 거쳐 실제 렌더링 된다.

Render Phase

함수(컴포넌트) 호출하여 리턴받은 React Element를
Virtual DOM에 업데이트(재조정) 한다.

Commit Phase

Virtual DOM에서 변경된 부분만 실제 DOM Tree에 업데이트 한다.

리액트에서 리렌더링 되는 조건

  • state가 바뀌었을 때
  • props가 바뀌었을 때

리렌더링 방지 기술

기본적으로 아래 함수나 hook들은 memoization하는 역할은 동일하지만
어떤 대상을 memoization하는 지가 다르다.
각각의 목적을 이해하고 있으면 될 것 같다.

useCallback

"함수 참조값"을 메모이제이션하기 위한 리액트 훅.
함수를 props로 전달하는 케이스 등에서 리렌더링을 방지하기 위한 용도.

부모 컴포넌트 예시

자식 컴포넌트 예시

어떤 자식 컴포넌트는 부모 컴포넌트에서 props로 내용이 바뀌지 않는 함수(onClick) 하나만을 받는다고 하자.

이 자식 컴포넌트는 부모 컴포넌트가 리렌더링 되었다고 다시 리렌더링이 될 필요가 없다 (함수 내용이 바뀌지 않으므로). 거기다가 자식 컴포넌트 내에서 위처럼 많은 작업이 필요하다면, 더더욱 리렌더링을 방지해야한다.

하지만, 부모 컴포넌트에서 prop으로 넘겨주는 함수인 handleClick 은 부모 컴포넌트가 리렌더링 될 때마다 새로운 참조값의 함수로 재생성되고, props로 전달되는 함수 역시 새로운 참조값이 되어
자식 컴포넌트 입장에서는 새로운 props가 전달되므로 리렌더링이 된다.

이럴 때 이용할 수 있는게 useCallback() 이다.
useCallback()은 함수 참조값을 메모이제이션하므로,
부모 컴포넌트가 리렌더링 되어도 useCallback()의 dependency가 바뀌지 않는다면 참조값이 변하지 않아
자식 컴포넌트로 전달되는 props는 동일한 값이 될 것이다.

이러면 자식 컴포넌트는 리렌더링 되지 않을까?
반은 맞고 반은 틀렸다.

리액트 내부적인 리렌더링 과정 중 Render Phase는 진행되고, Commit Phase에서 props의 값이 같으므로, 실제 브라우저 DOM tree에는 업데이트가 발생하지 않는다.

그렇다면 이 Render Phase까지 진행되지 않도록 할 수는 없을까?

React.memo

"컴포넌트"를 메모이제이션 하기 위한 함수. 고차 컴포넌트라고도 함.
컴포넌트를 인자로 받아 기본적으로 얕은 비교(참조값 비교)를 통해 props를 비교하여 동일한 값이면 component를 리렌더링 하지 않도록 설정.

React.memo() 를 사용하면 위와 같은 상황에서 자식 컴포넌트의 리렌더링을 방지할 수 있다.

useCallback 으로 부모 컴포넌트 함수의 참조값을 변경되지 않게 하고,
React.memo() 로 자식 컴포넌트로 전달되는 props가 바뀌지 않는다면 컴포넌트가 리렌더링이 되지 않도록 했다.

만약 여기서, props가 함수 형태가 아니라 객체 형태라면 어떨까?

객체 형태일 때도 컴포넌트 내부에서 선언된 객체라면 컴포넌트가 재호출 될 때마다 참조값이 변경될 것이다.
따라서 React.memo()를 사용해도 props가 변경되니 의미가 없다.

useMemo

특정 "값"을 메모이제이션 하기 위한 함수.

const memoizedValue = useMemo(() => object, []);

useMemo() 를 활용하여 props를 전달하면 의도한 대로 리렌더링이 발생하지 않는다.

useCallback, useMemo, React.memo() 를 무조건 사용하는 게 좋을까?

근본적인 코드를 먼저 개선하는 게 먼저다. 미리 최적화 하지 말고 필요할 때 하는게 최선.

기본적으로 위 함수들도 내부적인 어떤 비용들이 있고
props나 참조값이 무조건 변경되는 케이스에서 위 도구를 사용하는 경우 오히려 낭비가 될 수도 있다.

리팩토링

자식 컴포넌트가 계속 부모 컴포넌트 리렌더링에 의해 리렌더링 된다면,
자식 컴포넌트를 부모 컴포넌트의 props로 주입받는 방식으로 refactoring 할 경우
위와 같은 최적화 도구를 사용하지 않고도 자식 컴포넌트는 부모 컴포넌트의 리렌더링에 영향 받지 않는다.


모든 렌더링은 고유의 값을 가지고 있다

모든 컴포넌트는 어플리케이션 동작에 따라서 렌더링이 계속 발생할 것이고
그 중 특정 시점에서의 렌더링에서
컴포넌트 내부의 state, props, useEffect, 기타 내부 함수 등의 요소를 생각해보자.

이 값과 함수들은 특정 시점에서 어떤 값을 가지고 있을까?

우리가 코딩할 때 마치 state는 실시간으로 변하는 값으로 인식하고 있지만, 특정 렌더링 시점에서 state는 상수 처럼 고유한 값을 가지고 있다.

그리고 컴포넌트 내부 함수나 useEffect에서 이 state를 참조하고 있다면,

javascript의 closure 개념에 의해서
설령 setTimeout 으로 특정 시점이 아닌 미래의 state를 참조하는 함수 라고 해도 내부 함수는 항상 특정 시점에서의 state를 보고 있다.

그리고 이는 useEffect 도 마찬가지이다.
useEffect 내에서 state를 참조하고 있을 때 state의 최신 값을 매번 참조한다면, useEffect 가 변화하지 않고 state가 내부에서 변화하는 게 아니라

특정 시점의 useEffect 에서 참조하는 state는 고유하므로
useEffect 함수 자체가 매 렌더링마다 별도로 존재한다.

리액트에서의 라이프사이클

profile
GAAMZA

0개의 댓글