우당탕탕
JavaScript 클로저 실전에서 헷갈렸던 사례와 질문 8가지 답변 본문
사실 저는 JavaScript 클로저 때문에 몇 번이나 멘붕을 겪었어요. 실전에서 함수가 예상치 못한 값을 잡는 바람에 버그 찾는데 한참 헤맸던 적도 있고, 왜 클로저가 이렇게 동작하는지 개념은 아는데 예제로 직접 구현하다 보면 자꾸 헷갈리더라고요.
이 글에서는 제가 실제 프로젝트에서 클로저 쓰면서 자주 헷갈렸던 부분과, 여러분이 가장 많이 검색하는 질문들 8가지를 모아서 답해봤어요. 코드 예시도 직접 실행해 보면서 이해하기 쉽게 정리했으니 끝까지 보시면 클로저 고민 해결에 큰 도움이 될 거예요.
개발 환경 / 버전 정보
제가 테스트한 환경은 Node.js 18.x와 최신 크롬 브라우저 환경이에요. 물론 클로저는 JS 언어 자체 기능이니까 대부분 환경에서 똑같이 동작합니다.
클로저란 뭐고 왜 헷갈릴까요?
사실 이 부분이 가장 헷갈리는 분 많더라고요. 클로저는 말 그대로 어떤 함수가 자신이 선언된 렉시컬 환경(스코프)을 기억하고 참조하는 기능인데요. 이걸 이해했다고 생각해도, 실제로 변수가 계속 변하는 상황에서 어떤 값을 잡고 있나 보면 혼란스러워지죠.
제가 겪은 시행착오도 다 이런 '스코프 기억 vs 변수 값 변화' 차이였어요. 그럼 본격적으로 자주 묻는 질문들부터 풀어볼게요.
Q1. 클로저를 써도 왜 변수가 계속 변할까요? 값이 안 고정돼요!
저도 이게 가장 당황스러웠어요. 예를 들어 반복문에서 클로저를 만들어 함수들이 나중에 호출될 때 모두 마지막 i값을 참조하는 거요.
function makeClosures() {
var funcs = [];
for (var i = 0; i < 3; i++) {
funcs.push(function() {
console.log(i);
});
}
return funcs;
}
const fs = makeClosures();
fs[0](); // 3
fs[1](); // 3
fs[2](); // 3
이게 바로 var가 함수 스코프라서 반복문 돌면서 i값을 새로 만들지 않고 그대로 덮어쓰기 때문에 그래요. 클로저는 i 변수가 가진 값이 아니라 그 변수가 위치한 스코프 자체를 참조하는 거라 호출 시점에 i는 3이 된 상태였던 거죠.
해결책으로는 let 키워드를 쓰거나 즉시 실행 함수(IIFE)를 써서 각 함수가 독립된 스코프를 갖도록 만드는 방법이 있어요.
function makeClosuresFixed() {
var funcs = [];
for (let i = 0; i < 3; i++) {
funcs.push(function() {
console.log(i);
});
}
return funcs;
}
const fs2 = makeClosuresFixed();
fs2[0](); // 0
fs2[1](); // 1
fs2[2](); // 2
let은 블록 스코프라서 반복할 때마다 i가 새롭게 만들어지니 클로저가 각기 다른 i를 기억하게 돼요.
Q2. 클로저 때문에 메모리 누수 가능성 있나요?
많은 분들이 클로저가 오래된 스코프까지 계속 참조하니까 메모리를 잡아먹는 게 아닌지 걱정하시더라고요. 저도 처음에 비슷한 생각 했었고요.
실제로는 자바스크립트 엔진이 가비지 컬렉션을 똑똑하게 해서, 스코프 내 변수 중에서 참조가 끊긴 것들은 자동으로 메모리에서 해제해 줍니다. 클로저가 스코프 전체를 무조건 오래 잡고 있지는 않아요.
하지만 클로저 내에서 의도치 않게 대용량 객체나 DOM 노드를 계속 참조하는 구조를 만들면 메모리 누수가 생길 수 있으니 주의하세요.
Q3. 클로저 쓸 때 async 함수 안에서 변수 상태가 마음대로 바뀌는데 왜?
비동기 콜백이나 프로미스 안에서 클로저가 변수 상태를 '예상과 다르게' 잡아서 당황한 적 있으시죠? 저도 async await 쓰면서 가끔 그런 상황을 만났어요.
이건 클로저 자체 문제라기보단, 해당 변수의 값이 클로저가 만들어진 이후에도 계속 변하고 있기 때문이에요. 예를 들어 반복문 안에서 async 함수가 클로저를 참조하면, async 함수가 실행될 때는 이미 변수 값이 다 바뀐 상태일 수 있거든요.
async function testAsync() {
for (var i = 0; i < 3; i++) {
setTimeout(async function() {
console.log(i);
await Promise.resolve();
}, 100);
}
}
testAsync();
// 출력: 3, 3, 3
이럴 때는 역시 let이나 즉시 실행함수를 써서 i 각기 다른 값을 잡게 만들어야 해요.
Q4. 클로저에서 this는 어떻게 작동하나요?
클로저와 this의 조합은 더 헷갈리기 쉬운데요, 클로저가 this 바인딩을 직접 바꾸진 않아요. this는 함수 호출 방식에 따라 결정되고 클로저는 그저 스코프 체인에 있는 변수값을 참조할 뿐이에요.
예를 들어 메서드 안에 클로저 함수가 있을 때, 클로저 내부 this는 그 메서드의 this와 다른 경우가 많아 혼란스럽죠.
const obj = {
name: 'hello',
sayHi() {
setTimeout(function() {
console.log(this.name); // undefined 또는 window.name
}, 100);
}
};
obj.sayHi();
이 경우 setTimeout 안 함수는 전역 this를 가리켜서 obj.name이 아닌 undefined가 출력되죠. 그래서 화살표 함수로 바꿔주면 상위 스코프 this를 잡아서 해결돼요.
const obj = {
name: 'hello',
sayHi() {
setTimeout(() => {
console.log(this.name); // 'hello'
}, 100);
}
};
obj.sayHi();
결론: 클로저 변수 참조와 this 바인딩은 별개라서 각각 잘 구분해야 해요.
Q5. 클로저를 이용해 private 변수를 만드는 방법은?
클로저는 외부에서 직접 접근 못하는 변수를 만들 때 정말 유용해요. 저도 상태 은닉할 일이 많아서 이걸 자주 썼는데, 함수 하나 안에 변수를 두고 내부 함수를 통해서만 값을 읽거나 수정하게 하는 방식이에요.
function Counter() {
let count = 0; // 외부에서 못 건드림
return {
increment() {
count++;
console.log(count);
},
decrement() {
count--;
console.log(count);
},
getCount() {
return count;
}
};
}
const counter = Counter();
counter.increment(); // 1
counter.increment(); // 2
console.log(counter.getCount()); // 2
// 외부에서 count 직접 접근 불가
이런 식으로 내부 상태를 보호하면서 필요할 때만 함수로 조작하는 게 가능해요.
Q6. 클로저가 많아지면 성능 저하가 심한가요?
궁금해하는 분 많더라고요. 사실 클로저 자체가 느린 건 아니고, 너무 많은 클로저가 복잡하게 얽혀서 오래된 스코프를 계속 참조한다면 메모리 사용량이 늘 수 있습니다.
그래서 대규모 앱에서 클로저 쓸 때는 필요한 부분만 잘 쓰고, 불필요한 참조는 빨리 끊어주거나 변수를 넓은 스코프에 두는 식으로 최적화하는 게 좋아요.
Q7. 클로저가 없으면 어떻게 하나요? 대체 방법 있어요?
저도 오래된 코드 중에는 클로저 없이 전역 변수나 클래스 필드로 상태 관리하는 경우 봤는데, 요즘은 클로저가 훨씬 안전하고 깔끔해요.
하지만 ES6 클래스, 모듈 스코프, 심지어 심볼(Symbol)이나 WeakMap을 활용해서 은닉성을 어느 정도 구현할 수 있으니 프로젝트 성격에 따라 적절히 선택하는 게 좋아요.
Q8. 클로저 관련해서 가장 많이 하는 실수는 뭔가요?
한마디로 "변수 스코프와 호출 시점 변수 값 혼동"이에요. 클로저가 변수를 '복사'하는 게 아니라 참조한다는 점을 잊고, 호출 당시가 아닌 사용 시점 변수 상태를 기대하는 거죠.
또 반복문이나 비동기에서 클로저를 쓸 땐 let과 즉시 실행 함수 패턴을 꼭 기억하세요. 저처럼 이걸 빼먹으면 디버그 오래 걸립니다.
클로저 이해는 결국 스코프와 함수 실행 시점 변수 상태를 머릿속에 정확히 그리는 게 핵심이었어요.
저는 이 글을 통해 클로저가 왜 그렇게 동작하는지, 그리고 실전에서 어떤 실수가 자주 일어나는지 경험을 바탕으로 정리해 봤는데, 여러분도 한 번 코드 직접 써보면서 확인해 보시면 확실히 감이 잡힐 거예요.
이 밖에도 클로저를 활용할 수 있는 다양한 패턴들이 많으니, 익숙해지면 프론트엔드나 앱 개발에서 상태 관리할 때 유용하게 쓰인답니다.
'언어 > JavaScript' 카테고리의 다른 글
| Tailwind CSS 적용하면서 알게 된 실전 팁과 실패 경험들 (0) | 2026.06.16 |
|---|---|
| TypeScript 처음 도입하면서 헷갈렸던 부분 직접 겪어보니 (0) | 2026.06.14 |
| Zustand vs Redux 직접 써보고 비용과 성능 차이까지 비교했어요 (0) | 2026.06.08 |
| React useMemo, useCallback 진짜 필요할 때만 쓴 경험담 (0) | 2026.05.31 |
| Vite로 마이그레이션하면서 막혔던 설정, 이렇게 해결했습니다 (0) | 2026.05.31 |
