Skip to content
This page has been auto-translated and may contain errors.View in English

코드 분할

기본적으로 번들러는 앱 전체를 하나의 JavaScript 파일로 묶고, 파일 맨 위의 모든 import이 그 파일에 더해집니다. 이 단일 번들은 앱이 커질수록 커지고, 브라우저는 렌더링을 시작하기 전에 전체를 다운로드해야 합니다.

빠른 컴퓨터와 강한 Wi-Fi에서는 문제를 느끼지 못할 수도 있습니다. 약한 연결의 휴대폰에서는 무거운 라이브러리 하나가 그 라이브러리를 전혀 필요로 하지 않는 기능들을 포함해 전체 첫 로드를 차단할 수 있습니다. 이 과정에서는 큰 가짜 데이터 패키지에 의존하는 상품 목록으로 이를 보여줍니다. 모든 것이 하나의 번들에 있으면 같은 페이지의 카운터도 제한된 연결에서 20초 이상 빈 상태로 남아 있습니다.

**코드 분할**이 해결책입니다. 번들을 청크라고 부르는 조각으로 나누고, 사용자가 실제로 필요할 때만 청크를 다운로드합니다. 사용자가 절대 건드리지 않는 기능은 다운로드되지 않습니다.

Dynamic import()

기반이 되는 것은 순수 JavaScript입니다. 파일 맨 위의 import은 항상 실행됩니다. 대신 import()을 함수로 호출하면 코드의 어디서든 필요할 때 모듈을 로드하고, 그 모듈로 해결되는 프로미스를 반환합니다.

jsx
// 항상 로드되며 메인 번들의 일부입니다
import { formatPrice } from './format'

// 이 핸들러가 실행될 때만 로드됩니다
async function showChart() {
  const { renderChart } = await import('./charts')
  renderChart()
}

번들러는 모든 import() 호출을 잘라내는 지점으로 취급합니다. 가져온 모듈과 메인 번들과 공유하지 않는 의존성은 자신만의 청크를 가져가고, 호출이 처음 실행될 때 네트워크를 통해 가져옵니다.

lazy와 Suspense

React는 이 기본 요소를 한 쌍의 도구로 래핑합니다. lazy는 동적 import()를 반환하는 함수를 받아서 다른 컴포넌트처럼 렌더링할 수 있는 컴포넌트를 돌려줍니다. 로더의 프로미스는 기본 내보내기가 컴포넌트인 모듈로 해결되어야 합니다. 그 옆의 다른 이름 있는 내보내기는 괜찮습니다. 제어하지 않는 파일에서 이름 있는 컴포넌트만 내보낸다면 프로미스를 적응시키세요. lazy(() => import('./charts').then(m => ({ default: m.RevenueChart }))) 같은 방식으로요.

lazy 컴포넌트를 렌더링하면 일시 중지됩니다. React는 청크가 도착할 때까지 트리의 그 부분 렌더링을 멈춥니다. 그 자체로는 오류이므로 lazy 컴포넌트를 Suspense 경계로 감싸고 fallback prop으로 다운로드 중에 보여줄 UI를 지정합니다.

jsx
import { lazy, Suspense, useState } from 'react'

const ProductsList = lazy(() => import('./ProductsList'))

export default function App() {
  const [showProducts, setShowProducts] = useState(false)

  return (
    <>
      <Counter />
      <button onClick={() => setShowProducts(prev => !prev)}>
        상품 보기
      </button>
      {showProducts && (
        <Suspense fallback={<h2>로딩 중...</h2>}>
          <ProductsList />
        </Suspense>
      )}
    </>
  )
}

이제 메인 번들에는 ProductsList이나 그 무거운 의존성이 포함되지 않습니다. 카운터는 즉시 나타납니다. 버튼을 클릭하면 청크 다운로드가 시작되고, 로드되는 동안 fallback이 표시되고, 청크가 도착하면 컴포넌트가 렌더링됩니다. 첫 번째 로드 이후 모듈은 캐시되므로 다시 숨겼다가 보여주는 것은 즉시 이루어집니다.

버전 참고

lazySuspensereact에서 이름 있는 import입니다. 과정 레슨을 포함한 더 오래된 코드에서는 React.lazyReact.Suspense로 작성합니다. 같은 API입니다.

라우트부터 분할하기

개별 무거운 컴포넌트를 찾아다닐 수도 있지만, 대부분의 앱에서 기본 분할 지점은 라우트입니다. 각 페이지는 자연스러운 단위입니다. 홈페이지 방문자는 대시보드 코드가 필요 없으므로 모든 페이지를 lazy 컴포넌트로 만들고 라우팅이 어떤 청크를 다운로드할지 결정하게 합니다.

jsx
import { lazy, Suspense } from 'react'
import { BrowserRouter, Routes, Route } from 'react-router-dom'

const Home = lazy(() => import('./pages/Home'))
const Vans = lazy(() => import('./pages/Vans'))
const Dashboard = lazy(() => import('./pages/Dashboard'))

export default function App() {
  return (
    <BrowserRouter>
      <Suspense fallback={<h2>로딩 중...</h2>}>
        <Routes>
          <Route path="/" element={<Home />} />
          <Route path="/vans" element={<Vans />} />
          <Route path="/host" element={<Dashboard />} />
        </Routes>
      </Suspense>
    </BrowserRouter>
  )
}

라우트 주위에 하나의 Suspense 경계가 모든 페이지를 포괄합니다. 첫 로드는 메인 번들과 현재 URL에 해당하는 청크만 가져옵니다. 다른 페이지는 사용자가 네비게이션할 때 다운로드됩니다. 이 폭넓은 범위는 비용이 있습니다. 아직 도착하지 않은 청크의 페이지로 네비게이션하면 전체 라우트 영역이 fallback으로 바뀌므로 헤더와 네비게이션 같은 공유 레이아웃은 경계 밖에 두어 스왑할 때도 그대로 유지되도록 합니다.

분할에는 한계가 있습니다. 모든 청크는 추가 네트워크 요청과 fallback UI의 순간을 비용으로 하므로, 작은 컴포넌트를 분할하면 그 비용만큼 얻는 것이 없습니다. 작은 컴포넌트와 사용자가 즉시 보는 폴드 위의 모든 것은 메인 번들에 놔두세요. 무겁고 드물게 사용되거나 네비게이션 뒤에 있는 것을 분할합니다.

분할은 빌드 시에 일어납니다. 번들러는 정적으로 모든 import() 호출을 찾고, 그 모듈 그래프에 대한 별도 청크를 내보내며, 호출을 그 청크의 URL에 대한 fetch로 다시 씁니다.

lazy 자체는 얇습니다. 로더 함수의 프로미스를 보유하고 첫 렌더링에서 그것을 throw하는데, 이것이 일시 중지 신호입니다. React가 이를 포착하고 가장 가까운 Suspense 경계까지 올라가서 fallback을 커밋하고, 프로미스가 해결되면 렌더링을 다시 시도합니다. 로더는 한 번 실행되고, 그 후 해결된 컴포넌트는 lazy 래퍼에 캐시되어 동기적으로 렌더링되며, 브라우저 캐시는 재로드 간에도 재방문을 저렴하게 만듭니다.

경계 배치는 사용자 경험 결정입니다. 루트 근처의 단일 경계는 일시 중지된 모든 후손이 큰 영역을 비우고 콘텐츠가 도착할 때 레이아웃 이동을 초래할 수 있습니다. lazy 부분 주위에 경계를 엄격히 스코프하면 페이지의 나머지는 상호작용 가능하게 유지되는데, 이것이 이 과정이 선택한 방식입니다.

라우트 테이블은 두 규칙을 조화시키는 예외입니다. 그곳의 넓은 경계는 어차피 네비게이션이 그 전체 영역을 대체하기 때문에 그만한 가치가 있고, fallback은 바뀌려고 했던 페이지 대신 서 있습니다. 페이지 내에서는 엄격히 스코프하세요.

청크 로드 실패 시

도착하지 않는 청크를 대비하세요. 실패한 fetch는 로더의 프로미스를 거부하고, Suspense는 대기 상태만 포괄합니다. React는 렌더링 중에 거부를 다시 throw하고, 아무것도 포착하지 않는 렌더 오류는 전체 트리를 언마운트합니다.

경계를 error boundary와 짝을 지으세요. error boundary는 아래 렌더 오류를 포착하고 그 자리에 fallback UI를 보여주는 컴포넌트입니다. React의 기본 제공 버전은 이 핸드북에서 여전히 클래스 컴포넌트여야 하는 유일한 것입니다. 훅 동등물이 없기 때문에, 대부분의 프로젝트는 react-error-boundary를 설치하고 ErrorBoundary로 라우트를 래핑합니다.

일반적인 원인은 구 배포인데, 열려 있는 탭이 새 빌드가 더 이상 제공하지 않는 해시된 청크를 요청할 때이고, 사용자가 필요한 것은 재시도보다는 페이지 재로드입니다.

워터폴도 주의하세요. 청크가 도착한 후에만 데이터 가져오기를 시작하는 lazy 라우트는 두 개의 왕복을 직렬화합니다. React Router의 data API에서는 라우트의 로더가 렌더링 전에 실행되거나, 의도 시 미리 로드하면 (렌더링이 필요하기 전에 링크 호버나 포커스에서 import()를 시작) 그것을 제거합니다. 같은 import('./ProductsList')을 일찍 호출하는 것은 안전합니다. 모듈 레지스트리는 그것을 중복 제거하고, 나중의 lazy 렌더링은 이미 진행 중이거나 완료된 청크를 찾습니다.

Juno필요할 때만 코드를 다운로드하기 보통 브라우저는 뭔가를 보여주기 전에 앱의 모든 JavaScript를 다운로드해야 하고, 하나의 큰 기능이 나머지를 모두 인질로 잡을 수 있습니다. 코드 분할은 "이 부분은 나중에 로드하고, 사용자가 요청할 때만 로드해"라고 할 수 있게 해줍니다.

동적 import를 lazy로 감싸고, 컴포넌트를 로딩 메시지 같은 fallback을 가진 Suspense 안에 넣으면, React는 코드를 다운로드하는 동안 fallback을 보여줍니다. 나머지 앱은 바로 나타납니다.

Juno필요할 때만 코드를 다운로드하기const Page = lazy(() => import('./Page'))는 자신의 청크에 사는 컴포넌트를 줍니다. 첫 렌더링에서 가져와집니다. 파일은 컴포넌트를 기본 내보내야 하고, fallback을 가진 Suspense 경계가 다운로드를 포괄합니다.

라우트를 기본 분할 지점으로 만드세요. 각 페이지가 lazy 컴포넌트이고, 라우트 테이블 주위에 하나의 경계를 두세요. 작은 컴포넌트와 폴드 위의 UI를 분할하지 마세요. 각 청크는 요청의 비용이 들기 때문입니다.

Juno필요할 때만 코드를 다운로드하기 번들러는 빌드 시 모든 import() 호출 지점에서 청크를 자르고, lazy는 첫 렌더링에서 로더 프로미스를 throw하며, 가장 가까운 Suspense 경계는 그 fallback을 표시하고, 해결된 모듈은 캐시되어 나중의 렌더링은 동기적입니다.

얼마나 많은 UI가 일시 중지되는지 제어하려면 경계를 스코프하세요. 의도 시 미리 로드하거나 렌더링 전에 라우트의 데이터를 가져오게 해서 청크-그-다음-데이터 워터폴을 피하세요. 중복된 import() 호출은 중복 제거되므로 청크를 일찍 데우는 것은 무료입니다.

다음: 인증. 세션, 토큰, 서버 쪽 적용이 어떻게 모여서 작동하는지 알아봅니다.