이번 포스팅에서는 리액트에서 코드 스플리팅이 무엇이고 어떻게 하는지에 대한 방법을 간단하게 설명해 보려고 한다.
우리가 리액트 프로젝트를 완성한 후 서비스를 사용자에게 전달하기 위해서는 빌드를 통해서 배포해야 한다. 이 과정에서 파일 크기를 가능하면 최소화 하는 것이 바람직한데, 그 이유는 이 파일 크기가 성능을 결정하고 결과적으로 사용자 경험에까지 영향을 미치기 때문이다. 또한 브라우저에서 JSX나 최신 자바스크립트 문법 등이 문제없이 잘 실행될 수 있도록 트랜스파일링 하는 작업도 해 주어야 한다. 일반적으로 이러한 작업은 웹팩(webpack)에서 담당하며, 웹팩을 따로 설정해 주지 않으면 프로젝트의 모든 자바스크립트 파일은 하나의 파일로 합쳐지고, CSS 역시 하나의 파일로 합쳐지게 된다.
하나의 파일로 모든 자바스크립트를 묶어서 빌드하면 별로 좋지 않다. 왜냐하면 그 파일의 크기는 매우 클 것인데(프로젝트 안에서 모든 JS 코드가 다 들어 있으므로) 그에 비해 우리가 한 줄의 JS 코드만 수정해도 다시 모든 JS코드들을 새로 빌드해야 하는 비효율성을 가지게 된다. 사실 라이브러리와 같이 대부분의 코드는 거의 바뀔 일이 없기 때문에 처음에 빌드한 파일을 그대로 사용해도 괜찮은 경우도 많으며 이와 같이 자주 바뀌지 않는 코드에 대해서는 브라우저가 파일을 항상 새로 받을 필요 없이 캐싱의 이점을 누릴 수도 있게 된다.
이와 같이 파일을 분리하는 작업을 코드 스플리팅이라고 한다. 예를 들어 페이지가 /main, /about, /post 이렇게 세 가지 페이지로 이루어진 SPA를 개발한다고 할 때 /main 페이지를 들어가는 동안 /about이나 /post 페이지 정보는 사용자에게 필요하지 않을 확률이 높다. 그러한 파일들을 분리하여 지금 사용자에게 필요한 파일만 불러올 수가 있다면 로딩도 빠르게 이루어지고 트래픽도 줄어 사용자 경험이 좋아질 수가 있다. 이와 같이 더 나은 사용자 경험을 위해 코드를 비동기적으로 로딩하는 방법이 있는데 코드 비동기 로딩의 대표적인 예시가 바로 코드 스플리팅이다.
자바스크립트 함수 비동기 로딩
기본적으로 자바스크립트 함수는 import() 함수를 통해서 분리할 수 있다. import() 함수 형태로 메서드 안에서 사용하게 되면 필요할 때 해당 스크립트를 불러와서 사용할 수 있게 되는 것이다.
// src/notify.js
export default function notify() {
console.log('notify');
}
// src/App.js
import React from 'react';
function App() {
const onClick = () => {
import('./notify').then(res => res.default());
};
return (
<div>
<p onClick={onClick}>Hello World</p>
</div>
);
}
export default App;
import를 함수로 사용하면 Promise를 반환한다. 이 문법은 dynamic import 문법으로 현재는 웹팩에서도 지원하고 있다. 따라서 별도의 설정 없이 프로젝트에서 사용할 수가 있고 notify 모듈에서 default로 내보내었기 때문에 res.default로 참조해야 사용할 수 있다.
React.lazy와 Suspense를 통한 컴포넌트 코드 스플리팅
리액트에서도 코드 스플리팅을 지원한다. 유틸 함수인 React.lazy와 컴포넌트인 Suspense가 그것이다.
만약 React.lazy를 쓰지 않고 리액트에서 코드 스플리팅을 해야 한다면(16.6버전 이전에서는 이렇게 했었음) 분리할 컴포넌트를 state에 선언하여 해당 모듈을 불러와야 할 때 state를 바꾸어 주는 식으로 진행했었다. 어렵지는 않지만 조금 번거롭고 귀찮은 작업이었다.
React.lazy와 Suspense를 통해서는 state를 따로 선언하지 않고 코드 스플리팅을 할 수 있다. React.lazy는 컴포넌트를 렌더링하는 시점에서 비동기적으로 로딩할 수 있게 해 주는 유틸 함수이다.
Suspense는 리액트 내장 컴포넌트로 코드 스플리팅 된 컴포넌트를 로딩하도록 발동시킬 수 있고, 로딩이 끝나지 않았을 때 보여줄 UI를 설정해 줄 수 있다. fallback 이라는 props를 통해 로딩 중에 보여줄 JSX 문법을 지정할 수 있다.
import React, { Suspense } from 'react';
const OtherComponent = React.lazy(() => import('./OtherComponent'));
const AnotherComponent = React.lazy(() => import('./AnotherComponent'));
function MyComponent() {
return (
<div>
<Suspense fallback={<div>Loading...</div>}>
<section>
<OtherComponent />
<AnotherComponent />
</section>
</Suspense>
</div>
);
}
코드 분할을 결정하는 요소에는 여러가지가 있으며 그 중 가장 많이 쓰는 방법 중 하나는 라우트 기반 분할이다. 웹 페이지를 불러오는 시간은 페이지 전환에 어느정도 발생하며 대부분 페이지를 한 번에 렌더링하기 때문에 사용자가 페이지를 렌더링 하는 동안 다른 요소와 상호작용하지 않는다. React.lazy를 React Router 라이브러리를 사용해 라우트 기반 코드 분할을 설정하면 다음과 같이 나타낼 수 있다.
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>
);
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 프로퍼티를 작성하면 웹팩에서 자동으로 index와 another를 다른 chunk로 관리를 해서 로딩한다. 이 경우 웹팩은 둘 간의 의존성(dependency)도 분리를 해서 관리하는데, 만약 같은 의존성을 여러 엔트리 포인트에서 가지고 있다면 이 역시 중복된 로딩이 많아져 성능상에 저하를 일으킬 수 있으므로 중복되는 dependencies는 다른 chunk로 관리해 주는 것이 바람직하다.
이번 포스팅에서 간단하게 리액트에서 어떻게 코드 스플리팅을 할 수 있는지에 대해서 알아보았다. 다음번에는 실제로 코드 스플리팅을 했을 때 성능상에 어떠한 변화가 생겼는지를 실제로 적용해 보고 결과를 공유하는 내용의 포스팅을 작성해 보려고 한다.
참고자료
- 리액트 공식 문서
- 웹팩 공식 문서
- <리액트를 다루는 기술>, 김민준(velopert) 저
- 리액트에서 해보는 코드 스플리팅
'Web Frontend Developer' 카테고리의 다른 글
[Recoil] 리액트 상태관리 라이브러리 리코일 (1) | 2021.01.20 |
---|---|
CSS-in-JS에 대해서 알아보자 (1) | 2020.12.11 |
[React] Context API (0) | 2020.10.06 |
[리액트+TS] 우아한 테크러닝 3기 3주차 (0) | 2020.09.29 |
[리액트+TS] 우아한 테크러닝 3기 2주차 (0) | 2020.09.21 |