우당탕탕
JavaScript 클로저 실전에서 헷갈렸던 사례와 직접 겪은 실패담 본문
JavaScript 클로저를 활용하는 작업을 하다가 진짜 몇 번이나 코드를 다시 쓰고 오류를 잡느라 애먹었어요. 단순히 개념을 안다는 것과 현장에서 자유자재로 쓰는 건 완전 다른 문제더라고요.
이번 글에서는 제가 실제 프로젝트에서 클로저 때문에 겪은 삽질 사례들을 솔직하게 공유하려고 해요. 비슷한 고민하는 분들은 읽으면서 ‘아, 나도 그랬어!’ 하실 것 같고, 처음 클로저를 써보는 분들은 꼭 참고하셨으면 해요.
개발 환경 / 버전 정보
이 글에서 다룬 코드는 JavaScript ES6 기준이에요. 주로 React 18 환경과 Node.js 18에서 테스트했고, 브라우저는 Chrome 114 버전을 사용했습니다.
JavaScript 클로저 실전에서 헷갈렸던 사례 관련 정보
클로저의 개념은 알지만 실제로 이렇게 틀렸어요
사실 클로저가 뭔지 모르는 개발자는 거의 없을 거예요. 그런데 제가 한참 삽질한 부분은 "클로저가 유지하는 상태"에 대한 오해였어요. 저는 클로저 안에서 참조하는 변수가 변경되면 항상 그 최신 값으로 바뀌는 줄 알았거든요.
예를 들어 이런 코드를 처음에 작성했는데, 결과가 이상하게 나왔어요.
function makeCounter() {
let count = 0;
return function() {
count++;
console.log(count);
}
}
const counter = makeCounter();
counter(); // 1
counter(); // 2
// 그런데 이렇게 변수 변경이 반영 안 되는 줄 알았어요!
여기서 중요한 건 클로저가 가리키는 변수 자체가 아니라, 변수에 바인딩된 값이라는 부분인데, 저는 그걸 제대로 이해하지 못했죠.
클로저 변수 참조 관련해서 가장 많이 틀린 패턴
제가 직접 겪었던 최악의 실수 중 하나는 for 문과 클로저를 함께 썼을 때 발생했어요. 특히 이벤트 핸들러 등록 같은 코드에서 말이죠.
아래처럼 코드를 짰는데, 클릭할 때마다 모두 마지막 i 값만 출력돼서 멘붕이었어요.
for (var i = 0; i < 3; i++) {
setTimeout(function() {
console.log(i); // 모두 3이 출력됨
}, 1000);
}
저는 var를 썼고, 클로저가 각각의 i 값을 캡처하는 줄 알고 있었거든요. 그래서 한참 헤맸는데, 사실 var 변수는 함수 스코프여서 i가 계속 재할당돼 결국엔 3으로 값이 고정되는 거였더라고요.
이 문제는 let으로 바꾸거나, 클로저를 한 번 더 감싸는 함수로 해결할 수 있어요.
for (let i = 0; i < 3; i++) {
setTimeout(function() {
console.log(i); // 0, 1, 2 제대로 출력
}, 1000);
}
// 또는 이렇게 즉시 실행 함수로 감싸기
for (var i = 0; i < 3; i++) {
(function(j) {
setTimeout(function() {
console.log(j); // 0,1,2 출력
}, 1000);
})(i);
}
저는 이 코드를 처음 보고 "와, 클로저 때문에 이렇게 변수를 캡처하네"라고 생각했는데, 사실은 스코프 특성과 함께 작동하는 거라서 이 부분의 개념을 다시 잡으니 훨씬 이해가 쉬웠어요.
JavaScript 클로저 실전에서 헷갈렸던 사례 관련 정보
클로저로 상태 관리하려다 생긴 꼬임과 그 해결법
저도 처음에 클로저를 '상태 저장고'로만 생각해서, 몇몇 컴포넌트에서 상태 업데이트가 제대로 안 되는 문제를 겪었어요. 특히 React 함수형 컴포넌트 내부에서 useState 대신 클로저로 상태를 관리하려 했을 때였는데, 의도한 대로 값이 유지되지 않아서 고생했죠.
예를 들어 이런 게 문제였어요.
function Counter() {
let count = 0;
return () => {
count++;
console.log(count);
}
}
const counter = Counter();
counter(); // 1
counter(); // 2
// 그런데 React 상태가 아니어서 화면이 업데이트되지 않음
클로저 안에서 count 변수는 잘 유지되고 증가하지만, React 렌더링 단계에 영향을 주지 않아서 UI가 갱신되지 않는 거였어요. 저는 이걸 몰라서 상태가 이상하게 보일 때마다 클로저 변수 초기화 문제인 줄 알고 수없이 고쳤습니다.
이렇게 하면 됐어요: 클로저와 상태 관리의 적절한 분리
이 부분에서 삽질을 줄이려면, UI 상태는 React의 useState나 useReducer 같은 상태 관리 훅을 쓰고, 클로저는 특정 함수의 상태 캡처용이나 이벤트 핸들러 보조용으로 적당히 써야 해요.
예를 들어 이렇게 쓰면 안정적입니다.
import React, { useState, useCallback } from 'react';
function CounterComponent() {
const [count, setCount] = useState(0);
const increment = useCallback(() => {
setCount(prev => prev + 1);
}, []);
return (
<div>
<p>Count: {count}</p>
<button onClick={increment}>Increment</button>
</div>
);
}
export default CounterComponent;
이렇게 하면 상태가 React 렌더 시스템과 연동돼 제대로 화면에 반영되고, 클로저 관련 복잡한 문제에서 훨씬 자유로워져요.
JavaScript 클로저 실전에서 헷갈렸던 사례 관련 정보
여기서 많이 틀립니다: 클로저와 this의 결합 문제
제가 또 한참 헤맸던 부분은 클로저 내부에서 this가 가리키는 값이 달라지는 문제였어요. 특히 클래스를 써서 메서드를 정의할 때, 이벤트 핸들러에서 클로저가 this를 제대로 못 잡아서 에러가 났죠.
이런 식의 실수가 대표적이에요.
class Timer {
constructor() {
this.seconds = 0;
}
start() {
setInterval(function() {
this.seconds++;
console.log(this.seconds);
}, 1000);
}
}
const timer = new Timer();
timer.start();
// NaN, undefined 또는 전역 객체가 찍힘
여기서 setInterval 안의 함수가 별도의 this 컨텍스트를 갖기 때문에, 원래 클래스 인스턴스의 this가 아니어서 count가 제대로 증가하지 않았어요. 저는 이 문제를 한참 헤맸는데, 화살표 함수로 바꾸니 바로 해결됐죠.
class Timer {
constructor() {
this.seconds = 0;
}
start() {
setInterval(() => {
this.seconds++;
console.log(this.seconds);
}, 1000);
}
}
const timer = new Timer();
timer.start();
// 1, 2, 3 ... 정상 출력
이처럼 클로저 내부에서의 this 바인딩 문제도 자주 겪는 실수라서 꼭 주의해야 해요.
자주 물어보시는 것들
Q. 클로저가 메모리를 많이 차지해서 성능에 안 좋다는데 진짜인가요?
A. 클로저가 원래 참조하는 변수와 함수 때문에 해당 스코프가 해제되지 않고 유지되기 때문에, 불필요하게 오래 남으면 메모리 누수가 될 수 있어요. 하지만 일반적인 코드에서는 크게 문제 없고, 반복 생성해서 버리지 않는 구조가 문제입니다.
Q. 클로저와 모듈 패턴은 어떤 차이가 있나요?
A. 모듈 패턴도 클로저를 활용한 구조예요. 특정 변수를 외부에 노출하지 않고 내부 상태를 은닉하는 용도로 많이 쓰이고, 클로저가 유지하는 범위와 접근 방식에서 차이가 있을 뿐입니다.
제가 직접 겪으면서 클로저 개념이 막연하게 어려운 게 아니라, 실제 상황에서 어떻게 스코프와 this, 변수 선언 키워드(let/var) 등이 얽히는지 제대로 알면 금방 익숙해진다는 걸 배웠어요. 그래서 이 글이 그런 헷갈림을 조금이라도 덜어드렸으면 하네요.
앞으로도 실전에서 겪은 생생한 경험담을 더 공유할 테니, 혹시 비슷한 문제 있으면 꼭 찾아봐 주세요.
'언어 > JavaScript' 카테고리의 다른 글
| 2026년 Vite 마이그레이션 중 꼭 알아야 할 설정 변경점들 (0) | 2026.08.06 |
|---|---|
| TypeScript 제네릭 타입 추론, 2026년 바뀐 점까지 제가 겪은 경험 담았어요 (0) | 2026.08.04 |
| Next.js 13 App Router 마이그레이션 하면서 궁금했던 점들 답해봅니다 (0) | 2026.08.03 |
| JavaScript 클로저, 저도 처음엔 헷갈렸던 실전 사례들 (0) | 2026.07.22 |
| React useMemo와 useCallback, 진짜 필요할 때만 쓰는 법 알려드려요 (0) | 2026.07.18 |
