JavaScript Performance
JavaScript Performance: 성능 최적화 기법
JavaScript 성능 최적화는 사용자 경험을 개선하고, 웹 애플리케이션의 속도를 빠르게 하는 데 중요한 요소입니다. JavaScript는 인터프리터 방식으로 실행되기 때문에, 잘못 작성된 코드나 비효율적인 로직은 애플리케이션 성능을 저하시킬 수 있습니다. 이 가이드는 JavaScript 성능을 최적화하기 위한 다양한 기법과 모범 사례를 다룹니다.
1. 비효율적인 DOM 조작 줄이기
DOM 조작은 JavaScript에서 가장 성능에 영향을 많이 미치는 작업 중 하나입니다. DOM 요소를 빈번하게 조작하면 성능이 크게 저하될 수 있으므로, 가능한 최소화하고, 효율적인 방법으로 처리하는 것이 좋습니다.
1.1. DOM 요소 접근 최적화
DOM에 접근할 때마다 비용이 발생하므로, DOM 요소를 한 번만 접근하여 변수에 저장한 후 사용할 수 있습니다.
// 비효율적인 방법: DOM에 여러 번 접근
document.getElementById('element').style.color = 'red';
document.getElementById('element').style.backgroundColor = 'blue';
// 효율적인 방법: DOM 요소를 한 번만 접근
const element = document.getElementById('element');
element.style.color = 'red';
element.style.backgroundColor = 'blue';
1.2. Document Fragment 사용
- *
DocumentFragment
*는 메모리에서만 존재하는 가상의 DOM으로, DOM에 여러 번 접근하지 않고 한 번에 여러 요소를 추가할 수 있습니다.
const fragment = document.createDocumentFragment();
for (let i = 0; i < 100; i++) {
const div = document.createElement('div');
div.textContent = `Item ${i}`;
fragment.appendChild(div);
}
document.body.appendChild(fragment); // DOM에 한 번만 추가
2. 이벤트 위임 사용
여러 개의 자식 요소에 이벤트 리스너를 각각 추가하는 것은 비효율적일 수 있습니다. 대신, 이벤트 **위임(event delegation)**을 사용해 부모 요소에 한 번만 이벤트 리스너를 추가하고, 이벤트가 버블링되어 올라오면 처리를 할 수 있습니다.
2.1. 이벤트 위임 예시
document.getElementById('parent').addEventListener('click', function (event) {
if (event.target && event.target.matches('li.item')) {
console.log('List item clicked:', event.target.textContent);
}
});
위 예시에서는 parent
요소에 클릭 이벤트 리스너를 추가하고, 자식 요소들이 클릭되었을 때 한 번에 처리할 수 있습니다.
3. 메모리 관리와 가비지 컬렉션
JavaScript는 가비지 컬렉터를 통해 자동으로 메모리를 관리하지만, **메모리 누수(memory leak)**가 발생하지 않도록 주의해야 합니다. 특히, 불필요한 참조를 제거하고, 클로저나 이벤트 리스너에서 메모리 누수를 방지해야 합니다.
3.1. 메모리 누수 방지
- 클로저는 참조를 유지하므로, 필요하지 않은 경우 클로저를 제거합니다.
- 이벤트 리스너는 사용 후 **
removeEventListener
*를 통해 제거합니다.
// 이벤트 리스너 제거
const button = document.getElementById('myButton');
function handleClick() {
console.log('Button clicked');
}
button.addEventListener('click', handleClick);
// 리스너 제거
button.removeEventListener('click', handleClick);
4. 루프 최적화
반복문에서 불필요한 계산이나 DOM 접근을 줄이는 것이 성능 최적화에 중요합니다. 특히 중첩 루프에서의 최적화는 성능에 큰 영향을 미칩니다.
4.1. 루프 내 계산 최소화
루프 내부에서 반복적으로 계산하거나 DOM에 접근하는 것은 성능을 저하시킬 수 있습니다. 한 번만 계산하고, 결과를 재사용하도록 코드를 작성합니다.
// 비효율적인 방법
for (let i = 0; i < document.getElementsByTagName('li').length; i++) {
console.log(document.getElementsByTagName('li')[i]);
}
// 효율적인 방법
const items = document.getElementsByTagName('li');
for (let i = 0; i < items.length; i++) {
console.log(items[i]);
}
5. 이미지와 파일 로딩 최적화
페이지 성능을 저하시킬 수 있는 큰 요인 중 하나는 이미지나 파일의 로딩 시간입니다. **지연 로딩(Lazy Loading)**과 같은 기법을 사용하면 사용자가 필요할 때만 이미지를 로드할 수 있습니다.
5.1. 지연 로딩 (Lazy Loading)
이미지를 즉시 로드하지 않고, 스크롤 위치에 따라 필요한 이미지만 로드하도록 처리할 수 있습니다.
<img src="placeholder.jpg" data-src="image.jpg" class="lazy" alt="Lazy Loaded Image">
const lazyImages = document.querySelectorAll('img.lazy');
const lazyLoad = function () {
lazyImages.forEach(img => {
if (img.getBoundingClientRect().top < window.innerHeight && img.src === 'placeholder.jpg') {
img.src = img.dataset.src;
}
});
};
window.addEventListener('scroll', lazyLoad);
6. 코드 스플리팅과 비동기 로드
JavaScript 파일이 너무 크면 로드 시간이 길어질 수 있습니다. **코드 스플리팅(code splitting)**을 사용해 필요한 부분만 로드하거나, 비동기 로드를 통해 성능을 최적화할 수 있습니다.
6.1. 동적 import()
사용
동적 import()
를 사용하면 필요한 시점에 모듈을 비동기로 로드할 수 있습니다.
document.getElementById('loadButton').addEventListener('click', async () => {
const { loadContent } = await import('./content.js');
loadContent();
});
7. 클로저와 메모리 관리
- *클로저(closure)**는 함수가 선언된 환경을 기억하기 때문에, 불필요한 메모리가 해제되지 않고 참조될 수 있습니다. 필요한 시점에만 클로저를 사용하고, 사용하지 않는 클로저는 명시적으로 해제하는 것이 중요합니다.
7.1. 클로저로 인한 메모리 문제 해결
function outer() {
let largeData = new Array(1000000).fill('*');
return function inner() {
// largeData는 이 함수가 끝난 뒤에도 계속 참조됨
console.log(largeData.length);
};
}
const innerFunc = outer();
innerFunc(); // 실행 후 필요 없는 참조는 null로 초기화
8. 비동기 코드 최적화
비동기 작업이 많아지면 성능 저하가 발생할 수 있습니다. 이를 방지하기 위해 **Promise.all()**을 사용해 병렬 실행하거나, 중복된 비동기 호출을 줄이는 방법을 사용합니다.
8.1. Promise.all()
로 병렬 처리
const fetchData1 = fetch('/api/data1');
const fetchData2 = fetch('/api/data2');
Promise.all([fetchData1, fetchData2])
.then(([response1, response2]) => {
// 두 개의 비동기 작업을 병렬로 실행
});
9. 캐싱 활용
캐싱은 이미 계산되었거나 호출된 데이터를 저장하고, 이후에 재사용하여 성능을 높이는 중요한 기법입니다. 특히, 네트워크 요청이나 비싼 연산을 캐싱하면 큰 성능 향상을 얻을 수 있습니다.
9.1. 캐싱 예시
const cache = {};
function fetchData(id) {
if (cache[id]) {
return Promise.resolve(cache[id]); // 캐시된 데이터 반환
} else {
return fetch(`/api/data/${id}`).then(response => {
cache[id] = response;
return response;
});
}
}
10. Debouncing과 Throttling
Debouncing과 Throttling은 이벤트 핸들러를 성능적으로 최적화하는 기법입니다. 특히, 스크롤이나 리사이즈 같은 자주 발생하는 이벤트에서 유용합니다.
10.1. Debouncing
Debouncing은 이벤트가 일정 시간 동안 발생하지 않을 때만 함수를 실행하도록 하는 기법입니다.
function debounce(func, delay) {
let timeout;
return function (...
args) {
clearTimeout(timeout);
timeout = setTimeout(() => func.apply(this, args), delay);
};
}
window.addEventListener('resize', debounce(() => {
console.log('Window resized');
}, 500));
10.2. Throttling
Throttling은 이벤트가 일정 시간마다 실행되도록 제한하는 기법입니다.
function throttle(func, limit) {
let inThrottle;
return function (...args) {
if (!inThrottle) {
func.apply(this, args);
inThrottle = true;
setTimeout(() => inThrottle = false, limit);
}
};
}
window.addEventListener('scroll', throttle(() => {
console.log('Scroll event');
}, 200));
요약
- DOM 조작을 최소화하고, Document Fragment와 같은 기법을 사용해 성능을 최적화합니다.
- 이벤트 위임을 통해 불필요한 이벤트 리스너 추가를 줄이고, 메모리 누수를 방지합니다.
- 루프 내 불필요한 계산을 줄이고, 캐싱을 통해 비싼 연산을 재사용합니다.
- 지연 로딩과 코드 스플리팅을 통해 페이지 로딩 시간을 단축하고, 동적 **
import()
*로 필요한 시점에 모듈을 로드합니다. - Debouncing과 Throttling을 통해 이벤트 핸들러의 성능을 최적화합니다.
위의 모범 사례들을 따르면 JavaScript의 성능을 최적화하여 더 빠르고 반응성 높은 애플리케이션을 개발할 수 있습니다.