🔒 클로저란?
📖 정의
**클로저(Closure)**는 함수가 선언될 때의 렉시컬 환경(Lexical Environment)을 기억 하여, 함수가 외부에서 실행되어도 그 환경에 접근할 수 있는 것을 말합니다. 간단히 말하면, 함수가 자신이 태어난 곳을 기억하는 것입니다.
🎯 비유로 이해하기
배낭 비유
클로저를 배낭에 비유하면:
엄마가 아이를 학교에 보낼 때
├─ 아이 = 함수
├─ 배낭 = 클로저
├─ 배낭 속 물건 = 외부 변수
└─ 학교 = 실행 컨텍스트
과정:
1. 엄마(외부 함수)가 아이( 내부 함수)에게 배낭을 줌
2. 배낭에는 필요한 물건들(변수)이 들어있음
3. 아이는 학교(다른 곳)에 가서도
4. 배낭 속 물건을 꺼내 쓸 수 있음
5. 엄마가 없어도 배낭은 계속 있음
클로저도 마찬가지:
- 함수가 선언될 때 "배낭"을 받음
- 나중에 어디서 실행되든
- 배낭 속 변수에 접근 가능
- 외부 함수가 종료되어도 유지됨
은행 금고 비유
은행 금고 시스템
├─ 금고실 = 외부 함수
├─ 금고 = 외부 변수 (비밀 데이터)
├─ 열쇠 = 내부 함수 (접근 방법)
└─ 고객 = 외부 코드
특징:
1. 금고실이 문 닫혀도 (함수 종료)
2. 열쇠를 가진 사람은 (클로저)
3. 여전히 금고에 접근 가능 (변수 접근)
4. 다른 사람은 접근 불가 (캡슐화)
코드로:
function createVault(secret) { // 금고실
let money = secret; // 금고
return {
deposit(amount) { // 입금 열쇠
money += amount;
},
getBalance() { // 조회 열쇠
return money;
}
};
}
const myVault = createVault(1000);
myVault.deposit(500);
console.log(myVault.getBalance()); // 1500
// money에 직접 접근 불가! (보안)
사진첩 비유
사진 찍기
├─ 카메라 = 외부 함수
├─ 사진 = 클로저
├─ 배경/사람 = 변수들
└─ 현상된 사진 = 반환된 함수
과정:
1. 특정 시점에 사진 찍음 (함수 선언)
2. 그 순간의 모든 것이 사진에 담김 (변수 캡처)
3. 나중에 사진을 보면 (함수 실행)
4. 그때의 상황을 볼 수 있음 (변수 접근)
5. 실제 그 장소가 사라져도 (외부 함수 종료)
6. 사진은 영구히 남아있음 (클로저 유지)
예시:
function takePhoto(location, people) {
return function viewPhoto() {
console.log(`${location}에서 ${people}와 함께`);
};
}
const photo1 = takePhoto('제주도', '가족');
const photo2 = takePhoto('서울', '친구들');
// 나중에 어디서든
photo1(); // 제주도에서 가족과 함께
photo2(); // 서울에서 친구들과 함께
⚙️ 작동 원리
1. 렉시컬 스코프
// 스코프: 변수의 유효 범위
// 전역 스코프
let globalVar = '전역';
function outer() {
// outer 스코프
let outerVar = '외부';
function inner() {
// inner 스코프
let innerVar = '내부';
// inner는 모든 변수에 접근 가능
console.log(innerVar); // '내부'
console.log(outerVar); // '외부'
console.log(globalVar); // '전역'
}
// outer는 inner의 변수에 접근 불가
// console.log(innerVar); // ❌ 에러!
inner();
}
outer();
// 렉시컬 스코프 체인:
// inner → outer → global
// 안쪽에서 바깥쪽으로 찾아감
// 스코프는 선언 위치에 의해 결정됨 (정적)
let x = 1;
function foo() {
console.log(x);
}
function bar() {
let x = 2;
foo(); // 1 출력 (foo가 선언된 곳의 x)
}
bar();
2. 클로저 기본 예시
// 가장 간단한 클로저
function makeCounter() {
let count = 0; // 외부 함수의 변수
return function() { // 내부 함수 (클로저)
count++;
return count;
};
}
const counter = makeCounter();
console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3
// count는 counter 함수 안에 "살아있음"!
// makeCounter가 실행 완료되어도 count는 사라지지 않음
// 설명:
// 1. makeCounter 실행 → count = 0 생성
// 2. 내부 함수 반환
// 3. makeCounter 종료 (하지만 count는 유지!)
// 4. counter() 호출 시 count에 접근 가능
// 5. count는 counter 함수만 접근 가능 (캡슐화)
// 여러 개의 독립적인 클로저
const counter1 = makeCounter();
const counter2 = makeCounter();
console.log(counter1()); // 1
console.log(counter1()); // 2
console.log(counter2()); // 1 (독립적!)
console.log(counter2()); // 2
console.log(counter1()); // 3 (영향 없음)
3. 클로저의 실행 컨텍스트
// 실행 컨텍스트와 클로저
function outer(x) {
// outer 실행 컨텍스트 생성
// { x: 10 }
return function inner(y) {
// inner 실행 컨텍스트 생성
// { y: 5 }
// + outer의 컨텍스트 참조 (클로저)
return x + y;
};
}
const addToTen = outer(10);
// outer 실행 완료
// 하지만 { x: 10 }은 메모리에 유지됨
// addToTen이 참조하고 있기 때문
console.log(addToTen(5)); // 15
console.log(addToTen(20)); // 30
// 메모리 구조:
/*
Global Execution Context
├─ addToTen: function
│
Closure (addToTen)
├─ [[Environment]]: { x: 10 }
└─ function(y) { return x + y; }
addToTen(5) 실행 시:
├─ y: 5
├─ x: 10 (클로저에서 가져옴)
└─ return: 15
*/
4. 클로저와 메모리
// 클로저는 필요한 변수만 기억함
function outer() {
let used = '사용됨';
let unused = '사용 안 됨';
let alsoUnused = '이것도 안 씀';
return function inner() {
console.log(used); // used만 사용
};
}
const fn = outer();
// 클로저는 used만 기억
// unused, alsoUnused는 가비지 컬렉션됨
// 메모리 누수 주의!
function createHugeClosure() {
const hugeArray = new Array(1000000).fill('데이터');
return function() {
console.log(hugeArray[0]); // 배열 전체를 메모리에 유지!
};
}
// ✅ 개선: 필요한 것만 저장
function createOptimizedClosure() {
const hugeArray = new Array(1000000).fill('데이터');
const firstItem = hugeArray[0]; // 필요한 것만 복사
return function() {
console.log(firstItem); // 작은 데이터만 유지
};
}
5. 클로저의 일반적인 실수
// ❌ 흔한 실수: 반복문에서 클로저
for (var i = 0; i < 5; i++) {
setTimeout(function() {
console.log(i);
}, 100);
}
// 예상: 0, 1, 2, 3, 4
// 실제: 5, 5, 5, 5, 5
// 이유: 모든 클로저가 같은 i를 참조
// setTimeout이 실행될 때는 이미 i = 5
// ✅ 해결 1: let 사용 (블록 스코프)
for (let i = 0; i < 5; i++) {
setTimeout(function() {
console.log(i);
}, 100);
}
// 출력: 0, 1, 2, 3, 4
// let은 반복마다 새로운 i 생성
// ✅ 해결 2: IIFE (즉시 실행 함수)
for (var i = 0; i < 5; i++) {
(function(j) {
setTimeout(function() {
console.log(j);
}, 100);
})(i); // i를 복사하여 j로 전달
}
// ✅ 해결 3: 별도 함수로 분리
for (var i = 0; i < 5; i++) {
createTimeout(i);
}
function createTimeout(index) {
setTimeout(function() {
console.log(index);
}, 100);
}
💡 실제 예시
캡슐화 (Private 변수)
// 클로저를 이용한 프라이빗 변수
function createBankAccount(initialBalance) {
// 프라이빗 변수들
let balance = initialBalance;
let transactionHistory = [];
// 프라이빗 함수
function addTransaction(type, amount) {
transactionHistory.push({
type,
amount,
date: new Date(),
balance: balance
});
}
// 공개 API (메서드들)
return {
// 입금
deposit(amount) {
if (amount <= 0) {
throw new Error('입금액은 양수여야 합니다');
}
balance += amount;
addTransaction('입금', amount);
return balance;
},
// 출금
withdraw(amount) {
if (amount <= 0) {
throw new Error('출금액은 양수여야 합니다');
}
if (amount > balance) {
throw new Error('잔액이 부족합니다');
}
balance -= amount;
addTransaction('출금', amount);
return balance;
},
// 잔액 조회
getBalance() {
return balance;
},
// 거래 내역 조회
getHistory() {
// 원본을 보호하기 위해 복사본 반환
return [...transactionHistory];
}
};
}
// 사용
const myAccount = createBankAccount(10000);
console.log(myAccount.getBalance()); // 10000
myAccount.deposit(5000);
console.log(myAccount.getBalance()); // 15000
myAccount.withdraw(3000);
console.log(myAccount.getBalance()); // 12000
// ❌ 프라이빗 변수에 직접 접근 불가
console.log(myAccount.balance); // undefined
myAccount.balance = 1000000; // 해킹 불가!
console.log(myAccount.getHistory()); // 거래 내역 조회
// 장점:
// 1. 데이터 보호 (캡슐화)
// 2. 검증 로직 강제
// 3. 안전한 API
// 4. 내부 구현 숨김
함수 팩토리
// 클로저를 이용한 함수 생성기
// 1. 인사말 생성기
function createGreeter(greeting) {
return function(name) {
return `${greeting}, ${name}!`;
};
}
const sayHello = createGreeter('Hello');
const sayHi = createGreeter('Hi');
const sayAnnyeong = createGreeter('안녕하세요');
console.log(sayHello('John')); // Hello, John!
console.log(sayHi('Jane')); // Hi, Jane!
console.log(sayAnnyeong('철수')); // 안녕하세요, 철수!
// 2. 수학 연산 생성기
function createMultiplier(multiplier) {
return function(number) {
return number * multiplier;
};
}
const double = createMultiplier(2);
const triple = createMultiplier(3);
const quadruple = createMultiplier(4);
console.log(double(5)); // 10
console.log(triple(5)); // 15
console.log(quadruple(5)); // 20
// 3. 검증 함수 생성기
function createValidator(minLength, maxLength) {
return function(value) {
if (value.length < minLength) {
return `최소 ${minLength}자 이상이어야 합니다`;
}
if (value.length > maxLength) {
return `최대 ${maxLength}자 이하여야 합니다`;
}
return '유효한 입력입니다';
};
}
const validateUsername = createValidator(3, 15);
const validatePassword = createValidator(8, 30);
console.log(validateUsername('ab')); // 최소 3자 이상
console.log(validateUsername('john')); // 유효한 입력
console.log(validatePassword('1234567')); // 최소 8자 이상
console.log(validatePassword('securePass123')); // 유효한 입력
// 4. API 클라이언트 생성기
function createAPIClient(baseURL, token) {
return {
get(endpoint) {
return fetch(`${baseURL}${endpoint}`, {
headers: {
'Authorization': `Bearer ${token}`
}
});
},
post(endpoint, data) {
return fetch(`${baseURL}${endpoint}`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
});
}
};
}
const api = createAPIClient('https://api.example.com', 'my-secret-token');
// 사용
api.get('/users')
.then(res => res.json())
.then(users => console.log(users));
api.post('/users', { name: 'John' });
이벤트 핸들러와 클로저
// 클로저를 활용한 이벤트 핸들러
// 1. 카운터 버튼
function setupCounter() {
let count = 0;
const button = document.getElementById('counterBtn');
const display = document.getElementById('countDisplay');
button.addEventListener('click', function() {
count++; // 클로저로 count 접근
display.textContent = count;
});
// 리셋 버튼
const resetBtn = document.getElementById('resetBtn');
resetBtn.addEventListener('click', function() {
count = 0;
display.textContent = count;
});
}
// 2. 디바운스 (검색창)
function createDebounce(delay) {
let timeoutId;
return function(fn) {
clearTimeout(timeoutId); // 클로저로 timeoutId 접근
timeoutId = setTimeout(() => {
fn();
}, delay);
};
}
const debounce = createDebounce(500);
const searchInput = document.getElementById('search');
searchInput.addEventListener('input', function(e) {
debounce(() => {
console.log('검색:', e.target.value);
// API 호출
});
});
// 3. 버튼 클릭 횟수 제한
function createLimitedButton(maxClicks) {
let clickCount = 0;
return function(button, callback) {
button.addEventListener('click', function() {
if (clickCount < maxClicks) {
clickCount++;
callback(clickCount);
} else {
alert(`최대 ${maxClicks}번까지만 클릭 가능합니다`);
}
});
};
}
const setupLimitedButton = createLimitedButton(3);
const likeBtn = document.getElementById('likeBtn');
setupLimitedButton(likeBtn, (count) => {
console.log(`좋아요 ${count}번`);
});
// 4. 여러 버튼에 고유한 상태
function setupButtons() {
const buttons = document.querySelectorAll('.toggle-btn');
buttons.forEach((button, index) => {
let isActive = false; // 각 버튼마다 독립적인 상태
button.addEventListener('click', function() {
isActive = !isActive;
if (isActive) {
button.classList.add('active');
console.log(`버튼 ${index + 1} 활성화`);
} else {
button.classList.remove('active');
console.log(`버튼 ${index + 1} 비활성화`);
}
});
});
}
setupButtons();
커링 (Currying)
// 커링: 여러 인자를 받는 함수를 한 번에 하나씩 받는 함수로 변환
// 일반 함수
function add(a, b, c) {
return a + b + c;
}
console.log(add(1, 2, 3)); // 6
// 커링된 함수
function curriedAdd(a) {
return function(b) {
return function(c) {
return a + b + c;
};
};
}
console.log(curriedAdd(1)(2)(3)); // 6
// 또는
const add1 = curriedAdd(1);
const add1And2 = add1(2);
const result = add1And2(3);
console.log(result); // 6
// 실용적인 예시 1: URL 빌더
function createURL(protocol) {
return function(domain) {
return function(path) {
return `${protocol}://${domain}${path}`;
};
};
}
const httpURL = createURL('http');
const httpsURL = createURL('https');
const exampleHTTP = httpURL('example.com');
console.log(exampleHTTP('/api/users')); // http://example.com/api/users
const exampleHTTPS = httpsURL('example.com');
console.log(exampleHTTPS('/api/posts')); // https://example.com/api/posts
// 실용적인 예시 2: 로깅
function log(level) {
return function(module) {
return function(message) {
const timestamp = new Date().toISOString();
console.log(`[${timestamp}] [${level}] [${module}] ${message}`);
};
};
}
const logError = log('ERROR');
const logWarn = log('WARN');
const logInfo = log('INFO');
const authError = logError('Auth');
const dbError = logError('Database');
authError('로그인 실패'); // [2024-01-01...] [ERROR] [Auth] 로그인 실패
dbError('연결 실패'); // [2024-01-01...] [ERROR] [Database] 연결 실패
// 실용적인 예시 3: 할인 계산기
function discount(discountRate) {
return function(minPurchase) {
return function(price) {
if (price < minPurchase) {
return price;
}
return price * (1 - discountRate);
};
};
}
const member10 = discount(0.1)(50000); // 10% 할인, 5만원 이상
const member20 = discount(0.2)(100000); // 20% 할인, 10만원 이상
const vip30 = discount(0.3)(0); // 30% 할인, 최소 금액 없음
console.log(member10(60000)); // 54000 (10% 할인)
console.log(member10(30000)); // 30000 (할인 없음, 최소 금액 미달)
console.log(member20(150000)); // 120000 (20% 할인)
console.log(vip30(100000)); // 70000 (30% 할인)
// Arrow Function으로 간결하게
const curriedDiscount = discountRate => minPurchase => price =>
price < minPurchase ? price : price * (1 - discountRate);
const student = curriedDiscount(0.15)(0);
console.log(student(50000)); // 42500
메모이제이션 (Memoization)
// 클로저를 이용한 캐싱
// 1. 기본 메모이제이션
function memoize(fn) {
const cache = {}; // 클로저로 캐시 유지
return function(arg) {
if (arg in cache) {
console.log('캐시에서 가져옴:', arg);
return cache[arg];
}
console.log('계산 중:', arg);
const result = fn(arg);
cache[arg] = result;
return result;
};
}
// 무거운 계산 함수
function slowSquare(n) {
// 시뮬레이션: 느린 계산
let result = 0;
for (let i = 0; i < 1000000000; i++) {
result = n * n;
}
return result;
}
const fastSquare = memoize(slowSquare);
console.log(fastSquare(5)); // 계산 중: 5 → 25 (느림)
console.log(fastSquare(5)); // 캐시에서 가져옴: 5 → 25 (빠름!)
console.log(fastSquare(10)); // 계산 중: 10 → 100 (느림)
console.log(fastSquare(5)); // 캐시에서 가져옴: 5 → 25 (빠름!)
// 2. 여러 인자 메모이제이션
function memoizeMultiple(fn) {
const cache = {};
return function(...args) {
const key = JSON.stringify(args);
if (key in cache) {
console.log('캐시 사용:', args);
return cache[key];
}
console.log('계산:', args);
const result = fn(...args);
cache[key] = result;
return result;
};
}
function add(a, b) {
return a + b;
}
const memoizedAdd = memoizeMultiple(add);
console.log(memoizedAdd(1, 2)); // 계산: [1,2] → 3
console.log(memoizedAdd(1, 2)); // 캐시 사용: [1,2] → 3
console.log(memoizedAdd(2, 3)); // 계산: [2,3] → 5
// 3. 피보나치 with 메모이제이션
function createFibonacci() {
const cache = { 0: 0, 1: 1 };
return function fibonacci(n) {
if (n in cache) {
return cache[n];
}
console.log(`계산: fib(${n})`);
cache[n] = fibonacci(n - 1) + fibonacci(n - 2);
return cache[n];
};
}
const fib = createFibonacci();
console.log(fib(10)); // 계산 몇 번만
console.log(fib(15)); // 이전 결과 재사용
console.log(fib(20)); // 매우 빠름!
// 메모이제이션 없는 피보나치 (비교)
function slowFib(n) {
if (n <= 1) return n;
return slowFib(n - 1) + slowFib(n - 2);
}
console.time('memoized');
console.log(fib(40)); // 빠름!
console.timeEnd('memoized');
console.time('slow');
console.log(slowFib(40)); // 엄청 느림!
console.timeEnd('slow');
// 4. 캐시 크기 제한
function memoizeWithLimit(fn, limit = 100) {
const cache = new Map();
return function(...args) {
const key = JSON.stringify(args);
if (cache.has(key)) {
// LRU: 최근 사용을 위로
const value = cache.get(key);
cache.delete(key);
cache.set(key, value);
return value;
}
const result = fn(...args);
// 캐시 크기 제한
if (cache.size >= limit) {
// 가장 오래된 항목 제거
const firstKey = cache.keys().next().value;
cache.delete(firstKey);
}
cache.set(key, result);
return result;
};
}
React에서 클로저
import React, { useState, useEffect } from 'react';
// 1. useState와 클로저
function Counter() {
const [count, setCount] = useState(0);
// ❌ 클로저 함정
function handleClick() {
setTimeout(() => {
console.log(count); // 클릭 시점의 count
setCount(count + 1); // 문제: 오래된 count 사용
}, 1000);
}
// ✅ 해결 1: 함수형 업데이트
function handleClickCorrect() {
setTimeout(() => {
setCount(prevCount => prevCount + 1);
}, 1000);
}
return (
<div>
<p>Count: {count}</p>
<button onClick={handleClickCorrect}>+1</button>
</div>
);
}
// 2. useEffect와 클로저
function Timer() {
const [count, setCount] = useState(0);
useEffect(() => {
const timer = setInterval(() => {
// ❌ 항상 초기 count (0) 참조
console.log(count);
}, 1000);
return () => clearInterval(timer);
}, []); // 빈 배열: 마운트 시 한 번만
// ✅ 해결: 의존성 배열에 count 추가
useEffect(() => {
const timer = setInterval(() => {
console.log(count); // 최신 count 참조
}, 1000);
return () => clearInterval(timer);
}, [count]); // count 변경 시마다 재실행
return <div>Count: {count}</div>;
}
// 3. 이벤트 핸들러와 클로저
function UserList({ users }) {
return (
<ul>
{users.map((user, index) => (
<li key={user.id}>
{user.name}
<button onClick={() => {
console.log(`User ${index}: ${user.name}`);
// 클로저로 user와 index 캡처
}}>
Details
</button>
</li>
))}
</ul>
);
}
// 4. 커스텀 Hook과 클로저
function useCounter(initialValue = 0) {
const [count, setCount] = useState(initialValue);
// 클로저로 count와 setCount 캡처
const increment = () => setCount(c => c + 1);
const decrement = () => setCount(c => c - 1);
const reset = () => setCount(initialValue);
return { count, increment, decrement, reset };
}
// 사용
function App() {
const counter = useCounter(10);
return (
<div>
<p>Count: {counter.count}</p>
<button onClick={counter.increment}>+</button>
<button onClick={counter.decrement}>-</button>
<button onClick={counter.reset}>Reset</button>
</div>
);
}
🤔 자주 묻는 질문
Q1. 클로저는 언제 사용하나요?
A: 다음과 같은 상황에서 유용합니다:
// 1. 데이터 은닉/캡슐화
const useCases = {
dataPrivacy: `
// Private 변수 구현
function createPerson(name, age) {
// 외부에서 직접 접근 불가
let _name = name;
let _age = age;
return {
getName: () => _name,
getAge: () => _age,
setAge: (newAge) => {
if (newAge > 0 && newAge < 150) {
_age = newAge;
}
}
};
}
const person = createPerson('철수', 25);
console.log(person.getName()); // 철수
console.log(person._name); // undefined (접근 불가)
`,
factoryPattern: `
// 함수 생성기
function createAdder(x) {
return (y) => x + y;
}
const add5 = createAdder(5);
console.log(add5(10)); // 15
`,
eventHandlers: `
// 이벤트 핸들러에서 상태 유지
function setupButton(id) {
let clickCount = 0;
const button = document.getElementById(id);
button.addEventListener('click', () => {
clickCount++;
console.log(\`클릭 \${clickCount}번\`);
});
}
`,
memoization: `
// 계산 결과 캐싱
function memoize(fn) {
const cache = {};
return (arg) => {
if (!(arg in cache)) {
cache[arg] = fn(arg);
}
return cache[arg];
};
}
`,
partialApplication: `
// 부분 적용
function multiply(a, b) {
return a * b;
}
function partial(fn, ...fixedArgs) {
return (...remainingArgs) => {
return fn(...fixedArgs, ...remainingArgs);
};
}
const double = partial(multiply, 2);
console.log(double(5)); // 10
`
};
// 사용하지 말아야 할 때:
const avoidClosure = {
simpleCalculation: `
// ❌ 불필요한 클로저
function add(a, b) {
return function() {
return a + b;
};
}
// ✅ 간단하게
function add(a, b) {
return a + b;
}
`,
performanceIssue: `
// ❌ 반복문에서 많은 클로저 생성
for (let i = 0; i < 10000; i++) {
array[i] = function() {
return i * 2;
};
}
// ✅ 필요할 때만 생성
function createMultiplier(i) {
return i * 2;
}
`
};
Q2. 클로저와 메모리 누수의 관계는?
A: 클로저는 변수를 계속 참조하므로 메모리 관리에 주의해야 합니다:
// ❌ 메모리 누수 예시 1: 불필요한 참조
function createHugeClosures() {
const hugeData = new Array(1000000).fill('data');
return {
getFirst: () => hugeData[0], // 전체 배열 유지!
getSecond: () => hugeData[1]
};
}
// ✅ 개선: 필요한 것만 저장
function createOptimizedClosures() {
const hugeData = new Array(1000000).fill('data');
const first = hugeData[0];
const second = hugeData[1];
return {
getFirst: () => first, // 작은 데이터만
getSecond: () => second
};
}
// ❌ 메모리 누수 예시 2: 순환 참조
function createCircularReference() {
const obj = {};
const closure = () => obj;
obj.closure = closure; // 순환 참조!
return closure;
}
// ✅ 개선: 참조 정리
function createSafeClosure() {
const obj = {};
const closure = () => obj;
return {
getClosure: () => closure,
cleanup: () => {
obj.closure = null; // 참조 해제
}
};
}
// ❌ 메모리 누수 예시 3: 이벤트 리스너
function badEventHandler() {
const data = new Array(100000).fill('data');
document.getElementById('button').addEventListener('click', () => {
console.log(data.length); // data를 계속 참조
});
// 페이지를 벗어나도 data가 메모리에 남음!
}
// ✅ 개선: 리스너 제거
function goodEventHandler() {
const data = new Array(100000).fill('data');
function handleClick() {
console.log(data.length);
}
const button = document.getElementById('button');
button.addEventListener('click', handleClick);
// 정리 함수
return () => {
button.removeEventListener('click', handleClick);
// 이제 data는 가비지 컬렉션됨
};
}
// React에서 정리
function Component() {
useEffect(() => {
const data = new Array(100000).fill('data');
function handleResize() {
console.log(data.length);
}
window.addEventListener('resize', handleResize);
// 클린업 (중요!)
return () => {
window.removeEventListener('resize', handleResize);
};
}, []);
return <div>Component</div>;
}
// 메모리 누수 확인 방법:
// 1. Chrome DevTools → Memory → Heap Snapshot
// 2. 액션 전후 스냅샷 비교
// 3. Detached DOM 노드 확인
// 4. 클로저가 참조하는 변수 확인
Q3. 클로저와 this의 관계는?
A: 클로저와 this는 함께 사용할 때 주의가 필요합니다:
// 문제: this는 클로저에 저장되지 않음!
const person = {
name: '철수',
age: 25,
// ❌ 일반 함수 + setTimeout
greet: function() {
setTimeout(function() {
console.log(`안녕, ${this.name}`); // undefined
// this는 window를 가리킴!
}, 1000);
},
// ✅ 해결 1: Arrow Function
greetArrow: function() {
setTimeout(() => {
console.log(`안녕, ${this.name}`); // 철수
// Arrow function은 상위 스코프의 this 사용
}, 1000);
},
// ✅ 해결 2: 변수에 저장
greetSelf: function() {
const self = this; // this를 변수에 저장
setTimeout(function() {
console.log(`안녕, ${self.name}`); // 철수
}, 1000);
},
// ✅ 해결 3: bind 사용
greetBind: function() {
setTimeout(function() {
console.log(`안녕, ${this.name}`); // 철수
}.bind(this), 1000);
}
};
person.greet(); // 안녕, undefined
person.greetArrow(); // 안녕, 철수
person.greetSelf(); // 안녕, 철수
person.greetBind(); // 안녕, 철수
// React 클래스 컴포넌트에서
class Counter extends React.Component {
constructor(props) {
super(props);
this.state = { count: 0 };
// ❌ bind 안 하면
// this.handleClick = this.handleClick;
}
// ✅ 해결 1: bind
// this.handleClick = this.handleClick.bind(this);
// ✅ 해결 2: Arrow Function (권장)
handleClick = () => {
this.setState({ count: this.state.count + 1 });
}
render() {
return (
<button onClick={this.handleClick}>
Count: {this.state.count}
</button>
);
}
}
// 클로저 vs this 요약
const summary = {
closure: {
captures: '변수',
lexical: '선언 위치에서 결정',
arrow: '영향 없음'
},
this: {
captures: '실행 컨텍스트',
lexical: '호출 방식에 따라 결정',
arrow: '상위 스코프의 this 사용'
}
};
Q4. 클로저 디버깅은 어떻게 하나요?
A: Chrome DevTools를 활용하여 클로저를 확인할 수 있습니다:
// 1. console.dir로 클로저 확인
function outer() {
let secret = '비밀';
function inner() {
console.log(secret);
}
return inner;
}
const fn = outer();
console.dir(fn); // [[Scopes]] 항목에서 클로저 확인 가능
// 2. debugger로 중단점 설정
function createCounter() {
let count = 0;
return function() {
debugger; // 여기서 중단
count++;
return count;
};
}
const counter = createCounter();
counter(); // DevTools가 열리고 클로저 변수 확인 가능
// 3. 클로저 내용 출력 함수
function inspectClosure(fn) {
console.log('함수:', fn.name);
console.log('문자열:', fn.toString());
console.dir(fn);
}
// 4. 실전 디버깅 예시
function buggyCounter() {
let count = 0;
function increment() {
count++;
console.log('Count:', count);
console.log('Stack:', new Error().stack); // 호출 스택 확인
}
function getCount() {
debugger; // 클로저 상태 확인
return count;
}
return { increment, getCount };
}
const counter = buggyCounter();
counter.increment();
counter.increment();
console.log(counter.getCount());
// 5. 메모리 프로파일링
// Chrome DevTools:
// 1. Memory 탭 열기
// 2. Heap Snapshot 찍기
// 3. 작업 수행
// 4. 다시 Snapshot
// 5. Comparison 모드로 변화 확인
// 6. 클로저 내용 확인 유틸리티
function getClosureVars(fn) {
const result = {};
// 함수 문자열에서 참조된 변수 찾기 (근사치)
const fnStr = fn.toString();
const matches = fnStr.match(/\b([a-z_$][a-z0-9_$]*)\b/gi);
console.log('함수에서 참조하는 변수들:', [...new Set(matches)]);
return result;
}
const myFunc = (function() {
const x = 10;
const y = 20;
return function() {
return x + y;
};
})();
getClosureVars(myFunc);