비동기 최적화
Promise.all()로 독립적인 비동기 작업 병렬 처리하기.
서로 의존성이 없는 비동기 작업들은 Promise.all()을 사용해 동시에 실행하여 성능을 2-10배 개선할 수 있습니다.
Incorrect
const user = await fetchUser() // 1초 대기 const posts = await fetchPosts() // 1초 대기 const comments = await fetchComments() // 1초 대기 // 총 3초 소요
- 각 요청이 이전 요청이 완료될 때까지 기다림
- 3개의 독립적인 API호출이 순차적으로 실행됨.
- 불필요한 대기 시간 발생
Correct
const [user, posts, comments] = await Promise.all([ fetchUser(), fetchPosts(), fetchComments() ])
- 세 요청이 동시에 시작됨
- 네트워크 왕복(round trip)을 3번에서 1번으로 최적화
React Query에서는 기본적으로 자동으로 병렬 실행됨. (Parallel Queries | TanStack Query React Docs)
번들 최적화
Barrel File Imports 피하기
Barrel File에서 import 하면 수천 개의 불필요한 모듈까지 로드되어 개발, 빌드 속도가 크게 느려짐.
Direct imports provide 15-70% faster dev boot, 28% faster builds, 40% faster cold starts, and significantly faster HMR.
Barrel File이란?
여러 파일들을 한 곳에서 re-export 하는 진입점 파일.
export * from './Button' export * from './Input' export * from './Modal' export * from './Card'
Incorrect
import { Check, X, Menu } from 'lucide-react' import { Button, TextField } from '@mui/material'
- 개발 서버에서 추가적으로 시간이 소요됨 (더 많은 파일들을 로드해야됨)
- 필요한건 몇 개 안되지만 더 많은 용량을 결적으로 로드하게됨.
Correct
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' // Loads only 3 modules (~2KB vs ~1MB) import Button from '@mui/material/Button' import TextField from '@mui/material/TextField' // Loads only what you use ```
Conditional Module Loading - 조건부 모듈 로딩
큰 데이터나 모듈을 기능이 실제로 사용될 때만 로드하여 초기 번들 크기를 줄이고 성능을 개선. 특히 선택적 기능이나 무거운 라이브러리에 효과적.
function AnimationPlayer({ enabled, setEnabled }: { enabled: boolean; setEnabled: React.Dispatch<React.SetStateAction<boolean>> }) { const [frames, setFrames] = useState<Frame[] | null>(null) useEffect(() => { // 1. enabled가 true일 때만 // 2. frames가 아직 없을 때만 // 3. 브라우저 환경일 때만 if (enabled && !frames && typeof window !== 'undefined') { import('./animation-frames.js') .then(mod => setFrames(mod.frames)) .catch(() => setEnabled(false)) } }, [enabled, frames, setEnabled]) if (!frames) return <Skeleton /> return <Canvas frames={frames} /> }
import('./animation-frames.js') // 런타임에 필요할 때만 로드 // vs import frames from './animation-frames.js' // 초기 번들에 포함
Dynamic Imports for Heavy Components
무거운 컴포넌트의 경우에는 동적 로딩. 초기 번들에는 불필요한 경우에 사용
import dynamic from 'next/dynamic' // 페이지 로드될 때 로드, 초기 번들에는 제외 const MonacoEditor = dynamic( () => import('./monaco-editor'), { ssr: false } ) function CodeEditor() { return <MonacoEditor /> }
Conditional의 경우에는 사용자 액션으로 특정한 경우에서만 로드 되는 컴포넌트.
Dynamic Import의 경우에는 사용자 액션보다는 항상 그 컴포넌트에 가면 액션과 별개로 무조건 로드 되는 경우.
import dynamic from 'next/dynamic' // Dynamic Import: 탭 컴포넌트는 항상 필요 (하지만 무거움) const Tabs = dynamic(() => import('./Tabs')) function Dashboard() { const [activeTab, setActiveTab] = useState('overview') const [ChartLib, setChartLib] = useState(null) // Conditional Loading: 차트는 "분석" 탭에서만 필요 useEffect(() => { if (activeTab === 'analytics' && !ChartLib) { import('chart.js').then(mod => setChartLib(mod)) } }, [activeTab, ChartLib]) return ( <Tabs onChange={setActiveTab}> <TabPanel value="overview"> <Overview /> </TabPanel> <TabPanel value="analytics"> {ChartLib ? <Analytics chartLib={ChartLib} /> : <Skeleton />} </TabPanel> </Tabs> ) }
localStorage 데이터 버전 관리와 최소화
localStorage에 데이터를 저장할 때 버전 prefix를 추가하고 필요한 필드만 저장하여 스키마 충돌을 방지하고 저장 공간을 절약. 또한 try-catch로 에러처리 해야됨.
Incorrect
// No version, stores everything, no error handling localStorage.setItem('userConfig', JSON.stringify(fullUserObject)) const data = localStorage.getItem('userConfig')
Correct : 버전 prefix 추가
const VERSION = 'v2' function saveConfig(config: { theme: string; language: string }) { try { // 키에 버전 포함 localStorage.setItem(`userConfig:${VERSION}`, JSON.stringify(config)) } catch (error) { // 시크릿 모드, 용량 초과, localStorage 비활성화 대응 console.error('localStorage 저장 실패:', error) } } function loadConfig() { try { const data = localStorage.getItem(`userConfig:${VERSION}`) return data ? JSON.parse(data) : null } catch (error) { console.error('localStorage 읽기 실패:', error) return null } }
Correct: 필요한 필드만 저장
interface FullUser { id: number email: string name: string avatar: string token: string //저장하면 안 됨 preferences: { theme: string notifications: boolean language: string timezone: string // ... 20개 이상의 필드 } } // Incorrect function cacheUser(user: FullUser) { localStorage.setItem('user:v1', JSON.stringify(user)) // → token, 민감 정보 노출 위험 // → 불필요한 데이터로 용량 낭비 } // Correct function cachePrefs(user: FullUser) { try { localStorage.setItem('prefs:v1', JSON.stringify({ theme: user.preferences.theme, notifications: user.preferences.notifications, language: user.preferences.language })) } catch (error) { console.error('환경설정 저장 실패:', error) } }
렌더링 최적화
CSS content-visibility로 긴 리스트 최적화
content-visibility: auto를 사용하면 화면 밖 요소의 렌더링을 건너뛰어 초기 렌더링 속도를 10배 이상 개선할 수 있음.
요소가 화면에 보이지 않으면 렌더링을 작업을 건너뛰게 할 수 있는 CSS 속성
**CSS** .message-item { content-visibility: auto; contain-intrinsic-size: 0 80px; } function MessageList({ messages }: { messages: Message[] }) { return ( <div className="overflow-y-auto h-screen"> {messages.map(msg => ( <div key={msg.id} className="message-item"> <Avatar user={msg.author} /> <div>{msg.content}</div> </div> ))} </div> ) }
가상 스크롤(Virtual Scroll) 라이브러리와 비교.
- 가상스크롤의 경우에 더 강력한 성능→(DOM 요소 자체를 제거)
- 수만 개 항목도 처리 가능
- 정확한 스크롤 위치 제어 가능
- 라이브러리 의존성, 코드 복잡도 증가
Content-visibility
- CSS 한 줄로 간단하게 적용가능.
- 동적 높이 처리 쉬움
- 성능은 낮음
- 브라우저 지원 제한적
Functional setState Updates 사용하기
현재 상태 값을 기반으로 state를 업데이트할 때는 함수형 업데이트를 사용해야 한다. 이를 통해 stale closure 버그를 방지하고, 안정적인 콜백 참조를 유지하며, 불필요한 리렌더링을 막을 수 있다.
Incorrect
function TodoList() { const [items, setItems] = useState(initialItems) // items가 변경될 때마다 콜백 재생성 const addItems = useCallback((newItems: Item[]) => { setItems([...items, ...newItems]) }, [items]) // ❌ items dependency causes recreations // Stale Closure -> 초기 items만 참조 const removeItem = useCallback((id: string) => { setItems(items.filter(item => item.id !== id)) }, []) // ❌ Missing items dependency - will use stale items! return <ItemsEditor items={items} onAdd={addItems} onRemove={removeItem} /> }
Correct
function TodoList() { const [items, setItems] = useState(initialItems) // Stable callback, never recreated const addItems = useCallback((newItems: Item[]) => { setItems(curr => [...curr, ...newItems]) }, []) // ✅ No dependencies needed // Always uses latest state, no stale closure risk const removeItem = useCallback((id: string) => { setItems(curr => curr.filter(item => item.id !== id)) }, []) // ✅ Safe and stable return <ItemsEditor items={items} onAdd={addItems} onRemove={removeItem} />
Benefits
- Stable callback references - 안정적인 콜백 참조
- No stale closures - 오래된 클로저 X, 항상 최신 상태 값으로만 동작
- Fewer dependencies - 더 적은 의존성, 의존성 배열을 단순화하고 메모리 누수를 줄인다.
- Prevents bugs - 버그 방지, 리액트에서 가장 흔한 클로저 버그의 원인을 제거한다.