Skip to main content

🔒 클로저란?

📖 정의

**클로저(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);

Q5. 클로저의 성능 영향은?

A: 클로저는 편리하지만 성능에 영향을 줄 수 있습니다:

// 성능 비교

// 1. 클로저 vs 일반 함수
// ❌ 클로저 (느림)
function createAdderClosure() {
return function(a, b) {
return a + b;
};
}

// ✅ 일반 함수 (빠름)
function add(a, b) {
return a + b;
}

// 벤치마크
console.time('closure');
for (let i = 0; i < 1000000; i++) {
const adder = createAdderClosure();
adder(1, 2);
}
console.timeEnd('closure'); // ~50ms

console.time('normal');
for (let i = 0; i < 1000000; i++) {
add(1, 2);
}
console.timeEnd('normal'); // ~5ms

// 2. 클로저 생성 비용
// ❌ 반복문에서 클로저 생성 (비효율)
function inefficient() {
const handlers = [];

for (let i = 0; i < 10000; i++) {
handlers.push(() => {
return i * 2;
});
}

return handlers;
}

// ✅ 클로저를 재사용 (효율)
function efficient() {
const handlers = [];
const createHandler = (i) => () => i * 2;

for (let i = 0; i < 10000; i++) {
handlers.push(createHandler(i));
}

return handlers;
}

// 3. 메모리 사용량
// ❌ 큰 데이터 참조
function heavyClosure() {
const hugeArray = new Array(1000000).fill('data');

return () => hugeArray.length; // 전체 배열 메모리 유지
}

// ✅ 필요한 것만 저장
function lightClosure() {
const hugeArray = new Array(1000000).fill('data');
const length = hugeArray.length;

return () => length; // 숫자 하나만 메모리 유지
}

// 4. 최적화 팁
const optimizationTips = {
reuseClosures: `
// ❌ 매번 새 클로저 생성
function Component() {
return (
<button onClick={() => console.log('클릭')}>
Click
</button>
);
}

// ✅ 클로저 재사용
function Component() {
const handleClick = useCallback(() => {
console.log('클릭');
}, []);

return <button onClick={handleClick}>Click</button>;
}
`,

avoidHeavyCapture: `
// ❌ 불필요한 캡처
function process(data) {
const hugeObject = processData(data);

return {
getId: () => hugeObject.id,
getName: () => hugeObject.name
};
}

// ✅ 필요한 것만 캡처
function process(data) {
const hugeObject = processData(data);
const { id, name } = hugeObject;

return {
getId: () => id,
getName: () => name
};
}
`,

cleanup: `
// ✅ 사용 후 정리
function createHandler() {
let cache = new Map();

const handler = (data) => {
cache.set(data.id, data);
};

handler.cleanup = () => {
cache.clear();
cache = null;
};

return handler;
}

const handler = createHandler();
// 사용
handler.cleanup(); // 메모리 해제
`
};

// 5. 성능 측정
function measureClosurePerformance() {
// 메모리 사용량
if (performance.memory) {
const before = performance.memory.usedJSHeapSize;

const closures = [];
for (let i = 0; i < 10000; i++) {
closures.push((() => {
const data = new Array(100).fill(i);
return () => data;
})());
}

const after = performance.memory.usedJSHeapSize;
console.log(`메모리 증가: ${(after - before) / 1024 / 1024}MB`);
}

// 실행 시간
console.time('closure creation');
const fns = Array.from({ length: 10000 }, (_, i) => {
const value = i;
return () => value;
});
console.timeEnd('closure creation');

console.time('closure execution');
fns.forEach(fn => fn());
console.timeEnd('closure execution');
}

measureClosurePerformance();

🎓 다음 단계

클로저를 이해했다면, 다음을 학습해보세요:

  1. 프로토타입이란? - JavaScript의 객체지향 이해하기
  2. 동기 vs 비동기 - 클로저와 함께 자주 사용되는 비동기
  3. 이벤트 루프 - JavaScript 실행 모델 깊이 파기

🎬 마무리

클로저는 JavaScript의 강력한 기능입니다:

  • 개념: 함수가 선언될 때의 환경을 기억
  • 용도: 데이터 은닉, 팩토리 패턴, 이벤트 핸들러
  • 장점: 캡슐화, 상태 유지, 함수형 프로그래밍
  • 주의: 메모리 관리, 성능, this와의 관계

클로저를 마스터하면 더 안전하고 우아한 코드를 작성할 수 있습니다!