현대 웹 애플리케이션에서 JavaScript는 필수적인 요소다. 하지만 잘못 작성된 JavaScript 코드는 사용자 경험을 크게 해칠 수 있다. 페이지 로딩이 느리거나, 스크롤이 버벅거리거나, 클릭 반응이 늦어지는 것들이 바로 그 증거다.
성능 최적화는 단순히 코드를 빠르게 만드는 게 아니라, 사용자가 더 나은 경험을 할 수 있도록 하는 것이다. 오늘은 실무에서 바로 적용할 수 있는 JavaScript 성능 최적화 기법들을 알아보자.
// 나쁜 예시 - 매번 DOM을 조회함
for (let i = 0; i < 1000; i++) {
document.getElementById('list').innerHTML += '<li>Item ' + i + '</li>';
}
// 좋은 예시 - DOM 조회를 최소화하고 DocumentFragment 사용
const list = document.getElementById('list');
const fragment = document.createDocumentFragment();
for (let i = 0; i < 1000; i++) {
const li = document.createElement('li');
li.textContent = 'Item ' + i;
fragment.appendChild(li);
}
list.appendChild(fragment);
왜 이게 더 빠를까?
// 나쁜 예시 - 각 요소마다 이벤트 리스너 추가
const buttons = document.querySelectorAll('.button');
buttons.forEach(button => {
button.addEventListener('click', handleClick);
});
// 좋은 예시 - 이벤트 위임 사용
document.addEventListener('click', function(e) {
if (e.target.classList.contains('button')) {
handleClick(e);
}
});
이벤트 위임의 장점:
// 검색 입력 최적화
function debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
// 사용 예시
const searchInput = document.getElementById('search');
const debouncedSearch = debounce(function(e) {
console.log('검색어:', e.target.value);
// 실제 검색 로직
}, 300);
searchInput.addEventListener('input', debouncedSearch);
// 스크롤 이벤트 최적화
function throttle(func, limit) {
let inThrottle;
return function() {
const args = arguments;
const context = this;
if (!inThrottle) {
func.apply(context, args);
inThrottle = true;
setTimeout(() => inThrottle = false, limit);
}
}
}
// 사용 예시
const throttledScroll = throttle(function() {
console.log('스크롤 중...');
// 스크롤 관련 로직
}, 100);
window.addEventListener('scroll', throttledScroll);
// 피보나치 수열 계산 최적화
function memoize(fn) {
const cache = new Map();
return function(...args) {
const key = JSON.stringify(args);
if (cache.has(key)) {
console.log('캐시에서 가져옴:', key);
return cache.get(key);
}
const result = fn.apply(this, args);
cache.set(key, result);
return result;
};
}
// 피보나치 함수
function fibonacci(n) {
if (n < 2) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
}
// 메모이제이션 적용
const memoizedFibonacci = memoize(fibonacci);
console.time('일반 피보나치');
console.log(fibonacci(40));
console.timeEnd('일반 피보나치');
console.time('메모이제이션 피보나치');
console.log(memoizedFibonacci(40));
console.timeEnd('메모이제이션 피보나치');
// 나쁜 예시 - 메모리 누수 발생
function createHandler() {
const largeData = new Array(1000000).fill('data');
return function() {
// largeData를 참조하고 있어서 가비지 컬렉션되지 않음
console.log('Handler called');
};
}
// 좋은 예시 - 메모리 누수 방지
function createOptimizedHandler() {
const largeData = new Array(1000000).fill('data');
return function() {
console.log('Handler called');
// 사용 후 참조 해제
largeData.length = 0;
};
}
// 이벤트 리스너 정리
const button = document.getElementById('button');
const handler = createOptimizedHandler();
button.addEventListener('click', handler);
// 컴포넌트 제거 시 이벤트 리스너도 제거
function cleanup() {
button.removeEventListener('click', handler);
}
// 나쁜 예시 - 순차 처리 (느림)
async function fetchDataSequentially() {
const data1 = await fetch('/api/data1');
const data2 = await fetch('/api/data2');
const data3 = await fetch('/api/data3');
return [data1, data2, data3];
}
// 좋은 예시 - 병렬 처리 (빠름)
async function fetchDataParallel() {
const [data1, data2, data3] = await Promise.all([
fetch('/api/data1'),
fetch('/api/data2'),
fetch('/api/data3')
]);
return [data1, data2, data3];
}
// main.js
const worker = new Worker('worker.js');
worker.postMessage({ data: largeArray });
worker.onmessage = function(e) {
console.log('작업 완료:', e.data);
};
// worker.js
self.onmessage = function(e) {
const { data } = e.data;
// 무거운 계산 작업
const result = data.map(item => item * 2);
self.postMessage(result);
};
// 동적 import를 사용한 코드 스플리팅
async function loadChart() {
const { Chart } = await import('chart.js');
const ctx = document.getElementById('chart').getContext('2d');
new Chart(ctx, {
type: 'bar',
data: {
labels: ['Red', 'Blue', 'Yellow'],
datasets: [{
label: 'Votes',
data: [12, 19, 3]
}]
}
});
}
// 필요할 때만 차트 로드
document.getElementById('showChart').addEventListener('click', loadChart);
// 성능 측정
function measurePerformance(name, fn) {
const start = performance.now();
const result = fn();
const end = performance.now();
console.log(`${name} 실행 시간: ${end - start}ms`);
return result;
}
// 사용 예시
const result = measurePerformance('배열 정렬', () => {
return largeArray.sort((a, b) => a - b);
});
// 메모리 사용량 확인
function checkMemoryUsage() {
if (performance.memory) {
console.log('사용된 메모리:', performance.memory.usedJSHeapSize / 1024 / 1024, 'MB');
console.log('총 메모리:', performance.memory.totalJSHeapSize / 1024 / 1024, 'MB');
}
}
// 주기적으로 메모리 사용량 체크
setInterval(checkMemoryUsage, 5000);
JavaScript 성능 최적화는 한 번에 모든 걸 적용하려고 하지 말고, 점진적으로 개선해나가는 게 중요하다.
가장 먼저 해야 할 일은:
이런 식으로 접근하면 사용자 경험이 눈에 띄게 개선될 것이다. 성능 최적화는 개발자의 기본 소양이므로, 꾸준히 공부하고 실무에 적용해보자