Loadable Components - boostcampwm-2021/WEB08-AgileStorming GitHub Wiki

์ž‘์„ฑ์ž : ๊น€์œ ์„

๊ณต์‹ ๋ฌธ์„œ

Getting started

Install

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๋Š” ๋ณ„๊ฐœ์˜ ๋ฒˆ๋“ค์—์„œ ๋กœ๋“œ๋œ๋‹ค!

Code Splitting

์ฝ”๋“œ ์Šคํ”Œ๋ฆฟํŒ…์€ ๋ฒˆ๋“ค ์‚ฌ์ด์ฆˆ๋ฅผ ์ค„์ด๋Š” ํšจ์œจ์ ์ธ ๋ฐฉ๋ฒ•์ด๋‹ค. ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์˜ ๋กœ๋”ฉ ์†๋„๋ฅผ ๋†’์ด๊ณ , payload ์‚ฌ์ด์ฆˆ๋ฅผ ์ค„์—ฌ์ค€๋‹ค.

๋ฒˆ๋“ค๋ง์€ ๋ฉ‹์ง„ ๊ธฐ๋Šฅ์ด์ง€๋งŒ, third-party libraries ๋“ฑ์ด ํฌํ•จ๋˜๊ณ , ์•ฑ์ด ์ปค์ง€๋ฉด์„œ ๋ฒˆ๋“ค ๋˜ํ•œ ๋„ˆ๋ฌด ์ปค์งˆ ์ˆ˜ ์žˆ๋‹ค. ํ•„์š”์—†๋Š” ์ฝ”๋“œ๋ฅผ ์ฃผ์˜๊นŠ๊ฒŒ ๊ด€๋ฆฌํ•˜์ง€ ์•Š์œผ๋ฉด ๋กœ๋”ฉ์— ๋„ˆ๋ฌด ์˜ค๋žœ์‹œ๊ฐ„์ด ๊ฑธ๋ฆฌ๊ฒŒ๋˜๊ณ , ์ด๋Ÿฐ ์ƒํ™ฉ์„ ํ”ผํ•˜๊ธฐ ์œ„ํ•œ ์ข‹์€ ๋ฐฉ๋ฒ•์ด ๋ฒˆ๋“ค์„ ๋ถ„ํ• ํ•˜๋Š” ๊ฒƒ์ด๋‹ค. Code-splitting์€ ์›นํŒฉ ๋“ฑ ๋ฒˆ๋“ค๋Ÿฌ์—์„œ ์ง€์›ํ•˜๋Š” ๊ธฐ๋Šฅ์œผ๋กœ ๋Ÿฐํƒ€์ž„ ์‹œ ๋™์ ์œผ๋กœ ๋กœ๋“œ๋  ์ˆ˜ ์žˆ๋Š” ์—ฌ๋Ÿฌ ๋ฒˆ๋“ค์„ ๋งŒ๋“ค์–ด์ค€๋‹ค. ์ฆ‰, Code-splitting์€ 'lazy-load'๊ฐ€ ๊ฐ€๋Šฅํ•˜๊ฒŒ ํ•˜๊ณ , ์•ฑ์˜ ์„ฑ๋Šฅ์„ ํ–ฅ์ƒ์‹œ์ผœ ์ค„ ๊ฒƒ์ด๋‹ค. ์ „์ฒด ์ฝ”๋“œ๋Ÿ‰์€ ์ค„์ง€ ์•Š์•˜์ง€๋งŒ ๋ถˆํ•„์š”ํ•œ ์ฝ”๋“œ์˜ ๋กœ๋”ฉ์„ ํ”ผํ•˜๊ณ , ์ดˆ๊ธฐ ๋กœ๋”ฉ ์‹œ ํ•„์š”ํ•œ ์ฝ”๋“œ์˜ ์–‘์„ ์ค„์—ฌ์ค€๋‹ค.

webpack code splitting

import()

์ฝ”๋“œ ์Šคํ”Œ๋ฆฟํŒ…์„ ๋„์ž…ํ•˜๋Š” ๊ฐ€์žฅ ์ข‹์€ ๋ฐฉ๋ฒ•์€ 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์˜ ํ‘œ์ค€์ด ์•„๋‹ˆ๋‹ค.

Named import()

์›นํŒฉ์€ ๊ธฐ๋ณธ์ ์œผ๋กœ ์ฝ”๋“œ๋กœ ๊ฐ€์ ธ์˜ค๋Š” dynamic chunk ์ˆ˜์— ๋”ฐ๋ผ ์ฆ๊ฐ€ํ•˜๋Š” x ์ˆซ์ž๋กœ x.js์™€ ๊ฐ™์ด ์ด๋ฆ„์„ ์ง€์ •ํ•œ๋‹ค.์ด๋Š” ์ฝ”๋“œ์— ์–ด๋–ค ํŒŒ์ผ์ด ๋กœ๋“œ ๋๋Š”์ง€ ์•Œ๊ธฐ ์–ด๋ ต๊ฒŒํ•œ๋‹ค. ์›นํŒฉ์€ magic comments๋ฅผ ์†Œ๊ฐœํ•ด ์•„๋ž˜์™€ ๊ฐ™์ด ์ด๋ฆ„์„ ์ง€์ •ํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•œ๋‹ค.

import(/* webpackChunkName: "math" */ './math').then(math => {
  console.log(math.add(16, 26))
})

SSR์—์„œ๋Š” ์ฃผ์„๊ณผ ํŒŒ์ผ ๊ฒฝ๋กœ๊ฐ€ ์œ„์™€ ๊ฐ™์€ ์ˆœ์„œ์ธ์ง€ ํ™•์‹คํžˆ ํ•ด์•ผํ•œ๋‹ค.

Code Splitting + React

๋ฆฌ์•กํŠธ์—์„œ๋Š” 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'));

Comparison with React.lazy

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 โœ… โœ… โœ… โœ…

LIbrary splitting

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>
  )
}

Full dynamic import

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>
  )
}

Guides

Fallback without Suspense

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>
  )
}

Error Boundaries

-> 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>
)

Delay

๋„ˆ๋ฌด ๋นจ๋ฆฌ ๋กœ๋”ฉ๋˜์ง€ ์•Š๊ฒŒ ํ•˜๋ ค๋ฉด ์ตœ์†Œ ๋”œ๋ ˆ์ด ์‹œ๊ฐ„์œผ๋กœ ์‹คํ–‰๋˜๊ฒŒ ํ•  ์ˆ˜ ์žˆ๋‹ค. 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)
)

Timeout

๋ฌดํ•œํžˆ ๋กœ๋”ฉ๋˜์ง€ ์•Š๋„๋ก ํƒ€์ž„์•„์›ƒ์„ ์„ค์ •ํ•ด์•ผํ•œ๋‹ค. 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)
)

Prefetching

๋กœ๋”๋ธ”์€ ์›นํŒฉ๊ณผ ์™„์ „ํžˆ ์–‘๋ฆฝํ•  ์ˆ˜ ์žˆ๋‹ค.

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ํ•˜๊ณ  ๋„คํŠธ์›Œํฌ ์ƒํƒœ๋ฅผ ๊ณ ๋ คํ•˜์ง€ ์•Š๊ธฐ ๋•Œ๋ฌธ์— ์กฐ์‹ฌํžˆ ์‚ฌ์šฉํ•ด์•ผํ•œ๋‹ค.

Server Side Rendering

๋งํฌ ์ฐธ๊ณ 

Babel plugin

๋งํฌ ์ฐธ๊ณ 

โš ๏ธ **GitHub.com Fallback** โš ๏ธ