-
Notifications
You must be signed in to change notification settings - Fork 5
Loadable Components
작성자 : 김유석
npm install @loadable/component
# or use yarn
yarn add @loadable/component
@loadable/babel-plugin
,@loadable/server
와@loadable/webpack-plugin
은 Server Side Rendering 시 필요하다.
로더블은 동적인 import를 regular component로서 render하게 한다.
import loadable from '@loadable/component'
const OtherComponent = loadable(() => import('./OtherComponent'))
function MyComponent() {
return (
<div>
<OtherComponent />
</div>
)
}
OtherComponent
는 별개의 번들에서 로드된다!
코드 스플릿팅은 번들 사이즈를 줄이는 효율적인 방법이다. 애플리케이션의 로딩 속도를 높이고, payload 사이즈를 줄여준다.
번들링은 멋진 기능이지만, third-party libraries 등이 포함되고, 앱이 커지면서 번들 또한 너무 커질 수 있다. 필요없는 코드를 주의깊게 관리하지 않으면 로딩에 너무 오랜시간이 걸리게되고, 이런 상황을 피하기 위한 좋은 방법이 번들을 분할하는 것이다. Code-splitting은 웹팩 등 번들러에서 지원하는 기능으로 런타임 시 동적으로 로드될 수 있는 여러 번들을 만들어준다. 즉, Code-splitting은 'lazy-load'가 가능하게 하고, 앱의 성능을 향상시켜 줄 것이다. 전체 코드량은 줄지 않았지만 불필요한 코드의 로딩을 피하고, 초기 로딩 시 필요한 코드의 양을 줄여준다.
코드 스플릿팅을 도입하는 가장 좋은 방법은 dynamic import()문이다.
export default로 리액트 컴포넌트를 포함한 모듈을 resolve한 Promise를 반환한다.
// before
import { add } from './math'
console.log(add(16, 26))
// after
import('./math').then(math => {
console.log(math.add(16, 26))
})
현재 dynamic import syntax는 ECMAScript의 표준이 아니다.
웹팩은 기본적으로 코드로 가져오는 dynamic chunk 수에 따라 증가하는 x 숫자로 x.js
와 같이 이름을 지정한다.이는 코드에 어떤 파일이 로드 됐는지 알기 어렵게한다. 웹팩은 magic comments를 소개해 아래와 같이 이름을 지정할 수 있게 한다.
import(/* webpackChunkName: "math" */ './math').then(math => {
console.log(math.add(16, 26))
})
SSR에서는 주석과 파일 경로가 위와 같은 순서인지 확실히 해야한다.
리액트에서는 React.lazy를 통해 코드 스플릿팅을 지원하는데, 몇몇 제한이 있다. 이 때문에 @loadable/component가 존재한다.
리액트 앱에서는 대부분 컴포넌트들을 분할하고 싶을 것이다. 이는 컴포넌트가 로드되는 것을 기다리고 에러 핸들링이 가능함을 의미한다.
import loadable from '@loadable/component'
const OtherComponent = loadable(() => import('./OtherComponent'))
function MyComponent() {
return (
<div>
<OtherComponent />
</div>
)
}
// React.lazy
const OtherComponent = React.lazy(() => import('./OtherComponent'));
React.lazy
와 @loadable/components
의 차이점?
React.lazy은 Suspense를 이용하고 리액트에서 지원받는 코드 스플릿팅 solution이다.
React.lazy를 이미 사용하고 있고, 잘 사용한다면 @loadable/component는 필요하지 않지만 제한사항을 느낀다거나, SSR이 필요한 경우 @loadable/component가 좋은 solution이 될 것이다.
Library | Suspense | SSR | Library splitting |
import( ./${value})
|
---|---|---|---|---|
React.lazy |
✅ | ❌ | ❌ | ❌ |
@loadable/component |
✅ | ✅ | ✅ | ✅ |
loadable.lib
는 라이브러리의 로딩을 연기한다.
import loadable from '@loadable/component'
const Moment = loadable.lib(() => import('moment'))
function FromNow({ date }) {
return (
<div>
<Moment fallback={date.toLocaleDateString()}>
{({ default: moment }) => moment(date).fromNow()}
</Moment>
</div>
)
}
dynamic value를 받아 dynamic하게 import 하는 함수로 구성된다. (React.lazy에서는 지원 x)
// All files that could match this pattern will be automatically code splitted.
const loadFile = file => import(`./${file}`)
// In React, it permits to create reusable components:
import loadable from '@loadable/component'
const AsyncPage = loadable(props => import(`./${props.page}`))
function MyComponent() {
return (
<div>
<AsyncPage page="Home" />
<AsyncPage page="Contact" />
</div>
)
}
Babel 플러그인을 사용하는 경우, 동적 속성이 즉시 지원된다. 그렇지 않으면 cacheKey 함수를 추가해야 한다.
import loadable from '@loadable/component'
const AsyncPage = loadable(props => import(`./${props.page}`), {
cacheKey: props => props.page,
})
function MyComponent() {
const [page, setPage] = useState('Home')
return (
<div>
<button onClick={() => setPage('Home')}>Go to home</button>
<button onClick={() => setPage('Contact')}>Go to contact</button>
{page && <AsyncPage page={page} />}
</div>
)
}
fallback을 loadable 옵션에 추가할 수 있다.
또, props에 지정해도 된다.
// loadable 옵션에 추가
const OtherComponent = loadable(() => import('./OtherComponent'), {
fallback: <div>Loading...</div>,
})
function MyComponent() {
return (
<div>
<OtherComponent />
</div>
)
}
// props 지정
const OtherComponent = loadable(() => import('./OtherComponent'))
function MyComponent() {
return (
<div>
<OtherComponent fallback={<div>Loading...</div>} />
</div>
)
}
-> React hooks에서는 사용되지 않는다!
네트워크 문제 등으로 모듈을 로드하는 데 실패했다면 error를 발생시킨다. Error Boundaries로 에러를 UX적으로 더 멋지게 보여주거나, recovery를 관리할 수 있다. Error Boundaries는 lazy components 위 어디서나 사용할 수 있다.
import MyErrorBoundary from './MyErrorBoundary'
const OtherComponent = loadable(() => import('./OtherComponent'))
const AnotherComponent = loadable(() => import('./AnotherComponent'))
const MyComponent = () => (
<div>
<MyErrorBoundary>
<section>
<OtherComponent />
<AnotherComponent />
</section>
</MyErrorBoundary>
</div>
)
너무 빨리 로딩되지 않게 하려면 최소 딜레이 시간으로 실행되게 할 수 있다. built-in API는 아니지만 p-min-delay를 사용하면 된다.
import loadable from '@loadable/component'
import pMinDelay from 'p-min-delay'
// Wait a minimum of 200ms before loading home.
export const OtherComponent = loadable(() =>
pMinDelay(import('./OtherComponent'), 200)
)
무한히 로딩되지 않도록 타임아웃을 설정해야한다. third party 모듈인 promise-timeout을 사용할 수 있다.
import loadable from '@loadable/component'
import { timeout } from 'promise-timeout'
// Wait a maximum of 2s before sending an error.
export const OtherComponent = loadable(() =>
timeout(import('./OtherComponent'), 2000)
)
로더블은 웹팩과 완전히 양립할 수 있다.
webpackPrefetch와 webpackPreload를 사용할 수 있다.
prefetch(browser가 idle 상태일 때 로드 되는 것)을 원한다면 /* webpackPrefetch: true */
를 import 문 안에 넣으면 된다.
import loadable from '@loadable/component'
const OtherComponent = loadable(() =>
import(/* webpackPrefetch: true */ './OtherComponent'),
)
서버사이드에서 를 헤드에 더하므로써 prefetch된 리소스를 추출할 수 있다.
컴포넌트가 처음에 렌더되는 것같이 preload를 강제할 수 있다.
import loadable from '@loadable/component'
const Infos = loadable(() => import('./Infos'))
function App() {
const [show, setShow] = useState(false)
return (
<div>
<a onMouseOver={() => Infos.preload()} onClick={() => setShow(true)}>
Show Infos
</a>
{show && <Infos />}
</div>
)
}
preload는 서버사이드렌더링에선 불가능하다.
preload는 aggressive하고 네트워크 상태를 고려하지 않기 때문에 조심히 사용해야한다.