> Hello World !!!

     

@syaku

Webpack Code Splitting with react dynamic import : 웹팩 코드 분리 전략, 웹팩 리액트 성능 최적화 동적 임포트

Webpack Code Splitting with react : 웹팩 코드 분리 전략, 웹팩 리액트 성능 최적화

규모가 점차적으로 커지는 사이트를 개발하면 소스 용량이 어마어마하게 커진다. 용량이 커질수록 첫 페이지 로딩 시간이 길어지는 건 당연하다. 그리고 모듈러들을 이용하여 스크립트를 호출하면 필요없는 스크립트까지 로드될때가 많다. 이런 문제점을 어떻게 해결하고 왜 이렇게 해야하는 지에 대해 포스팅하였다.

웹 브라우저는 다음과 같이 스크립트를 처리한다.

<script src=""></script>

웹브라우저가 html 소스에서 위와 같은 태그를 만나게 되면 모든 작업을 멈추고 스크립트 파일을 다운로드한다. 그리고 다운로드된 파일을 실행한다. 그리고 그다음 작업을 진행하게 된다.

<head>
    <script src=""></script>
</head>

그래서 스크립트 태그가 상단에 있으면 어떠한 작업도 처리하지 못하고 스크립트부터 다운로드된다. 큰 용량의 스크립트 파일이라면 잠시동안 백지화면을 보고 있어야 한다. 그래서 닫는 body 태그 바로 위에 스크립트들을 삽입하여 페이지가 먼저 출력될 수 있게 작업을 한다.

<head>
</head>
<body>
    ... html dom ...
    
    <script src=""></script>
</body>

하지만 우리는 한개의 스크립트 파일만 사용하지 않는 다. 개발에 편리하고 좋은 오픈소스들이 많기 때문에 우리는 그것을 그대로 사용해야 한다. 그럼 아래와 같이 코딩하게 된다.

<head>
</head>
<body>
    ... html dom ...
    
    <script src=""></script>
    <script src=""></script>
    <script src=""></script>
    <script src=""></script>
    <script src=""></script>
    <script src=""></script>
    <script src=""></script>
</body>

이렇게 여러 스크립트 파일을 호출하게 되는 데 이때 파일을 요청하고 내려받는 작업을 여러번 나눠서 하면 오버헤드가 발생한다. 쉽게 생각하면 용량이 작은 파일을 여러개를 복사하는 것보다 용량이 큰 한개 파일이 더 빠른 처리를 한다. 여러번 요청함에 따라 연결과 끊기사이에 생기는 시간(오버헤드)이 발생하기 때문이다.

그래서 여러 소스 파일을 하나로 묶어 한번만 요청할 수 있게 한다. 파일 용량은 커지지만 여러개를 읽는 것보다 빠른 속도를 낼 수 있다.

하지만 한개 파일의 용량이 커진다는 것도 그다지 좋은 건 아니다. 그만큼 첫화면이 느려질 수밖에 없어는 데 이럴땐 한개 파일로 사용하는 것보다 조금은 분리하는 게 좋다.

무조건 호출되어야 하는 라이러리끼리 vendor 로 모으고, 개발자가 개발한 소스를 entry 로 모은다. 그리고 개발한 소스에서 공통으로 사용되는 것들은 commons 으로 모을 수 있다.

여기까지 웹팩 기본설정으로 쉽게 구현할 수 있는 부분이다. 웹팩의 위엄은 정말로 대단하다.

정말 중요한건 지금부터다...

위에서 한 작업들도 다 코드를 분리한 것이지만 그래도 문제는 여전히 있다. 그 문제는 바로 사용하지 않는 스크립트들도 호출되고 있다는 것이다.

상황에 따라 사용되지 않는 소스는 호출되지 않는 게 정상이다. 그래서 필요에 의해 사용될 소스를 동적임포트 방식을 이용하여 코드를 분리하면 된다.

이 동적임포트는 오래전부터 사용해왔다. 페이지 로드에 영향을 주지않고 스크립트가 요청이 있을때만 다운로드한다!!! 흔히 광고 스크립트에 많이 사용하는 방식이다.

dom 을 이용하여 <script 태그를 직접 생성하여 스크립트를 삽입하는 방식이다. 이방법은 동적으로 임포트되진 않지만 페이지로드에 영향을 주지않고 스크립트를 다운로드한다.

이방법을 좀 더 개선하여 ajax 를 이용하여 스크립트 태크를 생성한다면 필요에 의해 호출되는 동적임포트가 된다.

웹팩에서 동적임포트를 사용하기 위해 아래와 같이 하면된다.

웹팩설정

동적임포트를 사용하기 위해 두가지 방법이 있다. 참고: https://webpack.js.org/guides/code-splitting/

require.ensure 방식이 아닌 import 방식을 사용할 경우 아래의 작업이 필요하다.

동적임포트를 사용하기 위해 바벨 플러그을 사용하지만 웹팩을 지원하는 플로그인을 설치한다. 참고: https://babeljs.io/docs/plugins/syntax-dynamic-import/

$ npm install -D babel-plugin-dynamic-import-webpack

웹팩 설정부분에서 module > rules > 바벨로더의 플러그인에 추가한다.

'plugins': [
    'dynamic-import-webpack'
]

동적임포트 예제 소스

require.ensure(['./b'], function(require){
    ... todo ....
});


// es2015 방식
import('moment').then(function(moment) {
    ... todo ....
}).catch(function(err) {
    ... todo ....
});

// 난 eslint 오류가 발생하여 import 대신 System.import 로 사용하였다.

동적임포트는 비동기로 호출하기 때문에 프로그램 구현이 좀 불편하다. 즉 응답이 언제 올지 모르기 때문에 임포트 요청 구문안에 로직을 구현해야하는 단점이 있다.

리액트 컴포넌트를 동적임포트 한다면 아래와 같다.

ReactGridLayout 동적임포트로 구현한 것이다.

import React, { PropTypes } from 'react';

const propTypes = {
  children: PropTypes.node,
};

const defaultProps = {
  children: '',
};

class ReactGridLayout extends React.Component {
  constructor(props) {
    super(props);

     // 응답을 얻기 위해
    this.state = {
      loading: true,
    };

    // 요청
    System.import('react-grid-layout').then((module) => {
      this.setState({ loading: false, ReactGridLayout: module.WidthProvider(module.Responsive) });
    });
  }

  render() {
     // 응답 확인
    if (this.state.loading) return null;
    return (
      <this.state.ReactGridLayout {...this.props}>
        {this.props.children}
      </this.state.ReactGridLayout>
    );
  }
}

ReactGridLayout.propTypes = propTypes;

ReactGridLayout.defaultProps = defaultProps;

export default ReactGridLayout;

이런 동적임포트는 청크라는 뭉치로 모이게 된다. 중복되는 소스 청크가 생길 수 있지만 각 스크립트 파일 용량을 줄이고 좀 더 세분화할 수 있다 그리고 필요할때만 스크립트가 다운로드되고 실행될 수 있게 한다.

동적임포트 적용 전과 후

Webpack Bundle Analyzer 캡쳐


  • dashboard.js => webpack entry
  • vendors => webpack vendors
  • commons => webpack commons
  • 0.js => 동적임포트에 의해 생성된 청크
  • 1.js => 동적임포트에 의해 생성된 청크


ps. 이미 하나의 동적임포트를 사용한 상태라 적용전이라고 하기는 그렇지만 적용후는 더 많은 동적임포트를 사용했기때문에 구분하기에는 문제가 없습니다.

동적임포트 적용전

[수정 컨테이너 : 설정을 위한 0.js 스크립트를 동적으로 호출한다.]

[보기 컨테이너 : 동적으로 호출되는 스크립트가 없음]


동적임포트 적용후

[수정 컨테이너 : 설정을 위한 0.js 스크립트를 동적으로 호출한다.]


[보기 컨테이너 : 동적으로 호출되는 스크립트가 없음]


commons.js
vendors.js
dashboard.js
[number].js

스크립트만 확인한다.


적용전 보다 commons.js 용량은 늘었지만 dashboard.js 파일의 용량은 줄어들었다. 그리고 보기 컨테이너인 경우 1.js 를 제외한 청크 스크립트가 호출되지 않는 다. 또한 수정 컨테이너에서 청크 2.js 와 같은 스크립트는 이벤트가 호출될때만 로드 된다.


이벤트 요청이 있는 경우에만 스크립트 호출



설정 버튼을 누르면 0.js 스크립트가 호출되는 것을 확인할 수 있다


필요없는 스크립트 호출하지 않음




에디터 컨테이너에서는 모달 태그가 많지만 is_view=true 로 뷰 컨테이너로 넘어가면 모달 태그 자체가 보이지 않는 것을 확인할 수 있다.


현재 나는 대시보드 어플리케이션을 리액트를 이용하여 개발하고 있는 다. 이 프로그램을 기준으로 동적임포트 전략을 만든다면 다음과 같다.

대시보드에는 작은 프로그램들이 하나식 배치되는 데 이런 작은 프로그램들을 포틀릿이라고하며 이것들을 동적임포트 한다. 그래서 여러 포틀릿중에 사용되지 않는 것들은 사전에 스크립트를 호출되지 않게 하기 위함이다.

그리고 대시보드를 구성하는 에디터와 사용자가 데시보드를 보는 뷰가 있다. 에디터에는 많은 기능이 존재한다. 포틀릿을 관리하고 대시보드를 꾸미고 조절하는 기능들이다 하지만 뷰에는 포틀릿만 출력하는 기능 외에는 없다. 그래서 에디터와 뷰를 동적 임포트로 분리하여 뷰일때는 로드되는 코드를 최소화 하는 전략을 계획하고 있다.

아직 작업이 다되지 않았지만 완료되면 결과를 여기에 첨부할 생각이다.



posted syaku blog

Syaku Blog by Seok Kyun. Choi. 최석균.

http://syaku.tistory.com