프론트엔드

클로저

2026. 03. 23. 월요일 오후 10시 42분

클로저에 대하여

이상한 Javascript 문제

1function makeCounter() {
2  let count = 0;
3
4  return function () {
5    return ++count;
6  };
7}
8
9const counter = makeCounter();
10counter(); // 1
11counter(); // 2
12counter(); // 3
13

위의 코드에서 makeCounter는 이미 실행이 끝났습니다. counter를 또 호출한다면, count 변수가 다시 선언되어 항상 같은 결과가 출력될 것 같습니다. 하지만 counter()를 호출할 때마다 count가 계속 증가합니다. 왜 이렇게 동작하는걸까요?

이 문제를 해결하려면, 클로저를 이해해야 합니다.

클로저(Closure)란?

클로저란, 함수가 선언된 시점의 주변환경(Lexical Environment)을 기억하는 현상을 말합니다.

좀 더 풀어서 설명하면, 클로저는 자신의 스코프 안에 없는 변수라도 스코프 체인을 통해 알 수 있는 변수라면, 선언 시점에 해당 변수를 기억합니다. 그리고 이렇게 기억된 변수는 클로저에 의한 참조가 유지되는 한 가비지 컬렉터에 의해 수집되지 않습니다.

MDN에서는 클로저를 "함수와 함수가 선언될 당시의 주변환경(LexicalEnvironment)의 조합" 이라고 표현합니다.

코드로 이해하기

1function func1() {
2  // 이 변수가 클로저에 의해 참조되는 변수.
3  let a = 1;
4
5  function func2() {
6    console.log(++a);
7  }
8
9  return func2;
10}
11
12const test = func1();
13
14test(); // 2
15test(); // 3
16

func1은 이미 실행이 끝났습니다. 그런데 왜 a가 소멸되지 않을까요?

핵심은 여기 있습니다.

  • testfunc2를 참조하고 있습니다.
  • func2func1의 실행 컨텍스트에 있는 변수 a를 참조하고 있습니다.
  • 값을 참조하는 변수가 남아있다면, 가비지 컬렉터는 해당 값을 소멸시키지 않습니다.

따라서 func1의 실행이 끝나도 a는 살아있고, test()를 호출할 때마다 a의 값이 유지됩니다.


실전 예제

예제 1: 카운터 — 상태 은닉

클로저를 사용하면 외부에서 직접 접근할 수 없는 변수를 만들 수 있습니다.

1const counter = function () {
2  let count = 0;
3
4  function changeCount(number) {
5    count += number;
6  }
7
8  return {
9    increase: function () {
10      changeCount(1);
11    },
12    decrease: function () {
13      changeCount(-1);
14    },
15    show: function () {
16      console.log(count);
17    },
18  };
19};
20
21const counterClosure = counter();
22counterClosure.increase();
23counterClosure.show(); // 1
24counterClosure.decrease();
25counterClosure.show(); // 0
26
  • count 변수는 외부에서 직접 접근할 수 없습니다.
  • 오직 increase, decrease, show를 통해서만 간접적으로 접근 가능합니다.
  • increase, decrease, show 함수들이 countchangeCount에 대한 클로저입니다.
  • 이렇게 클로저를 통해 상태 은닉을 구현할 수 있습니다.

예제 2: 잘못된 이벤트 바인딩 — 클로저 함정

클로저를 이해하지 못하면 흔히 빠지는 함정입니다.

1<button id="target1">button</button>
2<button id="target2">button</button>
3<button id="target3">button</button>
4<button id="target4">button</button>
5<button id="target5">button</button>
6
7<script>
8  for (var i = 1; i <= 5; i++) {
9    document.getElementById('target' + i).onclick = function () {
10      console.log(i); // 항상 6이 출력된다!
11    };
12  }
13</script>
14

어떤 버튼을 눌러도 6이 출력됩니다. 이유는 이렇습니다.

  • 호이스팅에 의해, 실행 컨텍스트를 생성할 때 변수 i가 끌어올려집니다.
  • 콜백함수 안의 i는 끌어올려진 변수 i를 참조합니다. 모든 버튼의 콜백에서 같은 변수를 참조하는 상황입니다.
  • 버튼을 눌러 콜백이 호출되는 시점엔, 변수 i에는 6이 할당되어 있습니다. 따라서 어떤 버튼을 누르건 같은 변수를 참조하고, 항상 6이 출력됩니다.
  • 각각의 콜백에서 바인딩 시점의 i 값을 사용하게 하려면 어떻게 해야 할까요?

해결 방법: let 사용

1for (let i = 1; i <= 5; i++) {
2  document.getElementById('target' + i).onclick = function () {
3    console.log(i); // 각각 1, 2, 3, 4, 5 출력
4  };
5}
6
  • var 대신 let을 사용하면 됩니다.
  • var과 다르게 let은 블록 스코프입니다. for 루프에서 let을 사용하는 경우, 반복마다 독립된 i를 갖게 됩니다.
  • 따라서, 각각의 콜백은 독립된 i를 클로저로 기억하게 됩니다.
  • 덕분에 각 콜백은 서로 다른 i 값을 출력하게 됩니다.

예제 3: 디바운스

클로저를 사용하면 디바운스(debounce)를 쉽게 구현할 수 있습니다. 디바운스 함수는 타이머가 종료되어야 콜백을 호출하고, 그 전에 디바운스 함수가 재호출되면 타이머를 리셋시킵니다. 이렇게 하여 콜백이 빈번하게 호출되는 문제를 방지할 수 있습니다. 주로 자동완성 기능을 구현할 때 사용합니다.

1const input = document.querySelector('input');
2const div = document.querySelector('div');
3
4const debounce = function (eventName, callback, wait) {
5  let timeoutId = null;
6
7  return function (event) {
8    console.log('debounce invoked >>> ', eventName);
9    clearTimeout(timeoutId); // 클로저가 기억하는 timeoutId 초기화
10    timeoutId = setTimeout(() => callback(event), wait);
11  };
12};
13
14const onKeyDown = function (e) {
15  console.log('onKeyDown >>> ', e);
16  div.innerText = e.target.value;
17};
18
19// 이벤트 이름, 콜백, 대기 시간을 미리 넘겨서 부분 적용 함수를 만든다
20input.addEventListener('keydown', debounce('keydown', onKeyDown, 500));
21
  • 반환된 함수는 timeoutIdeventName을 클로저로 기억합니다.
  • 키 입력이 500ms 내에 반복되면 타임아웃을 초기화하므로, 빈번한 콜백 호출을 막습니다.
  • 이렇게 클로저를 사용하면 전역변수를 사용하지 않고도 디바운스를 쉽게 구현할 수 있습니다.

클로저 메모리 관리

클로저는 변수를 계속 메모리에 유지합니다. 더 이상 사용하지 않는 클로저는 참조를 끊어줘야 합니다.

1let test = func1();
2test();
3test();
4
5test = null; // 참조를 끊어 GC가 수집할 수 있도록 한다
6

정리

  • 클로저는 함수 선언 시점의 주변환경(LexicalEnvironment)을 기억하는 현상입니다.
  • 클로저가 살아 있는 한, 클로저가 기억하는 주변환경은 가비지 컬렉터에 의해 수집되지 않습니다.
  • 상태 은닉, 이벤트 바인딩, 디바운스 등 다양한 패턴에 활용됩니다.
  • 사용이 끝난 클로저는 참조를 끊어 메모리 누수를 방지해야 합니다.

마치며

이번 포스트에선 클로저에 대해 알아보았습니다. 클로저는 자바스크립트의 스코프와 실행 컨텍스트를 이해하는 데 있어 핵심적인 개념입니다. 긴 글 읽어주셔서 감사합니다.