[Javascript] 클로저(Closure)란?

우지끈·2024년 10월 17일

클로저는 함수와 그 함수가 선언된 렉시컬 환경(lexical environment)과의 조합이다.

const x = 1;

function outerFunc() {
  const x = 10;
  function innerFunc() {
    console.log(x); // 10
  }

  innerFunc();
}

outerFunc();

해당 코드에서 console.log(x)는 10을 출력하게 될 것이다.
innerFunc() 스코프 내부에서 x의 값을 먼저 찾았으나, 없기 때문에 scope chain에 의해 바깥쪽 스코프인 outer를 찾게 될 것이고, outer는 생성 시점의 LexicalEnvironment를 갖고 있다. 따라서 10에 접근하여 console.log(x)는 10이 출력되는 것이다.


렉시컬 스코프

Javascript 엔진은 함수를 어디서 호출했는지가 아니라 함수를 어디에 정의했는지에 따라 상위 스코프를 결정한다.
즉, 스코프에 대한 참조는 함수 정의가 평가되는 시점에함수가 정의된 환경(위치)에 의해 결정된다.
이것이 바로 렉시컬 스코프다.

const x = 1;

function foo() {
  const x = 10;

  // 상위 스코프는 함수 정의 환경(위치)에 따라 결정된다.
  // 함수 호출 위치와 상위 스코프는 아무런 관계가 없다.
  bar();
}

// 여기보세요 여기!
// 함수 bar는 자신의 상위 스코프, 즉 전역 렉시컬 환경을 저장하여
// 기억한다.
function bar() {
  console.log(x);
}

foo();
bar();

해당 코드를 보면 bar() 함수는 foo() 함수 내부에서 호출되긴 했지만 foo() 함수 내부에서 정의된 함수가 아니다. 따라서 bar() 함수의 outer는 전역이기에 console.log(x)는 1이 출력될 것이다.


클로저(Closure)

외부 함수보다 중첩 함수가 더 오래 유지되는 경우, 중첩 함수는 이미 생명 주기가 종료된 외부 함수의 변수를 여전히 참조할 수 있다.

해당 개념에서의 중첩 함수가 바로 클로저인 것이다.

const x = 1;

function outer() {
  const x = 10;
  const inner = function () {
    console.log(x);
  };
  return inner;
}

const innerFunc = outer();
innerFunc();

해당 코드에서 outer 함수는 inner 함수를 반환하며 바로 제거되지만, inner 함수는 종료된 outer의 변수를 여전히 참조하여 10을 출력하게 된다.

즉, outer 함수의 실행 컨텍스트는 실행 컨텍스트 스택에서 제거되지만 outer 함수의 렉시컬 환경까지 소멸되는 것은 아니다.

이게 가능한 이유는 outer 함수의 렉시컬 환경을 참조하는 곳이 있기에 가비지 컬렉터가 수거를 하지 않기 때문이다.

다른 예시 코드를 살펴보도록 하겠다.

// 카운트 상태 변경 함수 #3
const increase = (function () {
  // 카운트 상태 변수
  let num = 0;

  // 클로저
  return function () {
    return ++num;
  };
})();

// 이전 상태값을 유지
console.log(increase()); //1
console.log(increase()); //2
console.log(increase()); //3
  1. 즉시 실행 함수(IIFE):
    • (function() {...})()는 선언과 동시에 실행된다.
    • 이 함수가 실행될 때, 지역 변수 num이 선언되고 초기화 된다.
  2. 클로저 생성:
    • IIFE는 내부에서 정의된 익명 함수를 반환한다.
    • 이 익명 함수는 자신이 정의된 스코프(즉, IIFE의 내부)에 있는 변수 num을 참조하는 클로저이다.
    • 이 익명 함수가 increase라는 변수에 저장된다.
  3. 클로저의 참조 유지:
    • increase 변수에 저장된 익명 함수는 여전히 num을 참조하고 있다.
    • javascript는 이 참조를 인지하고 있기 때문에, num이 정의된 렉시컬 스코프를 메모리에 유지한다.
  4. 함수 호출과 변수 유지:
    • 이후에 increase()를 호출하면, 클로저가 참조하는 환경에서 num 값을 증가시킨다.
    • 이 과정에서 num은 초기화되지 않고, 이전 상태를 기억하고 있는다.
    • 클로저는 함수가 종료되었더라도 자신이 참조하는 스코프의 변수를 유지할 수 있다.

여기서 increase 함수가 다시 호출되지 않을 수도 있는데 어떻게 메모리에 계속 저장하고 있을 수 있는지 궁금해서 따로 찾아본 결과,
increase 함수가 다시 호출되지 않더라도, increase라는 변수 자체가 여전히 존재하고 있기 때문에, 클로저는 메모리에 남아 있는다고 한다.

따라서 익명 함수가 여전히 클로저로써 num을 참조하고 있기에 가비지 컬렉터는 이를 수거하지 않고 메모리에 남겨두는 것이며, 그에 따라 익명 함수는 이전의 num 값을 기억해 계속 증가시킬 수 있는 것이다.

0개의 댓글