웹사이트가 느려서 사용자가 떠나간 경험이 있으신가요?
•
아무리 좋은 서비스도 속도가 느리면 사용자는 기다려주지 않습니다. 구글 연구에 따르면 페이지 로딩이 3초를 넘어가면 53%의 사용자가 이탈한다고 합니다.
•
오늘은 웹 애플리케이션의 속도를 비약적으로 향상시킬 수 있는 모든 최적화 기법을 하나하나 자세히 알아보겠습니다.
1. 렌더링 최적화 - 화면을 빠르게 그리는 기술
메모이제이션(Memoization) - 똑같은 계산은 한 번만
메모이제이션은 한 번 계산한 결과를 메모리에 저장해두고, 똑같은 계산이 필요할 때 저장된 값을 재사용하는 기법입니다. 마치 수학 문제를 풀 때 이미 계산한 답을 옆에 적어두고 다시 쓰는 것과 같습니다.
1.
실제 구현 방법
// React에서 useMemo 사용 예시
const expensiveCalculation = useMemo(() => {
// 복잡한 계산 로직
return heavyComputation(data);
}, [data]); // data가 변경될 때만 재계산
JavaScript
복사
2.
useCallback으로 함수 메모이제이션
const handleClick = useCallback(() => {
// 클릭 처리 로직
}, [dependency]); // dependency가 변경될 때만 함수 재생성
JavaScript
복사
3.
언제 사용해야 하나요?
•
복잡한 계산이 반복되는 경우
•
자식 컴포넌트에 함수를 props로 전달할 때
•
리스트 렌더링에서 각 아이템이 무거운 연산을 포함할 때
React.memo - 컴포넌트 전체를 기억하기
React.memo는 컴포넌트 자체를 메모이제이션합니다. props가 변경되지 않았다면 컴포넌트를 다시 렌더링하지 않고 이전 결과를 재사용합니다.
1.
기본 사용법
const MyComponent = React.memo(function MyComponent({ name, age }) {
return <div>{name}님의 나이는 {age}살입니다</div>;
});
JavaScript
복사
2.
커스텀 비교 함수 활용
const MyComponent = React.memo(
function MyComponent(props) {
// 컴포넌트 로직
},
(prevProps, nextProps) => {
// true를 반환하면 리렌더링 건너뛰기
return prevProps.id === nextProps.id;
}
);
JavaScript
복사
3.
주의사항
•
모든 컴포넌트에 무작정 적용하면 오히려 성능 저하
•
props 비교 자체도 비용이 들기 때문에 신중하게 사용
•
자주 변경되는 props를 가진 컴포넌트에는 비효율적
리렌더링 방지 전략
불필요한 리렌더링은 웹앱 성능의 가장 큰 적입니다. 화면의 작은 부분만 바뀌어도 전체가 다시 그려지는 것을 방지해야 합니다.
1.
상태 분리하기
// 나쁜 예 - 전체 폼이 리렌더링
const Form = () => {
const [formData, setFormData] = useState({ name: '', email: '', age: '' });
};
// 좋은 예 - 각 필드가 독립적으로 관리
const NameField = () => {
const [name, setName] = useState('');
};
JavaScript
복사
2.
Context 분할하기
// UserContext와 ThemeContext를 분리
const UserProvider = ({ children }) => { /* ... */ };
const ThemeProvider = ({ children }) => { /* ... */ };
JavaScript
복사
3.
key 속성 최적화
// 인덱스 대신 고유한 ID 사용
items.map(item => <Item key={item.id} />)
JavaScript
복사
2. 이벤트 처리 최적화 - 과도한 실행 막기
디바운싱(Debouncing) - 마지막 액션만 처리
디바운싱은 연속적으로 발생하는 이벤트 중 마지막 이벤트만 처리하는 기법입니다. 검색창에 타이핑할 때마다 API를 호출하면 서버에 부담이 되는데, 디바운싱을 사용하면 사용자가 타이핑을 멈춘 후에만 검색을 실행합니다.
1.
구현 예시
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초 대기
JavaScript
복사
2.
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;
}
JavaScript
복사
3.
실제 활용 사례
•
검색 자동완성
•
폼 유효성 검사
•
윈도우 리사이즈 이벤트 처리
쓰로틀링(Throttling) - 일정 간격으로만 실행
쓰로틀링은 특정 시간 간격으로만 함수가 실행되도록 제한합니다. 스크롤 이벤트처럼 매우 빈번하게 발생하는 이벤트를 처리할 때 유용합니다.
1.
구현 방법
function throttle(func, limit) {
let inThrottle;
return function(...args) {
if (!inThrottle) {
func.apply(this, args);
inThrottle = true;
setTimeout(() => inThrottle = false, limit);
}
};
}
JavaScript
복사
2.
스크롤 이벤트 최적화
const handleScroll = throttle(() => {
// 스크롤 위치 계산 및 처리
}, 100); // 100ms마다 실행
window.addEventListener('scroll', handleScroll);
JavaScript
복사
3.
디바운싱 vs 쓰로틀링 선택 기준
•
디바운싱: 사용자 입력이 끝난 후 처리 (검색, 저장)
•
쓰로틀링: 지속적인 업데이트 필요 (스크롤, 드래그)
페이지네이션 - 데이터를 나누어 보여주기
대량의 데이터를 한 번에 모두 로드하면 초기 로딩이 느려지고 메모리를 많이 사용합니다. 페이지네이션으로 필요한 만큼만 보여주면 성능이 크게 향상됩니다.
1.
기본 페이지네이션
const itemsPerPage = 20;
const currentItems = data.slice(
(currentPage - 1) * itemsPerPage,
currentPage * itemsPerPage
);
JavaScript
복사
2.
무한 스크롤 구현
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로 스크롤 감지
};
JavaScript
복사
3.
가상 스크롤링
•
화면에 보이는 항목만 렌더링
•
react-window, react-virtualized 라이브러리 활용
•
수천 개의 항목도 부드럽게 스크롤 가능
3. 메모리 관리 - 누수 방지와 효율적 사용
메모리 누수 방지
메모리 누수는 더 이상 필요 없는 데이터가 메모리에서 해제되지 않는 현상입니다. 시간이 지날수록 앱이 느려지고 결국 멈출 수도 있습니다.
1.
이벤트 리스너 정리
useEffect(() => {
const handleResize = () => { /* ... */ };
window.addEventListener('resize', handleResize);
// cleanup 함수에서 반드시 제거
return () => {
window.removeEventListener('resize', handleResize);
};
}, []);
JavaScript
복사
2.
타이머 정리
useEffect(() => {
const timer = setInterval(() => {
// 주기적 작업
}, 1000);
return () => clearInterval(timer);
}, []);
JavaScript
복사
3.
구독 해제
useEffect(() => {
const subscription = dataSource.subscribe(handleChange);
return () => {
subscription.unsubscribe();
};
}, []);
JavaScript
복사
가비지 컬렉션 최적화
자바스크립트는 자동으로 메모리를 관리하지만, 개발자가 도울 수 있는 부분이 있습니다.
1.
순환 참조 방지
// 나쁜 예 - 순환 참조
const obj1 = {};
const obj2 = {};
obj1.ref = obj2;
obj2.ref = obj1;
// 좋은 예 - WeakMap 사용
const weakMap = new WeakMap();
weakMap.set(obj1, obj2);
JavaScript
복사
2.
전역 변수 최소화
// 나쁜 예
window.myGlobalData = hugeArray;
// 좋은 예 - 모듈 스코프 사용
const moduleData = hugeArray;
export { moduleData };
JavaScript
복사
3.
대용량 데이터 처리 후 정리
let bigData = processLargeDataset();
// 사용 완료 후
bigData = null; // 명시적으로 참조 해제
JavaScript
복사
4. 번들 최적화 - 다운로드 크기 줄이기
코드 스플리팅
애플리케이션을 여러 조각으로 나누어 필요한 시점에만 로드하는 기법입니다. 초기 로딩 속도가 크게 향상됩니다.
1.
라우트 기반 분할
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>
);
}
JavaScript
복사
2.
컴포넌트 레벨 분할
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>
)}
</>
);
}
JavaScript
복사
3.
동적 import 활용
button.addEventListener('click', async () => {
const module = await import('./heavyModule.js');
module.doSomething();
});
JavaScript
복사
레이지 로딩
리소스를 필요한 시점에 로드하여 초기 로딩 부담을 줄입니다.
1.
이미지 레이지 로딩
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} />;
};
JavaScript
복사
2.
컴포넌트 레이지 로딩
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>
);
};
JavaScript
복사
트리 쉐이킹
사용하지 않는 코드를 번들에서 제거하여 파일 크기를 줄입니다.
1.
ES6 모듈 사용
// 나쁜 예 - 전체 라이브러리 import
import _ from 'lodash';
// 좋은 예 - 필요한 함수만 import
import debounce from 'lodash/debounce';
JavaScript
복사
2.
사이드 이펙트 표시
// package.json
{
"sideEffects": false, // 또는 특정 파일만 지정
"sideEffects": ["*.css", "*.scss"]
}
JSON
복사
3.
Production 빌드 설정
// webpack.config.js
module.exports = {
mode: 'production',
optimization: {
usedExports: true,
minimize: true,
sideEffects: false
}
};
JavaScript
복사
5. 캐싱 전략 - 한 번 받은 데이터 재활용
브라우저 캐싱
브라우저의 캐싱 기능을 활용하면 반복적인 리소스 다운로드를 줄일 수 있습니다.
1.
HTTP 캐시 헤더 설정
// 서버 응답 헤더
res.setHeader('Cache-Control', 'public, max-age=31536000'); // 1년
res.setHeader('ETag', '"123456"'); // 버전 관리
JavaScript
복사
2.
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);
})
);
});
JavaScript
복사
3.
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()
}));
};
JavaScript
복사
CDN 활용
CDN을 사용하면 사용자와 가까운 서버에서 콘텐츠를 제공받아 로딩 속도가 향상됩니다.
1.
정적 자산 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>" />
HTML
복사
2.
CDN 캐시 무효화
// 버전 관리로 캐시 갱신
const assetUrl = `https://cdn.example.com/app.js?v=${VERSION}`;
JavaScript
복사
6. 렌더링 성능 극대화
가상 DOM 최적화
React나 Vue 같은 프레임워크는 가상 DOM을 사용하여 실제 DOM 조작을 최소화합니다.
1.
효율적인 리스트 렌더링
// key 속성으로 요소 추적
{items.map(item => (
<ListItem
key={item.id} // 안정적인 고유 키 사용
data={item}
/>
))}
JavaScript
복사
2.
조건부 렌더링 최적화
// 나쁜 예 - 매번 새로운 컴포넌트 생성
{isVisible ? <Component /> : null}
// 좋은 예 - display 속성 활용
<Component style={{ display: isVisible ? 'block' : 'none' }} />
JavaScript
복사
리플로우/리페인트 최소화
DOM 변경 시 브라우저가 레이아웃을 다시 계산(리플로우)하고 화면을 다시 그리는(리페인트) 과정을 최소화해야 합니다.
1.
DOM 조작 배치 처리
// 나쁜 예 - 여러 번 리플로우 발생
element.style.left = '10px';
element.style.top = '10px';
element.style.width = '100px';
// 좋은 예 - 한 번에 처리
element.style.cssText = 'left: 10px; top: 10px; width: 100px;';
JavaScript
복사
2.
GPU 가속 활용
/* transform과 opacity는 GPU에서 처리 */
.animated {
transform: translateX(100px);
opacity: 0.8;
will-change: transform, opacity;
}
CSS
복사
3.
레이아웃 스래싱 방지
// 나쁜 예 - 읽기와 쓰기 반복
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';
});
JavaScript
복사
Critical Rendering Path 최적화
브라우저가 HTML을 받아서 화면에 픽셀을 그리기까지의 과정을 최적화합니다.
1.
리소스 로딩 순서 최적화
<!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>
HTML
복사
2.
리소스 힌트 활용
<!-- 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>
HTML
복사
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, 비정규화, 쿼리배칭
웹 애플리케이션 최적화는 단순히 하나의 기법만 적용한다고 해결되는 것이 아닙니다.
•
프로젝트의 특성과 사용자 환경을 고려하여 여러 최적화 기법을 조합해야 합니다. 성능 측정 도구를 활용하여 병목 지점을 찾고, 우선순위를 정해 하나씩 개선해 나가는 것이 중요합니다.
•
오늘 소개한 최적화 기법들을 프로젝트에 적용해보시고, 사용자에게 더 나은 경험을 제공하는 빠른 웹 애플리케이션을 만들어보세요.