서론
JavaScript로 개발하다 보면 함수 안에 함수를 쓰는 경우가 정말 많음
React에서 useState, 이벤트 핸들러, useEffect 안에서 외부 변수를 참조하는 것도 사실 다 클로저와 관련이 있었음
처음엔 "함수가 끝났는데 왜 변수가 살아있지?" 싶었고, 면접 준비하면서 클로저가 단골 질문이라는 걸 알게 됨
"클로저가 뭔가요?"라는 질문에 제대로 대답하려면 렉시컬 스코프부터 알아야 해서, 처음부터 정리해봄
본론
1. 먼저 렉시컬 스코프(Lexical Scope)를 알아야 함
클로저를 이해하려면 렉시컬 스코프부터 알아야 함
function outer() {
const name = '그린티';
function inner() {
console.log(name); // '그린티'
}
inner();
}
outer();
inner함수는 자신이 선언된 위치 기준으로 상위 스코프(outer)의 변수name에 접근할 수 있음- 함수가 어디서 호출되었는지가 아니라, 어디서 선언되었는지에 따라 스코프가 결정되는 것 → 이게 렉시컬 스코프
여기까진 당연하게 느껴질 수 있음. 근데 함수를 밖으로 꺼내면 이야기가 달라짐
2. 클로저란?
inner를 return으로 밖으로 꺼내보자
function outer() {
const name = '그린티';
function inner() {
console.log(name);
}
return inner; // 함수 자체를 반환
}
const closureFn = outer(); // outer 실행 완료
closureFn(); // '그린티' — outer는 끝났는데 name에 접근 가능!
outer()는 이미 실행이 끝났음. 일반적으로 함수 실행이 끝나면 내부 변수는 메모리에서 사라져야 함
그런데 closureFn()을 호출하면 여전히 name에 접근할 수 있음
"함수가 끝났는데 왜 변수가 살아있지?" → 이게 바로 클로저
클로저 = 함수 + 그 함수가 선언된 렉시컬 환경(Lexical Environment)
반환된 inner 함수가 자신이 생성될 때의 스코프(변수 name이 있는 환경)를 기억하고 있기 때문에 가능한 것
3. 클로저가 없으면 생기는 문제
문제: 데이터를 보호할 수 없음
// 클로저 없이 카운터 만들기
let count = 0;
function increment() {
count++;
return count;
}
console.log(increment()); // 1
console.log(increment()); // 2
// 근데 누구나 직접 수정 가능...
count = 999;
console.log(increment()); // 1000 💀
문제점:
count가 전역에 노출되어 있어서 어디서든 수정 가능- 다른 코드가 실수로
count를 바꿔버리면 버그 발생 - 여러 개의 독립적인 카운터를 만들 수 없음
해결: 클로저로 데이터 은닉
function createCounter() {
let count = 0; // 외부에서 직접 접근 불가!
return {
increment() {
count++;
return count;
},
decrement() {
count--;
return count;
},
getCount() {
return count;
},
};
}
const counter = createCounter();
console.log(counter.increment()); // 1
console.log(counter.increment()); // 2
console.log(counter.decrement()); // 1
// counter.count → undefined (직접 접근 불가!)
// count = 999 같은 실수가 불가능!
// 독립적인 카운터도 만들 수 있음
const counter2 = createCounter();
console.log(counter2.increment()); // 1 (counter와 별개)
count 변수는 createCounter 안에 갇혀 있고, 오직 increment, decrement, getCount를 통해서만 접근 가능함. 이걸 모듈 패턴이라고도 부름
4. 클로저 실전 활용
4-1. 함수 팩토리
function multiply(x) {
return function (y) {
return x * y; // x를 클로저로 기억
};
}
const double = multiply(2);
const triple = multiply(3);
console.log(double(5)); // 10
console.log(triple(5)); // 15
multiply(2)를 호출하면 x = 2를 기억하는 클로저가 반환됨. 매번 새로운 함수를 만들 수 있어서 유용함
4-2. 디바운스 (실무에서 자주 씀)
검색 입력할 때마다 API를 호출하면 낭비니까, 타이핑을 멈춘 후 일정 시간 뒤에만 호출하는 패턴
function debounce(fn, delay) {
let timer = null; // 클로저로 timer 유지
return function (...args) {
clearTimeout(timer);
timer = setTimeout(() => {
fn(...args);
}, delay);
};
}
// 사용 예시
const searchAPI = debounce((query) => {
console.log(`API 호출: ${query}`);
}, 300);
searchAPI('리'); // 취소됨
searchAPI('리액'); // 취소됨
searchAPI('리액트'); // 300ms 후 "API 호출: 리액트" 실행
timer 변수가 클로저 덕분에 함수 호출 사이에서도 유지됨. 이게 없으면 매번 timer가 초기화돼서 디바운스가 안 됨
4-3. React에서의 클로저
사실 React 개발하면서 이미 클로저를 쓰고 있었음
function Counter() {
const [count, setCount] = useState(0);
const handleClick = () => {
// handleClick은 count를 클로저로 기억함
setCount(count + 1);
};
useEffect(() => {
// 이 콜백도 count를 클로저로 기억함
console.log(`현재 카운트: ${count}`);
}, [count]);
return <button onClick={handleClick}>{count}</button>;
}
handleClick과 useEffect의 콜백 함수가 count를 클로저로 기억하고 있음. React의 상태 관리가 클로저 위에서 동작하는 것!
5. 클로저 주의점
주의 1: var와 반복문의 함정 (면접 단골!)
// ❌ var 사용 — 의도대로 안 됨
for (var i = 0; i < 3; i++) {
setTimeout(function () {
console.log(i);
}, 1000);
}
// 출력: 3, 3, 3 (모두 3!)
var는 함수 스코프라서 i가 하나만 존재함. 루프가 끝난 시점의 i = 3을 세 함수가 공유하게 됨
// ✅ let 사용 — 정상 동작
for (let i = 0; i < 3; i++) {
setTimeout(function () {
console.log(i);
}, 1000);
}
// 출력: 0, 1, 2
let은 블록 스코프라서 각 반복마다 새로운 i가 생성되고, 각 클로저가 고유한 값을 기억함
주의 2: 메모리 누수
function heavyClosure() {
const bigData = new Array(1000000).fill('데이터');
return function () {
console.log(bigData.length);
};
}
const fn = heavyClosure();
// fn이 존재하는 한 bigData(약 4MB)는 메모리에서 해제되지 않음!
// 더 이상 필요없으면 참조를 끊어야 함
// fn = null;
클로저는 외부 변수에 대한 참조를 유지하기 때문에, 큰 데이터를 잡고 있으면 메모리 낭비가 됨. 사용이 끝나면 null로 참조를 끊어주는 게 좋음
6. 면접 예상 질문 & 답변
Q1. 클로저란 무엇인가요?
클로저는 함수가 선언될 당시의 렉시컬 환경을 기억하여, 함수가 해당 스코프 밖에서 실행되더라도 외부 변수에 접근할 수 있는 현상입니다.
Q2. 클로저는 어디에 쓰이나요?
데이터 은닉(private 변수), 함수 팩토리, 커링, 디바운스/쓰로틀, React의 useState/useEffect 등에 활용됩니다.
Q3. 클로저의 단점은?
외부 변수에 대한 참조를 계속 유지하므로, 불필요한 클로저가 남아있으면 메모리 누수가 발생할 수 있습니다. 사용이 끝난 클로저는 참조를 null로 끊어주는 것이 좋습니다.
Q4. for문에서 var와 let이 클로저에서 다르게 동작하는 이유는?
var는 함수 스코프이므로 반복문 전체에서 하나의 변수를 공유하지만,let은 블록 스코프이므로 각 반복마다 새로운 변수가 생성되어 각 클로저가 고유한 값을 기억합니다.
결론
클로저는 JavaScript에서 가장 중요한 개념 중 하나
- 렉시컬 스코프: 함수가 선언된 위치 기준으로 스코프 결정
- 클로저: 함수 + 선언 당시의 렉시컬 환경을 기억하는 현상
- 활용: 데이터 은닉, 함수 팩토리, 디바운스, React 상태 관리
- 주의: var 반복문 함정, 메모리 누수
| 개념 | 설명 |
|---|---|
| 렉시컬 스코프 | 함수가 선언된 위치 기준으로 스코프 결정 |
| 클로저 | 함수 + 선언 당시의 렉시컬 환경 |
| 활용 | 데이터 은닉, 함수 팩토리, 디바운스, React |
| 주의점 | var 반복문 함정, 메모리 누수 |
React 프로젝트에서 useState, useEffect, 이벤트 핸들러를 쓸 때마다 이미 클로저를 쓰고 있었다는 걸 알게 됨
앞으로 디바운스나 데이터 은닉이 필요할 때 클로저 패턴을 적극적으로 활용해봐야겠음!
'프론트엔드 > JavaScript' 카테고리의 다른 글
| (JavaScript) 스코프(Scope)와 스코프 체인 (0) | 2026.02.03 |
|---|---|
| (JavaScript) 호이스팅(Hoisting) 완벽 정리 (var, let, const, 함수) (0) | 2026.02.02 |
| (JavaScript) 10. this와 실행 컨텍스트 (0) | 2025.05.02 |
| (JavaScript) 9. 선택자와 이벤트 (0) | 2025.05.02 |
| (JavaScript) 8. 전개 연산자 (...)란? (0) | 2025.05.02 |