Loadable Components - boostcampwm-2021/WEB08-AgileStorming GitHub Wiki
์์ฑ์ : ๊น์ ์
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ํ๊ณ ๋คํธ์ํฌ ์ํ๋ฅผ ๊ณ ๋ คํ์ง ์๊ธฐ ๋๋ฌธ์ ์กฐ์ฌํ ์ฌ์ฉํด์ผํ๋ค.