코드 분할
기본적으로 번들러는 앱 전체를 하나의 JavaScript 파일로 묶고, 파일 맨 위의 모든 import이 그 파일에 더해집니다. 이 단일 번들은 앱이 커질수록 커지고, 브라우저는 렌더링을 시작하기 전에 전체를 다운로드해야 합니다.
빠른 컴퓨터와 강한 Wi-Fi에서는 문제를 느끼지 못할 수도 있습니다. 약한 연결의 휴대폰에서는 무거운 라이브러리 하나가 그 라이브러리를 전혀 필요로 하지 않는 기능들을 포함해 전체 첫 로드를 차단할 수 있습니다. 이 과정에서는 큰 가짜 데이터 패키지에 의존하는 상품 목록으로 이를 보여줍니다. 모든 것이 하나의 번들에 있으면 같은 페이지의 카운터도 제한된 연결에서 20초 이상 빈 상태로 남아 있습니다.
**코드 분할**이 해결책입니다. 번들을 청크라고 부르는 조각으로 나누고, 사용자가 실제로 필요할 때만 청크를 다운로드합니다. 사용자가 절대 건드리지 않는 기능은 다운로드되지 않습니다.
Dynamic import()
기반이 되는 것은 순수 JavaScript입니다. 파일 맨 위의 import은 항상 실행됩니다. 대신 import()을 함수로 호출하면 코드의 어디서든 필요할 때 모듈을 로드하고, 그 모듈로 해결되는 프로미스를 반환합니다.
// 항상 로드되며 메인 번들의 일부입니다
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를 지정합니다.
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이 표시되고, 청크가 도착하면 컴포넌트가 렌더링됩니다. 첫 번째 로드 이후 모듈은 캐시되므로 다시 숨겼다가 보여주는 것은 즉시 이루어집니다.
버전 참고
lazy와 Suspense는 react에서 이름 있는 import입니다. 과정 레슨을 포함한 더 오래된 코드에서는 React.lazy와 React.Suspense로 작성합니다. 같은 API입니다.
라우트부터 분할하기
개별 무거운 컴포넌트를 찾아다닐 수도 있지만, 대부분의 앱에서 기본 분할 지점은 라우트입니다. 각 페이지는 자연스러운 단위입니다. 홈페이지 방문자는 대시보드 코드가 필요 없으므로 모든 페이지를 lazy 컴포넌트로 만들고 라우팅이 어떤 청크를 다운로드할지 결정하게 합니다.
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를 lazy로 감싸고, 컴포넌트를 로딩 메시지 같은 fallback을 가진 Suspense 안에 넣으면, React는 코드를 다운로드하는 동안 fallback을 보여줍니다. 나머지 앱은 바로 나타납니다.
다음: 인증. 세션, 토큰, 서버 쪽 적용이 어떻게 모여서 작동하는지 알아봅니다.

