코드 분할(Code Spliting)이 필요한 이유
렌더링 최적화 도중 lighthouse 성능 측정 점수가 초반보다 현저히 떨어진 것을 알 수 있었다.
lighthouse에서 권장하는 방법 중 하나가 사용하지 않는 자바스크립트 줄이기였는데, 서버 측에서 렌더링하지 않는 경우 'React.lazy()'로 자바스크립트 번들을 분할하는 방법을 사용하라는 것이었다.
대부분의 React 앱들은 Webpack, Rollup 또는 Browserify 같은 툴을 사용하여 여러 파일을 하나로 병합한 번들 파일을 웹 페이지에 포함해 한 번에 전체 앱을 로드한다. 아래는 여러 파일이 하나의 파일로 번들링되는 예시이다.
App
// app.js
import { add } from './math.js';
console.log(add(16, 26)); // 42
// app.js
import { add } from './math.js';
console.log(add(16, 26)); // 42
Bundle
function add(a, b) {
return a + b;
}
console.log(add(16, 26)); // 42
실제 번들은 알아볼 수 없는 코드로 보인다.
하지만, 앱이 커지면 번들 파일도 커지게 된다. 특히 큰 규모의 서드 파티 라이브러리를 추가할 때 실수로 앱이 커저서 로드 시간이 길어지는 것을 방지하기 위해 주의해야 한다.
번들이 거대해지는 것을 방지하기 위한 좋은 해결방법은 번들을 나누는 것이다. 코드 분할은 런타임에 여러 번들을 동적으로 만들고 불러오는 것으로 Webpack, Rollup과 Browserify(factor-bundle) 같은 번들러가 지원하는 기능이다.
코드 분할은 앱을 "지연 로딩"하게 도와주고 앱의 획기적인 성능 향상을 돕는다. 앱의 코드 양을 줄이지 않고도 사용자가 필요하지 않은 코드를 불러오지 않게 하며 앱의 초기화 로딩에 필요한 비용을 줄여준다.
import
Webpack이 import 구문을 만나게 되면 앱의 코드를 분할한다. Create React App을 사용하고 있다면 이미 Webpack이 구성되어 있기 때문에 즉시 사용이 가능하다. Next.js 역시 지원한다.
React.lazy
React.lazy 함수를 사용하면 동적 import를 사용해 컴포넌트 렌더링이 가능하다.
Before
import OtherComponent from './OtherComponent';
After
const OtherComponent = React.lazy(() => import('./OtherComponent'));
MyComponent가 처음 렌더링될 때 OtherComponent를 포함한 번들을 자동으로 불러온다.
React.lazy는 동적 import()를 호출하는 함수를 인자로 가진다.
이 함수는 React 컴포넌트를 default export로 가진 모듈 객체가 이행되는 Promise를 반환한다.
lazy 컴포넌트는 반드시 Suspense 컴포넌트 하위에서 렌더링되어야 하며 Suspense는 lazy 컴포넌트가 로드되길 기다리는 동안 로딩 화면과 같은 예비 컨텐츠를 보여줄 수 있게 해준다.
import React, { Suspense } from 'react';
const OtherComponent = React.lazy(() => import('./OtherComponent'));
function MyComponent() {
return (
<div>
<Suspense fallback={<div>Loading...</div>}>
<OtherComponent />
</Suspense>
</div>
);
}
fallback prop은 컴포넌트가 로드될 때까지 기다리는 동안 렌더링하려는 React Element를 받아들인다. Suspense 컴포넌트는 lazy 컴포넌트를 감싸며, 하나의 Suspense 컴포넌트로 여러 lazy 컴포넌트를 감쌀 수도 있다.
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>
);
}
Error boundaries
네트워크 장애 같은 이유로 다른 모듈을 로드에 실패할 경우 에러를 발생시킬 수 있다. 이때 Error Boundaries를 이용하여 사용자의 경험과 복구 관리를 처리할 수 있다. Error Boundary를 만들고 lazy 컴포넌트를 감싸면 네트워크 장애가 발생했을 때 에러를 표시할 수 있다.
import React, { Suspense } from 'react';
import MyErrorBoundary from './MyErrorBoundary';
const OtherComponent = React.lazy(() => import('./OtherComponent'));
const AnotherComponent = React.lazy(() => import('./AnotherComponent'));
const MyComponent = () => (
<div>
<MyErrorBoundary>
<Suspense fallback={<div>Loading...</div>}>
<section>
<OtherComponent />
<AnotherComponent />
</section>
</Suspense>
</MyErrorBoundary>
</div>
);
React.lazy 적용
Content 파트는 모드 별로 각 화면을 그룹, 리스트, 다이어그램 형태로 다르게 보여주는 파트인데, 현재 코드 양이 가장 많이 몰려있는 곳이라 이 부분에 코드 분할을 적용해보기로 했다.
const Diagram = lazy(() => import('../view-diagram'));
const List = lazy(() => import('../view-list'));
const Tree = lazy(() => import('../view-tree'));
<Suspense fallback={<Loading />}>
{
(() => {
switch (viewMode) {
case 'list':
return (<List ... />)
case 'tree':
return (<Tree ... />)
case 'diagram':
return (<Diagram ... />)
default:
return (<div></div>)
}
})()
}
</Suspense>
위와 같이 코드를 변경했더니, 성능 점수가 크게 향상되었다.
댓글