Skip to content

Loadable Components

lapa edited this page Nov 15, 2021 · 1 revision

작성자 : 김유석

공식 문서

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

링크 참고

Clone this wiki locally