๐ก 25. ๋ฒ๋ค ์ต์ ํ & ์ฝ๋ ์คํ๋ฆฌํ : ์ค์ ์ฑ๋ฅ ๊ฐ์
๐ ๊ฐ์
์ฌ์ฉ์๊ฐ ์ต์ด ๋ฐฉ๋ฌธ ์ ๋ด๋ ค๋ฐ๋ JavaScript ๋ฒ๋ค์ ์ต์ํํ๋ ์ฝ๋ ์คํ๋ฆฌํ ์ ๋ต, React.lazy์ ๋์ import, ๊ทธ๋ฆฌ๊ณ Bundle Analyzer๋ก ๋ฒ๋ค ํฌ๊ธฐ ์๊ฐํ๊น์ง. ์ค๋ฌด ์ฑ๋ฅ ๊ฐ์ ์ ์ ๊ณผ์ ์ ๋ค๋ฃน๋๋ค.
๐ ๋ชฉ์ฐจ
- ์ด ๋ฌธ์๋ฅผ ์ฝ๊ธฐ ์ ์
- ์ ์์์ผ ํ๋๊ฐ: ๋ฒ๋ค์ด ๋ฌด๊ฑฐ์ฐ๋ฉด ์ฌ์ฉ์๊ฐ ๋ ๋๋ค
- ๋น์ ๋ก ๋จผ์ ์ดํดํ๊ธฐ
- ๋ฒ๋ค์ด๋ ๋ฌด์์ธ๊ฐ
- ์ฝ๋ ์คํ๋ฆฌํ โ ํ์ํ ๋๋ง ๋ด๋ ค๋ฐ๊ธฐ
- React.lazy + Suspense๋ก ์ปดํฌ๋ํธ ์คํ๋ฆฌํ
- ๋ผ์ฐํธ ๊ธฐ๋ฐ vs ์ปดํฌ๋ํธ ๊ธฐ๋ฐ ์คํ๋ฆฌํ
- Bundle Analyzer๋ก ๋ฒ๋ค ํด๋ถํ๊ธฐ
- Tree Shaking โ ์ ์ฐ๋ ์ฝ๋ ์๋ ์ ๊ฑฐ
- ์ด๋ฏธ์ง ์ต์ ํ์ Web Vitals
- ์๋ฌ ํด๊ฒฐ ์นดํ๋ก๊ทธ
- ์ด๋ฒ์ ๋ฐฐ์ด ๋ด์ฉ ์ด์ ๋ฆฌ
- ๋ง๋ฌด๋ฆฌ ํด์ฆ
- ๋ ์์๋ณด๊ธฐ
๐ ์ด ๋ฌธ์๋ฅผ ์ฝ๊ธฐ ์ ์
โฑ๏ธ ์์ ์ฝ๊ธฐ ์๊ฐ: 15๋ถ (์ ์ฒด) / ํต์ฌ ํํธ๋ง: 8๋ถ
๐บ๏ธ ์ด ๋ฌธ์์ ํ๋ฆ
[๋ฒ๋ค์ด ๋ญ๊ฐ] โ [์ฝ๋ ์คํ๋ฆฌํ
์๋ฆฌ] โ [React.lazy ์ค์ต] โ [๋ฒ๋ค ๋ถ์] โ [Tree Shaking] โ [Web Vitals]
๐ฏ ์ด ๋ฌธ์๋ฅผ ๋ค ์ฝ์ผ๋ฉด ํ ์ ์๋ ๊ฒ
-
React.lazy+Suspense๋ก ๋ผ์ฐํธ/์ปดํฌ๋ํธ ๋จ์ ์ฝ๋ ์คํ๋ฆฌํ ์ ์ ์ฉํ ์ ์๋ค - Bundle Analyzer ๋ก ์ด๋ค ๋ผ์ด๋ธ๋ฌ๋ฆฌ๊ฐ ๋ฒ๋ค์ ๋ฌด๊ฒ๊ฒ ๋ง๋๋์ง ์ฐพ์ ์ ์๋ค
- Tree Shaking์ด ์ ๋์ํ์ง ์๋์ง ์์ธ์ ์ง๋จํ ์ ์๋ค
๐บ๏ธ ์ด ๋ฌธ์์ ๋ฐฐ๊ฒฝ ์ธ๊ณ๊ด: '์์๋ค ์ปค๋ฎค๋ํฐ'
- ์์(PM): "์์ฒ ๋, Lighthouse ์ ์๊ฐ 42์ ์ด์์. ์ฒซ ํ๋ฉด ๋ก๋ฉ์ด 8์ด๋ ๊ฑธ๋ฆฐ๋ค๊ณ ์ฌ์ฉ์ ๋ฆฌ๋ทฐ๊ฐ ํญ๋ฐํ๊ณ ์์ด์. ๊ฒฝ์์ฌ๋ 2์ด์ธ๋ฐ!"
- ์์ฒ (์ ์ ): "๋ค? ๊ธฐ๋ฅ์ ๋ค ์ ๋์๊ฐ๋๋ฐ ์ ๋๋ฆฌ์ฃ ? ์ฝ๋๊ฐ ์ด๋์ ๋งํ๋ ๊ฑด์ง..."
- ์ํธ(๋ฆฌ๋): "๊ธฐ๋ฅ์ด ์ ๋์๊ฐ๋ ๊ฒ๊ณผ ๋น ๋ฅด๊ฒ ๋ก๋ฉ๋๋ ๊ฑด ๋ฌ๋ผ์. ๋ฒ๋ค์ด 4MB๋ผ๋ ๊ฒ ๋ฌธ์ ์์. ์ฌ์ฉ์๊ฐ ์ปค๋ฎค๋ํฐ ํ์ ๋ณด๋ ค๊ณ ์๋ํฐ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ํฌํจํ ์ฑ ์ ์ฒด๋ฅผ ๋ด๋ ค๋ฐ๊ณ ์์ด์. ์ฝ๋ ์คํ๋ฆฌํ ๋ถํฐ ์์ํฉ์๋ค."
๐ค ์ ์์์ผ ํ๋๊ฐ: ๋ฒ๋ค์ด ๋ฌด๊ฑฐ์ฐ๋ฉด ์ฌ์ฉ์๊ฐ ๋ ๋๋ค
๐ฏ ์ด ์น์ ์ ์ฝ๊ณ ๋๋ฉด:
- ๋ฒ๋ค ํฌ๊ธฐ๊ฐ ์ค์ ์ฌ์ฉ์ ์ดํ๊ณผ ์ด๋ป๊ฒ ์ฐ๊ฒฐ๋๋์ง ์ดํดํ๋ค
- LCP, FID, CLS ๋ฑ Core Web Vitals ์งํ์ ์ฑ๋ฅ์ ๊ด๊ณ๋ฅผ ์ดํดํ๋ค
Google ์ฐ๊ตฌ ๊ฒฐ๊ณผ (๋ฌด์์ด ์ฌ์ค๋ค):
| ๋ก๋ฉ ์๊ฐ | ์ดํ๋ฅ ์ฆ๊ฐ |
|---|---|
| 1์ด โ 3์ด | 32% ์ฆ๊ฐ |
| 1์ด โ 5์ด | 90% ์ฆ๊ฐ |
| 1์ด โ 10์ด | 123% ์ฆ๊ฐ |
ํ์ค: ์์๋ค ์ปค๋ฎค๋ํฐ ์ด๊ธฐ ๋ฒ๋ค
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ main.bundle.js [4.2 MB] โ
โ โ
โ โโโ React + ReactDOM 180KB โ
โ โโโ moment.js 400KB โ ๐ฑ โ
โ โโโ lodash (์ ์ฒด) 500KB โ ๐ฑ โ
โ โโโ ๋งํฌ๋ค์ด ์๋ํฐ 1.2MB โ ๐ โ
โ โโโ ์ฐจํธ ๋ผ์ด๋ธ๋ฌ๋ฆฌ 800KB โ ๐ฑ โ
โ โโโ ์ฐ๋ฆฌ ์ฑ ์ฝ๋ 120KB โ
โ โ
โ โ ํ ํ๋ฉด์ ๋ณด๋ ค๊ณ ๋งํฌ๋ค์ด โ
โ ์๋ํฐ์ ์ฐจํธ๋ฅผ ๋ชจ๋ ๋ด๋ ค๋ฐ์! โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
์ด ์ํฉ์ ํด๊ฒฐํ๋ ํต์ฌ ๋๊ตฌ๊ฐ ์ฝ๋ ์คํ๋ฆฌํ (Code Splitting) ์ด์์.
๐ก ํ ์ค๋ก ๊ธฐ์ตํ๊ธฐ
"์ฌ์ฉ์๋ ์ง๊ธ ๋น์ฅ ํ์ํ ์ฝ๋๋ง ๋ด๋ ค๋ฐ์์ผ ํด. ๊ฒ์๊ธ ๋ชฉ๋ก ํ๋ฉด์ ๋ณด๋ ค๊ณ ๋งํฌ๋ค์ด ์๋ํฐ ์ฝ๋๋ฅผ ๋ด๋ ค๋ฐ์ ์ด์ ๊ฐ ์์ด."
๐๏ธ ๋น์ ๋ก ๋จผ์ ์ดํดํ๊ธฐ
๐ง 5์ด์๊ฒ ์ค๋ช ํ๋ค๋ฉด?
๋์๊ด์ 1๋ง ๊ถ์ ์ฑ ์ด ์์ด. ์์ฒ ์ด ๋ฐฉ์์ ๋์๊ด์ ์ ์ฅํ๋ ๋ชจ๋ ์ฌ๋์๊ฒ 1๋ง ๊ถ์ ํต์งธ๋ก ์ง์ด์ง๊ฒ ํ๋ ๊ฑฐ์ผ. ์ฑ ํ ๊ถ ๋ณด๋ฌ ์๋๋ฐ 1๋ง ๊ถ์ ๋ค๊ณ ์ ์ฅํด์ผ ํด. ๋น์ฐํ ๋๋ฆฌ์ง.
์ฝ๋ ์คํ๋ฆฌํ ์ ๋์๊ด ์ง์์ด "์ง๊ธ ์ด๋ค ์น์ ์ ๊ฐ์๋์?"๋ฅผ ๋ฌผ์ด๋ณด๊ณ ๊ทธ ์น์ ์ฑ ๋ค๋ง ๊ฐ์ ธ๋ค์ฃผ๋ ๋ฐฉ์์ด์ผ. ์์ค ์น์ ์ ๊ฐ์ ๋ ๊ณผํ ์น์ ์ฑ ์ ์ง์ด์ง ํ์๊ฐ ์์ง. ํ์ํ ์๊ฐ์ ํ์ํ ๊ฒ๋ง ๊ฐ์ ธ์.
๐ฆ ๋ฒ๋ค์ด๋ ๋ฌด์์ธ๊ฐ ๐ข
๐ฏ ์ด ์น์ ์ ์ฝ๊ณ ๋๋ฉด:
- ๋ฒ๋ค์ด ์ด๋ป๊ฒ ๋ง๋ค์ด์ง๊ณ ๋ธ๋ผ์ฐ์ ์ ์ ๋ฌ๋๋์ง ์ดํดํ๋ค
- ๋ฒ๋ค์ด ํด์๋ก ์ ๋ก๋ฉ์ด ๋๋ฆฐ์ง ์ค๋ช ํ ์ ์๋ค
๐ ์ฉ์ด: ๋ฒ๋ค(Bundle) โ Webpack, Vite ๊ฐ์ ๋น๋ ๋๊ตฌ๊ฐ ์๋ฐฑ ๊ฐ์
.jsํ์ผ์ ํ๋(๋๋ ๋ช ๊ฐ)์ ํ์ผ๋ก ๋ฌถ์ ๊ฒ. ๋ธ๋ผ์ฐ์ ๋ ์ด ํ์ผ์ ๋ค์ด๋ก๋ํ๊ณ ํ์ฑํ ํ ์คํํด์.
๊ฐ๋ฐ ์ฝ๋ (์๋ฐฑ ๊ฐ ํ์ผ) โ ๋น๋ โ ๋ฒ๋ค (๋ธ๋ผ์ฐ์ ์ ์ ๋ฌ)
โโโ src/App.tsx โโโ main.bundle.js
โโโ src/components/... โโโ vendor.bundle.js
โโโ node_modules/react/... โโโ ...
โโโ node_modules/lodash/...
๋ฒ๋ค์ด ํด๋ฉด ๋๋ฆฐ ์ด์ 3๋จ๊ณ:
โ ๋ค์ด๋ก๋: ํ์ผ ํฌ๊ธฐ ํผ โ ๋คํธ์ํฌ ์ ์ก ์ค๋ ๊ฑธ๋ฆผ
โ
โก ํ์ฑ: JS ํ์ฑ = CPU ์ง์ค ์์
โ ์ ์ฌ์ ๊ธฐ๊ธฐ์์ ํนํ ๋๋ฆผ
โ
โข ์คํ: ๋ชจ๋ ์ฝ๋ ์ด๊ธฐํ โ ์ฒซ ํ๋ฉด ํ์ ์ง์ฐ (LCP ์์น)
๐ ์ฐ๊ฒฐ ๊ณ ๋ฆฌ
Core Web Vitals(LCP, INP, CLS)์ ๊ฐ๋ ์ Next.js ์ฌํ โ 08๋ฒ ์ฑ๋ฅ ์ต์ ํ ๋ฌธ์ ์ ์์ธํ ๋์ ์์ด. React ์ฑ๋ ๋์ผํ ์งํ๋ก ์ธก์ ํด.
๐ก ํ ์ค๋ก ๊ธฐ์ตํ๊ธฐ
"๋ฒ๋ค์ ์ฑ์ '์ํ๋ฌผ'์ด์ผ. ๋นํ๊ธฐ(๋ธ๋ผ์ฐ์ )์ ์ํ๋ฌผ์ด ๋ง์์๋ก ์ด๋ฅ(์ฒซ ๋ ๋๋ง)์ด ๋๋ ค. ๊ผญ ํ์ํ ์ง๋ง ์ฃ๋ ๊ฒ ์ฝ๋ ์คํ๋ฆฌํ ์ด์ผ."
โ๏ธ ์ฝ๋ ์คํ๋ฆฌํ โ ํ์ํ ๋๋ง ๋ด๋ ค๋ฐ๊ธฐ ๐ข
๐ฏ ์ด ์น์ ์ ์ฝ๊ณ ๋๋ฉด:
- ์ ์
import์ ๋์ import()์ ์ฐจ์ด๋ฅผ ์ฝ๋๋ก ์ค๋ช ํ ์ ์๋ค- ์ธ์ ๋์ import ๋ฅผ ์จ์ผ ํ๋์ง ํ๋จํ ์ ์๋ค
์ ์ import (ํ์ฌ):
// โ ๋ชจ๋ ๊ฒ์ ์ฆ์ ๋ก๋ โ ํ ํ๋ฉด์์๋ ์๋ํฐ ์ฝ๋๊ฐ ๋ก๋๋จ
import MarkdownEditor from './MarkdownEditor'; // 1.2MB
import ChartDashboard from './ChartDashboard'; // 800KB
function App() {
return (
<Router>
<Route path="/" component={Home} />
<Route path="/write" component={MarkdownEditor} />
<Route path="/stats" component={ChartDashboard} />
</Router>
);
}๋์ import (์ฝ๋ ์คํ๋ฆฌํ ):
// โ
ํ์ํ ์์ ์ ๋ก๋ โ ํ ํ๋ฉด์์ ์๋ํฐ ์ฝ๋๋ฅผ ๋ด๋ ค๋ฐ์ง ์์
// import()๋ Promise๋ฅผ ๋ฐํ โ ํด๋น ๋ชจ๋์ด ํ์ํ ๋ ๋คํธ์ํฌ ์์ฒญ ๋ฐ์
// /write ๊ฒฝ๋ก์ ์ง์
ํ ๋๋ง MarkdownEditor ๋ฒ๋ค ๋ค์ด๋ก๋ ์์
const loadEditor = () => import('./MarkdownEditor'); // Promise<module>
const loadChart = () => import('./ChartDashboard');
// ์ค์ ์ฌ์ฉ ์
loadEditor().then(module => {
const MarkdownEditor = module.default;
// ์ด์ MarkdownEditor ์ฌ์ฉ ๊ฐ๋ฅ
});๋์ import ์ฌ์ฉ ๊ธฐ์ค:
| ์ํฉ | ์ ์ import | ๋์ import |
|---|---|---|
| ํญ์ ์ฒซ ํ๋ฉด์ ํ์ํ ์ปดํฌ๋ํธ | โ | ๊ณผํจ |
| ํน์ ๋ผ์ฐํธ์์๋ง ์ฌ์ฉ | โ | โ |
| ์ฌ์ฉ์ ์ก์ (๋ฒํผ ํด๋ฆญ) ํ ์ฌ์ฉ | โ | โ |
| ๋ฌด๊ฑฐ์ด ๋ผ์ด๋ธ๋ฌ๋ฆฌ (์ฐจํธ, ์๋ํฐ) | โ | โ |
| ์กฐ๊ฑด๋ถ๋ก ์ฌ์ฉ๋๋ ๋ฌด๊ฑฐ์ด ์ปดํฌ๋ํธ | โ | โ |
๐ก ํ ์ค๋ก ๊ธฐ์ตํ๊ธฐ
"import ModuleName from '...'๋ ์ฑ ์์ ์ ์ฆ์ ๋ก๋.import('...')๋ ํธ์ถ ์์ ์ ๋คํธ์ํฌ ์์ฒญ โ ์ฝ๋ ์คํ๋ฆฌํ ์ ํต์ฌ์ด์ผ."
โ๏ธ React.lazy + Suspense๋ก ์ปดํฌ๋ํธ ์คํ๋ฆฌํ ๐ก
๐ฏ ์ด ์น์ ์ ์ฝ๊ณ ๋๋ฉด:
React.lazy์Suspense๋ฅผ ๊ฒฐํฉํด ์ปดํฌ๋ํธ ๋จ์ ์ฝ๋ ์คํ๋ฆฌํ ์ ๊ตฌํํ ์ ์๋ค- ๋ก๋ฉ UI ์ ์๋ฌ ์ฒ๋ฆฌ๋ฅผ ํจ๊ป ์ค์ ํ ์ ์๋ค
React.lazy ๋ ๋์ import() ๋ฅผ React ์ปดํฌ๋ํธ๋ก ๊ฐ์ธ์ฃผ๋ ํจ์์์.
import { lazy, Suspense } from 'react';
import { BrowserRouter as Router, Route, Routes } from 'react-router-dom';
// โ
lazy๋ก ์ปดํฌ๋ํธ๋ฅผ ์ ์ธ โ ์ด ์์ ์ ์๋ฌด๊ฒ๋ ๋ค์ด๋ก๋ ์ ๋จ
const MarkdownEditor = lazy(() => import('./pages/WritePage')); // ์ค์ import๋ ๋์ค์
const ChartDashboard = lazy(() => import('./pages/StatsPage'));
const AdminPanel = lazy(() => import('./pages/AdminPage'));
function App() {
return (
<Router>
{/* Suspense: lazy ์ปดํฌ๋ํธ๊ฐ ๋ก๋ฉ ์ค์ผ ๋ fallback UI ํ์ */}
<Suspense fallback={<PageSpinner />}>
<Routes>
<Route path="/" element={<Home />} /> {/* ์ ์ import: ํญ์ ํ์ */}
<Route path="/write" element={<MarkdownEditor />} /> {/* /write ์ง์
์ ๋ค์ด๋ก๋ */}
<Route path="/stats" element={<ChartDashboard />} /> {/* /stats ์ง์
์ ๋ค์ด๋ก๋ */}
<Route path="/admin" element={<AdminPanel />} /> {/* /admin ์ง์
์ ๋ค์ด๋ก๋ */}
</Routes>
</Suspense>
</Router>
);
}๋ก๋ฉ UI ์ธ๋ถํ:
// โ
๊ฐ ๋ผ์ฐํธ๋ง๋ค ๋ค๋ฅธ ๋ก๋ฉ UI ์ ์ฉ
function App() {
return (
<Router>
<Routes>
<Route path="/" element={<Home />} />
{/* ์๋ํฐ ์ ์ฉ ๋ก๋ฉ UI */}
<Route
path="/write"
element={
<Suspense fallback={<EditorSkeleton />}>
<MarkdownEditor />
</Suspense>
}
/>
{/* ์ฐจํธ ์ ์ฉ ๋ก๋ฉ UI */}
<Route
path="/stats"
element={
<Suspense fallback={<ChartSkeleton />}>
<ChartDashboard />
</Suspense>
}
/>
</Routes>
</Router>
);
}ErrorBoundary ์ ํจ๊ป ์ฌ์ฉ (๋คํธ์ํฌ ์ค๋ฅ ๋์):
// lazy ์ปดํฌ๋ํธ ๋ก๋ฉ ์คํจ(๋คํธ์ํฌ ๋๊น ๋ฑ)๋ฅผ ErrorBoundary๋ก ์ฒ๋ฆฌ
function App() {
return (
<ErrorBoundary fallback={<p>ํ์ด์ง๋ฅผ ๋ถ๋ฌ์ค์ง ๋ชปํ์ด์. ์๋ก๊ณ ์นจ ํด์ฃผ์ธ์.</p>}>
<Suspense fallback={<PageSpinner />}>
<Routes>...</Routes>
</Suspense>
</ErrorBoundary>
);
}๊ฒฐ๊ณผ: ๋คํธ์ํฌ ํญ์์ ํ์ธ๋๋ ๋ณํ:
// Before: ํ ๋ฐฉ๋ฌธ ์
main.bundle.js [4.2 MB] โ ํ ๋ฒ์ ๋ชจ๋ ๋ค์ด๋ก๋ ๐ฑ
// After: ํ ๋ฐฉ๋ฌธ ์
main.bundle.js [180 KB] โ ํต์ฌ๋ง
// /write ๋ฐฉ๋ฌธ ์ (์ถ๊ฐ ๋ค์ด๋ก๋)
write-page.js [1.3 MB] โ ํ์ํ ๋ ๋ค์ด๋ก๋
// /stats ๋ฐฉ๋ฌธ ์ (์ถ๊ฐ ๋ค์ด๋ก๋)
stats-page.js [820 KB] โ ํ์ํ ๋ ๋ค์ด๋ก๋
์ค์ต ํ ์ฒดํฌ๋ฆฌ์คํธ:
-
React.lazy(() => import('./Component'))๋ฌธ๋ฒ์ ์ธ์ ๋ค -
Suspense๊ฐ ์์ผ๋ฉดlazy์ปดํฌ๋ํธ๊ฐ ์๋ฌ๋ฅผ ๋์ง๋ค๋ ๊ฒ์ ์๊ณ ์๋ค - ๋ผ์ฐํธ๋ง๋ค ๋ณ๋
Suspense๋ฅผ ์ฐ๋ฉด ๋ก๋ฉ UI๋ฅผ ์ธ๋ถํํ ์ ์๋ค๋ ๊ฒ์ ์๊ณ ์๋ค
๐ก ํ ์ค๋ก ๊ธฐ์ตํ๊ธฐ
"lazy(() => import('./Page'))+<Suspense fallback={<Spinner/>}>โ ์ด ๋ ์ค ์กฐํฉ์ด React ์ฝ๋ ์คํ๋ฆฌํ ์ ์ ๋ถ์ผ. ๋ผ์ฐํธ๋ง๋ค ๊ฐ์ธ๋ฉด ๋ผ์ฐํธ ๊ธฐ๋ฐ ์คํ๋ฆฌํ ์์ฑ."
๐บ๏ธ ๋ผ์ฐํธ ๊ธฐ๋ฐ vs ์ปดํฌ๋ํธ ๊ธฐ๋ฐ ์คํ๋ฆฌํ ๐ก
๐ฏ ์ด ์น์ ์ ์ฝ๊ณ ๋๋ฉด:
- ์ด๋ค ์์ค์์ ์ฝ๋๋ฅผ ์ชผ๊ฐ์ผ ํจ๊ณผ์ ์ธ์ง ํ๋จํ ์ ์๋ค
๋ผ์ฐํธ ๊ธฐ๋ฐ ์คํ๋ฆฌํ (๊ฐ์ฅ ํจ๊ณผ์ , ๊ธฐ๋ณธ):
// โ
๊ฐ ํ์ด์ง๋ฅผ ๋ณ๋ ์ฒญํฌ(chunk)๋ก ๋ถ๋ฆฌ
const HomePage = lazy(() => import('./pages/HomePage')); // chunk: home
const WritePage = lazy(() => import('./pages/WritePage')); // chunk: write
const ProfilePage = lazy(() => import('./pages/Profile')); // chunk: profile์ปดํฌ๋ํธ ๊ธฐ๋ฐ ์คํ๋ฆฌํ (๋ฌด๊ฑฐ์ด ์ปดํฌ๋ํธ์ ์ ํ์ ์ ์ฉ):
// โ
ํน์ ๋ฌด๊ฑฐ์ด ์ปดํฌ๋ํธ๋ง ์ง์ฐ ๋ก๋ฉ
import { lazy, Suspense, useState } from 'react';
// ๋งํฌ๋ค์ด ์๋ํฐ: 1.2MB โ ๊ฒ์๊ธ ์์ฑ ๋ฒํผ์ ํด๋ฆญํ ๋๋ง ํ์
const MarkdownEditor = lazy(() => import('./MarkdownEditor'));
function PostForm() {
const [isEditorOpen, setIsEditorOpen] = useState(false);
return (
<div>
<button onClick={() => setIsEditorOpen(true)}>์๋ํฐ ์ด๊ธฐ</button>
{isEditorOpen && (
<Suspense fallback={<p>์๋ํฐ ๋ก๋ฉ ์ค...</p>}>
<MarkdownEditor /> {/* ๋ฒํผ ํด๋ฆญ ํ์์ผ 1.2MB ๋ค์ด๋ก๋ ์์ */}
</Suspense>
)}
</div>
);
}์ธ์ ์ปดํฌ๋ํธ ๊ธฐ๋ฐ ์คํ๋ฆฌํ ์ ์ธ๊น?
// ์คํ๋ฆฌํ
ํจ๊ณผ๊ฐ ์๋ ๊ฒฝ์ฐ
const RichTextEditor = lazy(() => import('react-quill')); // 500KB ์ด์ โ ํจ๊ณผ์
const PdfViewer = lazy(() => import('./PdfViewer')); // ๋ฌด๊ฑฐ์ด PDF ๋ ๋๋ฌ
const VideoPlayer = lazy(() => import('./VideoPlayer')); // ๋น๋์ค ํ๋ ์ด์ด
// ์คํ๋ฆฌํ
ํจ๊ณผ๊ฐ ์๊ฑฐ๋ ์คํ๋ ค ์ํด์ธ ๊ฒฝ์ฐ
const SmallButton = lazy(() => import('./Button')); // โ 2KB ์ปดํฌ๋ํธ โ ์ค๋ฒํค๋๊ฐ ๋ ํผ
// โ ๋คํธ์ํฌ ์์ฒญ ์ค๋ฒํค๋ > ํ์ผ ํฌ๊ธฐ ์ ์ฝ ํจ๊ณผ๐ก ํ ์ค๋ก ๊ธฐ์ตํ๊ธฐ
"๋ผ์ฐํธ๋ง๋ค ์คํ๋ฆฌํ ์ ๊ธฐ๋ณธ๊ฐ์ด์ผ. ์ปดํฌ๋ํธ ์คํ๋ฆฌํ ์ 50KB ์ด์์ ๋ฌด๊ฑฐ์ด ์ปดํฌ๋ํธ์๋ง ์ ์ฉํด. ๋๋ฌด ์๊ฒ ์ชผ๊ฐ๋ฉด ๋คํธ์ํฌ ์์ฒญ ์๊ฐ ๋์ด๋ ์คํ๋ ค ๋๋ ค์ง ์ ์์ด."
๐ Bundle Analyzer๋ก ๋ฒ๋ค ํด๋ถํ๊ธฐ ๐ก
๐ฏ ์ด ์น์ ์ ์ฝ๊ณ ๋๋ฉด:
- Bundle Analyzer ๋ฅผ ์ค์นํ๊ณ ์คํํ ์ ์๋ค
- ์ด๋ค ๋ผ์ด๋ธ๋ฌ๋ฆฌ๊ฐ ๋ฒ๋ค์ ๊ฐ์ฅ ๋ฌด๊ฒ๊ฒ ๋ง๋๋์ง ์ฐพ์ ์ ์๋ค
์ค์น & ์คํ:
# Create React App (CRA) ์ฌ์ฉ ์
npm install --save-dev source-map-explorer
# package.json์ ์คํฌ๋ฆฝํธ ์ถ๊ฐ:
# "analyze": "source-map-explorer 'build/static/js/*.js'"
npm run build && npm run analyze
# Vite ์ฌ์ฉ ์
npm install --save-dev rollup-plugin-visualizer
# vite.config.ts์ ์ถ๊ฐ:
# plugins: [visualizer({ open: true })]
npm run build # ๋น๋ ์ ์๋์ผ๋ก ์๊ฐํ ํ์ผ ์์ฑ
# Next.js ์ฌ์ฉ ์
npm install --save-dev @next/bundle-analyzer
# next.config.js์ ์ค์ ์ถ๊ฐ ํ:
ANALYZE=true npm run buildBundle Analyzer ํ๋ฉด ์ฝ๋ ๋ฒ:
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ ๋ฒ๋ค ์๊ฐํ (ํฌ๊ธฐ = ์ฌ๊ฐํ ๋ฉด์ ) โ
โ โ
โ โโโโโโโโโโโโโโโโโ โโโโโโโโ โโโโโโโโโโโโโโโโ โ
โ โ โ โReact โ โ โ โ
โ โ moment.js โ โ DOM โ โ lodash โ โ
โ โ (400KB) โ โ(180K)โ โ (500KB) โ โ
โ โ โ โ โ โ โ โ
โ โโโโโโโโโโโโโโโโโ โโโโโโโโ โโโโโโโโโโโโโโโโ โ
โ โ
โ ๐ ๋ฉด์ ์ด ํฐ ๊ฒ = ๋ฒ๋ค์์ ์ฐจ์งํ๋ ๋น์ค์ด ํผ โ
โ ๐ ํฐ ์ฌ๊ฐํ๋ถํฐ ๊ฒฝ๋ํ ๋์ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
ํํ "๋ฒ๋ค ๋ฒ์ธ" ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ ๋์:
| ๋ผ์ด๋ธ๋ฌ๋ฆฌ | ํฌ๊ธฐ | ๋ฌธ์ | ๋์ |
|---|---|---|---|
moment.js | ~400KB | ์ ์ฒด ๋ก์ผ์ผ ํฌํจ | date-fns (ํ์ํ ํจ์๋ง) ๋๋ dayjs (2KB) |
lodash (์ ์ฒด) | ~500KB | import _ from 'lodash' ๋ก ์ ์ฒด ๋ก๋ | import { debounce } from 'lodash-es' ๋๋ ์ง์ ๊ตฌํ |
react-icons (์ ์ฒด) | ๊ฐ๋ณ์ | import * as Icons from 'react-icons/fa' | import { FaUser } from 'react-icons/fa' (๊ฐ๋ณ import) |
xlsx | ~800KB | ์คํ๋ ๋์ํธ ์ ์ฒด ๊ธฐ๋ฅ | ์๋ฒ์์ ์์ฑ ํ ๋ค์ด๋ก๋ ๋งํฌ ์ ๊ณต |
moment.js โ dayjs ๋ง์ด๊ทธ๋ ์ด์ ์์:
// โ moment.js: 400KB
import moment from 'moment';
const formatted = moment(date).format('YYYY๋
MM์ DD์ผ');
// โ
dayjs: 2KB (200๋ฐฐ ์ฐจ์ด!)
import dayjs from 'dayjs';
import 'dayjs/locale/ko'; // ํ๊ตญ์ด ๋ก์ผ์ผ๋ง ์ถ๊ฐ (์ ํ์ )
dayjs.locale('ko');
const formatted = dayjs(date).format('YYYY๋
MM์ DD์ผ');
// โ
date-fns: ํ์ํ ํจ์๋ง ํธ๋ฆฌ์
ฐ์ดํน
import { format } from 'date-fns';
import { ko } from 'date-fns/locale';
const formatted = format(date, 'yyyy๋
MM์ dd์ผ', { locale: ko });๐ก ํ ์ค๋ก ๊ธฐ์ตํ๊ธฐ
"Bundle Analyzer ๋ฅผ ์ฒ์ ์ผ๋ฉด 'moment.js์ lodash ์ ์ฒด' ๊ฐ ๋ฒ๋ค์ ์ ๋ฐ ์ด์์ ์ฐจ์งํ๋ ๊ฒฝ์ฐ๊ฐ ๋ง์. ์ด ๋์ ๊ฒฝ๋ ๋์์ผ๋ก๋ง ๋ฐ๊ฟ๋ 30~50% ๋ฒ๋ค ๊ฐ์๋ ๊ธฐ๋ณธ์ด์ผ."
๐ฒ Tree Shaking โ ์ ์ฐ๋ ์ฝ๋ ์๋ ์ ๊ฑฐ ๐ก
๐ฏ ์ด ์น์ ์ ์ฝ๊ณ ๋๋ฉด:
- Tree Shaking์ด ์ด๋ป๊ฒ ๋์ํ๋์ง ์ดํดํ๋ค
- Tree Shaking ์ด ๋์ง ์๋ ํํ ์ค์๋ฅผ ํผํ ์ ์๋ค
๐ ์ฉ์ด: Tree Shaking โ ๋๋ฌด๋ฅผ ํ๋ค๋ฉด ์ฃฝ์ ๋๋ญ์์ด ๋จ์ด์ง๋ฏ, ๋ฒ๋ค์์ ์ฌ์ฉํ์ง ์๋ ์ฝ๋๋ฅผ ์๋์ผ๋ก ์ ๊ฑฐํ๋ ์ต์ ํ ๊ธฐ๋ฒ. Webpack, Vite๊ฐ ๋น๋ ์ ์๋์ผ๋ก ์ํํด์.
Tree Shaking ์๋ ์กฐ๊ฑด:
// โ
Tree Shaking์ด ๋๋ named export
import { debounce } from 'lodash-es'; // debounce๋ง ๋ฒ๋ค์ ํฌํจ
// โ ๋๋จธ์ง lodash ํจ์ ์๋ฐฑ ๊ฐ๋ ์ ๊ฑฐ๋จ
// โ Tree Shaking์ด ์ ๋๋ default export (์ ์ฒด import)
import _ from 'lodash'; // lodash ์ ์ฒด ๋ฒ๋ค์ ํฌํจ!
const debounced = _.debounce(); // debounce ํ๋๋ง ์จ๋ lodash ์ ์ฒด๊ฐ ๋ค์ด์ดTree Shaking์ด ์ ๋๋ ํํ ์ค์:
// โ 1. CommonJS ๋ชจ๋์ Tree Shaking ๋ถ๊ฐ
const { debounce } = require('lodash'); // require๋ Tree Shaking ๋ถ๊ฐ!
// โ ESM(import/export)๋ง Tree Shaking ๊ฐ๋ฅ
// โ 2. ์์ผ๋์นด๋ import
import * as Icons from 'react-icons/fa'; // ๋ชจ๋ ์์ด์ฝ ํฌํจ!
import { FaUser, FaHeart } from 'react-icons/fa'; // โ
๊ฐ๋ณ import
// โ 3. ์ฌ์ด๋ ์ดํํธ๊ฐ ์๋ ๋ชจ๋
import 'some-css-library'; // CSS ํ์ผ์ Tree Shaking ๋์ ์๋
import './polyfills'; // ์คํ๋์ด์ผ ํ๋ ์ฝ๋๋ Tree Shaking ๋์ ์๋
// โ
์ฌ๋ฐ๋ฅธ ๊ฐ๋ณ import ํจํด
import { format, parseISO } from 'date-fns'; // ๋ ํจ์๋ง ๋ฒ๋ค์ ํฌํจsideEffects ์ค์ ์ผ๋ก Tree Shaking ๊ฐํ (๋ผ์ด๋ธ๋ฌ๋ฆฌ ๊ฐ๋ฐ ์):
// package.json
{
"sideEffects": false // ๋ชจ๋ ํ์ผ์ด ์ฌ์ด๋ ์ดํํธ ์์ โ ๋ฒ๋ค๋ฌ๊ฐ ์ ๊ทน์ ์ผ๋ก ์ ๊ฑฐ
// ๋๋
"sideEffects": ["*.css", "./src/polyfills.js"] // ํน์ ํ์ผ๋ง ์ฌ์ด๋ ์ดํํธ ์์
}๐ก ํ ์ค๋ก ๊ธฐ์ตํ๊ธฐ
"Tree Shaking์ ESM(named export)์์๋ง ์๋ํด.import _ from 'lodash'๋์import { debounce } from 'lodash-es'โ ์ด ํ ์ค์ ์ฐจ์ด๊ฐ ์๋ฐฑKB ์ฐจ์ด๋ฅผ ๋ง๋ค์ด."
๐ ์ด๋ฏธ์ง ์ต์ ํ์ Web Vitals ๐ก
๐ฏ ์ด ์น์ ์ ์ฝ๊ณ ๋๋ฉด:
- LCP ์ต์ ํ๋ฅผ ์ํ ์ด๋ฏธ์ง ์ฒ๋ฆฌ ์ ๋ต์ ์ ์ฉํ ์ ์๋ค
- Lighthouse ์ ์๋ฅผ ์ค์ ๋ก ๊ฐ์ ํ๋ ๋ฐฉ๋ฒ์ ์๋ค
Lighthouse๋ก ์ฑ๋ฅ ์ธก์ :
# Chrome DevTools โ Lighthouse ํญ โ Generate Report
# ๋๋ CLI
npm install -g lighthouse
lighthouse https://your-site.com --viewLCP ๊ฐ์ ์ฒดํฌ๋ฆฌ์คํธ:
// โ
1. ์ด๋ฏธ์ง ์ง์ฐ ๋ก๋ฉ (์คํฌ๋กค ํ๋จ ์ด๋ฏธ์ง)
<img src="photo.jpg" loading="lazy" alt="๊ฒ์๊ธ ์ด๋ฏธ์ง" />
// โ
2. ์ฒซ ํ๋ฉด ์ด๋ฏธ์ง๋ preload ํํธ
<link rel="preload" as="image" href="/hero-image.webp" />
// โ
3. ํ๋์ ์ด๋ฏธ์ง ํฌ๋งท ์ฌ์ฉ
// WebP๋ JPEG ๋๋น 25-35% ์์
// AVIF๋ WebP ๋๋น 20% ๋ ์์
<picture>
<source srcSet="hero.avif" type="image/avif" />
<source srcSet="hero.webp" type="image/webp" />
<img src="hero.jpg" alt="ํ์ด๋ก ์ด๋ฏธ์ง" /> {/* ํด๋ฐฑ */}
</picture>
// โ
4. ์ด๋ฏธ์ง ํฌ๊ธฐ ๋ช
์ (CLS ๋ฐฉ์ง)
<img src="photo.jpg" width={400} height={300} alt="..." />
// width/height ์์ผ๋ฉด ์ด๋ฏธ์ง ๋ก๋ ์ ํฌ๊ธฐ๋ฅผ ๋ชจ๋ฆ โ ๋ ์ด์์ ์ด๋(CLS) ๋ฐ์!React์์ ์ฑ๋ฅ ์ธก์ (Profiler API):
import { Profiler } from 'react';
function onRenderCallback(
id: string, // Profiler์ id prop
phase: 'mount' | 'update', // ์ฒซ ๋ง์ดํธ์ธ์ง ์
๋ฐ์ดํธ์ธ์ง
actualDuration: number, // ์ค์ ๋ ๋ ์๊ฐ (ms)
baseDuration: number, // ์ต์ ํ ์๋ ์์ ๋ ๋ ์๊ฐ
) {
// ๋ ๋ ์๊ฐ์ด 16ms(60fps) ์ด๊ณผ ์ ์ต์ ํ ํ์
if (actualDuration > 16) {
console.warn(`[${id}] ๋ ๋ ์ต์ ํ ํ์: ${actualDuration.toFixed(2)}ms`);
}
}
// ์ธก์ ํ๊ณ ์ถ์ ์ปดํฌ๋ํธ ํธ๋ฆฌ๋ฅผ Profiler๋ก ๊ฐ์ธ๊ธฐ
function App() {
return (
<Profiler id="PostFeed" onRender={onRenderCallback}>
<PostFeed />
</Profiler>
);
}๐ก ํ ์ค๋ก ๊ธฐ์ตํ๊ธฐ
"LCP๋ ์ฒซ ํ๋ฉด์ ๊ฐ์ฅ ํฐ ์ด๋ฏธ์ง/ํ ์คํธ ๋ ๋ ์๊ฐ์ด์ผ. WebP ํฌ๋งท + ์ด๋ฏธ์ง ํฌ๊ธฐ ๋ช ์ + preload ํํธ ์ธ ๊ฐ์ง๋ง์ผ๋ก๋ LCP๋ฅผ ํฌ๊ฒ ๊ฐ์ ํ ์ ์์ด."
๐ฅ ์๋ฌ ํด๊ฒฐ ์นดํ๋ก๊ทธ
โ A React component suspended while rendering, but no fallback UI was specified
์ธ์ ๋์ค๋๊ฐ?
Error: A React component suspended while rendering, but no fallback UI was specified.
์์ธ: lazy ๋ก ๋ถ๋ฌ์จ ์ปดํฌ๋ํธ๋ฅผ Suspense ๋ก ๊ฐ์ธ์ง ์์์ด์.
ํด๊ฒฐ์ฑ :
// โ
lazy ์ปดํฌ๋ํธ๋ ๋ฐ๋์ Suspense๋ก ๊ฐ์ธ์ผ ํจ
const LazyComponent = lazy(() => import('./LazyComponent'));
function App() {
return (
<Suspense fallback={<div>๋ก๋ฉ ์ค...</div>}>
<LazyComponent />
</Suspense>
);
}โ React.lazy๊ฐ default export๋ง ์ง์ํจ
์ธ์ ๋์ค๋๊ฐ?
// named export ์ปดํฌ๋ํธ๋ฅผ lazy๋ก import ์๋
const { MyComponent } = lazy(() => import('./MyComponents')); // โํด๊ฒฐ์ฑ :
// ๋ฐฉ๋ฒ 1: ํด๋น ํ์ผ์์ default export ์ถ๊ฐ
// MyComponent.tsx
export default function MyComponent() { ... }
// ๋ฐฉ๋ฒ 2: ๋ํผ๋ฅผ ํตํด named โ default ๋ณํ
const MyComponent = lazy(() =>
import('./MyComponents').then(module => ({ default: module.MyComponent }))
);โ Tree Shaking์ด ์ ์ฉ๋๋๋ฐ๋ ๋ฒ๋ค ํฌ๊ธฐ๊ฐ ์ ์ค์ด๋ฆ
์์ธ ์ง๋จ:
# 1. ํด๋น ํจํค์ง๊ฐ ESM์ ์ง์ํ๋์ง ํ์ธ
cat node_modules/lodash/package.json | grep '"module"'
# "module" ํ๋๊ฐ ์์ผ๋ฉด CommonJS โ Tree Shaking ์ ๋จ
# 2. lodash-es ๋์ lodash ์ฐ๊ณ ์์ง ์์์ง ํ์ธ
# lodash๋ CommonJS, lodash-es๋ ESMํด๊ฒฐ์ฑ :
// lodash (CommonJS) โ lodash-es (ESM)๋ก ๋ณ๊ฒฝ
// Before
import { debounce } from 'lodash';
// After
import { debounce } from 'lodash-es';
// ๋๋ ์ง์ ๊ตฌํ (debounce๋ ๋ช ์ค์ด๋ฉด ๊ฐ๋ฅ)๐ ์ด๋ฒ์ ๋ฐฐ์ด ๋ด์ฉ ์ด์ ๋ฆฌ
๐ ์ฝ๋ ์คํ๋ฆฌํ ํจํด
| ํจํด | ์ฝ๋ | ์ ์ฉ ์๊ธฐ |
|---|---|---|
| ๋ผ์ฐํธ ์คํ๋ฆฌํ | lazy(() => import('./Page')) | ๊ธฐ๋ณธ๊ฐ, ๋ชจ๋ ๋ผ์ฐํธ |
| ์ปดํฌ๋ํธ ์คํ๋ฆฌํ | lazy(() => import('./HeavyComp')) | 50KB+ ๋ฌด๊ฑฐ์ด ์ปดํฌ๋ํธ |
| ์กฐ๊ฑด๋ถ ์คํ๋ฆฌํ | isOpen && <Suspense><LazyComp/></Suspense> | ๋ฒํผ ํด๋ฆญ ํ ๋ํ๋๋ ๋ฌด๊ฑฐ์ด UI |
๐ ๋ฒ๋ค ์ต์ ํ ์ฒดํฌ๋ฆฌ์คํธ
| ํญ๋ชฉ | ํ์ธ | ์์ ํจ๊ณผ |
|---|---|---|
| moment.js โ dayjs ๊ต์ฒด | [ ] | -400KB |
| lodash ์ ์ฒด โ ๊ฐ๋ณ import | [ ] | -400KB |
| ๋ผ์ฐํธ๋ณ ์ฝ๋ ์คํ๋ฆฌํ | [ ] | ์ด๊ธฐ ๋ก๋ฉ -50% |
| ์ด๋ฏธ์ง WebP ํฌ๋งท ์ ์ฉ | [ ] | ์ด๋ฏธ์ง ํฌ๊ธฐ -30% |
| ์ด๋ฏธ์ง์ width/height ๋ช ์ | [ ] | CLS 0์ |
| Bundle Analyzer ์ ๊ธฐ ์คํ | [ ] | ๋ฒ๋ค ๋ชจ๋ํฐ๋ง |
โ ๏ธ ์ ๋ ํ์ง ๋ง ๊ฒ
| โ ๋์ ์ | โ ์ข์ ์ | ์ด์ |
|---|---|---|
import _ from 'lodash' | import { debounce } from 'lodash-es' | ์ ์ฒด ๋ฒ๋ค ํฌํจ |
import * as Icons from 'react-icons' | import { FaUser } from 'react-icons/fa' | ๋ชจ๋ ์์ด์ฝ ํฌํจ |
| lazy ์์ด ๋ฌด๊ฑฐ์ด ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์ง์ import | lazy(() => import('./Heavy')) | ์ด๊ธฐ ๋ฒ๋ค ๋น๋ |
| Suspense ์์ด lazy ์ฌ์ฉ | <Suspense fallback={...}> ๋ก ๊ฐ์ธ๊ธฐ | ๋ฐํ์ ์๋ฌ |
| 2KB ์ปดํฌ๋ํธ์ ์ฝ๋ ์คํ๋ฆฌํ | ์ ์ import ์ ์ง | ์ค๋ฒํค๋๊ฐ ๋ ํผ |
๐ฃ ์์ฒ ์ด์ ํด๊ทผ ์ผ๊ธฐ
๊ธฐ๋ฅ๋ง ์ ๊ฐ๋ฐํ๋ฉด ๋์ธ ์ค ์์๋ ๋ด ์ค๋งํจ์ด, ๋ฒ๋ค ์ฌ์ด์ฆ 4MB๋ผ๋ ์ฒ์ฐธํ ์ซ์๋ก ๋์์์๋ค. ๊ฒ์๊ธ ํ๋ ๋ณด๋ ค๋๋ฐ ์๋ํฐ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๊น์ง ํต์งธ๋ก ๋ค์ด๋ฐ๊ฒ ๋ง๋ค์๋ค๋... ์ ์ ๋ค์ด ์ ์ฒซ ํ๋ฉด ๋ก๋ฉ์ด ๋๋ฆฌ๋ค๊ณ ํ๋ฅผ ๋๋์ง ์ด์ ์๊ฒ ๋ค.
๐ก "์ ์ ์ ์ง(๋ฒ๋ค)์ ๋์ด์ฃผ๋ ์๊ฐ ์ง์ง ํ๋ก ํธ์๋ ์ฅ์ธ์ด๋ค. ํ์ํ ์๊ฐ์๋ง ๋์ ์ผ๋ก ์ฝ๋๋ฅผ ๋ถ๋ฌ์ค๋ Code Splitting์ ์ ํ์ด ์๋ ํ์!"
React.lazy๋ Suspense ์ค์ ์กฐ๊ธ๋ง ํ์ ๋ฟ์ธ๋ฐ ์ฑ ์ฉ๋์ด ๋๋ ๋จ์ด์ง๋ ๊ฑธ Bundle Analyzer๋ก ์ง์ ๋์ผ๋ก ํ์ธํ๋ ์์ด ๋ค ์์ํ๋ค. lodash ์ ์ฒด๋ฅผ ๋ค ๋ถ๋ฌ์ค๋ ๋ฏธ๋ จํ ์ฝ๋๋ ๋น์ฅ ๋ค named export๋ก ๋ฐ๊ฟจ๋ค. ์ฑ๋ฅ ์ต์ ํ, ๋จ์ ์ผ์ธ ์ค ์์๋๋ฐ ์ด์ ์์ ๊ฐ์ด ๋ถ์๋ค.
๐ ๋ง๋ฌด๋ฆฌ ํด์ฆ
Q1. ์ฝ๋ ์คํ๋ฆฌํ ํจ๊ณผ๊ฐ ๊ฐ์ฅ ํฐ ๊ฒ์?
- A) 1KB ์ง๋ฆฌ ์ ํธ ํจ์๋ฅผ
lazy๋ก ๋์ ๋ก๋ - B) ๊ฒ์๊ธ ๋ชฉ๋ก ํ์ด์ง์ ๋ ์ด์์ ์ปดํฌ๋ํธ๋ฅผ
lazy๋ก ๋์ ๋ก๋ - C) ๋งํฌ๋ค์ด ์๋ํฐ(1.2MB)๋ฅผ ์๋ํฐ ํ์ด์ง ์ง์ ์์๋ง ๋์ ๋ก๋
- D) React ์์ฒด๋ฅผ ๋์ ๋ก๋
โ ์ ๋ต: C
- A: 1KB โ ์ค๋ฒํค๋ > ์ ์ฝ ํจ๊ณผ, ์ญํจ๊ณผ
- B: ๋ ์ด์์์ ๋ชจ๋ ํ์ด์ง์ ๊ณตํต โ ์คํ๋ฆฌํ ํจ๊ณผ ์์
- C: 1.2MB์ ๋ฌด๊ฑฐ์ด ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ํ์ํ ํ์ด์ง์์๋ง ๋ก๋ โ ๊ฐ์ฅ ํฐ ํจ๊ณผ โ
- D: React๋ ๋ชจ๋ ์ปดํฌ๋ํธ์ ์ ์ โ ๋์ ๋ก๋ ๋ถ๊ฐ๋ฅ
๐ ํต์ฌ ๊ธฐ์ต๋ฒ: "์ฝ๋ ์คํ๋ฆฌํ ์ ํฌ๊ณ ์ ํ์ ์ธ ๊ฒ์ ์ ์ฉํ ์๋ก ํจ๊ณผ๊ฐ ์ปค. ์๊ณ ๊ณตํต์ ์ธ ์ฝ๋์๋ ์คํ๋ ค ์ํด."
Q2. ์๋ ๋น์นธ์ ์ฑ์๋ณด์.
// ์๋ํฐ ํ์ด์ง๋ฅผ ์ง์ฐ ๋ก๋ฉํ๋, ๋ก๋ฉ ์ค์ <Spinner/>๋ฅผ ํ์ํด์ผ ํด
const EditorPage = ________(() => import('./EditorPage')); // 1๋ฒ
function App() {
return (
<________ fallback={<Spinner />}> // 2๋ฒ
<EditorPage />
</________>
);
}โ ์ ๋ต: 1๋ฒ:
lazy, 2๋ฒ:Suspense๐ ํต์ฌ ๊ธฐ์ต๋ฒ: "
lazy+Suspense๋ ํญ์ ๋ถ์ด๋ค๋ . lazy๊ฐ ์ ์ธ, Suspense๊ฐ ํํ์ด์ผ."
Q3. ์น๊ตฌ์๊ฒ ์ค๋ช ํ๋ค๋ฉด?
Tree Shaking์ด
import _ from 'lodash'์์๋ ์ ์ ๋๊ณ ,import { debounce } from 'lodash-es'์์๋ ๋๋์ง ์ค๋ช ํด๋ด.
์์ ๋ต๋ณ:
"Tree Shaking์ '์ ์ฐ๋ ์ฝ๋๋ฅผ ์๋ฅด๋ ๊ฒ'์ธ๋ฐ, ์๋ฅด๋ ค๋ฉด ์ด๋ค ์ฝ๋๊ฐ ์ด๋์ ์๋์ง ์ ์ ์ผ๋ก ๋ถ์ํ ์ ์์ด์ผ ํด.
import _ from 'lodash'๋_๊ฐ์ฒด ํ๋์ ๋ชจ๋ ํจ์๊ฐ ๋ด๊ฒจ์๊ณ ,_.debounce()์ฒ๋ผ ์ฐ๋ฉด ๋น๋ ๋๊ตฌ๊ฐ_์ค์ ๋ญ ์ฐ๋์ง ์ ์ ์์ด์ ์ ๋ถ ๋ฒ๋ค์ ํฌํจํด.import { debounce } from 'lodash-es'๋debounce๋ง ๊ฐ์ ธ์จ๋ค๊ณ ๋ช ์์ ์ผ๋ก ์ ์ธํ๋๊น ๋น๋ ๋๊ตฌ๊ฐ ๋๋จธ์ง๋ ์์ ํ๊ฒ ์ ๊ฑฐํ ์ ์์ด. ESM์ named export๊ฐ ์ ์ ๋ถ์์ ๊ฐ๋ฅํ๊ฒ ํด์ค."
๐ ๋ ์์๋ณด๊ธฐ
- Chrome DevTools โ Performance ํญ
- web.dev โ Core Web Vitals
- Bundlephobia โ npm ํจํค์ง ๋ฒ๋ค ํฌ๊ธฐ ์ฌ์ ํ์ธ
- Next.js ๋ฒ๋ค ๋ถ์ โ 08๋ฒ ์ฌํ ๋ฌธ์
- 16๋ฒ ๋ฌธ์ โ Suspense & ErrorBoundary โ Suspense ๊ธฐ์ด