Dynamic Import 로 웹페이지 성능 올리기

박성룡 ( Andrew park )
10 min readMay 24, 2020

--

최근 Dynamic Import 를 사용할 일이 생겨 이를 정리해보고자 한다.

모든 웹 페이지의 속도를 결정 짓는 첫번째 요소는 첫 페이지를 그릴때 필요한 자원양이다.

필요한 자원이 많으면 많을 수록 네트워크 상에서 다운받는 시간이 오래걸린다.

Webpack 같은 bundler 들은 모든 JS 코드를 하나의 거대한 파일로 만들어 내었고, 이는 웹페이지에서 페이지를 그릴때 필요한 자원의 양 또한 크게 증가 하게 되었다.

웹 페이지에서는 큰 JS 파일을 모두 불러올 때까지 Rendering 을 멈추어, 첫 페이지를 그리기까지 오래 걸리기 때문에 웹페이지 경험에 좋지 않은 영향을 끼치게 된다.

때문에 웹사이트의 속도를 올리기 위해서는 큰 JS 파일의 용량을 줄여하는데, 크게 코드의 크기를 줄이는 방법 과, 코드를 분할하는 방법 이 있다.

코드 줄이기

코드를 줄이기 위해서는 렌더링에 필요없는 코드 를 줄일 필요가 있다.

불필요한 공백이나, 줄바꿈 혹은 주석 과 같은 렌더링 과정에 필요없는 코드를 제거 할 수 있는데 이를 Minify (압축) 라고 부른다.

webpack.optimizationminimizer 옵션을 이용해 압축하거나 uglify-js 를 이용할 수 있다.

// javascript

function sum(a, b) {
// sum test
var t = a + b;
return t;
}
sum(10, 20);
// ----function sum(u,n){return u+n}sum(10,20);

압축 과정은 일반적인 코드들을 제거할 수 있지만, 중복되거나, 사용 되지 않는 코드까지 제거하지 못한다.

이 경우 Tree Shaking 으로 사용 되지 않는 코드를 제거할 수 있다.

Webpack 은 Code 를 Build 하는 시점에 import 된 module 중 사용하지 않는 코드를 제거할 수 있다.

사용 되지 않는 코드를 최대한 줄이고, 코드를 재사용 하더라도, 전체 페이지의 코드가 한파일에 작성되어 있다면, 줄이는 데에는 한계가 있다.

때문에, 사용자가 보는 첫 렌더링 화면에 필요 없는 코드를 분할 하여, 이후 필요할때 마다, 불러오는 방식을 사용하면 코드 더 줄일 수 있다.

코드 분할하기 Code Splitting

웹 사이트는 많은 정보를 담고 있으며, 이를 한 화면에 표현하는 데에는 한계가 있다.

때문에 웹 사이트는 화면보다 클 경우가 많으며, 여러 페이지로 존재할 확률이 높다.

Webpack 으로 빌드한 파일은 전체 페이지의 내용을 하나의 파일 에 담고 있고, 이를 모두 렌더링 하더라도, 사용자가 보는 페이지는 얼마 되지 않는다.

이를 여러 페이지에서 필요한 파일로 나누고, 작은 화면에서 필요한 시점마다 불러올 수 있도록 나눈다면, 첫 렌더링에 필요한 파일의 크기를 줄여 더 빠르게 렌더링 할 수 있다.

Webpack 의 Entry 와 SplitChunksPlugin

Webpack 에서는 코드를 분할 하기위해, 목적 별로 여러 Entry 로 분할 할 수 있다.

페이지 별로 분할 할 수 있고, 특정 렌더링을 위한 코드 로 분할하여, 각 페이지에서 사용할 수 있다.

// webpack.config.js
module.exports = {
entry: {
home: 'home.js',
about: 'about.js',
}
};

분할된 Entry 파일에는 각 Entry 들이 필요한 여러 모듈 들을 모두 포함 하고 있다.

분할 에는 성공했지만, 전체 크기로 봤을때 배로 커지게 된다.

때문에, 중복된 코드도 분할 하여, 관리할 필요가 있는데, webpackSplitChunksPlugin 를 이용하면 쉽게 분할 할 수 있다.

// webpack.config.js
module.exports = {
optimization: {
splitChunks: {
cacheGroups: {
commons: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
chunks: 'all'
}
}
}
};

commons 에 설정한 값은 각각의 entry 가 공통적으로 사용하여, 중복되는 module 들을 chunk 파일로 추출한다.

이렇게 열심히 분할한 enrtychunk 파일도 한 화면에서 필요한 요소보다 더많은 화면들을 그리게 된다.

동적 코드 분할 Dynamic Imports

대부분의 코드들은 사용자가 보는 첫 페이지에는 필요하지 않다.

첫 페이지 진입시에 필요한 최소한의 코드만 다운 받고, 사용자가 특정 페이지나 위치에 도달할 때마다 코드를 로드 한다면, 첫 페이지의 초기 성능을 올릴 수 있다.

이런 방식을 lazy-load 게으른 로딩이라고 한다.

Dynamic Import 를 사용하면, 런타임시에 필요한 moduleimport 할 수 있다.

import(경로) 형태로 사용한다.

Promise 를 반환 하며, export 하는 값들을 가진 객체를 반환한다.

import('module.js')
.then((module) => {
const { default: Component, a, b } = module;
});

(async () => {
await import('module.js');
const { default: Component, a, b } = await import('module.js');
})();

Dynamic Import 는 일반적인 정적인 Module Import필요한 시점 에 로드 할 수 있도록 도와준다.

이를 이용하면, 거대한 하나의 JS 를 여러 JS 로 쪼갤수 있게되고, 화면이 위치할 때마다, 사용자가 클릭할 때마다 Import 함으로서, 분할하여 관리할 수 있게 된다.

또한 일반적인 Import 에서는 불가능한 방법도 가능하다.

if(true) {
import('a.js');
}
import(`${id}.js`)Promise.all([
import('b.js'),
import('c.js')
]);

하지만 Dynamic Import 는 아직 모든 브라우저에서 지원하지 않는다.

하지만 Webpack 같은 Bundler 를 이용하면, 매우 쉽게 구현할 수 있다.

Webpack 과 Babel

Webpack 에서 Dynamic Import 를 사용하기 위해서는 babel-plugin-syntax-dynamic-import 플러그인이 필요하다.

// babel.config.json
{
"plugins": ["@babel/plugin-syntax-dynamic-import"]
}

Build 시점에 import() 모듈을 chunk 파일로 만들며, 필요한 시점에 header 에 script 를 세팅하여 JS 파일을 다운로드 한다.

<script charset="utf-8" src="/static/js/2.chunk.js"></script>

이때 chunk 파일 명은 webpackoutput 에 세팅 한 파일명을 따라간다.

module.exports = {
output: {
filename: '[name].js',
chunkFilename: '[name].chunk.js',
},
}

만약 파일명을 지정하고 싶다면, 주석을 추가하여 파일명을 명시적으로 지정할 수 있다.

import((/* webpackChunkName: "test" */ 'module.js')
.then((module) => {
const { default: Component, a, b } = module;
});

Dynamic Import 시점 (prefetch / preload)

첫 페이지에는 필요하지 않지만 앞으로 필요할 만한 JS 를 미리 다운로드 받아 둘 수 있다.

  • Preload
    프리로드 는 호출 시점에 브라우저가 반드시 가져오게 한다.
  • Prefetch
    프리패치 는 호출 시점에 가져오지 않고, 브라우저가 여유가 될때 가져오게 한다.

Webpack 4 부터 해당 기능을 쓸 수 있다.

import(/* webpackPreload: true */ 'module.js');
import((/* webpackPrefetch: true */ 'module.js')
.then((module) => {
const { default: Component, a, b } = module;
});

이때 webpackPrefetch 가 먼저 호출되었더라도 webpackPreload 가 먼저 처리된다.

이는 브라우저의 지원방식에 따라 다를 수 있다.

<link rel="preload" as="script" href="module.js"><link rel="prefetch" as="script" href="module.js">

기능이 아니라 호출 시점으로 제어 할 수도 있다.

React Dynamic Import: Lazy 와 Suspense

React 를 사용한다면, Lazy 를 이용해, Component 가 사용 되는 시점에 가져오도록 구현할 수 있다.

const Component = React.lazy(() => import('./Component'));const Wrapper = () => {
return <div><Component/></div>
}

하지만 이 경우 아무것도 없는 페이지가 노출 되었다가, 페이지가 전환된다.

Suspense 를 이용하여, 로딩중 같은 전환 화면을 구현 할 수 있다. 이때 React.lazy 는 여러 개여도 상관 없다.

const A = React.lazy(() => import('./A'));
const B = React.lazy(() => import('./B'));
const Wrapper = () => {
return <div>
<Suspense fallback={<div>Loading...</div>}>
<A/>
<B/>
</Suspense>
</div>
}

만약 SPA 로 페이지를 운영한다면, 이를 이용하여 페이지 별로 파일을 나눌수 있다.

import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
import React, { Suspense, lazy } from 'react';
const A = lazy(() => import('./page/a'));
const B = lazy(() => import('./page/b'));
const Page = () => (
<Router>
<Suspense fallback={<div>Loading...</div>}>
<Switch>
<Route exact path="/a" component={A}/>
<Route path="/b" component={B}/>
</Switch>
</Suspense>
</Router>
);

--

--

박성룡 ( Andrew park )
박성룡 ( Andrew park )

Written by 박성룡 ( Andrew park )

Javascript is great We may not be great

Responses (1)