2.1 Avoid Barrel File Imports
- 핵심: 대형 라이브러리는 배럴 import 대신 직접 import를 사용한다.
- 왜 중요한가: 불필요한 모듈 해석/번들 포함을 줄인다.
- 코드리뷰 신호: 아이콘/컴포넌트 라이브러리를 루트 엔트리에서 구조분해 import한다.
실무에서 아이콘/컴포넌트 라이브러리를 루트 엔트리에서 구조분해 import한다 부분만 단발성으로 고치면 같은 문제가 다시 나오기 쉽다.
작은 단위(한 페이지, 한 훅, 한 API)부터 대형 라이브러리는 배럴 import 대신 직접 import를 사용한다 패턴을 적용하고, 경계 케이스를 함께 정리해야 회귀를 줄일 수 있다. 이후 번들 분석 결과(엔트리 청크, 라우트별 청크)와 초기 JS 파싱 시간을 배포 전후로 비교해 불필요한 모듈 해석/번들 포함을 줄인다 개선이 지속되는지 확인한다.
예시 (Bad) ▼
import { Check, X, Menu } from 'lucide-react'
// 모듈 1,583개 로드, 개발 환경에서 약 2.8초 추가 소요
// 런타임 비용: 콜드 스타트마다 200-800ms 추가
import { Button, TextField } from '@mui/material'
// 모듈 2,225개 로드, 개발 환경에서 약 4.2초 추가 소요
예시 (Good) ▼
import Check from 'lucide-react/dist/esm/icons/check'
import X from 'lucide-react/dist/esm/icons/x'
import Menu from 'lucide-react/dist/esm/icons/menu'
// 3개 모듈만 로드 (~2KB vs ~1MB)
import Button from '@mui/material/Button'
import TextField from '@mui/material/TextField'
// 사용하는 항목만 로드
2.2 Conditional Module Loading
- 핵심: 기능이 켜졌을 때만 관련 모듈을 로딩한다.
- 왜 중요한가: 초기 번들에서 미사용 코드를 제거할 수 있다.
- 코드리뷰 신호: rarely-used 기능 코드가 항상 초기 로드된다.
rarely-used 기능 코드가 항상 초기 로드된다 코드는 보통 구현 편의 때문에 생기지만, 트래픽이 늘면 병목으로 확대된다. 이 항목의 핵심은 문법 변경이 아니라 실행/렌더 흐름을 정리해 기능이 켜졌을 때만 관련 모듈을 로딩한다를 기본값으로 만드는 데 있다.
변경 뒤에는 번들 분석 결과(엔트리 청크, 라우트별 청크)와 초기 JS 파싱 시간과 회귀 테스트를 같이 보면서 초기 번들에서 미사용 코드를 제거할 수 있다 결과가 재현되는지 점검한다.
예시 (Bad) ▼
import MonacoEditor from '@monaco-editor/react'
function SettingsPage() {
const [open, setOpen] = useState(false)
return (
<>
<button onClick={() => setOpen(true)}>에디터 열기</button>
{open ? <MonacoEditor height="60vh" defaultLanguage="typescript" /> : null}
</>
)
}
예시 (Good) ▼
import dynamic from 'next/dynamic'
const MonacoEditor = dynamic(() => import('@monaco-editor/react'), {
ssr: false,
loading: () => <div>에디터 로딩 중...</div>
})
function SettingsPage() {
const [open, setOpen] = useState(false)
return (
<>
<button onClick={() => setOpen(true)}>에디터 열기</button>
{open ? <MonacoEditor height="60vh" defaultLanguage="typescript" /> : null}
</>
)
}
2.3 Defer Non-Critical Third-Party Libraries
- 핵심: 분석/로그 SDK는 hydration 이후에 로딩한다.
- 왜 중요한가: 핵심 인터랙션 경로를 먼저 확보할 수 있다.
- 코드리뷰 신호: 레이아웃/루트에서 비핵심 SDK를 동기적으로 불러온다.
코드리뷰에서 레이아웃/루트에서 비핵심 SDK를 동기적으로 불러온다 신호가 보이면 먼저 책임 경계를 다시 나누는 것이 우선이다. 책임이 정리되면 초기 경로에 꼭 필요한 코드와 지연 로딩 가능한 코드를 먼저 분리한다 이 원칙을 자연스럽게 적용할 수 있고, 분석/로그 SDK는 hydration 이후에 로딩한다 규칙도 팀 기준으로 고정하기 쉬워진다.
마지막으로 번들 분석 결과(엔트리 청크, 라우트별 청크)와 초기 JS 파싱 시간을 기준으로 핵심 인터랙션 경로를 먼저 확보할 수 있다 개선이 체감과 일치하는지 확인한다.
예시 (Bad) ▼
import { Analytics } from '@vercel/analytics/react'
export default function RootLayout({ children }) {
return (
<html>
<body>
{children}
<Analytics />
</body>
</html>
)
}
예시 (Good) ▼
import dynamic from 'next/dynamic'
const Analytics = dynamic(
() => import('@vercel/analytics/react').then(m => m.Analytics),
{ ssr: false }
)
export default function RootLayout({ children }) {
return (
<html>
<body>
{children}
<Analytics />
</body>
</html>
)
}
2.4 Dynamic Imports for Heavy Components
- 핵심: 무거운 UI는 next/dynamic으로 지연 로딩한다.
- 왜 중요한가: 초기 청크를 줄여 LCP/TTI를 개선한다.
- 코드리뷰 신호: 에디터/차트가 초기 번들에 포함된다.
에디터/차트가 초기 번들에 포함된다 징후는 기능을 빠르게 추가할 때 쉽게 누적된다. 먼저 초기 경로에 꼭 필요한 코드와 지연 로딩 가능한 코드를 먼저 분리한다. 이렇게 구조를 잡아두면 무거운 UI는 next/dynamic으로 지연 로딩한다 원칙을 예외 없이 유지하기가 훨씬 쉬워진다.
적용 후에는 번들 분석 결과(엔트리 청크, 라우트별 청크)와 초기 JS 파싱 시간을 확인해 초기 청크를 줄여 LCP/TTI를 개선한다 효과가 실제로 나타나는지 검증한다.
예시 (Bad) ▼
import { MonacoEditor } from './monaco-editor'
function CodePanel({ code }: { code: string }) {
return <MonacoEditor value={code} />
}
예시 (Good) ▼
import dynamic from 'next/dynamic'
const MonacoEditor = dynamic(
() => import('./monaco-editor').then(m => m.MonacoEditor),
{ ssr: false }
)
function CodePanel({ code }: { code: string }) {
return <MonacoEditor value={code} />
}
2.5 Preload Based on User Intent
- 핵심: hover/focus 같은 의도 신호에서 preload/prefetch를 건다.
- 왜 중요한가: 클릭 이후 대기 시간을 체감 전에 없앨 수 있다.
- 코드리뷰 신호: 클릭 시점에 처음으로 대형 청크 로딩이 시작된다.
실무에서 클릭 시점에 처음으로 대형 청크 로딩이 시작된다 부분만 단발성으로 고치면 같은 문제가 다시 나오기 쉽다. 작은 단위(한 페이지, 한 훅, 한 API)부터 hover/focus 같은 의도 신호에서 preload/prefetch를 건다 패턴을 적용하고, 경계 케이스를 함께 정리해야 회귀를 줄일 수 있다. 이후 번들 분석 결과(엔트리 청크, 라우트별 청크)와 초기 JS 파싱 시간을 배포 전후로 비교해 클릭 이후 대기 시간을 체감 전에 없앨 수 있다 개선이 지속되는지 확인한다.
예시 (Bad) ▼
import dynamic from 'next/dynamic'
const CheckoutModal = dynamic(() => import('./CheckoutModal'))
function CheckoutButton() {
const [open, setOpen] = useState(false)
return (
<>
<button onClick={() => setOpen(true)}>결제하기</button>
{open ? <CheckoutModal /> : null}
</>
)
}
예시 (Good) ▼
import dynamic from 'next/dynamic'
const loadCheckoutModal = () => import('./CheckoutModal')
const CheckoutModal = dynamic(loadCheckoutModal)
function CheckoutButton() {
const [open, setOpen] = useState(false)
return (
<>
<button
onMouseEnter={() => void loadCheckoutModal()}
onFocus={() => void loadCheckoutModal()}
onClick={() => setOpen(true)}
>
결제하기
</button>
{open ? <CheckoutModal /> : null}
</>
)
}
'Develop > Web' 카테고리의 다른 글
| [React Best Practices] 1. 워터폴 제거 (0) | 2026.03.29 |
|---|---|
| [Web][React] 디자인 시스템 JSDoc에 스크린샷 자동 주입하기 (1) | 2026.01.13 |
| [Web] JSDoc과 Storybook으로 컴포넌트 문서화 (0) | 2025.12.25 |
| [Web] 시맨틱 태그 적용 작업 (2) | 2025.12.12 |
| [Web][Issue] MUI의 INP 성능 저하 문제 (0) | 2025.12.12 |