성능 향상을 위한 코드 분할 (React.lazy, Suspense)

Gee·2022년 8월 20일
0

사용배경

예전에 강의를 들으면서 "code splitting을 하면 성능 향상이 될겁니다." 라고만 듣고 무작정 사용하였지만, 어떻게? 왜? 라는 질문을 던지지 않았다.
하지만, 최근에 원티드 프론트엔드 챕터를 들으면서 How, What, Why의 자세를 갖추라는 말을 들었고 나태했던 자신을 반성하게되었고 다시 code splitting을 적용하면서 어떻게 향상이 되는 것인지를 공부하게 되었다.

그리고, Todo앱에 React-query를 도입하여 개발하였는데 loading, error값을 반환하다보니 return 값이 너무 많아지는 거 같다는 고민을 하였고 이를 분리하는 방법으로 Suspense 도입도 하게 되었다.
( return 값이 너무 많으면 메모리도 많이 쓰게 된다고하네요..)

그럼 궁극적으로 코드분할이 필요한 이유는?

거의 대부분의 React앱은 Webpack, Rollup, vite 등..해당 툴을 이용하여 여러가지 파일들을 하나의 파일로 만들고 그 파일을 웹 페이지에 포함해 한 번에 전체 앱으로 로드한다.

하지만 앱이 커지면 커질수록 번들 사이즈도 커지게 되다보니 로드하는 시간이 길어지게 된다.

이를 방지하기 위해서는 코드 분할이 필요하다! 즉, 번들을 나누는 것이다.

코드 분할을 하게되면 사용자가 필요하지 않은 코드를 불러오지 않게함으로써 앱을 처음 로드할 때 시간이 줄게 됩니다.

dynamic import

코드 분할에 가장 좋은 방법은 dynamic import라고 합니다.
여기서 dynamic import가 무엇일까요 ? 저희가 흔히 사용하는 export, import 문은 '정적인' 방식입니다.
그럼 dynamic import는 동적인 방식이라는 것인데, 어떻게 동작하는지 살짝 맛보겠습니다..

import () 표현식

import(module) 표현식은 모듈을 읽고 이 모듈이 내보내는 것들을 모두 포함하는 객체를 담은 이행된 프라미스를 반환합니다. 호출은 어디서나 가능합니다.

let modulePath = prompt("어떤 모듈을 불러오고 싶으세요?");

import(modulePath)
  .then(obj => <모듈 객체>)
  .catch(err => <로딩 에러, e.g. 해당하는 모듈이 없는 경우>)

참고 문서 : https://ko.javascript.info/modules-dynamic-imports

React.lazy

React.lazy 함수를 사용하면 위에서 설명드린 dynamic import를 사용해 컴포넌트 렌더링이 가능합니다!

Before

import OtherComponent from './OtherComponent';

After

import React from 'react';

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

Before일 때는 해당 코드가 반영된 파일이 MyComponent.js라고 했을 때 처음 MyComponent가 랜더링될 때 OtherComponent를 포함한 번들을 자동으로 불러오게 됩니다.

하지만, After에서 React.lazy() 함수를 사용하게 되면 import()구문을 반환하는 콜백함수를 인자로 받게된다. 동적 불러오기로 불러오는 모듈은 React Component를 포함하며, default export를 가진 모듈이어야 한다. 그리고, 컴포넌트를 반환하게 된다.

React.lazy()는 단독으로 쓰일 수 없고, Suspense와 같이 쓰이게 됩니다.
React.lazt()를 이용하여 불러온 컴포넌트 즉, 동적으로 불러온 컴포넌트는 랜더링되는 동안 일부 대체 콘텐츠를 표시할 수 있게 되는데 이걸 Suspense를 이용하여 표시할 수 있습니다.

Suspense

import React, { Suspense } from 'react';

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

function MyComponent() {
  return (
    <div>
      <Suspense fallback={<div>Loading...</div>}>
        <OtherComponent />
      </Suspense>
    </div>
  );
}

Suspense의 prop인 fallback은 구성 요소가 로드되기를 기다리는 동안 랜더링하려는 모든 React 요소를 허용합니다.

이로써 로딩 될 동안 사용자는 "로딩"표시를 받게되죠 !
swr, react-query를 사용하며 loading 값을 매번 return 하지않으면서 이 방법을 사용하게되면
코드도 줄고 사용자 친화적으로 UI/UX를 개선할 수 있게됩니다 :)

참고 자료 : https://reactjs.org/docs/code-splitting.html#import

참고

React.lazy는 SSR에서 작동하지 않습니다.
SSR 웹 앱에서 코드 분할을 하고 싶다면 @loadable/component 라이브러리를 사용해야 합니다. 이는 React 공식문서에서도 권장하는 바라고 합니다.

React.lazy와 Suspense는 아직 서버 사이드 렌더링을 할 수 없습니다. 서버에서 렌더링 된 앱에서 코드 분할을 하기 원한다면 Loadable Components를 추천합니다. 이는 서버 사이드 렌더링과 번들 스플리팅에 대한 좋은 가이드입니다.

아직 Next.js 등..을 활용하여 SSR 환경을 구축해본적이 없어서 해당 내용은 나중에 다루도록 할게요 :)

느낀점

이번 포스트에서 코드 분할에 대해서 조금 더 깊게 공부를 하게되었다.
무지성으로 그렇구나..로 끝난게 아니라 왜 그런가에 대해서 공부를 하니 편-안..해진 기분이다!

다음 토이프로젝트에서도 해당 방법을 이용하면 좋을 거 같고, 나태해졌던 나 자신 반성한다..
( 공부할게 많다 ! )

+모든 기능에는 장점만 존재하는 것은 아니다. ( 11/25 내용 추가 )
대게 해당 기능에 대한 성능 향상으로는 '페이지 로딩 시간 감소(PLT)', '리소스 사용 최적화' 효과를 볼 수 있을텐데 해당 기능이 꼭 많을수록 이러한 효과를 볼 수 있는 것은 아니다.

리소스가 초기에 다운로드가 되는 것이 아니기 때문에 특정 페이지 이동 or 컨텐츠 이동에 있어서 지연이 발생할 수 있을 것이다.
그리고 검색엔진 최적화(SEO)에도 어느 정도 영향을 줄 것으로 예상이된다.

profile
작은 실패, 빠른 피드백, 다시 시도

0개의 댓글