01.18 리액트 코드 스플리팅

React.js

01.18. 리액트 코드 스플리팅

싱글 페이지 애플리케이션단점은 페이지 로딩 속도지연될 수 있다는 것이다. 로딩 속도가 지연되는 이유는 자바스크립트 번들 파일에 모든 애플리케이션로직을 불러오므로 규모가 커지면서 용량도 커지기 때문이다. 하지만 이 문제는 코드 스플리팅(code splitting)을 하면 해결할 수 있다.

코드 스플리팅(code splitting)은 말 그대로 코드를 분할한다는 의미이다. webpack에서 프로젝트를 번들링할 때 파일 하나라 아니라 여러 개로 분리시켜서 결과물을 만들 수 있다. 또 페이지를 로딩할 때 한꺼번에 불러오는 것이 아니라 필요한 시점에 불러올 수도 있다.

01.18.1. 코드 스플리팅의 기본

webpack4 이전의 버전에서는 vendor 를 직접 설정해야 했지만, webpack4 부터는 자동으로 생성해주기 때문에 별도의 설정은 하지 않아도 된다.

01.18.1.1. 비동기적 코드 불러오기: 청크생성

페이지에서 필요한 코드들만 불러오려면, 청크(chunk)를 생성해야한다. 청크를 생성하면 페이지를 로딩할 때 필요한 파일만 불러올 수 있고, 아직 불러오지 않은 청크 파일들은 나중에 필요할 때 비동기적으로 불러와 사용하라 수 있다.
// src/components/SplitMe.js
import React from 'react';

const SplitMe = () => {
    return (
        <h3>청크</h3>
    );
};

export default SplitMe;
청크를 생성할 컴포넌트 자체는 특별히 하는 것이 없다. 다만 이 컴포넌트를 불러오는 것이 평상시와는 조금 다르다. 비동기적으로 파일을 불러오려면 import 를 코드 최상단에 적는 것이 아니라, 특정 함수 내부에서 작성한다. LifeCycle 메서드 안에 넣을 수도 있고, 별도의 이벤트를 설정하여 불러오도록 설정할 수도 있다.

SplitMe 를 비동기적으로 불러올 AsyncSplitMe 컴포넌트를 만들어 버튼을 눌렀을 때, SplitMe 컴포넌트를 불러와 state 에 담고 이를 렌더링한다.
// src/components/AsyncSplitMe.js
import React, {Component} from 'react';

class AsyncSplitMe extends Component {
    state = {
        SplitMe: null
    }

    loadSplitMe = () => {
        // 비동기적으로 코드를 불러온다. 함수는 Promise 를 결과로 반환한다.
        // import() 는 모듈의 전체 네임스페이스를 불러오므로, default 를 직접 지정해야한다.
        import('./SplitMe').then(({ default: SplitMe }) => {
            this.setState({
                SplitMe
            });
        });
    }

    render() {
        const { SplitMe } = this.state;
        // SplitMe 가 있으면 이를 렌더링하고, 없으면 버튼을 렌더링한다.
        // 버튼을 누르면 SplitMe 를 불러온다.
        return SplitMe ? <SplitMe /> : <button onClick={this.loadSplitMe}>SpdlitMe 로딩</button>
    }
}

export default AsyncSplitMe;

// src/App.js
import React from 'react';
import {Route} from 'react-router-dom';

import {Home, About, Posts} from 'pages';
import Menu from 'components/Menu';

import AsyncSplitMe from 'components/AsyncSplitMe';

const App = () => {
  return (
    <div>
      <Menu />
      <AsyncSplitMe />
      (...)
    </div>
  );
}

export default App;
크롬 개발자 도구의 Network 탭을 열어 SplitMe 비동기적 로딩 버튼을 누르면 네트워크에는 2.chunk.js 파일을 불러온 기록이 남는다.
이렇게 코드 위에서 import __ from __ 이 아닌 import() 함수로 컴포넌트를 불러오면 webpack 은 청크를 생성하여 저장한다. 

01.18.1.2. 라우트에 코드 스플리팅

SplitMe, AsyncSplitMe 는 더 이상 사용하지 않기 때문에 삭제하고, App 에서 import 했던 코드도 삭제한다.

- asyncComponent 함수 생성
비동기적으로 불러올 코드가 많으면 청크를 생성할 때마다 파일에 비슷한 코드들을 반복하여 작성해야한
다. 조금 더 편하게 구현할 수 있도록 따로 함수화하여 재사용한다.
// lib/asyncComponent.js
import React from 'react';

export default function asyncComponent (getComponent) {
    return class AsyncComponent extends React.Component {
        static Component = null;
        state = { Component: AsyncComponent.Component };

        constructor(props) {
            super(props);
            if (AsyncComponent.Component) return ;
            getComponent().then(({default: Component}) => {
                AsyncComponent.Component = Component;
                this.setState({Component});
            });
        }

        render() {
            const { Component } = this.state
            if (Component) {
                return <Component {...this.props} />
            }
            return null;
        };
    }
};
이 함수는 컴포넌트를 import 하는 함수를 호출하는 함수를 파라미터로 받는다.
asyncComponent( () => import('./Home') );
그리고 파라미터로 받은 함수는 constructor 에서 실행하여 컴포넌트를 불러온다. 해당 컴포넌트를 실제로 렌더링할 때 파일을 불러오도록 설정한 것이다. 컴포넌트가 로딩되면 불러온 컴포넌트를 state 에 집어넣고, 또 static 값으로도 설정한다.
컴포넌트가 언마운트되었다가 나중에 다시 마운트될 때는 컴포넌트를 다시 새로 불러오지 않고, static 값으로 남아있는 이전에 불러온 컴포넌트 정보를 재사용한다.

01.18.1.3. 라우트 코드 스필리팅용 인덱스 생성

// src/pages/index.async.js
import asyncComponent from '../lib/asyncComponent';

export const Home = asyncComponent( () => import('./Home') );
export const About = asyncComponent( () => import('./About') );
export const Post = asyncComponent( () => import('./Post') );
export const Posts = asyncComponent( () => import('./Posts') );

// src/App.js
import React from 'react';
import {Route} from 'react-router-dom'; 

import {Home, About, Posts} from 'pages/index.async.js';
import Menu from 'components/Menu';

const App = () => {
  return (
    <div>
      <Menu />
      <Route exact path="/" component={Home} />
      <Route path="/about/:name?" component={About} />
      <Route path="/posts" component={Posts} />
    </div>
  );
};

export default App;
크롬 개발자 도구의 Network 탭을 연 상태에서 메뉴의 링크를 클릭해서 이동해보면 비동기 로딩이 처릭 될 것이다.( Posts 컴포넌트는 index.async.js 파일로 치환하지 않았기 때문에 현재는 비동기 로딩이 되지 않는다. 나중에 설정할 것이기 때문에 무시해도 된다.)

코드 스플리팅은 프로젝트 규모가 클수록 효과가 있다. 소규모 프로젝트라면 코드 스플리팅을 해 보았자 3kb 미만으로 큰 효과는 없다.
소규모 프로젝트라면 코드 스플리팅은 생략해도 된다. 나중에 파일 크기가 좀 커졌다고 느낄 때 코드 스플리팅을 구현해도 무방하다.

댓글

이 블로그의 인기 게시물

01.7 React ref (DOM에 이름 달기)

01.11 리액트 컴포넌트 CSS 적용 ( CSS Module, Sass, styled-components )

01.9 React 컴포넌트 라이프사이클