JavaScript 성능 최적화에 관하여



JavaScript 성능 최적화가 왜 중요한가?

현대 웹 애플리케이션에서 JavaScript는 필수적인 요소다. 하지만 잘못 작성된 JavaScript 코드는 사용자 경험을 크게 해칠 수 있다. 페이지 로딩이 느리거나, 스크롤이 버벅거리거나, 클릭 반응이 늦어지는 것들이 바로 그 증거다.

성능 최적화는 단순히 코드를 빠르게 만드는 게 아니라, 사용자가 더 나은 경험을 할 수 있도록 하는 것이다. 오늘은 실무에서 바로 적용할 수 있는 JavaScript 성능 최적화 기법들을 알아보자.



1. DOM 조작 최적화

문제가 되는 코드

// 나쁜 예시 - 매번 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);

왜 이게 더 빠를까?

  • DOM 조회를 한 번만 한다
  • DocumentFragment를 사용해서 리플로우/리페인트 횟수를 줄인다
  • innerHTML 대신 createElement를 사용해서 더 안전하다



2. 이벤트 위임 패턴

문제가 되는 코드

// 나쁜 예시 - 각 요소마다 이벤트 리스너 추가
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);
    }
});

이벤트 위임의 장점:

  • 메모리 사용량을 감소시킨다
  • 동적으로 추가되는 요소에도 자동으로 이벤트가 적용된다
  • 이벤트 리스너 개수를 대폭 감소시킨다



3. 디바운싱과 쓰로틀링

디바운싱 (Debouncing)

// 검색 입력 최적화
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);

쓰로틀링 (Throttling)

// 스크롤 이벤트 최적화
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);



4. 메모이제이션 (Memoization)

// 피보나치 수열 계산 최적화
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('메모이제이션 피보나치');



5. 가비지 컬렉션 최적화

메모리 누수 방지

// 나쁜 예시 - 메모리 누수 발생
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);
}



6. 비동기 처리 최적화

Promise.all vs 순차 처리

// 나쁜 예시 - 순차 처리 (느림)
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];
}

Web Workers 활용

// 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);
};



7. 코드 스플리팅과 지연 로딩

// 동적 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);



8. 성능 측정 도구

Performance API 활용

// 성능 측정
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);
});

Memory API로 메모리 사용량 확인

// 메모리 사용량 확인
function checkMemoryUsage() {
    if (performance.memory) {
        console.log('사용된 메모리:', performance.memory.usedJSHeapSize / 1024 / 1024, 'MB');
        console.log('총 메모리:', performance.memory.totalJSHeapSize / 1024 / 1024, 'MB');
    }
}

// 주기적으로 메모리 사용량 체크
setInterval(checkMemoryUsage, 5000);



9. 실무 체크리스트

성능 최적화 체크리스트

  1. DOM 조작
    • DOM 조회를 최소화했는가?
    • DocumentFragment를 사용했는가?
    • innerHTML 대신 createElement를 사용했는가?
  2. 이벤트 처리
    • 이벤트 위임을 사용했는가?
    • 불필요한 이벤트 리스너를 제거했는가?
    • 디바운싱/쓰로틀링을 적용했는가?
  3. 메모리 관리
    • 메모리 누수가 없는가?
    • 불필요한 참조를 제거했는가?
    • 가비지 컬렉션을 고려했는가?
  4. 비동기 처리
    • Promise.all을 사용했는가?
    • Web Workers를 활용했는가?
    • 코드 스플리팅을 적용했는가?



마무리

JavaScript 성능 최적화는 한 번에 모든 걸 적용하려고 하지 말고, 점진적으로 개선해나가는 게 중요하다.

가장 먼저 해야 할 일은:

  1. 성능 측정 - 현재 상태를 파악한다
  2. 병목 지점 찾기 - 가장 느린 부분부터 개선한다
  3. 점진적 적용 - 하나씩 최적화 기법을 적용한다
  4. 지속적 모니터링 - 개선 효과를 확인한다

이런 식으로 접근하면 사용자 경험이 눈에 띄게 개선될 것이다. 성능 최적화는 개발자의 기본 소양이므로, 꾸준히 공부하고 실무에 적용해보자



참고 자료