Skip to main content

인포메이트 6주차 공유(최적화)

· 15 min read

인포메이트 6주차 공유(최적화) 모음입니다.

리액트 리렌더링 최소화 하기 - 윤해진
Web Vitals 최적화를 위한 Tip.. - 김하나

리액트 리렌더링 최소화 하기 - 윤해진

들어가는 말

react developer tool이라는 크롬 확장 프로그램을 아시나요?

해당 확장자는 리액트로 만들어진 페이지의 렌더링을 직접 눈에 보이게 표시해 주는 프로그램입니다.

해당 프로그램을 다운 받고 작업하던 페이지의 렌더링 과정을 살펴보니 불필요한 렌더링이 너무 많이 일어나는 것을 확인할 수 있었습니다.

변화가 없는 컴포넌트가 렌더링 된다면 이는 분명 낭비가 발생하게 될 것입니다.

따라서 위와 같은 낭비를 최소화하기 위해 렌더링 최적화와 관련된 주제로 정해보았습니다.

렌더링을 실행하는 경우

리액트의 렌더링이란 함수를 실행하는 것이라고 할 수 있습니다.

(app 컴포넌트의 내부 로직을 실행)

렌더링을 실행하는 경우는 아래와 같습니다.

  • props가 변경되었을 때
  • state가 변경되었을 때

또한, 부모 컴포넌트가 렌더링이 되었을 경우 자식 컴포넌트도 렌더링이 됩니다.

  • parent 컴포넌트
    • childA 컴포넌트
    • childB 컴포넌트

위와 같은 구조로 구성이 되어있는 컴포넌트라 가정해 봅시다.

불필요한 렌더링이 생기는 경우

  • parent 컴포넌트 **(state 정의, handle 함수 정의)**
    • childA컴포넌트 **(props로 state 전달)**
    • childB컴포넌트 **(props로 handle 함수 전달)**

위와 같은 기준으로 state와 함수를 정의해 보았습니다.

parent에서 state의 값이 변경되었을 때, 렌더링이 되는 기준으로 생각해 보면

state 전달이 된 childA의 컴포넌트리렌더링이 일어나는 것이 맞습니다.

그러나, 새로 그려질 필요가 없는 childB의 경우에도 리렌더링이 일어나게 됩니다.(불필요 리렌더링 발생)

childB가 리렌더링이 일어나게 될 경우 childB 하위에 있는 자식 컴포넌트들 또한 리렌더링이 일어나게 되어 메모리 및 자원 낭비가 일어날 것입니다.

그렇다면 어떻게 childB에서 리렌더링을 막을 수 있을까요?

불필요한 리렌더링 막기

새로 그려질 필요가 없는 childB는 왜 리렌더링이 되는 것일까요?

간단하게 설명을 하자면, parent 컴포넌트에 정의되어 있는 state가 변경이 되며 새로 렌더링이 되는 parent 컴포넌트에서 hadle 함수가 재정의 되며 이전의 함수와 다른 참조값을 가지고 있기에 다른 함수로 인식하여 새롭게 childB 또한 리렌더링 되는 것입니다. (props의 값이 변경되면 재렌더링 된다는 개념에 의거)

그렇다면 함수의 참조값이 바뀌지 않게 한다면 리렌더링이 일어나지 않게 할 수 있지 않을까요?

⇒ 이는 react에서 사용하는 useCallback 훅을 사용하여 구현할 수 있습니다.

useCallback

  • 함수를 메모이제이션 해주는 훅
  • 기존에 수행한 연산의 결과값을 어딘가 저장해두고 나중에 필요할 때 재사용하는 기법
  • 함수를 useCallback으로 감싸주면 의존성 배열이 변하지 않는 이상, 함수를 새로 생성하지(동일한 참조값을 가짐) 않음
  • const handleSample = useCallback( ( ) ⇒ { }, [ ] );

그렇다면 위의 useCallback을 통해서 childB의 불필요한 렌더링은 일어나지 않을까요?

아쉽게도 그렇지는 않습니다.

이는 parent 컴포넌트가 컴파일 되는 과정에서의 이해가 조금 필요합니다.

state가 변경된 parent 컴포넌트는 리렌더링이 일어납니다.

이 경우 js 컴파일러인 babel이 실행되며 parent 컴포넌트의 자식들인 childA, childB 컴포넌트를 React.creatElement를 통해 새로운 요소를 생성하게 됩니다.

이러한 이유로 useCallback을 사용하여 childB에 전달되는 prop를 이전과 동일하게 해주어도 불필요한 리렌더링이 일어나는 이유입니다.

+그렇다면 useCallback을 사용하는 것은 기능적으로 이득이 없나요?

  • 아닙니다. 리액트 렌더링 과정에는 render phase, commit phase가 있습니다. useCallback을 사용하게 된다면 commit phase의 과정이 생략되는 기능이 있습니다. 위 내용은 설명이 길어져 간단하게 넘어가도록 하겠습니다.

그렇다면 더욱 확실하게 childB의 리렌더링을 막기 위해 (render phase)를 막기 위해서는 어떻게 할 수 있을까요?

React.memo

  • 전달받은 props가 이전과 비교했을 때, 같으면 컴포넌트의 리렌더링을 막아주고, 렌더링 된 결과를 재사용
  • 기본적으로 얕은 비교를 통해 props 비교 진행 (원시 타입 데이터는 값 비교, 참조 타입 데이터는 참조값 비교)

만약 childB 컴포넌트에 props로 함수가 아닌 변하지 않는 parent에 정의된 test라는 객체 값이 담긴다면?

(객체를 porp로 넘겨준다면)

  • parent 객체가 리렌더링 됩니다. ⇒ item이라는 객체가 매번 새로 생성됩니다. (참조값인 객체는 매번 다른 참조 값을 가지게 됩니다.)
  • childB에게 매번 다른 참조값을 가지는 객체를 전달
  • React.memo가 작동 x (이전과 비교했을 때 같지 않기 때문.)

useMemo

  • 값에 대한 메모이제이션을 제공하는 hook
  • 의존성 배열에 들어있는 값이 변경되지 않는 이상 같은 값 반환
  • const memoizedTest = useMemo( ( ) ⇒ test, [ ] );

위처럼 useMemo를 사용하게 된다면 test는 동일한 참조값을 가지고 있을 것이며 이는 React.memo를 의도한 대로 사용할 수 있을 것입니다.

좋은 최적화란?

useMemo, useCallback, React.memo 이는 렌더링을 줄이는 기능을 가지고 있지만 이 또한도 하나의 코드이며 동작입니다. 최적화를 위해 사용했다 오히려 부담이 더 가는 코드를 만들 수도 있습니다.

따라서 무작정 사용하는 것이 아닌 기획의도와 어떻게 사용하여야 좋을지 고민하여 적절한 사용이 가장 옳은 최적화라고 할 수 있겠습니다.

또한, 최적화 도구들을 사용하기 이전, 기초적이고 근본적으로 코드를 수정하고 잘 만드는 것에 초점을 맞추는 것이 매우 중요하겠습니다.

부모 컴포넌트 안의 자식 컴포넌트로 존재하고 있는 구조를 children을 통해 주입시킨다면 이는 부모 컴포넌트가 재실행 되어도 리렌더링 되지 않을 것입니다.

+최적화가 필요해요?

많은 블로그 영상들에서 이른 최적화 만큼이나 불필요한 것이 없다고 이야기합니다.

그러나 모르고 쓰는 것과 알고 쓰는 것은 다르다는 말에 매우 공감되었습니다.

최적화 도구들과 근본적 코드 수정법 등을 접해보고 이후 필요할 때에 적절한 방법을 통해 렌더링을 최적화할 수 있는 능력을 기르는 것은 꽤나 중요하다고 느꼈습니다. 이 외에도 다양한 기법을 익혀보면 좋을 것 같습니다.

React 렌더링 이해하기
https://www.youtube.com/watch?v=1YAWshEGU6g


Web Vitals 최적화를 위한 Tip.. - 김하나

웹 성능 최적화는 사용자 경험을 향상시키는 주요 요소로, 서비스 관점에서도 중요한 역할을 합니다. 로딩 속도가 느릴수록 사용자는 웹사이트를 떠날 가능성이 높아지는데, 0.1초의 성능 개선도 conversion rate를 높일 수 있습니다.

주로 LCP, FID, INP와 같은 web vitals로 측정되며, 이러한 지표는 사용자의 첫 반응을 기준으로 합니다. (최근에는 이를 개선하기 위해 INP가 도입되었는데, 이는 모든 입력 지연 시간의 평균을 측정하는 등 더 나은 측정이 가능하게 합니다.)

이러한 web vitals 를 어떻게 개선할 수 있는지, 여러가지 팁들을 정리해봤습니다.

LCP 최적화

  1. LCP 자원이 HTML 내에서 빨리 찾아져야 합니다.
  2. LCP 자원이 우선시되어 다운로드될 수 있도록 해야 합니다.
  3. CDN을 사용하여 돈을 투자합니다.
    • 사용자가 가장 가까운 서버에서 데이터를 전달받을 수 있도록 합니다.
    • 물리적으로 최대한 가까운 서버를 활용합니다.

실제 사례:

  • 이미지는 <img> 엘리먼트에 넣고 srcsrcset 속성을 이용하여 첫 번째 렌더링 시 필요한 이미지를 알려줍니다.
  • SSR을 사용합니다.
  • 내부에서 호스팅하지 않는 이미지에는 <link rel="preload"> 태그를 사용하여 브라우저에게 빨리 로드해야 하는 소스임을 알려줍니다.
  • <img> 태그에 fetchpriority="high" 속성을 추가하여 LCP 이미지를 우선적으로 브라우저가 다운로드하게 합니다.
  • <img> 엘리먼트에 loading="lazy" 속성을 추가하여 LCP 가 아닌 이미지를 나중에 로딩할 수 있도록 합니다.

CLS 최적화

  • 컨텐츠의 명확한 사이즈를 설정합니다.
  • 흔들리는 CSS 애니메이션 및 전환 효과를 지양합니다.
  • bfcache를 사용하여 캐싱을 최적화합니다.
    • 모든 지표를 향상시킬 수 있는 도구입니다.
    • 이를 위해 캐싱을 막아놓지 않았는지 확인합니다.

실제 사례:

  • widthheight를 명시적으로 지정합니다.
  • aspect-ratio를 사용하여 width만 지정하고 height를 유동적으로 설정합니다.
  • min-height를 지정하여 요소가 덜 밀리도록 방지합니다.

FID 최적화

  • 긴 작업을 작게 분할합니다.
  • 불필요한 자바스크립트 코드를 최소화합니다.
  • 큰 렌더링 업데이트를 피합니다.
    • 부분적으로만 업데이트합니다.

실제 사례:

  • 긴 작업을 작게 분할합니다.
    • webpack과 같은 도구를 사용하여 chunking이 가능하게 합니다.
  • 메인 쓰레드에서 잠시 자원을 양보합니다.
  • coverage 도구를 활용합니다.
  • 코드를 분할합니다.
  • 불필요한 트래킹 태그를 제거합니다.
  • requestAnimationFrame() 사용을 지양하고 DOM 크기를 최소화합니다.

reference

https://web.dev/articles/vitals
23년 6월 Tech 세미나 - 웹 프론트엔드 성능 최적화 방법 및 적용 사례