포스트

[코어 자바스크립트] 5. 클로저

클로저의 개념을 Lexical Environment와 GC의 관점에서 이해하고 활용 방법까지 정리해보기

[코어 자바스크립트] 5. 클로저

✍🏻 핵심 정리

  • 클로저란 내부 함수가 외부 변수를 참조하고 있을 때, 외부 함수의 실행이 종료된 후에도 그 환경을 기억하여 변수에 계속 접근할 수 있는 현상을 말한다.

클로저란?

MDN에서는 클로저를 함수와 그 함수가 선언될 당시의 Lexical Environment의 상호관계에 따른 현상이라고 정의한다.
조금 더 쉽게 풀어서 정의하면 다음과 같다.

  • 어떤 함수(A)에서 선언한 변수(a)를 참조하는 내부함수(B)를 외부로 전달하거나, 외부에서 참조할 수 있는 상태로 만드는 경우에
  • A의 실행 컨텍스트가 종료된 이후에도 변수 a에 계속 접근할 수 있는 현상

핵심은 환경이 유지되는 것이다.
내부 함수가 외부 변수를 참조하고 있고, 그 내부 함수가 살아있는 한 (참조되고 있는 한)
JS 엔진의 가비지 컬렉터(GC)는 해당 변수들이 포함된 환경을 수거하지 않는다.

클로저 vs 스코프 체인

  • 클로저: 외부 함수가 종료된 이후에도 그 환경을 기억하고 접근할 수 있는 현상
  • 스코프 체인: 함수 실행 중에 상위 스코프의 변수를 탐색하는 규칙

클로저와 메모리 관리

클로저는 의도적으로 특정 데이터를 메모리에 유지하는 기법이다.
따라서 더 이상 필요하지 않게 되었을 때는 메모리 누수를 방지하기 위해 참조를 끊어주어야 한다.

참조 끊기 (GC 대상 만들기)

참조형 데이터(변수)에 null이나 undefined를 할당하면 된다.

1
2
3
4
5
6
7
8
9
10
let outer = function () {
  let a = 1;
  return function () {
    return ++a;
  };
};

let addr = outer();
console.log(addr()); // 2
addr = null; // 참조 해제 -> 메모리 회수 대상

클로저의 활용

1. 콜백 함수 내부에서 외부 데이터 사용

이벤트 핸들러나 비동기 작업 내에서 외부 변수를 안전하게 유지할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
var alertFruitBuilder = function (fruit) {
  return function () {
    alert("your choice is " + fruit);
  };
};

fruits.forEach(function (fruit) {
  var $li = document.createElement("li");
  $li.innerText = fruit;
  // alertFruitBuilder가 반환한 함수가 fruit을 클로저로 기억함
  $li.addEventListener("click", alertFruitBuilder(fruit));
  $ul.appendChild($li);
});

2. 정보 은닉 (Private 변수 구현)

자바스크립트에서 객체의 상태를 외부에서 직접 수정하지 못하게 캡슐화할 때 유용하다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function Person(name) {
  let _name = name; // 외부에서 접근 불가 (Private)

  return {
    getName: function () {
      return _name;
    },
    setName: function (newName) {
      _name = newName;
    }
  };
}

const p = Person("yeeun");
console.log(p.getName()); // 'yeeun'
console.log(p._name); // undefined (접근 불가)

클로저로 반환하는 값 자체가 참조형(객체, 배열)일 경우, 사용자가 마음만 먹으면 내부를 수정할 수도 있다.
따라서 중요한 정보는 기본형 데이터로 관리하거나, 반환 시 깊은 복사를 수행하는 것이 안전하다.

최근(ES2019~)에는 클래스의 #private 필드를 통해 보다 명확한 정보 은닉이 가능하다.

3. 부분 적용 함수 (Partially Applied Function)

여러 개의 인자를 받는 함수에 미리 일부 인자를 넘겨 기억시켜두는 방식이다.
대표적으로 디바운스 구현에 사용된다.

1
2
3
4
5
6
7
8
var debounce = function (eventName, func, wait) {
  var timeoutId = null;
  return function (event) {
    var self = this;
    clearTimeout(timeoutId); // 이전 timeoutId를 클로저를 통해 기억하고 있음
    timeoutId = setTimeout(func.bind(self, event), wait);
  };
};

4. 커링 함수 (Currying Function)

여러 개의 인자를 받는 함수를 하나의 인자만 받는 함수들로 쪼개어 순차적으로 호출하는 방식이다.

1
2
3
var curry = (func) => (a) => (b) => (c) => func(a, b, c);
const getMax = curry(Math.max);
console.log(getMax(1)(2)(3)); // 3

🔍 코드로 확인하기

클로저 발생 조건 확인

내부 함수가 외부 변수를 참조한다고 해서 무조건 클로저가 유지되는 것은 아니다.
내부 함수가 외부에서 참조될 수 있는 상태여야 한다. (반드시 return일 필요는 없다)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// 1. 단순 스코프 체인 동작 (클로저 현상 유지 X)
function outer() {
  let a = 1;
  function inner() {
    console.log(a);
  }
  inner();
}
outer();

// 2. 클로저 현상 발생 (변수 a가 메모리에 유지됨)
function outer() {
  let a = 1;
  return function inner() {
    return ++a;
  };
}
const getA = outer();
console.log(getA()); // 2

// 3. return 없이도 클로저 발생
let fn;
function outer() {
  let a = 1;
  fn = function () {
    console.log(a);
  };
}
outer();
fn(); // 1

💼 실무 연결 포인트

  • 상태 관리: 전역 변수를 오염시키지 않고 특정 모듈 내부에서만 상태를 안전하게 관리함
  • 함수형 프로그래밍: 커링, 부분 적용 함수 등을 활용하여 재사용성이 높은 유틸리티를 만듦
  • 대규모 애플리케이션에서 클로저를 남발할 경우 메모리 점유율이 높아지므로, 반드시 필요하지 않은 참조는 해제해야 한다.

🗣️ 면접 대비 Q&A

Q1. 클로저란 무엇인가요?

클로저는 외부 함수가 종료된 후에도 내부 함수가 외부 함수의 스코프(Lexical Environment)에 접근할 수 있는 현상을 말합니다. 이때 내부 함수가 외부에서 계속 참조되고 있어야 해당 환경이 유지됩니다. 함수가 선언될 때의 환경을 기억함으로써 상태를 유지하거나 데이터를 은닉하는 데 사용됩니다.

Q2. 클로저가 발생하는 이유는 무엇인가요?

자바스크립트 엔진의 가비지 컬렉터는 어떤 데이터가 어디선가 참조되고 있다면 메모리에서 해제하지 않습니다. 내부 함수가 외부 함수의 변수를 참조하고 있고, 그 내부 함수 자체가 외부에서 참조되고 있다면, 외부 함수의 실행 컨텍스트가 종료되더라도 Lexical Environment는 사라지지 않고 유지되기 때문입니다.

Q3. 클로저의 단점과 해결 방법은?

메모리를 의도적으로 점유하기 때문에 과도하게 사용하면 성능 저하나 메모리 누수가 발생할 수 있습니다. 이를 해결하기 위해 사용이 끝난 클로저 변수에는 null이나 undefined를 할당하여 가비지 컬렉터가 수거할 수 있도록 명시적으로 참조를 제거해야 합니다.


💡 최종 인사이트

클로저는 자바스크립트의 Lexical ScopeGC 동작 방식이 만들어내는 현상이다.
함수가 외부 스코프를 참조한 채로 전달될 수 있기 때문에, 이미 실행이 끝난 함수의 변수라도 계속 접근 가능한 상태가 유지된다.
이러한 특성을 의도적으로 활용하면 상태 유지, 캡슐화, 함수형 패턴 구현이 가능하다.
다만 클로저는 메모리를 의도적으로 붙잡는 것이기 때문에 생명주기를 명확히 관리하는 습관을 들여야 한다.

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.