dynamic import와 React.lazy

불꽃남자·2021년 6월 9일
19

토이 프로젝트를 진행하면서 느낀 점이, 웹 사이트의 퍼포먼스 속도가 너무 느리다는 것이다. chrome dev의 lighthouse가 퍼포먼스 점수를 40점 정도로 책정하고 있었다.

그래서 성능 개선 기술에 관해 찾아보다가 code splitting에 대해 알게 되었고, 이번 포스팅에선 그에 대해 배워본다.

code splitting

code splitting은 코드 분할이다. 어떤 코드를 분할하느냐? 바로 번들 코드를 분할한다.

React같은 SPA웹앱을 개발하고 나면 webpack같은 번들러로 코드를 번들하고, html 파일에서 번들된 자바스크립트 파일을 불러와서 웹앱을 브라우저에서 실행한다.

그런데 번들 파일이 다 불러와져야 웹앱이 실행되다 보니, 웹앱의 크기가 커지면 커질 수록 성능에 악영향을 끼치게 된다. 특히 서드 파티 라이브러리같은 경우 그 크기가 굉장히 큰 경우가 많기 때문에 번들 파일의 크기도 금방금방 커져버린다.

그런 때에 고려할 사안이 바로 코드 분할이다. 이는 번들 파일의 코드를 분할하여, 모든 코드를 한 번에 불러오지 않고 사용자가 필요로 할 때에 필요한 코드만 불러오는 개념이다.

우선 코드 분할의 기초 개념인 dynamic import에 대해 알아본다.

dynamic import

왜 사용하는가?

dynamic import는 동적 불러오기이다. 기존에 코드 파일의 가장 상위에서 import 구문을 사용하여 불러오는 것은 static import(정적 불러오기)라고 한다.

정적 불러오기 같은 경우 문서의 가장 상위에 위치해야하고(바닐라 js에선 맨 밑에 위치해도 되지만, react-app에선 컴파일 에러가 발생한다), 블록문 안에 위치할 수 없는 등의 제약 사항이 있다.

JAVASCRIPT.INFO의 동적으로 모듈 가져오기 문서에서는 정적 불러오기에 이런 제약사항이 발생한 이유에 대해 이렇게 서술하고 있다.

이런 제약사항이 만들어진 이유는 import/export는 코드 구조의 중심을 잡아주는 역할을 하기 때문입니다. 코드 구조를 분석해 모듈을 한데 모아 번들링하고, 사용하지 않는 모듈은 제거(가지치기)해야 하는데, 코드 구조가 간단하고 고정되어있을 때만 이런 작업이 가능합니다.

이런 장점들을 내려놓고서라도 동적 불러오기를 사용해야 하는 이유가 바로 코드 분할이다.

사용법

동적 불러오기는 다음과 같이 사용한다.

import('./sum').then(sum => {
  console.log(sum(1 + 2));
});

동적 불러오기는 import() 구문을 사용하는데, 프로미스 객체를 반환한다. 프로미스 객체의 반환값은 불러온 모듈이다. 함수를 호출하는 문법을 취하고 있으나, import는 함수가 아니다.

동적 불러오기는 코드의 위치에 관계없이 사용이 가능하기 때문에, 모듈들을 사용자가 필요로 할 때에 불러오게끔 할 수 있다.

다음으로 React.lazy에 대해 알아보자.

React.lazy

왜 사용하는가?

React에서 컴포넌트 파일을 정의하고 동적 불러오기를 사용하면 에러가 발생한다.
컴포넌트를 동적으로 불러오기위해선 React.lazy를 사용해야한다.

사용법

React.lazy를 사용한 예시 코드이다.

import { Suspense } from 'react';

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

const MyComponent = () => {
  return (
    <Suspense fallback={<div>로딩 중. . .</div>}>
      <SomeComponent />
    </Suspense>
  );
}

React.lazy()import() 구문을 반환하는 콜백함수를 인자로 받는다. 동적 불러오기로 불러와지는 모듈은 1. ReactComponent를 포함하며 2. default export를 가진 모듈이어야 한다. 그리고 불러온 컴포넌트를 반환한다.

React.lazy로 불러온 컴포넌트는 단독으로 쓰일 수 없고, React.Suspense 컴포넌트로 하위에서 렌더링되어야 한다.

Suspense 컴포넌트는 fallback prop을 필수로 가진다. fallback prop은 로딩 표시기로 사용할 컴포넌트를 받는다.
fallback이 가질 수 있는 type에 null과 Boolean도 있는데, 이것의 존재이유에 대해서는 아직 찾지 못 하였다.

with React Router

그렇다면 React.lazy를 어디에 적용하는 것이 좋을까?

React 공식문서의 코드 분할 항목에 의하면, Router 바로 아래에 Suspense를 위치시키고, Route로 보여줄 컴포넌트들을 React.lazy로 불러올 것을 권장하고 있다.
아래가 그 예시 코드이다.

import React, { Suspense, lazy } from 'react';
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';

const Home = lazy(() => import('./routes/Home'));
const About = lazy(() => import('./routes/About'));

const App = () => (
  <Router>
    <Suspense fallback={<div>Loading...</div>}>
      <Switch>
        <Route exact path="/" component={Home}/>
        <Route path="/about" component={About}/>
      </Switch>
    </Suspense>
  </Router>
);

이를 시작하기 좋은 장소는 라우트입니다. 웹 페이지를 불러오는 시간은 페이지 전환에 어느 정도 발생하며 대부분 페이지를 한번에 렌더링하기 때문에 사용자가 페이지를 렌더링하는 동안 다른 요소와 상호작용하지 않습니다.

SSR에서의 코드 분할

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

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

하지만 loadable 라이브러리에 대해 이 포스트에서 다루진 않을 것이다.
글이 너무 길어지기도 하고, 내가 아직 next.js같은 SSR 환경의 앱에 대해 제대로 배우지 않았기 때문이다.

자세히 알고 싶다면 loadable component 공식문서, 혹은 다른 글을 찾아보라. 공식문서는 그 명세가 굉장히 상세하지만 한국어는 지원하지 않는다.

webpack에서의 코드 분할 설정

webpack은 코드 분할을 적극적으로 지원하고 있으며, 코드 분할 설정 또한 상세히 할 수 있다.

etnry에 의한 코드 분할

webpack.config의 property 중에는 entry 객체가 있다. webpack은 번들링을 할 때에, entry 객체에 있는 파일 시작점으로하여 의존성을 띄는 파일들을 모두 번들링한다.
그렇기 때문에 어떤 React 웹앱을 개발한 뒤 최상위 컴포넌트인 App.js 파일을 App.bundle.js 파일로 번들링하면, html에서 bundle.js를 불러오기만 하면 React 웹앱이 실행되는 것이다.

이 entry 객체는 여러개의 파일을 value로 가질 수도 있는데, A.js 파일과 B.js 파일을 entry로 설정하고 번들링하면 A가 번들링 된 파일, B가 번들링 된 파일 두 가지가 나온다.

webpack 공식문서에 의하면 엔트리 포인트를 분리하는 것에 대해 이렇게 권장하고 있다.

엔트리 포인트를 분리하는 경우는 싱글 페이지 애플리케이션이 아닌 특정 페이지로 진입했을 때 서버에서 해당 정보를 내려주는 형태의 멀티 페이지 애플리케이션에 적합합니다.

SplitChunkPlugin

SplitChunkPlugin은 webpack에서 지원하는 플러그인 중 하나이다.
이 플러그인을 사용하는 주 목적은 chunk 코드의 중복된 의존성 제거이다.

예를 들어, A.js 파일과 B.js 가 있다고 가정하자.
각각의 파일은 API를 요청하는 비즈니스 로직이 담겨 있어서, 둘 다 axios 라이브러리를 불러와서 사용하는데, 동적 불러오기 구문을 사용해서 axios를 불러온다.

// A.js

...
import(axios)

... // API를 요청하는 로직
// B.js

...
import(axios)

... // API를 요청하는 또 다른 로직

그럼 A.chunk.js 와 B.chunk.js가 만들어지고, 각 청크에는 axios를 불러오는 코드가 담겨있을 것이다.

어짜피 하나의 html 파일에서 사용될 것인데, 중복된 라이브러리 코드를 여러개 요청하는 것은 로딩 시간만 늘릴 뿐이다.
이런 중복된 청크 코드를 또 다른 청크 코드로 추출하는 일을 하는 것이 바로 SplitChunkPlugin이다.

webpack 공식문서의 SplitChunkPlugin 항목에 의한 사용법은 다음과 같다.

// webpack.config

module.exports = {
  //...
  optimization: {
    splitChunks: {
      // include all types of chunks
      chunks: 'all',
    },
  },
};

chunks 옵션은 'initial' | 'async' | 'all' 중 하나를 값으로 가지는데, initial은 정적 불러오기에만, async는 동적 불러오기에만, all은 모든 불러오기에 splitChunks를 적용한다.

이외에도 정말 다양한 옵션이 있지만, 그것은 공식문서에서 확인해보라.

CRA로 만든 웹앱에는 splitChunks가 기본적으로 적용되어 있다.

이상으로 글을 마친다.

🍕

이번 포스트에선 코드 분할이란 무엇인가에 대하여, 그리고 React 환경에서의 코드 분할 방법에 대해 배웠다.

이 포스트를 작성하기 전에 진행한 토이 프로젝트에서는 이런 걸 까맣게 몰랐기 때문에 소개된 내용 중 어떤 것도 적용되어 있지 않다. 항상 배움을 얻고 나면 자신의 무지에 대해 부끄러움을 가지게 된다. 하지만 그것 또한 배움의 즐거움이 아니겠는가.
또한 배우고 나면 실천을 하여 자기 것으로 만드는 마음가짐을 지니고 있어야 할 것이다.

다음 포스팅은 testing-library 아니면 git을 통한 프로젝트 형상관리에 대해 다룰 생각이다. 어줍잖게 알던 부분들에 대해 포스팅하려고 하면 더욱 자세하고 확실하게 알게 되기 때문이다.

profile
프론트엔드 꿈나무, 탐구자.

0개의 댓글