게시글

5만명이 선택한 평균 별점 4.9의 제로초 프로그래밍 강좌! 로드맵만 따라오면 됩니다! 클릭
강좌3 - Webpack - 8년 전 등록 / 4년 전 수정

웹팩5로 청크 관리 및 코드 스플리팅 하기

안녕하세요. 이번 시간에는 마지막으로 웹팩5코드 스플리팅 하는 방법에 대해 알아보겠습니다. 코드 스플리팅, 용어부터 좀 생소하죠? 보통 싱글페이지 웹앱에서 많이 쓰이지만 멀티페이지 웹사이트에서도 쓰일 수 있습니다.

싱글페이지 웹앱의 단점 중 하나가 초기 로딩 때 해당 웹앱의 모든 것을 다 불러와야 한다는 겁니다. 페이스북이나 트위터같은 큰 앱은 처음 불러올 때 모든 걸 다 불러온다면 어마어마한 로딩 시간이 걸리겠죠? 또 이용자가 모든 페이지를 이용하는 게 아님에도 쓸데없는 페이지까지 한 번에 다 불러와야 합니다.(관리자 페이지같은 것도요) 보통 로딩 시간이 3초가 넘어가면 이용자들이 포기하기 시작한다고 하는데요.(제 블로그는 가끔 10초도 넘어갑니다... 뜨끔) 이용자 한 명 한 명이 미래의 수익인 서비스에서는 이용자 한 명도 쉽게 포기할 수 없습니다.

따라서 느린 초기 로딩시간을 극복하고자 나온 게 웹팩의 코드 스플리팅입니다. 싱글페이지 앱이더라도 해당 라우트를 방문했을 때만 관련된 모듈들을 로딩하도록 하는 겁니다. 내부 구현은 너무 궁금해하지 마세요. 중요한 것은 우리가 그것을 이용해서 사용자 경험을 향상할 수 있다는 겁니다.

보통 라우팅 부분에 코드 스플리팅을 구현해 라우터를 통째로 코드 스플리팅하는 경우가 많습니다. react-router는 component 대신 getComponent Props로 코드 스플리팅을 지원합니다.

코드 스플리팅을 하는 방법은 크게 두 가지 입니다. require.ensure을 사용하는 방법과, 웹팩2에서부터 도입된 import를 사용하는 방법입니다. require.ensure부터 알아보겠습니다.

require.ensure(['디펜던시'], function(require) {
  var module = require('경로 또는 모듈'); // 이렇게 require해서 사용하면 됨
}, '청크 이름');

여러분의 코드 아무 곳에나 위와 같이 require.ensure로 시작하는 함수를 호출하면 됩니다. 뜬금없다고 생각할 수도 있지만 그냥 그렇게 하면 코드 분리가 됩니다. 세 번째 인자로 넣어준 청크 이름을 가진 청크가 하나 생성됩니다. 청크란 하나의 덩어리라는 뜻으로, 코드 스플리팅 시 생성되는 자바스크립트 파일 조각을 의미합니다. 아래는 실제 사용 예시입니다.

require.ensure([], function(require) {
  var filepicker = require('react-filepicker').default;
  filepicker.init();
});

제가 만든 파일피커라는 모듈을 동적으로 로딩합니다. 평소에 파일피커를 사용하지 않을 때는 로딩하지 않다가, 파일피커를 사용하는 페이지에 들어갔을 때에만 로딩합니다. 배열 안의 디펜던시와 세 번째 인자 청크 이름은 필수가 아니기 때문에 안 줘도 됩니다.

다른 방식으로 ES 스펙을 만족하는 import 함수를 사용하는 방법이 있습니다. 위와 같은 코드를 다음과 같이 쓸 수 있습니다. 단, 먼저 @babel/plugin-syntax-dynamic-import를 설치하고, babel-loader의 plugins에 넣어주어야 합니다.

npm i -D @babel/plugin-syntax-dynamic-import
{
  loader: 'babel-loader',
  options: {
    plugins: ['@babel/plugin-syntax-dynamic-import']
  }
}
import(/* webpackChunkName: "청크 이름" */ 'react-filepicker').then(function(filepicker) {
  filepicker.init();
}).catch(function(err) {
  console.error('filepicker error', err);
});

혹시 import 말고 System.import를 사용하는 것을 보셨다면 잊어주세요. System.import는 더이상 사용되지 않습니다. 그냥 import 하시면 됩니다.

import 방식이 require.ensure보다 더 좋습니다. import 방식은 보다시피 catch를 붙여 에러가 났을 때 대처할 수 있습니다. 그리고 웹팩3에 와서는 require.ensure처럼 청크 이름을 지정해줄 수 있습니다. import 다음에 나오는 주석 부분에 청크 이름을 적어주면 알아서 해석합니다. 이제 위의 두 방식을 사용해서 자유자재로 코드를 쪼개면 됩니다. 로딩은 걱정하지 마세요. 웹팩에서 알아서 그 코드가 필요할 때마다 비동기적으로 로딩해주니까요.

역시 웹팩답게 설정이 중요합니다. 위와 같이 하면 entry에 적어놓은 것 외에도 chunk파일들이 같이 생성됩니다.

{
  entry: {
    app: "./client"
  },
  output: {
    filename: '[name].js'
  }
}

가 있으면 app.js외에도 0.js라는 것이 생성됩니다. 만약 require.ensure에 세 번째 인자로 filepicker을 넣어줬다면 0.js가 생성되고 filepicker라는 꼬리표가 붙습니다. import 함수로는 청크 이름을 정하지 못하기 때문에 항상 0.js, 1.js, 2.js, ... 이렇게 됩니다. 따로 청크파일 이름 구성을 정해주려면 chunkFilename 옵션을 사용하면 되긴 합니다. 기본은 [id]로 되어있는데 [name]으로 바꾸면 require.ensure에서 준 청크 네임이 청크 파일의 이름이 됩니다.

{
  output: {
    chunkFilename: '[id].js' // 기본 설정인데 [id]는 청크 순서대로 0,1,2,3,...을 부여합니다.
  }
}

이렇게 코드가 나눠지면 파일들이 많아지게 되는데요. 처음에 웹팩을 파일들을 하나로 합치기 위해서 사용한다고 했죠? 그런데 코드 스플리팅을 해버리면 파일들이 오히려 많아지게 됩니다. http 요청이 늘어나는 거죠. 그래서 사용하는 게 캐싱입니다. 캐시에 저장하는 건데요. 문제는 지금 상태는 파일 이름이 항상 app.js0.js로 고정이라 한 번 캐싱되면 새로 업데이트 했을 때 업데이트된 걸 가져오기 힘들어집니다.

그래서 첫 시간에 했던 output의 filename 부분에서 hash 또는 chunkhash를 사용합니다. 또한 id 대신 name을 사용해 청크 아이디보다 청크 이름을 사용하는 것이 보기 좋습니다.

{
  filename: '[name].[hash].js'
}
{
  filename: '[name].[chunkhash].js'
}

위 두 개의 차이를 말씀드렸는데요. 다시 한 번 더 설명하자면, hash는 웹팩 빌드를 할 때마다 고유값이 설정됩니다. 즉 [name].[hash].js가 있고 첫 번째 빌드를 하면 app.고유해시값1.js청크이름.고유해시값1.js가 생성됩니다. 다음에 빌드를 한 번 더 하면 app.고유해시값2.js청크이름.고유해시값2.js가 생성됩니다. 이제 빌드별로 고유해시값이 매겨지기 때문에 새로운 빌드를 할 때마다 새 파일들을 불러올 수 있습니다.

하지만 위 방식에도 문제가 있습니다. 빌드마다 모든 파일에 고유해시값이 공통적으로 부여되기 때문에, app.js의 내용은 바뀌었는데 0.js의 내용은 안 바뀐 경우에도 새로운 청크이름.고유해시값.js를 불러오게 됩니다. 한 파일만 바뀌어도 나머지 청크들도 다 새로 불러오게 되는 거죠.

그래서 있는 게 chunkhash입니다. 이 방식대로 하면 청크별로 해시값이 부여됩니다. app.app해시1.js청크이름.청크해시1.js가 생성되는 거죠. 만약 app.js의 내용만 바뀌었다면 다음 빌드 때 app.app해시2.js와 청크이름.청크해시1.js가 생성됩니다. 청크이름.js는 기존 청크해시 값을 그대로 사용하기 때문에 캐싱 시 이득을 볼 수 있습니다.

undefined

실제로 청크해시를 사용한 제 블로그입니다. app과 entry고 나머지 숫자들은 다 코드 스플리팅으로 만들어진 것들입니다. 청크해시가 파일별로 서로 다른 점을 주목해서 보세요. 마지막 열 Post, Write, Home 등은 require.ensure에서 세 번째 인자로 넣어준 청크 이름입니다. 아래 나오겠지만 vendor은 청크들간 공통적으로 사용하는 모듈들을 모아둔 겁니다. 조금 뒤에 vendor를 만드는 방법에 대해 설명합니다.

hashchunkhash의 공통적인 문제가 있습니다. app.js를 쓰다가 청크해시를 준 이후부터는 app.청크해시.js를 사용해야 하는데요. 문제는 청크해시 부분이 어떻게 나올지 미리 예측할 수가 없다는 겁니다. <script src="app.청크해시.js"></script>를 할 때 청크해시 부분에 뭐를 넣어줘야할지 모르는 거죠. 그래서 나온게 manifest입니다.

npm i -D webpack-manifest-plugin

설치를 하고

var ManifestPlugin = require('webpack-manifest-plugin');
{
  plugins: [
    ... // 다른 플러그인들
    new ManifestPlugin({
      fileName: 'assets.json',
      basePath: '/'
    }),
  ],
}

webpack.config.js에 추가하면 output의 path 경로에 assets.json이 생깁니다. 그 파일을 열어보면

{
  "app.js": "app.청크해시값.js",
  "0.js": "0.청크해시값.js"
}

이렇게 미리 청크해시값을 알 수 있게 json 구조로 나와있습니다. 이 데이터를 사용하셔서 script의 src로 사용하시면 됩니다. 단, HTML에서는 이 데이터를 사용하기 어려우므로 ejs, nunjucks, pug같은 템플릿 엔진을 사용해서 데이터로부터 js파일 이름을 꺼내오셔야 합니다.

마지막으로 CommonsChunkPlugin에 대해 알아보겠습니다. CommonsChunkPlugin은 여러 청크들 간에 공통적으로 사용되는 모듈들을 따로 하나로 모아두는 역할을 합니다. CommonsChunkPlugin이 웹팩4에서 optimize 속성의 splitChunks로 이전되었습니다. 

아래의 예시는 청크간에 겹치는 패키지들을 별도의 파일로 추출해주는 코드입니다. 벤더(vendor)라고 표현하죠. 웹팩3까지는 직접 벤더를 지정해야 했지만, 웹팩4에서는 알아서 벤더를 만들어줍니다. 

벤더를 만드는 이유는 다음과 같습니다. A 청크가 (a, b, c) 패키지를 가지고 있고, B 청크가 (a, b, d) 패키지를 가지고 있다면, a와 b 패키지가 겹치기 때문에 두 번 불러와서 쓸데없는 용량을 잡아먹습니다. 이런 것은 vendor~A~B (a, b)로 만들어주고, A 청크는 (c), B 청크는 (d)로 만들어 중복을 최소화해줍니다.

{
  entry: {
    app: './client'
  },
  optimization: {
    splitChunks: {
      cacheGroups: {
        vendor: {
          chunks: 'initial',
          name: 'vendor',
          enforce: true,
        },
      },
    }.
  }.
}

splitChunks은 옵션들이 매우 많기 때문에 여기서 다 알려드리기는 힘들 것 같습니다. 특정한 조건을 만족하는 공통 모듈을 추출한다든가 하는 게 가능합니다. 심지어 공통 모듈을 비동기로 로딩하는 것도 가능합니다. 공식 문서 를 참조하세요. 그런데 대부분의 경우는 이 옵션을 수정하기보다, 기본값을 그대로 사용하는 것이 효율적인 경우가 더 많습니다.

이상으로 웹팩5에 대해 간략하게 알아보았습니다. 기본적인 내용을 다뤘는데도 상당히 양이 많네요. 사실 웹팩 전체 기능의 1/3도 못 알려드린 거 같습니다. 웹팩은 설정이 복잡한 만큼 오류가 많이 납니다. 하지만 오류가 났을 때 그 오류를 구글에 검색하면 해결법도 바로 나옵니다. 따라서 자잘한 설정보다는 기본적인 원리(entry -> loader -> plugins -> output의 흐름)를 익히는 게 중요합니다. 여기까지 다 읽으셨다면 이제 공식문서를 보면서 웹팩5에 얼마나 많은 기능이 있는지 살펴보세요!

조회수:
0
목록
투표로 게시글에 관해 피드백을 해주시면 게시글 수정 시 반영됩니다. 오류가 있다면 어떤 부분에 오류가 있는지도 알려주세요! 잘못된 정보가 퍼져나가지 않도록 도와주세요.
Copyright 2016- . 무단 전재 및 재배포 금지. 출처 표기 시 인용 가능.
5만명이 선택한 평균 별점 4.9의 제로초 프로그래밍 강좌! 로드맵만 따라오면 됩니다! 클릭

댓글

7개의 댓글이 있습니다.
일 년 전
asdf
4년 전
안녕하세요 제로초님. 혹시 캐싱된 파일들을 위의 이미지같이 확인할 수 있는방법을 알려주실수 있으시가면 감사하겠습니다!
6년 전
안녕하세요! 글 잘 읽었습니다!! 한가지 궁금한 것이 있는데, 브라우저에서 변경된 파일을 잘 읽어오도록 하고자 webpack.config.prod.js output의 filename을 [name].[chunkhash].js 로만 수정하고, 다른 설정은 건드리지 않았는데요. 이 경우에도 프로젝트를 build하고 나면, js 파일마다 chunkhash 값이 잘 붙나요??(=>브라우저에서 변경된 파일을 잘 캐치하여 읽어주나요?) 감사합니다!!
6년 전
네 알아서 읽어옵니다.
6년 전
오 혹시 리액트의 서버사이드 렌더링에 대한 웹팩 설정에 대해서 질문올려도 될까요??
6년 전
네 됩니다. 그런데 서버사이드렌더링이 특정한 웹팩 설정을 요구했는지 잘 모르겠네요.
7년 전
require.ensure로 코드는 분리(app.[청크해시].js)가 됐는데, 청크해시값이 vender.js 안에 있습니다. 그래서 vender.js 가 갱신이 되어야 바뀐 app.청크해시.js를 불러올수 있습니다. vender의 경우 라이브러리가 많아서 캐시가 되어야 하는데 ~~ 구조를 어떻게 하면 잘 잡을까요?
7년 전
코드 스플릿의 개념을 잡을수 잇었네요 감사합니다
8년 전
웹팩2 글이 앞으로 더 나올거 같은데, 따로 분리하는게 어떨까요? Javascript에 있는게 맞는지 약간... 애매한거 같아요. 그리고 글 잘 읽고 있습니다 :) 너무 좋네요.
8년 전
다른 카테고리로 이전을 생각해보겠습니다. 그라바타 귀엽네요!