JavaScript 성능 최적화 10가지 팁 – 개발자가 꼭 알아야 할 핵심 팁
도입 – 성능 최적화의 중요성
🔗 관련 에러 해결 가이드
현대 웹 애플리케이션에서 JavaScript 성능 최적화 10가지 팁은 사용자 경험과 직결되는 핵심 요소입니다. 페이지 로딩 속도가 1초 지연될 때마다 전환율이 7% 감소한다는 연구 결과가 있을 정도로, 성능 최적화는 비즈니스 성공의 필수 조건이 되었습니다. 이 글에서는 실무에서 즉시 적용 가능한 JavaScript 성능 최적화 기법들을 구체적인 코드 예시와 함께 살펴보겠습니다. 메모리 관리부터 렌더링 최적화까지, 각 팁은 실제 프로젝트에서 검증된 방법들입니다.
핵심 팁 10가지
1. 디바운싱과 스로틀링으로 이벤트 최적화
스크롤, 리사이즈, 입력 이벤트는 초당 수십 번 발생할 수 있습니다. 디바운싱(debouncing)은 이벤트가 멈춘 후 실행하고, 스로틀링(throttling)은 일정 간격으로 실행을 제한합니다. 검색 자동완성에는 디바운싱을, 무한 스크롤에는 스로틀링을 적용하면 불필요한 함수 호출을 95% 이상 줄일 수 있습니다.
// 디바운싱 구현
function debounce(func, delay) {
let timeoutId;
return function(...args) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => func.apply(this, args), delay);
};
}
// 스로틀링 구현
function throttle(func, limit) {
let inThrottle;
return function(...args) {
if (!inThrottle) {
func.apply(this, args);
inThrottle = true;
setTimeout(() => inThrottle = false, limit);
}
};
}
// 사용 예시
const searchInput = document.querySelector('#search');
searchInput.addEventListener('input', debounce((e) => {
fetchSearchResults(e.target.value);
}, 300));
2. 가상 스크롤링으로 대용량 리스트 렌더링
수천 개의 항목을 한 번에 렌더링하면 DOM 노드가 과도하게 생성되어 메모리 사용량이 급증합니다. 가상 스크롤링은 화면에 보이는 항목만 렌더링하여 성능을 극대적으로 개선합니다. 10,000개 항목 리스트에서 실제로는 20-30개만 DOM에 존재하게 되어 초기 렌더링 시간을 90% 이상 단축할 수 있습니다. React에서는 react-window, Vue에서는 vue-virtual-scroller 라이브러리를 활용할 수 있습니다.
// 간단한 가상 스크롤링 구현
class VirtualScroller {
constructor(container, items, itemHeight) {
this.container = container;
this.items = items;
this.itemHeight = itemHeight;
this.visibleCount = Math.ceil(container.clientHeight / itemHeight);
container.addEventListener('scroll', () => this.render());
this.render();
}
render() {
const scrollTop = this.container.scrollTop;
const startIndex = Math.floor(scrollTop / this.itemHeight);
const endIndex = startIndex + this.visibleCount;
const visibleItems = this.items.slice(startIndex, endIndex);
this.container.innerHTML = visibleItems.map((item, i) =>
`${item}`
).join('');
}
}
3. 이미지 지연 로딩(Lazy Loading) 구현
페이지에 포함된 모든 이미지를 초기에 로드하면 초기 로딩 시간이 크게 증가합니다. Intersection Observer API를 활용한 지연 로딩은 뷰포트에 진입할 때만 이미지를 로드하여 초기 페이지 로드 시간을 50% 이상 단축합니다. 네이티브 loading=’lazy’ 속성도 지원하지만, 더 세밀한 제어가 필요할 때는 직접 구현이 유리합니다. 저화질 placeholder를 먼저 보여주는 LQIP 기법과 결합하면 사용자 경험이 더욱 개선됩니다.
// Intersection Observer를 활용한 이미지 지연 로딩
const imageObserver = new IntersectionObserver((entries, observer) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src;
img.classList.add('loaded');
observer.unobserve(img);
}
});
}, {
rootMargin: '50px' // 뷰포트 50px 전에 미리 로드
});
document.querySelectorAll('img[data-src]').forEach(img => {
imageObserver.observe(img);
});
4. 웹 워커로 무거운 연산 분리
JavaScript는 싱글 스레드로 동작하므로 복잡한 계산이 UI를 블로킹합니다. Web Worker를 사용하면 백그라운드 스레드에서 연산을 처리하여 메인 스레드의 응답성을 유지할 수 있습니다. 대용량 데이터 처리, 이미지 조작, 암호화 작업 등에 활용하면 UI가 멈추지 않고 부드럽게 동작합니다. 워커와의 통신은 postMessage로 이루어지며, 직렬화 가능한 데이터만 전달할 수 있다는 점을 유의해야 합니다.
// worker.js
self.addEventListener('message', (e) => {
const { data } = e;
// 무거운 연산 수행
const result = data.map(item => complexCalculation(item));
self.postMessage(result);
});
// main.js
const worker = new Worker('worker.js');
worker.postMessage(largeDataArray);
worker.addEventListener('message', (e) => {
console.log('연산 완료:', e.data);
updateUI(e.data);
});
5. 메모이제이션으로 중복 계산 방지
동일한 입력에 대해 같은 결과를 반환하는 순수 함수는 메모이제이션으로 최적화할 수 있습니다. 계산 결과를 캐싱하여 이후 호출에서 즉시 반환하므로, 피보나치 수열 같은 재귀 함수에서 지수적 성능 향상을 얻을 수 있습니다. React의 useMemo, useCallback도 같은 원리를 활용합니다. 다만 메모리 사용량이 증가하므로 실제로 성능 이득이 있는 경우에만 적용해야 합니다.
// 범용 메모이제이션 함수
function memoize(fn) {
const cache = new Map();
return function(...args) {
const key = JSON.stringify(args);
if (cache.has(key)) {
return cache.get(key);
}
const result = fn.apply(this, args);
cache.set(key, result);
return result;
};
}
// 사용 예시
const fibonacci = memoize((n) => {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
});
console.log(fibonacci(40)); // 메모이제이션 없이는 수 초 소요
6. DOM 조작 최소화 및 일괄 처리
DOM 조작은 매우 비용이 높은 작업입니다. 특히 reflow와 repaint를 유발하는 속성 변경은 성능에 큰 영향을 미칩니다. DocumentFragment를 사용하거나 요소를 먼저 생성한 후 한 번에 추가하면 성능이 크게 개선됩니다. 또한 classList를 활용하여 여러 스타일 변경을 한 번에 처리하고, 읽기와 쓰기 작업을 분리하여 강제 동기 레이아웃을 방지해야 합니다. requestAnimationFrame을 활용한 배치 업데이트도 효과적입니다.
// 나쁜 예: 개별 DOM 조작
for (let i = 0; i < 1000; i++) {
const div = document.createElement('div');
div.textContent = i;
document.body.appendChild(div); // 1000번의 reflow
}
// 좋은 예: DocumentFragment 사용
const fragment = document.createDocumentFragment();
for (let i = 0; i < 1000; i++) {
const div = document.createElement('div');
div.textContent = i;
fragment.appendChild(div);
}
document.body.appendChild(fragment); // 단 1번의 reflow
// 스타일 일괄 변경
element.classList.add('optimized-class'); // CSS에서 여러 속성 정의
7. 코드 스플리팅과 동적 import
모든 JavaScript를 하나의 번들로 제공하면 초기 로딩 시간이 길어집니다. 코드 스플리팅은 필요한 시점에 필요한 코드만 로드하여 초기 번들 크기를 줄입니다. 동적 import()를 사용하면 라우트별, 기능별로 코드를 분할할 수 있습니다. Webpack, Vite 같은 번들러는 자동으로 청크를 생성하며, React.lazy와 Suspense를 활용하면 컴포넌트 단위 분할도 간단합니다. 초기 로딩 시간을 40-60% 단축할 수 있습니다.
// 동적 import로 필요할 때만 로드
button.addEventListener('click', async () => {
const module = await import('./heavy-module.js');
module.initialize();
});
// React에서 컴포넌트 지연 로딩
import React, { lazy, Suspense } from 'react';
const HeavyComponent = lazy(() => import('./HeavyComponent'));
function App() {
return (
로딩 중...
cannabis tincture for focus supports productivity and energy