xtring.dev

[React] 코드 분할(Code Splitting) - React 더 잘 사용하기 본문

Front-End/React

[React] 코드 분할(Code Splitting) - React 더 잘 사용하기

xtring 2020. 6. 29. 21:25

React

 

 

 

https://ko.reactjs.org/docs/code-splitting.html

 

코드 분할 – React

A JavaScript library for building user interfaces

ko.reactjs.org

Code Splitting은 왜 필요할까?
  최근 높은 인터넷 속도와 좋은 프로세서가 일반화 되면서 웹서비스를 이용하는데 발생하는 불편한 점들을 잘 인식 못 할 수 있습니다. 그리고 웹 서비스가 배포되어질 때 모든 코드들은 하나의 번들로 묶여(번들링) 집니다. 서비스가 비교적 가벼운 편이라면 큰 문제가 없겠지만 프로젝트가 덩치가 커지고 전달해야하는 데이터의 양이 커지게 되면 성능적 문제가 발생하게 됩니다.

  그래서 개발자는 사용자가 위와 같은 불편함을 느끼지 못하도록 하는 것이 큰 문제입니다. 보통 유저가 당장 필요한 정보에 우선순위를 두어 순서대로 로딩하는 방법이 있습니다. 앱에서 아직 정보를 로딩중이더라도, 유저에게는 티를 내지 않는 방법이죠. 결국 이는 UX와 이어지게 됩니다.

 

 

 

 

 

번들링(Bundling)

  Create React App이나 Next.js, Gatsby 혹은 비슷한 툴을 사용하면 Webpack이 함께 설치되어 번들링 시 Webpack에 의해 자동으로 설정된 번들링이 됩니다.

 

// app.js
import { add } from './math.js';

console.log(add(16, 26)); // 42
// math.js
export function add(a, b) {
  return a + b;
}

 

이 두 예시가 Bundling 되어 지면 

 

function add(a, b) {
  return a + b;
}

console.log(add(16, 26)); // 42

 

이와 같이 형태가 되어집니다.

 

주의! 실제 번들은 위 예시와는 많이 다르게 보일 것 입니다.

 

 

 

 

코드 분할(Code Splitting)

 

  번들링 되어진 앱은 최적화되어지지만 앱이 커지게 되면서 번들도 커지게 됩니다. 특히 큰 규모의 서드 파티 라이브러리를 추가할 때 실수로 앱이 커져서 로드 시간이 길어지는 것을 방지하기 위해 코드를 주의 깊게 살펴야 합니다.

  

  번들이 거대해지는 것을 방지하기 위한 좋은 해결방안은 번들을 분할(나누는)하는 것입니다. 코드 분할(Code Splitting)은 런타임에 여러 번들을 동적으로 만들고 불러오는 것으로 Webpack, Rollup, Browserify 같은 번들러가 지원하는 기능입니다.

 

  코드 분할은 앱을 Lazy Loading(지연 로딩) 할 수 있게 도와주고 앱 사용자에게 획기적인 성능 향상을 하게 합니다. 앱의 코드 양을 줄이지 않고도 사용자가 필요하지 않은 코드를 불러오지 않게 하며 앱의 초기화 로딩에 필요한 비용(resource)를 줄여줍니다.

 

 

 

 

import()

  앱에 코드 분할을 도입하는 가장 좋은 방법은 dynamic import() 문법을 사용하는 방법입니다. 

 

 

before

import { add } from './math';

console.log(add(16, 26));

 

after

import("./math").then(math => {
  console.log(math.add(16, 26));
});

 

 

Webpack이 이 구문을 만나게 되면 앱의 코드를 분할합니다. Create React App을 사용하고 있다면 이미 Webpack이 구성 되어 있기 때문에 자동으로 실행될 것입니다. 

 

 

 

 

React.lazy

주의!
  React.lazy와 Suspense는 아직 서버 사이드 렌더링(SSR)을 할 수 없습니다. 서버 사이드 렌더링에서 코드 분할을 사용하고자 한다면
loadable component를 사용하는 것도 방법입니다.

 

  React.lazy 함수를 사용하면 dynamic import를 사용해서 컴포넌트를 렌더링할 수 있습니다.

 

 

before

import OtherComponent from './OtherComponent';

 

after

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

 

 

 

  MyComponent가 처음 렌더링 될 때 OtherComponent를 포함한 버들을 자동으로 불러옵니다.

 

  React.lazy는 동적 import()를 호출하는 함수를 인자로 가집니다. 이 함수는 React 컴포넌트를 포함하며 default export를 가진 모듈로 결정되는 Promise로 반환해야 합니다.

 

  React.lazy와 Suspense를 사용하면 Code Splitting을 하기 위해 state를 따로 선언하지 않고도 간편하게 컴포넌트 코드 스플리팅을 할 수 있습니다. React.lazy는 컴포넌트를 렌더링하는 시점에서 비동적으로 로딩할 수 있게 해주는 유틸함수입니다.

 

  React.lazy로 불러진 컴포넌트는 Suspense 컴포넌트 하위에서 렌더링되어야 하며, Suspense는 lazy 컴포넌트가 로드되길 기다리는 동안 로딩 화면과 같은 예비 컨텐츠를 보여줄 수 있게 해줍니다. 아래 예시와 같이 Suspense 컴포넌트 에 props로 fallback={/* component */}를 넣어줍니다. 이와 같은 사용은 state을 선언하지 않고도 로딩화면을 적절히 보여 줄 수 있습니다.

 

import React, { Suspense } from 'react';

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

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

 

  fallback prop은 컴포넌트가 로드될 때까지 기다리는 동안 렌더링하려는 React 엘리먼트를 불러옵니다.(그래서 지연로딩, lazy loading이라고 부른다.) Suspense 컴포넌트는 lazy 컴포넌트를 감쌉니다. 하나의 Suspense 컴포넌트로 여러 lazy 컴포넌트를 감쌀 수도 있습니다.

 

...
    <div>
      <Suspense fallback={<div>Loading...</div>}>
        <section>
          <OtherComponent />
          <AnotherComponent />
        </section>
      </Suspense>
    </div>
...

 

  이렇게 dynamic loading으로 split하는 경우는 주로 3가지가 있습니다.

 

1. Route level: 각 react-router 마다 dynamic loading을 설정해줍니다.

  Splitting을 할 곳을 가장 찾기 쉬운 방법입니다. 각 라우트가 다른 컴포넌트로 관리를 하고 있는 경우, 각 라우트를 import 함수를 통해 분리된 빌드 파일로 관리 할 수 있습니다. 유저가 다른 페이지로 넘어갈 때만 그 페이지를 비동기(Async)로 로딩하는 것이죠.

 

2. Component level: 유저의 input으로 인해 나타나는 컴포넌트

  페이지가 처음으로 Load되었을 때, 그 페이지 안에 있지만 보이지 않는 컴포넌트가 존재할 수 있습니다. 이를 dynamic loading으로 처리해주면 필요한 경우에 불러오게 되어 초기 Load 속도를 높일 수 있겠네요.

 

3. 하나의 페이지를 스플리팅하기

  페이지 하나가 되게 긴 경우, 그 페이지에 들어갈 때 당장 보이는 부분을 나머지와 분리하고, 그 뒷 부분을 다른 컴포넌트로 만들어 스플리팅 할 수 있습니다.

 

 

Error bounaries 

  네트워크 장애 같은 이유로 다른 모듈을 로드에 실패하는 경우 에러가 발생할 수 있습니다. 로딩 화면이 지속적으로 뜬다던가... 아예 화면이 뜨지 않는... 이때 Error Boundaries를 이용하여 사용자의 경험과 복구 관리를 처리할 수 있습니다. Error Boundary를 만들고 lazy 컴포넌트를 감싸면 네트워크 장애가 발생했을 때 에러를 표시할 수 있습니다.

 

import React, { Suspense } from 'react';
import MyErrorBoundary from './MyErrorBoundary';

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

const MyComponent = () => (
  <div>
    <MyErrorBoundary>
      <Suspense fallback={<div>Loading...</div>}>
        <section>
          <OtherComponent />
          <AnotherComponent />
        </section>
      </Suspense>
    </MyErrorBoundary>
  </div>
);

 

 

 

 

Route-based code splitting

  앱에 코드 분할을 어느 곳에 도입할지 결정하는 것은 조금 까다롭습니다. 이를 시작하기 위한 좋은 장소는 Route입니다. 웹 페이지를 불러오는 시간은 페이지 전환에 어느 정도 발생하며 대부분 페이지를 한번에 렌더링하기 때문에 사용자가 페이지를 렌더링하는 동안 다른 요소와 상호작용하지 않습니다.

 

  React.lazy를  React Router 라이브러리를 사용해서 애플리케이션에 라우트 기반 코드 분할을 설정하는 예시를 봅시다.

 

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>
);

 

 

 

Named Exports

  React.lazy는 현재 default exports만 지원합니다. named exports를 사용하고자 한다면 default로 이름을 재정의한 중간 모듈을 생성할 수 있습니다. 이렇게 하면 tree shaking이 계속 동작하고 사용하지 않는 컴포넌트는 가져오지 않습니다.

 

// ManyComponents.js
export const MyComponent = /* ... */;
export const MyUnusedComponent = /* ... */;
// MyComponent.js
export { MyComponent as default } from "./ManyComponents.js";
// MyApp.js
import React, { lazy } from 'react';
const MyComponent = lazy(() => import("./MyComponent.js"));
반응형
Comments