[React]성능최적화-React.lazy, Suspense, Debounce, Throttle

김건휘·2024년 12월 3일
0

React

목록 보기
15/19
post-thumbnail

이번 시간에는 성능최적화 기법으로 활용되는 React.lazy, Debounce, Throttle 에 대해서 알아보는 시간을 갖도록 하겠습니다.

ReactSPA(Single-Page-Application)으로, 사용하지 않는 모든 컴포넌트까지 초기에 한 번에 불러오기 때문에 첫 화면이 렌더링 될때까지의 시간이 오래걸리게 된다. 그래서 사용하지 않는 컴포넌트는 필요한 시점에 로딩하는 방법을 통해서 초기 랜더링 지연시간을 개선할 수 있다.

dynamic import(동적 불러오기)를 통해서 위의 문제를 개선할 수 있다. 우리들이 일반적으로 사용하는 방법은 코드 파일의 가장 최상위에서 import 지시자를 사용해 사용하고자 하는 라이브러리 및 파일을 불러오는 방법을 사용했다. 이를 static import(정적 불러오기)라고 한다.

React.lazy()

React.lazy 함수를 사용하면 dynamic import를 사용해 컴포넌트를 렌더링할 수 있다. React는 SPA(Single-Page-Application)이므로 한 번에 사용하지 않는 컴포넌트까지 불러오는 단점이 있다고 앞선 내용에서 언급했다. React는 React.lazy를 통해 컴포넌트를 동적으로 import를 할 수 있기 때문에 이를 사용하면 초기 렌더링 지연시간을 어느정도 줄일 수 있게 된다.

/* React.lazy로 dynamic import를 감싸준다. */
const Calendar = lazy(() => import('@/shared/components/Calendar'));

React.lazy로 감싼 컴포넌트는 단독으로 쓰일 수는 없고, React.suspense 컴포넌트의 하위에서 렌더링을 해야 한다. 왜냐하면, React.lazy는 동적 import를 통해 컴포넌트를 비동기로 로드하기 때문에 로드 중 상태를 처리할 방법이 필요하다. 이를 위해서 Suspense를 통해 로딩 상태(fallback)를 제공해줄 수 있다.

Suspense

아래 코드처럼 lazy를 통해 import하면 해당 컴포넌트를 불러오는 시점에 로딩하는 시간이 생기게 된다. Suspense는 아직 렌더링이 준비되지 않은 컴포넌트가 있을 때 로딩 화면을 보여주고, 로딩이 완료되면 렌더링이 준비된 컴포넌트를 보여주는 기능이다. 이를 fallback UI를 제공한다고 한다.

/* suspense 기능을 사용하기 위해서는 import 해와야 한다. */
import { Suspense } from 'react';

const OtherComponent = React.lazy(() => import('./OtherComponent'));
const AnotherComponent = React.lazy(() => import('./AnotherComponent'));

function MyComponent() {
  return (
    <div>
			{/* 이런 식으로 React.lazy로 감싼 컴포넌트를 Suspense 컴포넌트의 하위에 렌더링합니다. */}
      <Suspense fallback={<div>Loading...</div>}>
				{/* Suspense 컴포넌트 하위에 여러 개의 lazy 컴포넌트를 렌더링시킬 수 있습니다. */}
        <OtherComponent />
				<AnotherComponent />
      </Suspense>
    </div>
  );
}

Supense 컴포넌트의 fallback prop은 컴포넌트가 로드될 때까지 기다리는 동안 로딩 화면으로 보여줄 React 엘리먼트를 받아들인다. Suspense 컴포넌트 하나로 여러 개의 lazy 컴포넌트를 보여줄 수도 있다.

React.lazy()와 Suspense를 활용한 성능최적화



해당 사진은 제가 진행하고 있는 프로젝트에서 lazy를 적용하지 않았을 때의 Treemap과 성능 지표(lighthouse)이다. 초기 HomePage를 로딩하는데 필요하지 않은 DatePicker(캘린더) 번들이 꽤나 많은 용량을 차지하고 있다.




해당 사진은 lazy를 적용한 후의 Treemap과 성능 지표(lighthouse)이다. 초기 HomePage를 로딩에 DatePicker(캘린더) 번들을 불러오고 있지 않고, 성능이 개선된 것을 확인할 수 있다.

하지만 lazy로딩suspense로 동적으로 컴포넌트를 임포트를하게 되면 해당 컴포넌트를 사용하는 시점에 로딩이 발생할 수 있어서 오히려 성능을 저해하거나 사용자 경험(UX)을 저해하지 않을까?

정답은 그렇다이다.

Lazy Loading의 단점과 문제점

  • 사용자 인터랙션 시 지연 발생
    사용자가 특정 컴포넌트에 접근할 때마다 네트워크 요청이 발생하면 해당 컴포넌트가 렌더링되기 전에 지연이 발생한다.
  • 네트워크 지연 문제
    사용자가 네트워크 속도가 느린 환경에 있으면 lazy가 적용된 컴포넌트가 로드되는데 시간이 걸릴 수 있다.

해결 방안

Preloading을 활용하는 것이다. onMouseEnter와 같은 속성을 활용하여 lazy로딩이 적용된 컴포넌트가 사용되는 시점을 미리 예측하여 불러오는 것이다.


해당 영상을 확인해보면 날짜토글 버튼에 마우스가 올라가면 Calendar를 불러오고 있는 것을 네트워크탭에서 확인할 수 있다.

해당 프로젝트의 로직을 살펴보면 날짜 토글버튼을 클릭하면 Calendar을 불러와야한다. 그렇기 때문에 onMouseEnter속성을 활용하여 날짜 토글버튼에 마우스가 올라가면(사용자가 토글버튼을 클릭할 것을 예상) Calendar를 Preload(미리로드)하여 지연이 발생할 수 있는 문제를 해결할 수 있었다.

Debounce

이벤트가 연속적으로 발생할 때, 제일 마지막 이벤트가 발생한 후 일정 시간이 지난 후에 함수를 호출하는 방법.

Debounce 적용 X


위의 사진과 같이 0.1초 간격으로 이벤트가 발생하게되면, 이벤트가 발생할 때마다 함수가 호출된다. 즉, 짧은 시간에 이벤트가 발생하게되면, 발생한 횟수만큼 함수가 호출되게 된다.
=> 성능저하 문제 뿐만 아니라, 해당 이벤트가 서버 비용을 유발하는 함수(api)라면 더더욱 치명적인 문제가 발생할 수 있다.(서버 과부화, 금전적 문제)

Debounce 적용 O


위의 사진과 같이 마지막 이벤트가 발생하고 일정 시간(delay)동안 이벤트가 발생하지 않을시 함수가 호출되게 된다.
=> 이벤트가 멈춘 후 일정 시간 뒤에 딱 한 번만 함수가 실행되므로 효율적이고, 해당 이벤트가 서버 비용을 유발하는 함수(api)라면 서비 비용을 절약할 수 있다.

🧐Debounce는 어디에 활용할 수 있을까?(웹페이지)

  • 폼 유효성 검사 (Form Validation)
  • 검색 자동 완성 (Search Autocomplete)
  • 스크롤 이벤트 최적화 (Scroll Event Optimization)
  • 반응형 레이아웃 재계산 (Responsive Layout Recalculation)
  • 페이지 콘텐츠 자동 저장 (Auto Save)

Throttle

특정 이벤트가 연속적으로 발생할 때, 일정 시간 간격으로 한 번씩만 실행되도록 하는 기법.


이벤트 발생 시 처음에 실행되고, 이후 일정 간격(limit)마다 실행하도록 컨트롤한다.

🧐Throttle이 왜 필요할까?

예를들어서, 버튼을 클릭하면 무거운 계산을 하는 함수가 존재한다고 하면, 사용자가 버튼을 연속적으로 클릭하는 상황이 생기면 상당한 CPU가 발생하게 된다. => 함수가 지나치게 자주 호출되는 것을 방지할 필요가 있다!

🧐Throttle는 어디에 활용할 수 있을까?(웹페이지)

  • 무한 스크롤
  • 스크롤 이벤트 최적화
  • 버튼 클릭 이벤트 제한

요약

🧐궁금증

예를들어서, 버튼 클릭 이벤트의 경우 DebounceThrottle을 모두 활용할 수 있는거 아니야? 어떤 기준으로 선택하지?

Throttle vs Debounce 사용 시 고려 사항

1. 즉각적 피드백 제공 여부

  • 사용자가 클릭 후 바로 UI 반응(버튼 활성화, 시각적 효과 등)을 보아야 한다면 → Throttle
  • 클릭 후 작업이 느려도 문제가 없거나, 클릭을 멈춘 후 작업만 중요하다면 → Debounce

2. 서버 요청의 특성

  • 요청이 부분적으로 처리 가능하거나 여러 번 실행해도 무방하다면 → Throttle
  • 요청이 중복되면 안 되거나 최종 작업만 중요하다면 → Debounce

결론

둘 다 버튼 클릭 이벤트에서 사용할 수 있지만, 목적과 사용자 경험에 따라 선택하는 것이 중요하다.

profile
공유할 때 행복을 느끼는 프론트엔드 개발자

2개의 댓글

comment-user-thumbnail
2024년 12월 4일

안녕하세요 웹 YB 김태욱입니다!
이번 6주차 아티클도 너무 잘 읽었습니다.
lazy와 suspense를 횔용한 성능 최적화 방법을 잘 설명해주셔서 이해하기 쉬었어요! 또, lighthouse를 통해 직관적인 성능 변화를 보여주신 것도 인상 깊었습니다.
lazy loading의 단점까지 저는 찾아보지 못했었는데 건휘님 아티클을 통해 어떤 상황에서는 사용하면 안되는지, 또 이를 preload를 활용하여 개선시킬 수 있다는 것을 알게되었습니다.
debounce와 throttle도 그림 예시 덕분에 무엇이고 언제 사용해야하는지 쉽게 알 수 있었습니다!
이번 주차도 좋은 아티클 작성해주시느라 고생 많으셨습니다😄

답글 달기
comment-user-thumbnail
2024년 12월 4일

아티클 잘 읽었습니다!
합동 세미나 발표에서도 느꼈지만 정말 성능 최적화 부분을 많이 생각하고 개선하려고 정말 노력하시는 게 보이시는 것 같습니다. 특히 Lazy, Suspense 부분을 읽으면서 한 대 맞은 느낌이 들었습니다. 관련 부분을 공부하면서 해당 내용은 사용하면 무조건 좋은 게 아닌가 생각이 들었는데 이 부분에서도 지연이 발생해서 UX에 영향이 갈 수 있군요..!!

그리고 그걸 PreLoading이라는 방식으로 onMouseEnter 이벤트 발생 시 미리 예측해서 받아온다는 방식이 정말 인상 깊었습니다! 장점만 볼 것이 아니라 단점도 함께 바라보고, 거기서 멈추지 않고 해결방법까지 접근하는 과정이 너무 좋았던 것 같습니다! 또한 Debounce와 Throttle의 사용 상황들을 비교해주셔서 둘의 차이를 정확히 알 수도 있었네요 수고하셨습니다 :)

답글 달기