1. 렌더링 최적화 - 화면을 빠르게 그리는 기술
메모이제이션(Memoization) - 똑같은 계산은 한 번만
메모이제이션은 한 번 계산한 결과를 메모리에 저장해두고, 똑같은 계산이 필요할 때 저장된 값을 재사용하는 기법입니다. 마치 수학 문제를 풀 때 이미 계산한 답을 옆에 적어두고 다시 쓰는 것과 같습니다.
- 실제 구현 방법
// React에서 useMemo 사용 예시 const expensiveCalculation = useMemo(() => { // 복잡한 계산 로직 return heavyComputation(data); }, [data]); // data가 변경될 때만 재계산 - useCallback으로 함수 메모이제이션
const handleClick = useCallback(() => { // 클릭 처리 로직 }, [dependency]); // dependency가 변경될 때만 함수 재생성 - 언제 사용해야 하나요?
- 복잡한 계산이 반복되는 경우
- 자식 컴포넌트에 함수를 props로 전달할 때
- 리스트 렌더링에서 각 아이템이 무거운 연산을 포함할 때
React.memo - 컴포넌트 전체를 기억하기
React.memo는 컴포넌트 자체를 메모이제이션합니다. props가 변경되지 않았다면 컴포넌트를 다시 렌더링하지 않고 이전 결과를 재사용합니다.
- 기본 사용법
const MyComponent = React.memo(function MyComponent({ name, age }) { return <div>{name}님의 나이는 {age}살입니다</div>; }); - 커스텀 비교 함수 활용
const MyComponent = React.memo( function MyComponent(props) { // 컴포넌트 로직 }, (prevProps, nextProps) => { // true를 반환하면 리렌더링 건너뛰기 return prevProps.id === nextProps.id; } ); - 주의사항
- 모든 컴포넌트에 무작정 적용하면 오히려 성능 저하
- props 비교 자체도 비용이 들기 때문에 신중하게 사용
- 자주 변경되는 props를 가진 컴포넌트에는 비효율적
리렌더링 방지 전략
불필요한 리렌더링은 웹앱 성능의 가장 큰 적입니다. 화면의 작은 부분만 바뀌어도 전체가 다시 그려지는 것을 방지해야 합니다.
- 상태 분리하기
// 나쁜 예 - 전체 폼이 리렌더링 const Form = () => { const [formData, setFormData] = useState({ name: '', email: '', age: '' }); }; // 좋은 예 - 각 필드가 독립적으로 관리 const NameField = () => { const [name, setName] = useState(''); }; - Context 분할하기
// UserContext와 ThemeContext를 분리 const UserProvider = ({ children }) => { /* ... */ }; const ThemeProvider = ({ children }) => { /* ... */ }; - key 속성 최적화
// 인덱스 대신 고유한 ID 사용 items.map(item => <Item key={item.id} />)
2. 이벤트 처리 최적화 - 과도한 실행 막기
디바운싱(Debouncing) - 마지막 액션만 처리
디바운싱은 연속적으로 발생하는 이벤트 중 마지막 이벤트만 처리하는 기법입니다. 검색창에 타이핑할 때마다 API를 호출하면 서버에 부담이 되는데, 디바운싱을 사용하면 사용자가 타이핑을 멈춘 후에만 검색을 실행합니다.
- 구현 예시
function debounce(func, delay) { let timeoutId; return function(...args) { clearTimeout(timeoutId); timeoutId = setTimeout(() => func.apply(this, args), delay); }; } // 사용 예시 const searchAPI = debounce((query) => { // API 호출 로직 }, 500); // 0.5초 대기 - React Hook으로 구현
function useDebounce(value, delay) { const [debouncedValue, setDebouncedValue] = useState(value); useEffect(() => { const handler = setTimeout(() => { setDebouncedValue(value); }, delay); return () => clearTimeout(handler); }, [value, delay]); return debouncedValue; } - 실제 활용 사례
- 검색 자동완성
- 폼 유효성 검사
- 윈도우 리사이즈 이벤트 처리
쓰로틀링(Throttling) - 일정 간격으로만 실행
쓰로틀링은 특정 시간 간격으로만 함수가 실행되도록 제한합니다. 스크롤 이벤트처럼 매우 빈번하게 발생하는 이벤트를 처리할 때 유용합니다.
- 구현 방법
function throttle(func, limit) { let inThrottle; return function(...args) { if (!inThrottle) { func.apply(this, args); inThrottle = true; setTimeout(() => inThrottle = false, limit); } }; } - 스크롤 이벤트 최적화
const handleScroll = throttle(() => { // 스크롤 위치 계산 및 처리 }, 100); // 100ms마다 실행 window.addEventListener('scroll', handleScroll); - 디바운싱 vs 쓰로틀링 선택 기준
- 디바운싱: 사용자 입력이 끝난 후 처리 (검색, 저장)
- 쓰로틀링: 지속적인 업데이트 필요 (스크롤, 드래그)
페이지네이션 - 데이터를 나누어 보여주기
대량의 데이터를 한 번에 모두 로드하면 초기 로딩이 느려지고 메모리를 많이 사용합니다. 페이지네이션으로 필요한 만큼만 보여주면 성능이 크게 향상됩니다.
- 기본 페이지네이션
const itemsPerPage = 20; const currentItems = data.slice( (currentPage - 1) * itemsPerPage, currentPage * itemsPerPage ); - 무한 스크롤 구현
const InfiniteScroll = () => { const [items, setItems] = useState([]); const [page, setPage] = useState(1); const loadMore = useCallback(() => { // 다음 페이지 데이터 로드 fetchData(page + 1).then(newItems => { setItems(prev => [...prev, ...newItems]); setPage(prev => prev + 1); }); }, [page]); // Intersection Observer로 스크롤 감지 }; - 가상 스크롤링
- 화면에 보이는 항목만 렌더링
- react-window, react-virtualized 라이브러리 활용
- 수천 개의 항목도 부드럽게 스크롤 가능
3. 메모리 관리 - 누수 방지와 효율적 사용
메모리 누수 방지
메모리 누수는 더 이상 필요 없는 데이터가 메모리에서 해제되지 않는 현상입니다. 시간이 지날수록 앱이 느려지고 결국 멈출 수도 있습니다.
- 이벤트 리스너 정리
useEffect(() => { const handleResize = () => { /* ... */ }; window.addEventListener('resize', handleResize); // cleanup 함수에서 반드시 제거 return () => { window.removeEventListener('resize', handleResize); }; }, []); - 타이머 정리
useEffect(() => { const timer = setInterval(() => { // 주기적 작업 }, 1000); return () => clearInterval(timer); }, []); - 구독 해제
useEffect(() => { const subscription = dataSource.subscribe(handleChange); return () => { subscription.unsubscribe(); }; }, []);
가비지 컬렉션 최적화
자바스크립트는 자동으로 메모리를 관리하지만, 개발자가 도울 수 있는 부분이 있습니다.
- 순환 참조 방지
// 나쁜 예 - 순환 참조 const obj1 = {}; const obj2 = {}; obj1.ref = obj2; obj2.ref = obj1; // 좋은 예 - WeakMap 사용 const weakMap = new WeakMap(); weakMap.set(obj1, obj2); - 전역 변수 최소화
// 나쁜 예 window.myGlobalData = hugeArray; // 좋은 예 - 모듈 스코프 사용 const moduleData = hugeArray; export { moduleData }; - 대용량 데이터 처리 후 정리
let bigData = processLargeDataset(); // 사용 완료 후 bigData = null; // 명시적으로 참조 해제
4. 번들 최적화 - 다운로드 크기 줄이기
코드 스플리팅
애플리케이션을 여러 조각으로 나누어 필요한 시점에만 로드하는 기법입니다. 초기 로딩 속도가 크게 향상됩니다.
- 라우트 기반 분할
const Home = lazy(() => import('./pages/Home')); const About = lazy(() => import('./pages/About')); function App() { return ( <Suspense fallback={<Loading />}> <Routes> <Route path="/" element={<Home />} /> <Route path="/about" element={<About />} /> </Routes> </Suspense> ); } - 컴포넌트 레벨 분할
const HeavyComponent = lazy(() => import('./HeavyComponent')); function MyComponent() { const [showHeavy, setShowHeavy] = useState(false); return ( <> <button onClick={() => setShowHeavy(true)}> 무거운 컴포넌트 보기 </button> {showHeavy && ( <Suspense fallback={<div>로딩중...</div>}> <HeavyComponent /> </Suspense> )} </> ); } - 동적 import 활용
button.addEventListener('click', async () => { const module = await import('./heavyModule.js'); module.doSomething(); });
레이지 로딩
리소스를 필요한 시점에 로드하여 초기 로딩 부담을 줄입니다.
- 이미지 레이지 로딩
const LazyImage = ({ src, alt }) => { const [imageSrc, setImageSrc] = useState(null); const imgRef = useRef(); useEffect(() => { const observer = new IntersectionObserver( entries => { entries.forEach(entry => { if (entry.isIntersecting) { setImageSrc(src); observer.unobserve(entry.target); } }); } ); if (imgRef.current) { observer.observe(imgRef.current); } return () => observer.disconnect(); }, [src]); return <img ref={imgRef} src={imageSrc} alt={alt} />; }; - 컴포넌트 레이지 로딩
const LazySection = () => { const [isVisible, setIsVisible] = useState(false); const sectionRef = useRef(); useEffect(() => { const observer = new IntersectionObserver( ([entry]) => { if (entry.isIntersecting) { setIsVisible(true); } }, { threshold: 0.1 } ); if (sectionRef.current) { observer.observe(sectionRef.current); } }, []); return ( <div ref={sectionRef}> {isVisible && <ExpensiveComponent />} </div> ); };
트리 쉐이킹
사용하지 않는 코드를 번들에서 제거하여 파일 크기를 줄입니다.
- ES6 모듈 사용
// 나쁜 예 - 전체 라이브러리 import import _ from 'lodash'; // 좋은 예 - 필요한 함수만 import import debounce from 'lodash/debounce'; - 사이드 이펙트 표시
// package.json { "sideEffects": false, // 또는 특정 파일만 지정 "sideEffects": ["*.css", "*.scss"] } - Production 빌드 설정
// webpack.config.js module.exports = { mode: 'production', optimization: { usedExports: true, minimize: true, sideEffects: false } };
5. 캐싱 전략 - 한 번 받은 데이터 재활용
브라우저 캐싱
브라우저의 캐싱 기능을 활용하면 반복적인 리소스 다운로드를 줄일 수 있습니다.
- HTTP 캐시 헤더 설정
// 서버 응답 헤더 res.setHeader('Cache-Control', 'public, max-age=31536000'); // 1년 res.setHeader('ETag', '"123456"'); // 버전 관리 - Service Worker 캐싱
self.addEventListener('install', event => { event.waitUntil( caches.open('v1').then(cache => { return cache.addAll([ '/', '/styles/main.css', '/scripts/app.js' ]); }) ); }); self.addEventListener('fetch', event => { event.respondWith( caches.match(event.request).then(response => { return response || fetch(event.request); }) ); }); - localStorage 활용
// 데이터 캐싱 const getCachedData = (key) => { const cached = localStorage.getItem(key); if (cached) { const { data, timestamp } = JSON.parse(cached); const isExpired = Date.now() - timestamp > 3600000; // 1시간 if (!isExpired) return data; } return null; }; const setCachedData = (key, data) => { localStorage.setItem(key, JSON.stringify({ data, timestamp: Date.now() })); };
CDN 활용
CDN을 사용하면 사용자와 가까운 서버에서 콘텐츠를 제공받아 로딩 속도가 향상됩니다.
- 정적 자산 CDN 배포
<!-- 라이브러리 CDN 사용 --> <script src="<https://cdn.jsdelivr.net/npm/react@18/umd/react.production.min.js>"></script> <!-- 이미지 CDN --> <img src="<https://cdn.example.com/images/hero.jpg>" /> - CDN 캐시 무효화
// 버전 관리로 캐시 갱신 const assetUrl = `https://cdn.example.com/app.js?v=${VERSION}`;
6. 렌더링 성능 극대화
가상 DOM 최적화
React나 Vue 같은 프레임워크는 가상 DOM을 사용하여 실제 DOM 조작을 최소화합니다.
- 효율적인 리스트 렌더링
// key 속성으로 요소 추적 {items.map(item => ( <ListItem key={item.id} // 안정적인 고유 키 사용 data={item} /> ))} - 조건부 렌더링 최적화
// 나쁜 예 - 매번 새로운 컴포넌트 생성 {isVisible ? <Component /> : null} // 좋은 예 - display 속성 활용 <Component style={{ display: isVisible ? 'block' : 'none' }} />
리플로우/리페인트 최소화
DOM 변경 시 브라우저가 레이아웃을 다시 계산(리플로우)하고 화면을 다시 그리는(리페인트) 과정을 최소화해야 합니다.
- DOM 조작 배치 처리
// 나쁜 예 - 여러 번 리플로우 발생 element.style.left = '10px'; element.style.top = '10px'; element.style.width = '100px'; // 좋은 예 - 한 번에 처리 element.style.cssText = 'left: 10px; top: 10px; width: 100px;'; - GPU 가속 활용
/* transform과 opacity는 GPU에서 처리 */ .animated { transform: translateX(100px); opacity: 0.8; will-change: transform, opacity; } - 레이아웃 스래싱 방지
// 나쁜 예 - 읽기와 쓰기 반복 elements.forEach(el => { el.style.height = el.offsetHeight + 10 + 'px'; }); // 좋은 예 - 읽기 후 쓰기 const heights = elements.map(el => el.offsetHeight); elements.forEach((el, i) => { el.style.height = heights[i] + 10 + 'px'; });
Critical Rendering Path 최적화
브라우저가 HTML을 받아서 화면에 픽셀을 그리기까지의 과정을 최적화합니다.
- 리소스 로딩 순서 최적화
<!DOCTYPE html> <html> <head> <!-- 중요한 CSS는 인라인으로 --> <style> .critical { /* 초기 렌더링에 필요한 스타일 */ } </style> <!-- 나머지 CSS는 비동기 로드 --> <link rel="preload" href="styles.css" as="style" onload="this.rel='stylesheet'"> <!-- 중요하지 않은 JS는 defer --> <script defer src="app.js"></script> </head> <body> <!-- 콘텐츠 --> <!-- 분석 스크립트는 최하단 --> <script async src="analytics.js"></script> </body> </html> - 리소스 힌트 활용
<!-- DNS 미리 연결 --> <link rel="dns-prefetch" href="//api.example.com"> <!-- 연결 미리 설정 --> <link rel="preconnect" href="//api.example.com"> <!-- 리소스 미리 가져오기 --> <link rel="prefetch" href="next-page.js"> <!-- 중요 리소스 미리 로드 --> <link rel="preload" href="font.woff2" as="font" crossorigin>
7. 최적화를 위한 용어 - 이걸 알아야 AI 에게 지시 가능
- 렌더링 최적화
메모이제이션, React.memo, useMemo, useCallback, 리렌더링, shouldComponentUpdate, PureComponent, 가상DOM, 리플로우, 리페인트, 레이아웃스래싱, CriticalRenderingPath, FOUC, FOIT, 프레임드롭, requestAnimationFrame, will-change, GPU가속, 컴포지팅, 레이어생성
- 번들 및 로딩 최적화
코드스플리팅, 레이지로딩, 트리쉐이킹, 번들링, 미니파이, 압축, gzip, brotli, 청크, 동적임포트, prefetch, preload, preconnect, dns-prefetch, async, defer, 모듈페더레이션, 웹팩최적화, 롤업, 파셀, Vite, esbuild
- 이벤트 처리 최적화
디바운싱, 쓰로틀링, 이벤트위임, 이벤트버블링, 이벤트캡처링, 패시브리스너, 이벤트풀링, 배치업데이트, RAF쓰로틀링
- 데이터 처리 최적화
페이지네이션, 무한스크롤, 가상스크롤, 윈도잉, 커서페이지네이션, 오프셋페이지네이션, 데이터정규화, 데이터캐싱, 옵티미스틱업데이트, 낙관적UI
- 메모리 관리
메모리누수, 가비지컬렉션, 메모리프로파일링, 힙스냅샷, WeakMap, WeakSet, 순환참조, 클로저누수, 이벤트리스너정리, 타이머정리, 옵저버해제, 메모리풀링, 객체풀링
- 캐싱 전략
브라우저캐싱, HTTP캐싱, 서비스워커, 캐시무효화, ETag, Last-Modified, Cache-Control, localStorage, sessionStorage, IndexedDB, 메모리캐시, 디스크캐시, CDN캐싱, 엣지캐싱, 캐시워밍, 캐시히트율
- 네트워크 최적화
CDN, 엣지서버, HTTP2, HTTP3, QUIC, 멀티플렉싱, 서버푸시, 헤더압축, 커넥션풀링, Keep-Alive, 도메인샤딩, 리소스힌트, 프리페칭, 배치요청, GraphQL, 데이터로더
- 이미지 최적화
이미지압축, WebP, AVIF, 반응형이미지, srcset, picture요소, 이미지스프라이트, Base64인코딩, 블러업, 프로그레시브JPEG, 이미지CDN, 이미지최적화API, 아트디렉션
- 폰트 최적화
폰트서브세팅, WOFF2, 폰트디스플레이, 폰트프리로드, 가변폰트, 폰트페이스옵저버, 로컬폰트, 시스템폰트스택, 폰트로딩전략
- 애니메이션 최적화
CSS애니메이션, 트랜지션, 트랜스폼, FLIP기법, 모션블러, 60FPS, 하드웨어가속, 컴포짓레이어, 페인트최적화, 레이아웃경계
- 상태 관리 최적화
상태정규화, 상태분리, 컨텍스트분할, 리듀서최적화, 셀렉터메모이제이션, 불변성, Immer, 구조적공유, 상태구독, 원자적상태
- 빌드 최적화
프로덕션빌드, 소스맵, 환경변수, 빌드캐싱, 증분빌드, 병렬빌드, 빌드타임최적화, 정적사이트생성, 서버사이드렌더링, 정적최적화
- 성능 측정
Lighthouse, WebVitals, LCP, FID, CLS, TTFB, FCP, TTI, SpeedIndex, 성능프로파일링, 크롬데브툴스, 성능모니터링, RUM, 합성모니터링
- API 최적화
RESTful, GraphQL, gRPC, WebSocket, SSE, 롱폴링, 폴링최적화, API게이트웨이, 레이트리미팅, API캐싱, 배치API, 페이지네이션API
- 모바일 최적화
터치최적화, 뷰포트설정, 모바일퍼스트, AMP, PWA, 오프라인지원, 앱셸모델, PRPL패턴, 적응형디자인, 터치제스처
- 보안 관련 최적화
CSP, CORS, XSS방지, CSRF보호, 서브리소스무결성, HTTPS, SSL최적화, 보안헤더, 난독화, 코드서명
- 서버 최적화
서버사이드캐싱, 리버스프록시, 로드밸런싱, 오토스케일링, 서버리스, 엣지컴퓨팅, 마이크로서비스, 컨테이너화, 클러스터링
- 데이터베이스 최적화
쿼리최적화, 인덱싱, 커넥션풀링, 읽기전용복제본, 샤딩, 파티셔닝, 캐싱레이어, NoSQL, 비정규화, 쿼리배칭