렉시컬 환경(Lexical Environment), 클로저(Closure)

SeongMok Hong·2023년 4월 11일
0

❓ 클로저(Closure)란 무엇인가요? 클로저의 원리와 사용 이유에 대해 설명해주세요.


프론트엔드 개발자 기술면접에서 단골 질문 질문 중 하나입니다.

클로저에 대해 알기 위해 Mozilla 사이트에서 해당 내용을 찾아보면 다음과 같은 설명을 확인할 수 있습니다.

클로저(Closure)는 함수와 함수가 선언된 어휘적 환경의 조합(Lexical Environment)이다. 클로저를 이해하려면 자바스크립트가 어떻게 변수의 유효범위를 지정하는지를 먼저 이해해야 한다.

클로저 - JavaScript | MDN

클로저를 이해하기 위해서는 렉시컬 환경이라고도 불리는 자바스크립트의 어휘적 환경에 대해 이해하고 있어야 합니다.

렉시컬 환경(Lexical Environment)

자바스크립트에서의 함수, 코드 블록, 스크립트 전체는 렉시컬 환경(Lexical Environment)이라 불리는 내부 숨김 연관 객체를 가지며, 이 객체는 두 부분으로 구성됩니다.

  • 환경 레코드(Environment Record) : 모든 지역 변수를 프로퍼티로 저장하고 있는 객체입니다.
  • 외부 렉시컬 환경(Outer Lexical Environment)에 대한 참조 : 외부 코드와 연관되어 있습니다.

자바스크립트 코드가 실행되면 전역 렉시컬 환경(Global Lexical Environment) 객체가 생성이 됩니다.

간단한 코드를 예시로 들며 설명하겠습니다.

전역 렉시컬 환경 객체가 생성되었습니다. 환경 레코드에는 message를 프로퍼티로 저장하고 있고, 전역 렉시컬 환경 객체이기 때문에 외부 참조는 없습니다.

스크립트의 실행 흐름에 따른 변화는 다음과 같습니다.

  • 스크립트가 실행되면 스크립트 내에서 선언된 모든 변수가 렉시컬 환경에 올라갑니다. 이때 변수(let, const로 선언된)의 상태는 특수 내부 상태인 uninitialized로, let을 만나기 전에 참조시 ReferenceError를 발생시킵니다.
  • 선언부가 나타나게 되면, 렉시컬 환경의 프로퍼티 값을 undefined로 변경합니다.
  • 변수에 새로운 값을 대입하면 해당 값으로 변경합니다.

그럼 함수를 호출하는 경우에는 어떻게 될까요?
함수를 호출하여 실행하게 되면 새로운 렉시컬 환경이 자동으로 만들어 집니다. 함수의 렉시컬 환경에는 매개변수와 함수 내의 지역 변수가 프로퍼티로 저장됩니다.

  • 내부 렉시컬 환경은 매개변수로 넘겨받은 name이 프로퍼티로 저장되어있습니다. 외부 참조는 전역 렉시컬 환경입니다.
  • 전역 렉시컬 환경은 message 변수와 greet 함수를 프로퍼티로 가지고 있습니다.

주의깊게 볼 점은, greet 함수 내에서 message에 접근하는 점입니다.
코드에서 변수에 접근할 땐, 먼저 내부 렉시컬 환경에서 해당 변수가 있는지 찾습니다. 이 때, 원하는 변수를 찾지 못하면 해당 환경이 참조하는 외부 렉시컬 환경으로 넘어가서 찾게 됩니다. 전역 렉시컬 환경에 도달할때 까지 반복하여 찾고, 끝까지 찾지 못하게 되면 ReferenceError를 발생시킵니다.

[[Environment]] 프로퍼티

자바스크립트 함수에는 [[Environment]] 라는 특별한 프로퍼티가 있습니다. 이 프로퍼티에는 생성 시점의 렉시컬 환경에 대한 참조가 저장됩니다.
예시와 함께 설명해보겠습니다. 다음은 함수를 반환하는 함수 makeCounter의 예시 코드입니다.

function makeCounter() {
  let count = 0;

  return function () {
    // [[Environment]] => makeCounter Lexical Environment
    return count++;
  };
}

let counter = makeCounter();

counter();   // [count: 1]
counter();   // [count: 2]

makeCounter 함수는 지역변수로 count 변수를 포함하고 있고, 해당 변수 값을 1 증가시키는 익명 함수를 반환하는 함수입니다.
makeCounter를 호출하면 새로운 렉시컬 환경 객체가 생성되고, 필요한 변수들이 프로퍼티로 저장됩니다.

이때, counter.[[Environment]] 에는 count 변수가 있는 makeCounter 의 렉시컬 환경에 대한 참조가 저장됩니다. [[Environment]]는 함수가 생성될 때 한번 값이 저장되고 변경되지 않습니다.

이후 counter() 를 호출하면 호출을 할 때 마다 새로운 렉시컬 환경이 생성됩니다. 이때, 외부 렉시컬 환경의 참조는 counter.[[Environment]] 에 저장되어 있는 count 값이 저장되어 있는 렉시컬 환경을 참조하고 있습니다.
그 다음 count++ 를 실행하게 되는데, 자체 렉시컬 환경에는 존재하지 않기 때문에 외부 렉시컬 환경에서 count 변수를 찾아서 더하게 됩니다.
이로 인해 counter() 를 두번 호출했을 때, count 값이 2로 변하게 됩니다.

다른 예시도 한번 들어보겠습니다.

const makeCounter = () => {
  let count = 0;

  return function () {
    return count++;
  };
};

counter1 = makeCounter();
counter2 = makeCounter();

counter1();  // 1
counter1();  // 2
counter2();  // 1
counter2();  // 2

makeCounter() 를 두번 호출하여 각각 counter1counter2에 저장하였습니다.
이때, counter1.[[Environment]]counter2.[[Environment]] 는 별개의 렉시컬 환경입니다. 따라서 counter1() 을 호출할 때와, counter2()를 호출할 때는 다른 렉시컬 환경에 존재하는 count의 값이 더해지는 것을 알 수 있습니다.

클로저(Closure)

클로저(Closure)란 외부 변수를 기억하고, 이 외부 변수에 접근할 수 있는 함수를 의미합니다. 특정 언어에서는 클로저 구현이 불가능하거나, 독특한 방식으로 함수를 작성해야 클로저를 구현할 수 있습니다.
하지만 자바스크립트에서는 렉시컬 환경과 [[Environment]] 프로퍼티로 인해 모든 함수가 자연스럽게 클로저가 됩니다.

profile
안녕하세요. 홍성목입니다.

0개의 댓글