본문 바로가기

Front/React

[React] 코드 스플리팅

코드 스플리팅이란

  • 리액트 프로젝트 : 빌드 거친 뒤 배포
    • 주석, 경고 메시지, 공백 등을 제거하여 파일 크기 최소화
    • 브라우저에서 JSX 문법이나 다른 최신 자바스크립트 문법이 원활하게 실행되도록 코드의 트랜스파일 작업
    • 정적 파일 존재 -> 경로 설정됨 -> 웹팩담당
  • 웹팩에서 별도의 설정을 하지 않으면 프로젝트에서 사용 중인 모든 자바스크립트 파일이 하나의 파일로 합쳐지고, 모든 CSS 파일도 하나의 파일로 합쳐진다.
  • CRA로 프로젝트를 빌드할 경우 최소 두 개 이상의 자바스크립트 파일 생성
    • CRA의 기본 웹팩 설정에는 SplitChunks라는 기능이 적용되어 node_modules에서 불러온 파일, 일정 크기 이상의 파일, 여러 파일 간에 공유된 파일을 자동으로 따로 분리시켜 캐싱의 효과 극대화

 

build/static 디렉터리

  • 파일 이름에 해시값(8c8b27...)이 포함
    • 빌드 과정에서 해당 파일의 내용에 따라 생성되며, 이를 통해 브라우저가 새로 파일을 받아야 할지 받지 말아야 할지를 알 수 있다.
  • 현재 2로 시작하는 파일  -> React, ReactDOM등 node_modules에서 불러온 라이브러리 관련 코드가 들어있다.
  • main으로 시작하는 파일 -> 직접 프로젝트에서 작성하는 App같은 컴포넌트에 대한 코드가 들어있다.
  • SplitChunks라는 웹팩 기능을 통해 자주 바꾸지 않는 코드들이 2로 시작하는 파일에 들어있어 캐싱의 이점을 더 오래 누릴 수 있다.
  • 이렇게 파일을 분리하는 작업이 코드 스플리팅
  • 리액트 프로젝트에 별도로 설정하지 않으면 여러 컴포넌트에 대한 코드가 모두 한 파일에 저장 -> 파일 크기 커짐
    • 문제점 해결 방법 중 한가지 : 코드 비동기 로딩
  • 코드 비동기 로딩을 통해 자바스크립트 함수, 객체, 혹은 컴포넌트를 처음에 불러오지 않고 필요한 시점에 불러와서 사용할 수 있다.

 

 

1. 자바스크립트 함수 비동기 로딩

컴포넌트 코드를 스플리팅하기에 앞서 일반 자바스크립트 함수를 스플리팅 해보겠다.

 

[src/notify.js]

export default function notify() {
    alert('안녕하세요!')
}

 

Hello React! 문구를 누르면 notify함수가 실행되도록 App.js 를 수정

[src/App.js]

import logo from './logo.svg';
import './App.css';
import notify from './notify'

function App() {
  const onClick = () => {
    notify();
  }
  return (
    <div className="App">
      <header className="App-header">
        <img src={logo} className="App-logo" alt="logo" />
        <p onClick={onClick}> Hello React!</p>
      </header>
    </div>
  );
}

export default App;

 

이렇게 코드를 작성하고 빌드하면 notify 코드가 main 파일 안에 들어가게 된다.

하지만 다음과 같이 import 를 상단에서 하지 않고 import() 함수 형태로 메서드 안에서 사용하면, 파일을 따로 분리시켜서 저장한다.

그리고 실제 함수가 필요한 지점에 파일을 불러와서 함수를 사용

 

[src/App.js]

import logo from './logo.svg';
import './App.css';

function App() {
  const onClick = () => {
    import('./notify').then(result => result.default());
  }
  return (
    <div className="App">
      <header className="App-header">
        <img src={logo} className="App-logo" alt="logo" />
        <p onClick={onClick}> Hello React!</p>
      </header>
    </div>
  );
}

export default App;
  • import 함수로 사용하면 Promise로 반환한다.
  • 이렇게 import를 함수로 사용하는 문법은 비록 아직 표준 자바스크립트가 아니지만, state-3단계에 있는 dynamic import라는 문법이다.
  • 현재는 웹팩에서 지원하고 있으므로 별도의 설정 없이 프로젝트에 바로 사용할 수 있다.
  • 이 함수를 통해 모듈을 불러올 때 모듈에서 default로 내보낸 것은 result.default를 참조해야 사용할 수 있다.
  • 브라우저를 열고 개발자 도구의 Network탭을 연 다음, Hello React!를 클릭해보자

[결과]

  • Hello React! 를 클릭하는 시점에 새로운 자바스크립트 파일을 불러올 것이다.
  • 불러온 파일의 내용을 확인해 보면 notify에 관련된 코드만 들어있다.

이제 yarn build를 입력하여 빌드해본다.

 

이제 3으로 시작하는 파일 안에 notify 관련 코드가 들어간다.

 

 

2. React.lazy와 Suspense를 통한 컴포넌트 코드 스플리팅

코드 스플리팅을 위해 리액트에 내장된 기능으로 유틸 함수인 React.lazy와 컴포넌트인 Suspense가 있다.

이 기능은 리액트 16.6 버전부터 도입. 이전 버전에서는 import 함수를 통해 불러온 다음, 컴포넌트 자체를 state에 넣는 방식으로 구현해야 한다.

 

2.1. state를 사용한 코드 스플리팅

  • React.lazy 를 사용하기에 앞서, React.lazy 없이 컴포넌트의 코드를 스플리팅한다면 어떻게 해야하는지 알아보자

먼저 코드 스플리팅을 할 간단한 컴포넌트를 만든다.

 

[SplitMe.js]

import React from 'react';

const SplitMe = () => {
    return <div>SplitMe</div>
};

export default SplitMe;

 

App컴포넌트를 클래스형 컴포넌트로 전환해준다.

handleClick메서드를 만들고, 그 내부에서 SplitMe컴포넌트를 불러와 state에 넣겠다.

또한 render함수에서는 state안에 있는 SplitMe가 유효하다면 SplitMe 컴포넌트를 렌더링해 주어야 한다.

 

[App.js]

import React, {Component} from 'react';
import logo from './logo.svg';
import './App.css';

class App extends Component{
  state = {
    SplitMe: null
  };
  handleClick = async () => {
    const loadedModule = await import('./SplitMe');
    this.setState({
      SplitMe: loadedModule.default
    });
  };
  render() {
    const {SplitMe} = this.state;
    return (
      <div className="App">
        <header className="App-header">
          <img src={logo} className="App-logo" alt="logo" />
          <p onClick={this.handleClick}> Hello React!</p>
          {SplitMe && <SplitMe />}
        </header>
      </div>
    )
  }
}

export default App;

 

[결과]

  • state를 사용하여 컴포넌트 코드 스플리팅 하는 것이 그렇게 어렵지는 않지만, 매번 state를 선언해 주어야 한다는 점이 불편하다.

 

2.2. React.lazy와 Suspense 사용하기

  • React.lazy는 컴포넌트를 렌더링하는 시점에서 비동기적으로 로딩할 수 있게 해 주는 유틸함수
  • 사용방법은 다음과 같다.
const SplitMe = React.lazy(() => import('./SplitMe'));

 

  • Suspense는 리액트 내장 컴포넌트로서 코드 스플리팅된 컴포넌트를 로딩하도록 발동시킬 수 있다.
  • 로딩이 끝나지 않았을 때 보여줄 UI를 설정할 수 있다.
  • 사용법은 다음과 같다.
import React, {Suspense} from 'react';

(...)
<Suspense fallback={<div>loading...</div>}
   <SplitMe />
</Suspense>
  • Suspense에서 fallback props를 통해 로딩 중에 보여 줄 JSX를 지정할 수 있다.

[App.js]

import React, {Suspense, useState} from 'react';
import logo from './logo.svg';
import './App.css';
const SplitMe = React.lazy(() => import('./SplitMe'));

function App() {
  const [visible, setVisible] = useState(false)
  const onClick = () => {
    setVisible(true)
  };
  return (
    <div className='App'>
      <header className="App-header">
          <img src={logo} className="App-logo" alt="logo" />
          <p onClick={onClick}> Hello React!</p>
          <Suspense fallback={<div>loading...</div>}>
            {visible && <SplitMe />}
          </Suspense>
        </header>
    </div>
  );
}

export default App;
  • 단순히 SplitMe 컴포넌트의 가시성을 의미하는 visible이라는 상태만 업데이트하여 코드 스플리팅된 컴포넌트를 보여주었다.

 

2.3. Loadable Components를 통한 코드 스플리팅

  • Loadable Components는 코드 스플리팅을 편하게 하도록 도와주는 서드파티 라이브러리
  • 서버사이드 렌더링 지원(React.lazy와 Suspense는 아직 서버 사이드 렌더링을 지원하지 않는다.)
  • 렌더링하기 전에 필요할 때 스플리팅된 파일을 미리 불러올 수 있는 기능
  • 서버사이트 렌더링이란
    • 웹 서비스의 초기 로딩 속도 개선
    • 캐싱 및 검색 엔진 최적화를 가능
    • 웹 서비스의 초기 렌더링을 사용자의 브라우저가 아닌 서버 쪽에서 처리
    • 서버에서 렌더링한 html 결과물을 받아 와서 그대로 사용하기 때문에 초기 로딩 속도 개션, 검색 엔진에서 크롤링 할때도 문제 없음

먼저 라이브러리 설치 

$ yarn add @loadable/component

 

사용법은 React.lazy와 비슷. 단 Suspense를 사용할 필요는 없다.

 

[App.js]

import React, {Suspense, useState} from 'react';
import logo from './logo.svg';
import './App.css';
import loadable from '@loadable/component';
const SplitMe = loadable(() => import('./SplitMe'));

function App() {
  const [visible, setVisible] = useState(false)
  const onClick = () => {
    setVisible(true)
  };
  return (
    <div className='App'>
      <header className="App-header">
          <img src={logo} className="App-logo" alt="logo" />
          <p onClick={onClick}> Hello React!</p>
          <Suspense fallback={<div>loading...</div>}>
            {visible && <SplitMe />}
          </Suspense>
        </header>
    </div>
  );
}

export default App;

 

로딩 중 다른 UI를 보려주고 싶다면 loadable을 사용하는 부분을 다음과 같이 수정한다.

 

[App.js]

const SplitMe = loadable(() => import('./SplitMe'), {
   fallback: <div>loading...</div>
}

 

이번에는 컴포넌트를 미리 불러오는  preload 방법을 알아보겠습니다.

 

[App.js]

import React, {useState} from 'react';
import logo from './logo.svg';
import './App.css';
import loadable from '@loadable/component';
const SplitMe = loadable(() => import('./SplitMe'), {
  fallback: <div>loading...</div>
});

function App() {
  const [visible, setVisible] = useState(false)
  const onClick = () => {
    setVisible(true)
  };
  const onMouseOver = () => {
    SplitMe.preload();
  };
  return (
    <div className='App'>
      <header className="App-header">
          <img src={logo} className="App-logo" alt="logo" />
          <p onClick={onClick} onMouseOver={onMouseOver}> 
            Hello React!
          </p>
            {visible && <SplitMe />}
        </header>
    </div>
  );
}

export default App;
  • 마우스 커서를 Hello React! 위에 올리기만 해도 로딩이 시작된다.
  • 클릭했을 때 렌더링

 

[결과]