Jello's development blog

Jello's development blog

webpack을 이용한 react 어플리케이션 코드 스플릿

웹 어플리케이션을 개발할 때에 로딩 시간은 중요한 부분을 차지한다. 이를 서비스할 때에 로딩 시간이 길어지면 사용자들은 기다리지 않고 바로 이탈해버리기 때문이다. 실제로 한 대형 이커머스 서비스를 조사한 결과, 웹 페이지의 이탈율이 초당 2.7%씩 증가하고, 15초가 넘어서면 50%까지 도달하게 된다. 이처럼 로딩 시간은 상당히 중요한 이슈인데, 구글은 모바일 페이지 로딩 시간을 획기적으로 단축시킬 수 있는 AMP(Accelerated Mobile Pages) 프로젝트를 진행하고 있기도 하다. 실제로 페이스북이나 구글에서 AMP를 이용해 만들어진 페이지를 들어가보면 웹 로딩이 아니라 네이티브로 만들어진 것처럼 로딩 시간은 거의 느껴지지 않고 빠르게 렌더링된다.

이 로딩 시간은 SPA(Single Page Application)에게는 약점이 될 수 있다. 한 번에 모든 데이터를 로딩한 뒤에서야 빠른 속도를 보여주기 때문이다. 따라서 초기 로딩 시간은 여러 개의 페이지를 가진 어플리케이션보다 느릴 수밖에 없는데, webpack은 이를 보완하여 초기 로딩에는 처음 보여질 화면에 필요한 컴포넌트만 로드하고, 라우팅을 할 때에 새롭게 바뀔 페이지에 필요한 컴포넌트를 로딩하게 할 수 있다.

비결은 webpack에 동적 로딩을 가능하게 하는 기능이 있기 때문이다. (Webpack Code Splitting) 문서를 보면 여러 가지 방법이 나와 있는데, 여기서는 의존성이 가장 적은 require.ensure()를 사용하겠다 (현재는 import()를 쓰라고 권장하고 있다).

require.ensure()의 구문은 다음과 같다.

require.ensure([디펜던시], function(require) {
    require(불러올 모듈 경로);
}, 나눠질 파일명);

디펜던시는 보통 비워두고 (있는 경우를 보지 못했다), 다음 인자로 줄 함수 안에 불러올 모듈을 require 해주면 된다. 그렇게 하면 webpack이 bundling을 하는 과정에서 이를 인식하고, 파일을 나눠주는듯 하다.

이 구문이 어디에 들어가야 하냐면, 바로 react-router의 getComponent property이다.

<Router history={history}>
    <IndexRoute getComponent={(loc, cb) => {
        require.ensure([], (require) => { cb(null, require('./containers/Home.js').default); }, 'Home');
    }}/>
    <Route path="list" getComponent={(loc, cb) => {
        require.ensure([], (require) => { cb(null, require('./containers/List.js').default); }, 'List');
    }} onEnter={requireAuth}/>
    <Route path="login" getComponent={(loc, cb) => {
        require.ensure([], (require) => { cb(null, require('./containers/Login.js').default); }, 'Login');
    }} onEnter={requireAnonymous}/>
    <Route path="register" getComponent={(loc, cb) => {
        require.ensure([], (require) => { cb(null, require('./containers/Register.js').default); }, 'Register');
    }} onEnter={requireAnonymous}/>
</Router>

대부분 component property를 사용하고 있을텐데, getComponent는 함수를 값으로 받기 때문에 좀 더 동적인 설정이 가능한 것 같다. 여기서 getComponent에 들어갈 함수가 반복되어서, 반복되는 함수를 생성해주는 creator 함수를 따로 만들어서 인자로 줘 봤지만, 말을 듣지 않았다. Webpack이 코드를 모두 돌려보고 파일을 나눠주지 않기 때문에 어쩌면 당연한 결과였을지도 모른다.

다음으로 설정해야 할 것은 webpack.config.js 파일이다.

module.exports = {
    entry: {
        app: path.join(__dirname, '../app/src/routes.js')
    },
    output: {
        filename: '[name].[chunkhash].js',
        // chunkFilename을 지정해준다.
        chunkFilename: '[name].[chunkhash].js',
        path: path.join(__dirname, '../app/dist'),
    },
    module: {
        rules: [
            ...
        ]
    },
    plugins: [
        ...
    ]
};

output의 chunkFilename을 위와 같이 지정해주면 아래와 같이 파일이 생성된다.

[name].[chunkhash].js에 따라 생성

chunkhash를 사용하는 이유는, 웹 브라우저가 파일명으로 캐싱을 하게 되는데, 코드 업데이트를 할 경우에 캐싱으로 인해 변경사항이 반영되지 않을 수 있다. 따라서 해시코드를 바꿔줌으로써 bundling을 할 때마다 다른 해시코드가 파일에 붙여지게 되고, 캐싱되지 않은 새로운 파일을 불러올 수 있게 하는 것이다.

이렇게 코드 스플릿을 함으로써 SPA의 초기 로딩 시간을 단축시킬 수 있다. 이 밖에도 uglify를 하거나, 모듈, 비즈니스 로직을 vendor.js, app.js로 나누어서 로드하는 등 webpack을 이용한 여러 최적화 방법이 있다.