[React] 코드 스플리팅 (Code Splitting)

스플리팅 발단

리액트 어플리케이션의 경우 빌드를 통해서 배포한다. 이 과정에서 파일 크기를 가능하면 최소화하는 것이 바람직하다.

왜냐하면 파일 크기가 성능을 결정하고 결과적으로 사용자 경험에까지 영향을 미치기 때문이다.

또한 브라우저에서 JSX나 최신 자바스크립트 문법 등이 문제없이 잘 실행될 수 있도록 트랜스파일링하는 작업도 해주어야 한다.

💡 일반적으로 이러한 작업은 웹팩(webpack)에서 담당한다.
따로 웹팩을 설정해주지 않으면 프로젝트의 모든 자바스크립트 파일은 하나의 파일로 합쳐지고, CSS 역시 하나의 파일로 합쳐지게 된다.

하나의 파일로 모든 자바스크립트를 묶어서 빌드하면 파일의 크기가 매우 크고, 한 줄의 자바스크립트 코드만 수정해도 다시 모든 자바스크립트 코드들을 새로 빌드해야 하기 때문에 비효율성을 가지게 된다.


코드 스플리팅


파일을 분리하는 작업

❗더 나은 사용자 경험을 위해 코드를 비동기적으로 로딩하는 방법

예를 들어 페이지가 /main, /about, /post 이렇게 세 가지 페이지로 이루어진 SPA를 개발한다고 할 때

/main으로 들어가는 동안 /about이나 /post 페이지 정보는 사용자에게 필요하지 않을 확률이 높다.

💡 그러한 파일들을 분리하여 지금 사용자에게 필요한 파일만 불러올 수가 있다면 로딩도 빠르게 이루어지고 트래픽도 줄어 사용자 경험이 좋아질 수가 있다.

지금 당장 필요한 코드가 아니면 따로 분리시켜서, 나중에 필요할 때 불러와서 사용할 수 있다.


리액트에서 코드 스플리팅


React.lazy

💡 컴포넌트를 렌더링하는 시점에 비동기적으로 로딩할 수 있게 해주는 유틸 함수이다.

Suspense

💡 리액트 내장 컴포넌트로 코드 스플리팅 된 컴포넌트를 로딩하고, 로딩이 끝나지 않았을 때 보여줄 UI를 설정할 수 있다.

fallback이라는 props를 통해 로딩 중에 보여줄 JSX 문법을 지정할 수 있다.

React.lazy + Suspense

import React, { Suspense } from 'react';

const SomeComponent = React.lazy(() => import('./SomeComponent'));

const myComponent = {
	return (
		<Suspense fallback={<div>Loading...</div>}>
			<SomeComponent />
		</Suspense>
	)
}

만약 React.lazy를 쓰지 않고 스플리팅을 해야 한다면? 🙄

16.6버전 이전에 했던 방식으로 분리할 컴포넌트를 state에 선언하여 해당 모듈을 불러와야할 때 state를 바꾸어 주는 식으로 진행한다.


코드 분할을 결정하는 요소


그 중 가장 많이 쓰이는 방법 중 하나는 라우트 기반 분할이다.

  1. Route level
import React, { Suspense, lazy } from 'react';
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';

const Home = lazy(() => import('./routes/Home'));
const About = lazy(() => import('./routes/About'));

const App = () => (
  <Router>
    <Suspense fallback={<div>Loading...</div>}>
      <Switch>
        <Route exact path="/" component={Home}/>
        <Route path="/about" component={About}/>
      </Switch>
    </Suspense>
  </Router>
);

💡 라우트마다 다른 컴포넌트로 관리를 하고 있을 경우, 각 라우트를 import 함수를 통해 분리된 빌드 파일로 관리 할 수 있다.

유저가 다른 페이지로 넘어갈때만 그 페이지를 비동기적으로 로딩할 수 있다.


  1. Component level

    💡 페이지 안에 있지만 보이지 않는 컴포넌트가 존재할 수 있다.

    예를 들어 유저가 이메일 페이지에서 새로운 메일을 작성하고자 할 때, 작성하기 버튼을 눌러 모달이 뜨게 된다면 그 모달을 import()로 스플리팅해서 관리할 수 있다.

모달 대신 alert 함수 스플리팅하기

Notify.tsx

const Notify = () => {
	window.alert('notify!');
};

export default Notify;

App.tsx

import React, {useState} from 'react';
import {TextField, Box, Button} from '@mui/material';

const App = () => {
	const handleNotify = () => {
			import('./Notify').then(({default: Notify}) => {
				Notify();
			});
		};

	return (
		<Button variant='contained' onClick={handleNotify}>
			추가
		</Button>
	)
}

export default App

💡 Button을 눌렀을 때 example.chunk.js라는 파일을 불러오게 된다.

import 함수를 사용하면 웹팩이 알아서 코드를 분리해서 저장해주고, import가 호출할 때 불러와서 사용할 수 있게 해준다.

❗여기서는 함수 level을 스플리팅했다.


**chuck 파일 생성 차이**

스플리팅 전

스플리팅 후

  1. 하나의 페이지를 스플리팅하기

    💡 페이지 하나가 되게 긴 경우,
    그 페이지에 들어갈 때 당장 보이는 부분을 나머지와 분리하고 그 뒷부분을 다른 컴포넌트로 만들어 스플리팅할 수 있다.


Webpack: Entry Point


Entry Point는 웹팩이 앱에서 번들링하려는 모듈의 진입파일이다.

리액트 앱이 여러 엔트리 포인트를 설정한다면 각각의 엔트리 포인트 마다 코드 스플리팅이 가능하다.

// webpack.config.js
const path = require('path');

module.exports = {
  mode: 'development',
  entry: {
    index: './src/index.js',
    another: './src/another-module.js',
  },
  output: {
    filename: '[name].bundle.js',
    path: path.resolve(__dirname, 'dist'),
  },
};

💡 entry 프로퍼티를 작성하면 웹팩에서 자동으로 indexanother를 다른 chunk로 관리를 해서 로딩한다.
웹팩은 둘 간의 의존성(dependency)도 분리를 해서 관리를 하는데, 만약 같은 의존성을 여러 entry point에서 가지고 있다면, 중복된 로딩이 많아져서 성능 저하를 일으킬 수 있다.
중복 되는 dependencies는 다른 chunk로 관리해주는 것이 바람직하다.


출처


https://devowen.com/342

https://hoony-gunputer.tistory.com/entry/6편-Code-Splitting-과-React-lazy-Suspense

https://darrengwon.tistory.com/609

https://zereight.tistory.com/969

좋은 웹페이지 즐겨찾기