클로저는 JavaScript에서 외부 함수의 변수를 내부 함수가 참조할 때 생성되는 특별한 메커니즘입니다. 클로저가 생성되면 내부 함수는 외부 함수가 실행을 마친 후에도 외부 변수에 대한 접근을 유지합니다. 이러한 동작은 JavaScript의 렉시컬 스코프와 메모리 관리 방식, 그리고 가비지 컬렉션 메커니즘이 결합된 결과입니다. 클로저가 메모리를 유지하는 원리를 이해하면 예상치 못한 메모리 누수를 방지하고, 효율적인 상태 관리 패턴을 설계할 수 있습니다.
핵심 특징
- 외부 함수가 종료된 후에도 내부 함수가 외부 변수를 참조할 수 있음
- 참조되는 변수는 가비지 컬렉션 대상에서 제외되어 메모리에 유지됨
- 클로저는 변수의 실제 값이 아닌 참조를 유지하여 동적 변경을 반영함
- 여러 클로저가 동일한 외부 변수를 공유할 수 있어 상태 공유 패턴이 가능함
- 클로저가 제거되지 않으면 참조된 변수도 메모리에서 해제되지 않음
실무에서의 영향
클로저의 메모리 유지 메커니즘은 이벤트 핸들러, 타이머 콜백, 비동기 처리 등 실무 JavaScript 개발의 핵심을 이룹니다. 예를 들어, 버튼 클릭 핸들러가 컴포넌트 상태를 참조할 때 클로저가 생성되며, 이 클로저는 핸들러가 제거되기 전까지 상태 변수를 메모리에 유지합니다. 하지만 이벤트 리스너를 제거하지 않거나 타이머를 정리하지 않으면 불필요한 클로저가 메모리를 계속 점유하여 메모리 누수가 발생할 수 있습니다. React의 useEffect 훅이나 Vue의 onUnmounted 같은 정리 메커니즘이 필요한 이유도 바로 클로저의 메모리 유지 특성 때문입니다. 클로저의 메모리 관리 원리를 정확히 이해하면 SPA(Single Page Application)에서 발생하는 메모리 누수를 예방하고, 장시간 실행되는 애플리케이션의 성능을 최적화할 수 있습니다.
핵심 개념
클로저의 변수 참조 메커니즘
입문
클로저는 함수가 만들어질 때 주변에 있던 변수들을 기억해요. 마치 사진을 찍으면 그 순간의 풍경이 사진 속에 남는 것처럼, 함수도 만들어질 때의 변수들을 기억하는 거죠!
📸 사진과 메모리의 차이점 사진은 그 순간을 ‘복사’해서 저장하지만, 클로저는 다릅니다. 클로저는 변수를 복사하는 게 아니라 ‘어디에 있는지 주소’를 기억해요. 마치 친구 집 주소를 외우는 것처럼요. 친구가 집을 꾸미거나 가구를 바꾸면, 다음에 방문했을 때 변화된 모습을 볼 수 있죠?
🏠 함수가 끝나도 남아있는 비밀 보통 함수가 실행을 마치면 그 안에서 사용한 변수들은 사라져야 해요. 마치 학교 수업이 끝나면 칠판을 지우는 것처럼요. 그런데 클로저가 있으면 특별한 일이 일어나요. 내부 함수가 “나 이 변수 아직 쓸 거야!”라고 주장하면, 그 변수는 지워지지 않고 계속 남아있어요.
🔗 참조는 복사가 아니에요 중요한 점은 클로저가 변수의 ‘값’을 복사하는 게 아니라 ‘참조(주소)‘를 기억한다는 거예요. 만약 10명의 친구가 모두 “김철수네 집”이라는 주소를 외우고 있다면, 김철수가 집을 파란색으로 칠하면 다음에 방문하는 모든 친구가 파란 집을 보게 되는 것과 같아요.
🎁 여러 함수가 같은 변수를 공유할 수 있어요 하나의 외부 함수 안에서 여러 개의 내부 함수를 만들 수 있어요. 이 모든 내부 함수들은 같은 외부 변수를 바라보고 있어요. 마치 같은 냉장고를 공유하는 가족처럼, 누군가 우유를 마시면 다른 가족도 우유가 줄어든 걸 알 수 있죠.
중급
클로저는 함수가 생성될 때 해당 함수의 렉시컬 환경(Lexical Environment)을 캡처합니다. 이때 외부 변수의 값을 복사하는 것이 아니라 변수에 대한 **참조(reference)**를 유지합니다.
참조 기반 캡처의 특징
- 클로저는 변수의 현재 상태를 실시간으로 반영
- 외부 함수 실행이 끝나도 참조된 변수는 메모리에 유지
- 여러 클로저가 동일한 변수를 공유 가능
function createCounter() {
let count = 0; // 외부 변수
return {
increment: function() { count++; },
decrement: function() { count--; },
getCount: function() { return count; }
};
}
const counter = createCounter();
counter.increment(); // count = 1
counter.increment(); // count = 2
console.log(counter.getCount()); // 2 - 세 함수 모두 같은 count 참조
위 예시에서 세 개의 메서드(increment, decrement, getCount)는 모두 동일한 count 변수를 참조합니다. 하나의 메서드가 count를 변경하면 다른 메서드들도 변경된 값을 확인할 수 있습니다.
function createFunctions() {
const arr = [];
let i = 0;
for (; i < 3; i++) {
arr.push(function() {
console.log(i);
});
}
return arr;
}
const funcs = createFunctions();
funcs[0](); // 3 (0이 아님!)
funcs[1](); // 3
funcs[2](); // 3
// 모든 함수가 같은 i 변수를 참조하므로 최종 값 3을 출력
심화
클로저의 변수 참조는 ECMAScript 명세의 렉시컬 환경(Lexical Environment) 모델과 **환경 레코드(Environment Record)**의 상호작용을 통해 구현됩니다.
ECMAScript 명세 기반 렉시컬 환경 바인딩 ECMAScript 2024, Section 9.1 (Environment Records)에 따르면, 함수가 생성될 때 내부 슬롯 [[Environment]]에 현재 실행 컨텍스트의 렉시컬 환경 참조가 저장됩니다. 이는 값 복사가 아닌 환경 레코드에 대한 포인터입니다.
클로저 생성 과정:
- 함수 객체 생성 시 [[Environment]] 슬롯에 현재 렉시컬 환경 참조 저장
- 함수 호출 시 새로운 함수 환경 레코드 생성
- 외부 환경 참조(Outer Environment Reference)를 [[Environment]]로 설정
- 변수 접근 시 스코프 체인을 따라 환경 레코드 탐색
메모리 참조 구조와 공유 메커니즘 여러 클로저가 동일한 외부 환경을 참조할 때, 각 클로저의 [[Environment]]는 동일한 환경 레코드 객체를 가리킵니다. 이는 포인터 공유를 통한 메모리 효율성을 제공하지만, 의도치 않은 상태 공유를 유발할 수 있습니다.
V8 엔진의 Context 구현 V8 엔진에서 렉시컬 환경은 Context 객체로 구현됩니다:
- Variable Context: var 선언을 위한 함수 컨텍스트
- Block Context: let/const를 위한 블록 컨텍스트
- Closure Context: 클로저가 캡처한 변수들만 포함하는 최적화된 컨텍스트
V8은 클로저 생성 시 실제로 참조되는 변수만 선택적으로 Closure Context에 복사하여 메모리를 최적화합니다(Context Snapshot). 이는 명세와 다른 구현 최적화이지만, 동작은 명세를 준수합니다.
가비지 컬렉션과 클로저
입문
컴퓨터의 메모리는 한정되어 있어요. 그래서 더 이상 쓰지 않는 것들은 치워야 해요. 마치 방을 정리할 때 안 쓰는 물건을 버리는 것처럼요. JavaScript는 이 정리를 자동으로 해주는 ‘청소 로봇’이 있어요. 이걸 가비지 컬렉터라고 부릅니다!
🤖 자동 청소 로봇의 원리 가비지 컬렉터는 “이 물건을 누군가 쓰고 있나요?”라고 확인해요. 만약 아무도 쓰지 않으면 버리고, 누군가 쓰고 있으면 남겨둬요. 마치 도서관 사서가 대출 중인 책은 그대로 두고, 아무도 빌리지 않는 책만 정리하는 것과 같아요.
🔗 클로저가 변수를 지키는 방법 클로저는 변수에 대한 ‘연결 고리’를 만들어요. 가비지 컬렉터가 “이 변수 치울까요?”라고 물으면, 클로저가 “안 돼요! 나 이거 아직 쓰고 있어요!”라고 대답하는 거죠. 그래서 외부 함수가 끝나도 클로저가 참조하는 변수는 메모리에 남아있어요.
🎭 사라지지 않는 비밀 상자 일반적으로 함수가 끝나면 그 안의 변수들은 사라져요. 하지만 클로저가 있으면 다릅니다! 외부 함수는 끝났지만, 내부 함수가 “나 이 변수 필요해!”라고 계속 붙잡고 있으면, 그 변수는 비밀 상자처럼 메모리 어딘가에 계속 존재해요.
⚠️ 정리하지 않으면 생기는 문제 만약 더 이상 필요 없는 클로저를 계속 붙잡고 있으면 어떻게 될까요? 방에 안 쓰는 물건들이 계속 쌓이듯이, 메모리도 가득 차게 돼요. 이걸 ‘메모리 누수’라고 해요. 마치 수도꼭지를 잠그지 않아서 물이 계속 새는 것과 비슷해요.
중급
JavaScript의 가비지 컬렉터(Garbage Collector, GC)는 도달 가능성(Reachability) 알고리즘을 사용하여 메모리를 관리합니다. 클로저는 이 도달 가능성 체인에 영향을 미칩니다.
가비지 컬렉션 기본 원리
- 루트(Root) 객체에서 시작하여 참조 체인을 따라 도달 가능한 객체 마킹
- 도달 불가능한 객체는 메모리에서 해제
- 클로저가 외부 변수를 참조하면 해당 변수는 도달 가능한 상태로 유지
function createClosure() {
let largeData = new Array(1000000).fill('data'); // 큰 데이터
return function() {
console.log(largeData.length);
};
}
let closure = createClosure();
// createClosure 실행 완료 후에도 largeData는 메모리에 유지됨
// 이유: closure 함수가 largeData를 참조하고 있음
closure = null; // 클로저 참조 제거
// 이제 largeData는 도달 불가능 → 가비지 컬렉션 대상
클로저와 메모리 유지의 관계 클로저가 외부 변수를 참조하는 한, 해당 변수는 가비지 컬렉션되지 않습니다. 클로저 자체가 더 이상 필요 없어지면 (모든 참조가 제거되면) 클로저와 함께 캡처된 변수들도 가비지 컬렉션 대상이 됩니다.
function attachHandler() {
const element = document.getElementById('button');
const data = new Array(1000000).fill('leak');
element.addEventListener('click', function() {
console.log('Clicked');
// data를 사용하지 않지만, 클로저가 캡처함
});
// 문제: element가 DOM에 남아있는 한 클로저도 유지됨
// 해결: removeEventListener 또는 element = null 필요
}
심화
JavaScript의 가비지 컬렉션은 주로 Mark-and-Sweep 알고리즘을 기반으로 구현되며, 클로저는 이 알고리즘의 도달 가능성 그래프에 중요한 영향을 미칩니다.
Mark-and-Sweep 알고리즘과 클로저 가비지 컬렉터는 두 단계로 동작합니다:
- Mark Phase: GC 루트(전역 객체, 실행 스택, 레지스터)에서 시작하여 도달 가능한 모든 객체에 마킹
- Sweep Phase: 마킹되지 않은 객체의 메모리를 회수
클로저가 생성되면:
- 클로저 함수 객체의 [[Environment]] 슬롯이 외부 렉시컬 환경을 참조
- 이 참조는 Mark Phase에서 강한 참조(Strong Reference)로 간주
- 외부 환경의 환경 레코드와 그 안의 모든 변수 바인딩이 마킹됨
V8 엔진의 세대별 가비지 컬렉션 V8은 Generational GC를 사용하여 객체를 신생대(Young Generation)와 구세대(Old Generation)로 분리합니다:
- Scavenger (Minor GC): 신생대 대상, 빠른 주기 (1-10ms)
- Mark-Compact (Major GC): 구세대 대상, 긴 주기 (100-1000ms)
클로저가 오래 유지되면 구세대로 승격되어 GC 빈도가 감소합니다. 이는 성능상 이점이지만, 메모리 누수가 발생하면 장기간 탐지되지 않을 수 있습니다.
WeakMap/WeakSet을 활용한 메모리 관리 ES6의 WeakMap과 WeakSet은 **약한 참조(Weak Reference)**를 제공하여 클로저의 메모리 문제를 완화합니다:
const cache = new WeakMap();
function createClosure(key) {
return function() {
return cache.get(key);
};
}
// key 객체가 외부에서 제거되면 WeakMap 엔트리도 자동 GC
WeakMap의 키는 가비지 컬렉션을 방해하지 않으므로, 클로저가 캐시를 참조해도 메모리 누수가 발생하지 않습니다.
메모리 누수 패턴과 예방
입문
메모리 누수는 더 이상 필요 없는 것들이 메모리에 계속 남아있는 현상이에요. 마치 사용한 컵을 설거지하지 않고 계속 쌓아두는 것과 같아요. 처음에는 문제가 없지만, 시간이 지날수록 컵이 쌓여서 결국 싱크대가 가득 차게 되죠.
🚰 수도꼭지를 잠그지 않으면 프로그램에서 클로저를 만들 때, 더 이상 필요 없어지면 꼭 정리해줘야 해요. 만약 이벤트 리스너를 달아두고 제거하지 않으면, 클로저가 계속 메모리를 차지해요. 마치 수도꼭지를 잠그지 않아서 물이 계속 흐르는 것처럼요.
📱 앱이 점점 느려지는 이유 웹사이트를 오래 사용하다 보면 점점 느려지는 경험을 한 적 있나요? 그 이유 중 하나가 바로 메모리 누수예요. 페이지를 이동할 때마다 새로운 클로저가 만들어지는데, 이전 페이지의 클로저를 정리하지 않으면 메모리가 계속 쌓여요. 마치 방을 청소하지 않고 계속 새 물건만 들여놓는 것과 같아요.
🧹 정리하는 습관이 중요해요 문제를 예방하려면 ‘정리하는 습관’이 필요해요. 이벤트를 등록했으면 나중에 꼭 제거하고, 타이머를 설정했으면 꼭 멈춰야 해요. React 같은 프레임워크에서 cleanup 함수를 작성하는 이유도 바로 이거예요. 사용한 것은 꼭 치워야 해요!
⏰ 타이머를 멈추지 않으면 setTimeout이나 setInterval을 사용할 때 특히 조심해야 해요. 타이머 콜백 함수는 클로저를 만들어요. 만약 clearTimeout이나 clearInterval을 호출하지 않으면, 타이머가 계속 돌면서 메모리를 차지해요. 마치 알람을 끄지 않아서 계속 울리는 것과 같아요.
중급
메모리 누수는 클로저가 예상보다 오래 유지되어 불필요한 메모리를 점유하는 상황입니다. 실무에서 자주 발생하는 패턴들을 이해하고 예방하는 것이 중요합니다.
주요 메모리 누수 패턴
// ❌ 메모리 누수 발생
function setupComponent() {
const largeData = new Array(1000000).fill('data');
document.getElementById('btn').addEventListener('click', function() {
console.log(largeData.length);
});
// 컴포넌트 제거 시 이벤트 리스너 미제거 → 메모리 누수
}
// ✅ 올바른 정리
function setupComponent() {
const largeData = new Array(1000000).fill('data');
const btn = document.getElementById('btn');
const handler = function() {
console.log(largeData.length);
};
btn.addEventListener('click', handler);
// 정리 함수
return function cleanup() {
btn.removeEventListener('click', handler);
};
}
// ❌ 메모리 누수
function startPolling() {
const cache = new Map();
setInterval(function() {
// cache를 참조하는 클로저
cache.set(Date.now(), 'data');
}, 1000);
// clearInterval 호출 안 함 → cache 계속 증가
}
// ✅ 올바른 정리
function startPolling() {
const cache = new Map();
const intervalId = setInterval(function() {
cache.set(Date.now(), 'data');
}, 1000);
return function stopPolling() {
clearInterval(intervalId);
cache.clear();
};
}
React에서의 메모리 누수 예방 React는 useEffect의 cleanup 함수를 통해 메모리 누수를 예방합니다.
function Component() {
useEffect(() => {
const data = { /* large object */ };
const handler = () => {
console.log(data);
};
window.addEventListener('resize', handler);
// cleanup 함수 반환
return () => {
window.removeEventListener('resize', handler);
};
}, []);
}
심화
메모리 누수는 클로저가 형성하는 **참조 체인(Reference Chain)**이 예상보다 길어지거나, **순환 참조(Circular Reference)**가 발생할 때 심각해집니다.
DOM과 클로저의 순환 참조 문제 과거 IE8 이하에서는 DOM 노드와 JavaScript 객체 간의 순환 참조가 메모리 누수를 유발했습니다. 현대 브라우저는 이를 해결했지만, 여전히 주의가 필요합니다:
// Detached DOM Tree 메모리 누수
function createLeak() {
const parent = document.createElement('div');
const child = document.createElement('div');
parent.appendChild(child);
// 클로저가 parent를 캡처
child.addEventListener('click', function() {
console.log(parent.tagName);
});
document.body.appendChild(parent);
document.body.removeChild(parent); // DOM에서 제거
// 하지만 child의 이벤트 리스너가 parent를 참조
// → parent와 child 모두 메모리에 유지 (Detached DOM)
}
Chrome DevTools의 메모리 프로파일링 메모리 누수를 탐지하려면 Chrome DevTools의 Memory Profiler를 사용합니다:
- Heap Snapshot: 특정 시점의 메모리 상태 캡처
- Allocation Timeline: 시간에 따른 메모리 할당 추적
- Retainers: 객체가 GC되지 않는 이유 분석 (참조 체인 확인)
Retainers 뷰에서 클로저의 [[Scopes]] 속성을 확인하면 어떤 변수가 메모리를 점유하는지 파악할 수 있습니다.
V8의 Orinoco GC와 메모리 최적화 V8의 Orinoco GC(2016년 도입)는 Concurrent Marking과 Incremental Marking을 통해 메인 스레드 차단을 최소화합니다:
- Concurrent Marking: 백그라운드 스레드에서 마킹 수행
- Incremental Marking: 마킹 작업을 여러 프레임으로 분산
- Idle-time GC: 브라우저 유휴 시간을 활용한 GC
하지만 클로저로 인한 메모리 누수는 GC 알고리즘과 무관하게 발생합니다. 도달 가능한 객체는 GC 대상이 아니기 때문입니다.
WeakRef와 FinalizationRegistry (ES2021) ES2021은 약한 참조를 위한 새로운 API를 도입했습니다:
const registry = new FinalizationRegistry((heldValue) => {
console.log(`Object ${heldValue} was garbage collected`);
});
let obj = { data: 'important' };
const weakRef = new WeakRef(obj);
registry.register(obj, 'my-object');
// obj가 GC되면 콜백 호출
obj = null;
이는 캐시 구현 시 유용하지만, 일반적인 클로저 메모리 관리에는 명시적 cleanup이 더 명확합니다.
실무 메모리 관리 전략
입문
실무에서는 클로저를 안전하게 사용하기 위한 ‘규칙’들이 있어요. 마치 교통 규칙을 지키면 사고를 예방할 수 있듯이, 이 규칙들을 지키면 메모리 문제를 예방할 수 있어요!
📋 정리 목록 만들기 무언가를 설정하면 반드시 정리해야 해요. 이벤트를 등록했으면 제거하고, 타이머를 시작했으면 멈추고, 연결을 열었으면 닫아야 해요. 이걸 ‘정리 목록’이라고 생각하면 좋아요. 마치 소풍 갈 때 챙긴 물건 목록을 만들고, 돌아올 때 하나씩 확인하는 것처럼요.
🎯 필요한 것만 기억하기 클로저는 주변의 모든 변수를 기억할 수 있어요. 하지만 그럴 필요가 없어요! 정말 필요한 변수만 사용하도록 코드를 작성하면, 메모리를 절약할 수 있어요. 마치 여행 갈 때 필요한 물건만 가방에 넣는 것처럼요. 무거운 짐은 남겨두고 가벼운 짐만 챙기는 게 좋아요.
🔄 주기적으로 확인하기 개발할 때는 메모리 사용량을 주기적으로 확인해야 해요. 브라우저의 개발자 도구에는 메모리를 확인할 수 있는 기능이 있어요. 마치 체중계에 주기적으로 올라가서 건강을 체크하는 것처럼, 프로그램도 메모리를 체크해야 해요.
🏗️ 프레임워크의 도움 받기 React, Vue 같은 현대 프레임워크들은 메모리 관리를 도와줘요. 이 프레임워크들이 제공하는 ‘정리 기능’을 잘 활용하면, 메모리 문제를 쉽게 예방할 수 있어요. 마치 식기세척기가 설거지를 도와주는 것처럼, 프레임워크도 메모리 정리를 도와줘요!
💡 코드 리뷰에서 확인하기 팀에서 코드를 작성할 때는 서로의 코드를 확인해주는 ‘코드 리뷰’가 있어요. 이때 “이벤트 리스너 정리했나요?”, “타이머 멈췄나요?” 같은 질문을 하면, 실수를 미리 발견할 수 있어요!
중급
실무에서는 체계적인 메모리 관리 전략을 수립하여 클로저 관련 메모리 문제를 예방해야 합니다.
1. 명시적 정리 패턴 (Explicit Cleanup Pattern)
function createResource() {
const resources = [];
// 리소스 초기화
const interval = setInterval(() => { /* ... */ }, 1000);
const listener = () => { /* ... */ };
window.addEventListener('resize', listener);
// 정리 함수 반환
return {
cleanup() {
clearInterval(interval);
window.removeEventListener('resize', listener);
resources.length = 0;
}
};
}
const resource = createResource();
// 사용 후
resource.cleanup();
2. 스코프 최소화 전략
클로저가 캡처하는 변수의 범위를 최소화하여 메모리 점유를 줄입니다.
// ❌ 불필요한 변수 캡처
function createHandler() {
const largeData = new Array(1000000).fill('data');
const smallValue = 42;
return function() {
console.log(smallValue); // largeData도 함께 캡처됨
};
}
// ✅ 필요한 변수만 캡처
function createHandler() {
const smallValue = 42;
return function() {
console.log(smallValue); // smallValue만 캡처
};
}
// 또는 즉시 값 추출
function createHandler() {
const largeData = new Array(1000000).fill('data');
const extracted = largeData[0]; // 필요한 값만 추출
return function() {
console.log(extracted); // 작은 값만 캡처
};
}
3. 프레임워크별 정리 패턴
function useEventListener(eventName, handler) {
useEffect(() => {
window.addEventListener(eventName, handler);
return () => {
window.removeEventListener(eventName, handler);
};
}, [eventName, handler]);
}
import { onMounted, onUnmounted } from 'vue';
export function useInterval(callback, delay) {
let intervalId;
onMounted(() => {
intervalId = setInterval(callback, delay);
});
onUnmounted(() => {
clearInterval(intervalId);
});
}
4. 메모리 프로파일링 체크리스트
- 페이지 탐색 전후 메모리 비교 (Detached DOM 확인)
- 반복 작업 수행 후 메모리 증가 추이 관찰
- Heap Snapshot의 Retainers 분석
- Performance Monitor로 JS Heap Size 실시간 모니터링
심화
실무 메모리 관리는 정적 분석(Static Analysis), 런타임 모니터링(Runtime Monitoring), **아키텍처 패턴(Architectural Patterns)**의 조합으로 구성됩니다.
ESLint를 활용한 정적 분석 ESLint 규칙을 통해 메모리 누수 패턴을 사전에 탐지할 수 있습니다:
// eslint-plugin-react-hooks
// useEffect cleanup 함수 누락 경고
{
"rules": {
"react-hooks/exhaustive-deps": "warn"
}
}
커스텀 ESLint 규칙으로 addEventListener/removeEventListener 쌍을 검증할 수도 있습니다.
성능 예산(Performance Budget) 설정 Lighthouse CI나 Webpack Bundle Analyzer를 통해 번들 크기와 메모리 임계값을 설정합니다:
// lighthouse-ci.json
{
"ci": {
"assert": {
"assertions": {
"total-byte-weight": ["error", {"maxNumericValue": 350000}],
"dom-size": ["error", {"maxNumericValue": 1500}]
}
}
}
}
메모리 릭 감지 라이브러리
memlab (Facebook)은 E2E 테스트 중 메모리 누수를 자동 탐지합니다:
// memlab-scenario.js
const scenario = {
url: () => 'http://localhost:3000',
action: async (page) => {
await page.click('#create-widget');
await page.click('#destroy-widget');
},
back: async (page) => {
await page.click('#back');
}
};
// CLI: memlab run --scenario=memlab-scenario.js
아키텍처 패턴: 이벤트 버스 중앙화 이벤트 리스너를 중앙에서 관리하여 누락을 방지합니다:
class EventManager {
constructor() {
this.listeners = new Map();
}
on(element, event, handler, context) {
element.addEventListener(event, handler);
const key = `${context}-${event}`;
if (!this.listeners.has(key)) {
this.listeners.set(key, []);
}
this.listeners.get(key).push({ element, event, handler });
}
cleanupContext(context) {
for (const [key, handlers] of this.listeners.entries()) {
if (key.startsWith(context)) {
handlers.forEach(({ element, event, handler }) => {
element.removeEventListener(event, handler);
});
this.listeners.delete(key);
}
}
}
}
// 사용
const eventMgr = new EventManager();
eventMgr.on(button, 'click', handler, 'component-123');
// 컴포넌트 언마운트 시
eventMgr.cleanupContext('component-123');
메모리 최적화와 성능 트레이드오프 과도한 메모리 최적화는 CPU 사용량 증가를 유발할 수 있습니다:
- 메모리 우선: 클로저 최소화, 즉시 정리 → GC 빈도 증가 → CPU 사용량 증가
- CPU 우선: 클로저 재사용, 지연 정리 → 메모리 사용량 증가 → GC pause 증가
실무에서는 메모리 제약과 CPU 제약을 고려한 균형점을 찾아야 합니다. Chrome DevTools의 Performance 탭에서 GC pause 시간과 빈도를 측정하여 최적화 방향을 결정합니다.