이 포스트는 Velopert님의 '리액트를 다루는 기술 개정판' 19장 코드 스플리팅 편을 참고하여 작성되었습니다.
코드 스플리팅(Code Splitting)
이 무엇인지에 대해서 먼저 알아보겠습니다. 리액트 카테고리에 집어넣긴 했지만, 웹팩(webpack)
을 이용한 다른 어플리케이션에서도 모두 사용 가능한 언어입니다.
웹팩
도 간단히 설명하자면, 어플리케이션의 모든 파일들을 묶고 압축하여 하나의 결과물을 만들어주는 웹 개발 도구입니다.
개발자가 자바스크립트로 개발을 하고 배포하는 과정에서 빌드(build)
과정을 거치게 되는데, 이 과정에서 모든 파일들이 하나로 합쳐지게 됩니다. 우리가 index.js, components들로 나눴던 소스 코드들이 하나의 거대한 소스 코드로 합쳐진다는 말입니다. 간단한 프로젝트라면 영향이 적겠지만, 거대한 프로젝트라면(특히 SPA 페이지에서) 길고 많은 자바스크립트 코드(.css, .html도 마찬가지)가 탄생합니다. 이 경우 인터넷 환경이 좋지 못한 곳에서는 거대한 소스 코드들을 불러오는데 상당한 로딩시간을 갖게 됩니다. 이를 개선하고자 코드에서 당장 사용하는 부분만을 로딩하고, 현재 필요하지 않은 코드 부분은 따로 분리시켜 나중에 로드함으로써 로딩 시간을 개선하는 것이 코드 스플리팅
입니다.
실습을 위해 새로운 프로젝트를 create react-app으로 만들었습니다. 기본적으로 아래와 같은 파일 구조가 생성됩니다.여기서 build를 위한 명령어 yarn build
를 입력해보겠습니다. 기다리면 빌드가 완료되고, 프로젝트에 build라는 폴더가 하나 생깁니다.
yarn build
이 폴더 내부에는 다음과 같은 16진수로된 파일명을 가진 해시 값이 적혀있습니다.
App.js를 아무렇게나 수정하고 다시 빌드하면 main이라고 적힌 부분의 파일명이 변경되었습니다. 즉, App.js처럼 많이 사용되고 변경되는 부분은 main으로 그 외 부분은 787.~파일로 들어가서 파일이 분리되고, 필요한 경우 787.~부분을 로딩해서 사용하는 식으로 이용하는데, 이것을 코드 스플리팅
이라고 합니다.리액트 코드 스플리팅에는 여러 방법들이 존재하고 있지만, 이번에는 세가지 방식만 알아보도록 하겠습니다.
첫 번째 방법은 코드 비동기 로딩
입니다. 이 방식은 필요한 부분에서 파일을 import함으로써 필요한 순간에 코드를 불러오게 합니다. 즉, import를 함수형으로 사용하는 문법인데, dynamic import(동적 import)
라고 부릅니다. 정식 문법인가 하고 확인해봤는데, ES2022, import 구문 문서에 아직 등재되지 않은걸로 보아 정식 문법으로 채택되지는 않은 상태인 것 같습니다.
다음과 같은 분리된 코드의 함수를 준비해주세요.
export default function notify() {
alert('code-splitting');
}
다음으로는 App.js를 수정해주세요. code-splitting이라는 글자(p 태그)를 글릭하면 onClick 함수가 실행되고, onClick은 notify() 함수를 불러오는 역할을 합니다.
그런데 이렇게 작성하면, 이미 notify()가 선언되어 있어서, 누르기 전에도 notify() 함수 코드가 로드되어있는 상태입니다. 이런 상태가 사용하지 않는데 코드를 불러온 불필요한 로딩이 있는 사례입니다.
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}>code-splitting</p>
</header>
</div>
);
}
export default App;
그럼 이제 코드를 비동기적으로 로드해보겠습니다. 동적 import
의 사용법은 import를 함수처럼 사용합니다. 함수 처럼 사용한 import 구문은 Promise를 반환합니다. 코드를 다음과 같이 수정하고 어플리케이션을 실행해주세요. 그리고 개발자도구의 네트워크 탭을 열고 어떻게 동작하는지 살펴보세요.
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}>code-splitting</p>
</header>
</div>
);
}
export default App;
처음에 main만 로딩을 합니다. 하지만 사용자가 클릭을 하니 또 다른 코드를 불러왔습니다. 이렇게 동적 import를 통해서 필요할 때만 코드를 불러올 수 있습니다. 그리고 다시 build를 하면 notify와 관련된 새로운 코드가 들어있음을 볼 수 있습니다.
React.Lazy
와 Suspense
는 리액트 V16.6부터 추가된 기능입니다. 기존 버전에서는 동적 import를 통해 불러오고 컴포넌트를 state에 넣어서 구현했다고 합니다.
우선 기존 방식부터 알아보겠습니다. SplitMe.js라는 컴포넌트를 하나 만들어주세요.
import React from 'react';
const SplitMe = () => {
return (
<div>SplitMe</div>
);
};
export default SplitMe;
App.js도 수정해주세요. App에서는 handClick
이라는 메소드에서 동적 import로 SplitMe 컴포넌트를 불러옵니다. 그리고 불러온 컴포넌트를 state에 넣어줍니다. 불러온 컴포넌트는 render에서 렌더링 해줍니다.
import logo from './logo.svg';
import './App.css';
import React, {Component} from 'react';
class App extends Component {
state = {
SplitMe: null,
};
handClick = 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.handClick}>code splitting</p>
{SplitMe && <SplitMe/>}
</header>
</div>
);
}
}
export default App;
실행했을때 다음과 같이 코드 스플리팅이 잘 이루어졌음을 확인할 수 있습니다. 이 방식이 V16.6이전의 코드 스플리팅 방법이었습니다. 이 방식은 state를 매번 선언하는 과정이 번거로울 수 있습니다.
자 그럼 V16.6에서 추가된 React.lazy
와 Suspense
를 사용해서 코드 스플리팅
을 구현해보도록 하겠습니다. 이 두 기능의 역할은 기존 방식에서 state를 매번 선언해야하는 번거로움을 없애준 역할입니다.
React.lazy
부터 소개하자면, React.lazy
는 컴포넌트를 렌더링 할 때 비동기적으로 로딩하게 해주는 함수입니다. 다음과 같이 사용합니다.
React.lazy(() => 컴포넌트);
Suspense
는 코드 스플리팅되어 로딩되지 않은 컴포넌트를 로딩하게 만들어주는 컴포넌트입니다. 또 옵션으로 로딩이 끝나지 않았을 때 보여줄 ui를 따로 구성할 수도 있습니다. Suspense
는 다음과 같이 사용합니다.
import React, {Suspense} from 'react';
<Suspense fallback={fallback 코드}>
</Suspense>
Suspense의 props로 fallback
이라는 것이 나왔는데, fallback
은 로딩중 일 때 보여줄 ui의 코드를 넣는 공간입니다. Suspense 컴포넌트 사이에 로딩하고자 하는 컴포넌트를 삽입하면 됩니다.
이제, 두 기능의 사용법을 익혔으니 방금 전에 작성한 코드를 React.lazy
와 Suspense
를 이용한 코드로 변경해보겠습니다.
import logo from './logo.svg';
import './App.css';
import React, {useState, Suspense} from 'react';
const SplitMe = React.lazy(() => import('./SplitMe'));
const 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}>code splitting</p>
<Suspense fallback={<div>로딩중...</div>}>
{visible && <SplitMe/>}
</Suspense>
</header>
</div>
);
}
export default App;
이제 실행해봅시다. 제대로 스플리팅이 되었나요?
로딩중이라는 문구도 확인해봐야겠죠? 개발자도구의 네트워크 탭의 설정에서 속도를 느리게 할 수 있습니다.여기서 느린 3G
로 설정하면 다음과 같이 로딩하면서 로딩중이라는 문구를 확인할 수 있습니다.
마지막으로 소개할 방식은 Loadable Components
라이브러리입니다. 이 라이브러리는 코드 스플리팅을 편하게 도와주는 동시에 서버 사이드 렌더링이 가능하게 해줍니다. 리액트의 공식 문서에서도 서버 사이드 렌더링을 할 경우 이 라이브러리를 사용하도록 권고하고 있습니다.
서버 사이드 렌더링은 추후에 다룰예정이니 지금은 간략하게만 설명하고 넘어가자면, UI를 서버에서 렌더링 하는 것을 의미합니다. 우리가 지금까지 만들었던 리액트 앱들은 클라이언트 사이드 렌더링 앱이었습니다.
우선 지금은 코드 스플리팅 방법만 소개하고, 서버 사이드 렌더링과 자세한 스플리팅은 서버 사이드 렌더링과 함께 다시 소개드리도록 하겠습니다. 먼저 라이브러리를 다운로드 해 주세요.
yarn add @loadable/component
사용법 자체는 React.lazy에서 Suspense가 빠진 형태와 유사합니다. 코드가 줄어서 더 간결해보입니다.
import logo from './logo.svg';
import './App.css';
import React, {useState, Suspense} from 'react';
import loadable from '@loadable/component';
const SplitMe = loadable(() => import('./SplitMe'));
const 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}>code splitting</p>
{visible && <SplitMe/>}
</header>
</div>
);
}
export default App;
Suspense 컴포넌트의 fallback 처럼 로딩중에 보여주고 싶은 UI가 있다면 loadable
을 다음과 같이 사용해줍니다.
//변경 전
const SplitMe = loadable(() => import('./SplitMe'));
//변경 후
const SplitMe = loadable(() => import('./SplitMe'), {
fallback: <div>로딩중...</div>
});
마지막으로 더 좋은 UX를 제공하는 preload
방식도 알아보겠습니다. 여태까진 클릭을 하면 로딩이 시작되었는데, 이 방식은 클릭 하기 전에 컴포넌트에 마우스 커서가 올라가는 순간부터 로딩이 시작됩니다. 말로만 하면 복잡해보이지만, 마우스 오버 이벤트를 등록해주면 됩니다.
import logo from './logo.svg';
import './App.css';
import React, {useState, Suspense} from 'react';
import loadable from '@loadable/component';
const SplitMe = loadable(() => import('./SplitMe'));
const 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}>code splitting</p>
{visible && <SplitMe/>}
</header>
</div>
);
}
export default App;