애플리케이션이 커질 수록 번들도 커짐
코드 분할(Code-Splitting)
지연 로딩(laze-loading)
하게 도와줌import()
문법Webpack
이 이 구문을 만나면 애플리케이션의 코드를 분할함Babel
을 사용할 때는 Babel이 동적 import를 인식하지만 변환하지 않게 하기 위해서 @babel/plugin-syntax-dynamic-import
를 사용해야 함// before
import { add } from './math';
consol.log(add(16, 26));
// after
import("./math").then(math => {
console.log(math.add(16, 26));
});
import()
호출은 promise를 내부적으로 사용함import()
를 사용할 경우, es6-promise나 promise-polyfill과 같은 polyfill을 사용해서 Promise
를 지원해야 함// index.js
function getComponent() {
return import('lodash')
.then(({default: _ }) => {
const element = document.createElement('div');
element.innerHTML = _.join(['Hello', 'webpack']);
return element;
})
.catch((error) => 'An error occurred when loading the component');
}
getComponent().then((component) => {
document.body.appendChild(comonent);
});
default
가 필요한 이유 : webpack 버전 4부터 CommonJS module을 import할 때 더 이상 module.exports
의 값을 resolve하지 않고, 그 대신 Common JS module에 대한 인공적인 namespace 객체를 만들기 때문임
import()
가 promise를 반환하므로 async 함수를 사용할 수 있음
// index.js
async function getComponent() {
const element = document.createElement('div');
const { default: _ } = await import('lodash');
element.innerHTML = _.join(['Hello', 'webpack']);
return element;
}
getComponent().then((component) => {
document.body.appendChild(comonent);
});
한 페이지의 여러 컴포넌트에서 동시에 비동기 데이터를 읽어오는 경우, UI가 마치 폭포(waterfall)처럼 순차적으로 나타나는 현상이 발생할 수 있음
초기 렌더링 이후에 데이터 로딩 후 리렌더링하는 방법은 경쟁 상태에도 취약함
- 경쟁 상태 : 공유 자원에 대해 여러 개의 프로세스가 동시에 접근을 시도할 때 접근의 타이밍이나 순서 등이 결과값에 영향을 줄 수 있는 상태
if
조건문을 사용하여 어떤 컴포넌트를 보여줄지를 결정하는 것은 명령형(imperative) 코드에 가까움React.Suspense
를 사용함data fetching 요청 -> 로딩중 UI 렌더링 -> data 응답 -> 컴포넌트에 응답 반영
data fetching 요청 -> Suspense 하위의 컴포넌트에 요청 리소스를 반영 -> Suspense에 의해 로딩 UI 렌더링 -> data 응답 -> 컴포넌트에 응답 반영
- 요청 리소스는 Promise가 아니라 일반 객체임
A 프로필 요청 -> 로딩 UI 렌더링 -> A 프로필 응답 -> <Profile />에 응답
A 프로필 요청 -> <Profile />에 A 프로필 요청 리소스 반영 -> Suspense에 의해 A 프로필 요청에 대한 로딩 UI 렌더링 -> 요청 리소스로 A 프로필 응답 들어옴 -> <Profile />에 응답 반영
React.lazy
함수를 사용하면 동적으로 불러오는 컴포넌트를 정의할 수 있음React.lazy
함수는 동적 import()
를 호출하는 함수를 인자로 가짐default
export로 가진 모듈 객체가 이행되는 Promise를 반환해야 함// before
import OtherComponent from './OtherCompoent';
// after
const OtherComponent = React.lazy(() => import('./OtherComponent'));
lazy
한 컴포넌트를 렌더링하려면 렌더링 트리 상위에 <React.Suspense>
컴포넌트가 존재해야 함React.Suspense
를 사용하면 트리 상에 아직 렌더링이 준비되지 않은 컴포넌트가 있을 때 로딩 지시기(Loading indicator)를 나타낼 수 있음import React, { Suspense } from 'react';
const OtherComponent = React.lazy(() => import('./OtherComponent'));
function MyComponent() {
return (
// Displays <Spinner> untill OtherComponent loads
<Suspense fallback={<Spinner />}>
<div>
<OtherComponent />
</div>
</Suspense>
);
}
lazy
한 컴포넌트는 Suspense
트리 내의 깊숙한 곳에 위치할 수 있음Suspense
가 모든 컴포넌트를 감쌀 필요가 없음<Suspense>
를 작성하는 것이 가장 좋음Suspense
트리 내에서 code-splitting을 하고 싶은 어디 곳에서나 lazy()
를 사용해도 됨Suspense
는 element들이 모두 지연로드(laze-loading)된 경우에도 로드에서 element들을 일시 중단할 수 있음import React, { Suspense } from 'react';
const OtherComponent = React.lazy(() => import('./OtherComponent'));
const AnotherComponent = React.lazy(() => import('./AnotherComponent'));
function MyComponent() {
return (
<Suspense fallback={<Spinner />}>
<section>
<OtherComponent />
<AnotherComponent />
</section>
</Suspense>
);
}
주의
React.lazy
와React.Suspense
는 아직ReactDOMServer
에서 지원하지 않고, 아직 서버 사이드 렌더링을 할 수 없음
- 서버에서 렌더링하는 애플리케이션의 코드를 분할 하고 싶다면
Loadable Components
를 사용하길 추천함
Error Boundary
와 Suspense
를 사용함Error Boundary
와 Suspense
로 래핑함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>
);
// fetch API를 모방한 예시
export function fetchProfileData() {
let userPromise = fetchUser(); // 프로미스를 리턴
let postsPromise = fetchPosts();
return {
user: wrapPromise(userPromise),
posts: wrapPromise(postsPromise)
};
}
function wrapPromise(promise) {
let status = "pending"; // 최초의 상태
let result;
// 프로미스 객체 자체
let suspender = promise.then(
(r) => {
status = "success"; // 성공으로 완결시 success로
result = r;
},
(e) => {
status = "error"; // 실패로 완결시 error로
result = e;
}
);
// 위 함수의 로직을 클로저 삼아, 함수 밖에서 프로미스의 진행 상황을 읽는 인터페이스가 됨
return {
read() {
if (status === "pending") {
throw suspender; // 펜딩 프로미스를 throw 하면 Suspense의 Fallback UI를 보여줌
} else if (status === "error") {
throw result; // Error을 throw하는 경우 ErrorBoundary의 Fallback UI를 보여줌
} else if (status === "success") {
return result; // 결과값을 리턴하는 경우 성공 UI를 보여줌
}
}
};
}
import React, { Suspense, lazy } from 'react';
import {BrowserRouter, as Router, Routes, Route } from 'react-router-dom';
const Home = lazy(() => import('./routes/Home'));
const About = lazy(() => import('./routes/About'));
const App = () => {
<Router>
<Suspense fallback={<div>Loading...</div>}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
</Routes>
</Suspense>
</Router>
}
React.lazy
는 현재 default exports만 지원함// ManyComponents.js
export const MyComponent = /* ... */;
export const MyUnusedComponent = /* ... */;
// MyComponent.js
export { MyComponent as default } from './ManyComponents.js';
// MyApp.js
import React, { lazy } from 'react';
const MyComponent = lazy(() => import('./MyComponent.js'));