성능 최적화를 위해 리렌더링을 줄이자!
라고 하면서, React.memo()나 useMemo()를 남발하고 있지는 않았나 되돌아보자.
리액트에서 "렌더링"이란 무엇인지 정확히 알고, 최적화를 어떻게 시킬 수 있는지 알아보자!
리액트의 렌더링이란?
아래 과정 중 Render phase를 리액트에서 렌더링이라고 부른다.
Trigger phase
0️⃣ 최초 진입 시
0️⃣ state, 부모의 state 의 변경
- 각 컴포넌트에서 state의 변경 시 dirty check ➡️ batch 추가
Render phase
1️⃣ render() 호출 (클래스형 컴포넌트) 또는 함수 컴포넌트 호출 (함수형 컴포넌트)
2️⃣ 가상 DOM 재조정
- 이전 가상 DOM과 현재 가상 DOM을 비교 (Diffing Algorithm)
- 이때, O(N^3)의 비교 문제가 생기는데, 리액트는 휴리스틱 알고리즘을 사용하여 O(N)으로 처리한다.
- batch에 쌓인 dirty check된 element 처리
Commit phase
3️⃣ 렌더 단계의 변경 사항을 실제 DOM에 적용
재조정 과정의 Diffing Algorithm
- 이전 가상 DOM과 현재 가상 DOM의 root element부터 비교
- element 타입이 다른 경우 -> 이전 트리를 버리고 완전히 새로운 트리를 구축
- element 타입이 같고 속성이 다른 경우 -> 변경된 속성만 업데이트
- root element의 처리가 끝나면, 해당 노드의 자식들을 재귀적으로 처리한다.
여기까지 정리하자면,
1. 리액트의 렌더링은 Trigger -> Render -> Commit 으로 이루어진다.
2. Render phase의 트리거는 [최초 진입 / 컴포넌트의 state 또는 부모의 state 변화] 이다.
3. Commit phase의 트리거는 [리렌더링 시 재조정이 이루어졌을 때] 이다.
Render phase(리렌더링)를 막는 방법
React.memo()
공식문서에 따르면 다음과 같은 기능이다.
memo lets you skip re-rendering a component when its props are unchanged.
memo 는 컴포넌트의 props이 변경되지 않았다면 리렌더링을 스킵시켜준다.
부모의 state가 변경될 때, 자식 컴포넌트 역시 리렌더링된다. memo는 props이 변경되지 않았다면 리렌더링을 막는다.
아래와 같은 상황에서, Child 컴포넌트를 memo로 감싸주면 더이상 렌더링되지 않는다!
Commit phase를 막는 방법
useMemo()
useMemo is a React Hook that lets you cache the result of a calculation between re-renders.
useMemo 는 리렌더링 과정에서의 계산 결과를 캐싱해주는 리액트 훅이다.
부모 컴포넌트가 리렌더링 되면, 컴포넌트 내부의 변수는 초기화된다. 이 변수가 참조 타입의 값을 할당받는다면, 값을 자식 컴포넌트의 props으로 내려주면, 값의 실제 변화와는 상관 없이 부모 컴포넌트가 리렌더링 될때마다 자식 컴포넌트가 받은 값은 항상 다른 참조값을 가지므로 Diffing Algorithm에 따라 Commit phase로 넘어간다. useMemo는 주소값을 캐싱해주기 때문에 이러한 일을 막을 수 있다.
아래 글을 확인해보면, 원시타입은 값 그 자체를 할당하기 때문에 초기화 시에 계속해서 같은 값을 갖는 반면, 참조타입은 주소를 할당하기 때문에 초기화 시 값이 같더라도 다른 주소를 갖는다는 것을 알 수 있다.
즉 아래와 같은 상황에서, 변수 a는 주소값을 할당받고, b는 원시값을 할당받기에 FirstChild만 Commit phase에 들어가게 된다.
useCallback()
useCallback is a React Hook that lets you cache a function definition between re-renders.
useCallback 은 리렌더링 과정에서의 함수 선언을 캐싱해주는 리액트 훅이다.
마찬가지로 부모 컴포넌트가 리렌더링 되면, 컴포넌트 내부의 함수는 초기화된다. 함수 역시 참조타입이므로 이 함수를 자식 컴포넌트의 props으로 내려주면, 부모 컴포넌트가 리렌더링 될 때마다 자식 컴포넌트 리렌더링, Diffing Algorithm에 따라 Commit phase로 넘어간다. useCallback은 함수를 캐싱해주기 때문에 이러한 일을 막을 수 있다.
번외) React devtools 확장프로그램은 언제 하이라이팅할까
위 확장프로그램을 리액트 개발자라면 사용하고 있을 것이다.
여기서 하이라이팅은, 리렌더링 시 표시될까? 아니면 실제 DOM이 업데이트될 때 표시될까?
실험을 위해 간단한 코드를 작성했다. 아래 코드는 버튼을 클릭하면 state가 변화하지만, 해당 state가 어디에도 쓰이고 있지 않아 DOM 업데이트는 없는 상황이다.
코드를 실행시키면 아래와 같이 하이라이팅이 아주 잘 된다. 즉 commit phase 실행 기준이 아닌, render phase 기준으로 하이라이팅을 해준다는 것을 알 수 있다.
참고자료
- 김용찬, <모던 리액트 Deep Dive>, 위키북스, 2023.
'TIL > React' 카테고리의 다른 글
[240331] 모던 리액트 딥 다이브 스터디 3주차 - (2) children props (0) | 2024.03.31 |
---|---|
[240327] 모던 리액트 딥 다이브 스터디 3주차 - (1) useContext (0) | 2024.03.27 |
[240207] 리액트의 가상 돔(Virtual DOM) (1) | 2024.02.07 |
[240205] 리액트 라이프사이클, useEffect (0) | 2024.02.05 |