[원티드 FE 프리온보딩] 3-2. React 렌더링 최적화, React.memo, Advanced Hook

2023. 1. 6.·🎨 프론트엔드 공부/기타

원티드 프론트엔드 프리온보딩 (22.12.19 월 ~ 23.01.20 금)

 

1. 렌더링

📍렌더링이란?

브라우저에서의 렌더링 : DOM 요소를 계산하고 그려내는 것

- JavaScript DOM API 가 담당

 

React의 렌더링

- 실제 렌더링 과정은 React에서 대신 처리해주고, 개발자는 UI 를 설계하는데 집중 (선언형 프로그래밍)

- React 내부에서 렌더링이 어떻게 실행되는지 이해하고, 이를 최적화할 필요가 있다

 

📍React에서 리렌더링이 발생하는 시점

⭐state가 변했을 때

⭐특정 컴포넌트의 state가 변하면, 해당 컴포넌트와 하위 컴포넌트가 모두 리렌더링됨

 

state를 사용하는 이유

- state(변경될 수 있는 데이터)를 UI와 연동하기 위해서

 

📍React의 렌더링 과정

1️⃣기존 컴포넌트의 UI를 재사용할지 확인

2️⃣(함수형 컴포넌트) : 컴포넌트 함수 호출

(클래스형 컴포넌트) : render 메서드 호출

3️⃣새로운 VIrtual DOM 생성

4️⃣이전의 Virtual DOM과 새로운 Virtual DOM을 비교해서 실제 변경된 부분만 DOM에 적용

 

CRP (Critical Rendering Path)

- 브라우저가 화면을 보여주기 위해 HTML, CSS, JS를 다운로드받고, 이를 처리하여 픽셀 형태로 그려내는 과정

1️⃣HTML 파싱 -> DOM 생성

2️⃣CSS 파싱 -> CSSOM 생성

3️⃣DOM과 CSSOM을 결합해서 Render Tree를 생성

4️⃣Render Tree와 Viewport의 width를 통해 각 element의 위치와 크기 계산 (Layout)

5️⃣지금까지 계산된 정보를 이용해 Render Tree 상의 요소들을 실제 Pixel로 그림 (Paint)

 

Layout과 Paint는 많은 연산이 필요한 과정이며, 이 부분을 최소화하는 것이 성능 개선의 핵심

(React가 Virtual DOM을 사용하는 이유)

📍React에서 사용할 수 있는 최적화 아이디어

1️⃣새롭게 컴포넌트 함수를 호출하지 않기

- 이전의 결과값을 그대로 사용하도록 함

 

2️⃣새로 만드는 Virtual DOM이 기존 형태와 가급적 차이가 없게 하기

 

2. React.memo

📍사용 목적

⭐컴포넌트를 Memoization

state(props)가 변한 컴포넌트라면 당연히 리렌더링이 필요하지만, 하위 컴포넌트까지 불필요한 리렌더링을 할 필요가 없다

- 이런 경우에는 새롭게 컴포넌트 함수를 호출하지 않고, 이전 값(memoization)을 그대로 사용하는 것이 효율적이다

 

React.memo 는 props 변화에만 영향을 주며, useState, useReducer, useContext 훅이 사용되면, 여전히 state나 context가 변할 때 다시 렌더링된다

📍사용 방법

React.memo는 고차 컴포넌트이며, 컴포넌트(함수)를 인수로 받아 컴포넌트를 반환한다 (고차 함수처럼 동작)

const MyComponent = React.memo(function MyComponent(props) {
  /* render using props */
});

 

⭐React.memo로 감쌀  컴포넌트는 화살표 함수 대신 함수 선언문을 사용한다 (타입스크립트에서도 수월)

 

React.memo로 감싸진 컴포넌트의 경우, 부모 컴포넌트가 리렌더링된다고 무조건 리렌더링되는 것이 아니라, props가 바뀌는 경우에만 리렌더링 수행

- 이를 통해 불필요한 리렌더링을 막을 수 있음

 

props를 비교하는 방법

- 기본값 : shallow compare (모든 props를 비교하며 하나라도 다르면 다른 것으로 인식)

- 하지만, 메모리 주소가 바뀌면 다르다고 판단

- 비교하는 로직을 바꾸고 싶으면 compare 함수를 직접 작성할 수 있음

 

shallow compare의 한계

- props에 함수를 넣었을 경우, 함수는 기본적으로 객체이므로 리렌더링마다 메모리 주소가 달라진다- 따라서 shallow compare로는 같은 코드의 함수라도 다른 메모리 주소라서 다른 값이라고 판정

 

⭐compare 함수의 반환값이 true이면? 같은 컴포넌트이므로 재사용

⭐compare 함수의 반환값이 false이면? 리렌더링

function MyComponent(props) {
  /* render using props */
}

function areEqual(prevProps, nextProps) {
  /*
  true를 return할 경우 이전 결과를 재사용
  false를 return할 경우 리렌더링을 수행
  */
}

export default React.memo(MyComponent, areEqual);

 

예시)

⭐re render 버튼을 클릭해도 React.memo로 감싼 (memoization된) 컴포넌트는 리렌더링되지 않음

⭐그러나, compare 함수가 false를 반환하면, 무조건 리렌더링 (React.memo로 감싸는 경우에도)

import React, { useState } from "react";
import "./styles.css";

export default function App() {
  const [text, setText] = useState("");
  const [_, setState] = useState(1);

  const reRender = () => setState((prev) => prev + 1); // state를 바꿔 리렌더링 유발하는 함수

  return (
    <div className="App">
      <h1>Memoization Test</h1>
      <input value={text} onChange={(e) => setText(e.target.value)} />
      <button
        style={{ display: "block", margin: "20px auto" }}
        onClick={reRender}
      >
        re render
      </button>
      <ChildComponent name="memo X" value={text} />
      <MemoizedComponent name="memo O" value={text} />
      <ReturnFalseMemo name="return false" value={text} />
      <ReturnTrueMemo name="return true" value={text} />
    </div>
  );
}

function ChildComponent({ name, value }) {
  console.log(`${name} rendered`);

  return (
    <h3>
      {name}: {value}
    </h3>
  );
}

const MemoizedComponent = React.memo(ChildComponent); // 재사용
// 만약, 이 컴포넌트에 props로 함수를 전달했다면?
// shallow compare로는 다르다고 판정 (메모리 주소 바뀜)

// compare 함수가 항상 false를 반환하므로, 무조건 React.memo를 사용해도 무조건 리렌더링
const ReturnFalseMemo = React.memo(ChildComponent, () => false);

// compare 함수가 true를 반환하므로 어떠한 경우에도 리렌더링 X
const ReturnTrueMemo = React.memo(ChildComponent, () => true);

 

3. Advanced Hook (Memoization 관련)

📍useMemo

React.memo가 컴포넌트를 memoize한다면, useMemo는 값을 memoize한다

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

 

인수1️⃣ memo할 값을 리턴하는 콜백 함수

- 콜백 함수의 인수들은 의존성 배열의 원소로 추가되어야 함

 

인수2️⃣ 의존성 배열 (배열의 원소 중 하나라도 변경되면 새로운 값을 재계산)

- 의존성 배열이 없다면 렌더링마다 재계산

 

📍useCallback

useCallback은 메모이제이션된 콜백 함수를 반환

- useCallback(fn, deps)은 useMemo(() => fn, deps)와 같다

- 따라서 useMemo를 편하게 쓰는 방법이라 할 수 있다

const memorizedFunction = useMemo(() => () => console.log("Hello World"), []);

const memorizedFunction = useCallback(() => console.log("Hello World"), []);

 

⭐특정 원시값 하나를 메모하는 경우가 아니면 대 useCallback을 사용하면 된다

⭐shallow compare의 문제 해결 가능!

- 만약 props에 함수를 넣는다면, useCallback으로 그 함수를 메모이제이션해주고 넣으면, 메모리 주소가 바뀌지 않아서 shallow compare에서 같다고 판단

 

📍언제 Memoization을 해야할까?

1️⃣새로운 값을 만드는 연산이 복잡할 때

- 예) 1만개의 원소를 가진 배열을 생성해야 할 때

 

2️⃣컴포넌트의 렌더링 전후 값의 동일성을 보장하고 싶을 때

- 예) shallow compare의 메모리 주소 바뀌면 다르다고 판단하는 문제를 useCallback으로 해결

 

최적화가 무조건 좋은 것은 아님을 명심

- 최적화로 인해 프로그램 복잡도 증대로 유지보수 비용 증가

- 성능 이슈가 확실하게 예상될 경우에만 시도

'🎨 프론트엔드 공부/기타' 카테고리의 다른 글
  • [원티드 FE 프리온보딩] 4-1. 클린 코드, 관심사의 분리, React Custom Hook
  • [원티드 FE 프리온보딩] 3-3. useEffect, Context API
  • [원티드 FE 프리온보딩] 3-1. React 프로젝트 리팩토링 아이템
  • React Query(v4) - 9. InfiniteScroll by react-infinite-scroller
지식물원
지식물원
지식이 자라는 식물원!
  • 지식물원
    지식물원
    지식물원
  • 전체
    오늘
    어제
    • 분류 전체보기 (516)
      • 🎨 프론트엔드 공부 (253)
        • JS & TS (92)
        • HTML & CSS (22)
        • React & Next (49)
        • Vue & Nuxt (22)
        • 기타 (68)
      • 🤓 기술 학습 & 공부 기록 (116)
        • Node.js (0)
        • Python (37)
        • 백엔드 (0)
        • 딥러닝 (1)
        • 컴퓨터 일반 (72)
        • 개발 인프라 (6)
      • 👨‍💻 프로젝트 경험 (6)
        • Work (0)
        • Toy (6)
      • ⚙️ 개발 팁 & 노하우 (21)
        • 프론트엔드 (6)
        • 기타 (15)
      • ☕️ 커리어 & 인터뷰 준비 (88)
        • 코딩 테스트 (88)
      • 📰 기술 트렌드 & 생각 정리 (4)
      • 📚 기타 (25)
        • 마케팅 (15)
        • 비개발서적 (10)
  • 블로그 메뉴

    • 태그
  • 링크

  • 공지사항

    • 모바일 접속 시 코드 하이라이팅 깨질 때
  • 인기 글

  • hELLO· Designed By정상우.v4.10.3
지식물원
[원티드 FE 프리온보딩] 3-2. React 렌더링 최적화, React.memo, Advanced Hook
상단으로

티스토리툴바