렉시컬 환경은 현재 함수의 지역 변수(집의 가구)와 외부 렉시컬 환경의 참조(집 주소)를 저장합니다. 출처: copilot

let globalVariable = "글로벌 변수";

function logOuterVariable() {
  console.log(globalVariable);
}

위의 코드와 같이 자바스크립트에서는 함수가 함수 외부의 변수에 접근할 수 있습니다. 함수는 어떤 기준으로 외부의 변수에 접근하는 것일까요? 이는 렉시컬 환경과 큰 연관이 있습니다. 렉시컬 환경은 코드가 작성된 시점의 변수 스코프와 그 접근 권한을 정의하는 환경으로 함수가 선언되는 순간 결정됩니다. 그리고 이 렉시컬 환경을 기억하고 그 환경에 접근할 수 있는 함수를 뜻합니다.

렉시컬 환경(Lexical Environment)

먼저, 외부 변수에 접근하는 함수를 생성해 보겠습니다. 외부 변수에 접근하는 함수의 예시치고는 복잡하지만 렉시컬 환경부터 클로저까지 하나의 코드로 이해하기 위함입니다.

function outerFunction() {
  let count = 0;

  function innerFunction() {
    const localVariable = "innerFunction 실행";
    console.log(`${localVariable}, count의 값: ${count}`);
    count++;
  }

  return innerFunction;
}

countUpAndLog 함수는 자신의 외부에 있는 count에 접근하여 값을 1만큼 증가시킨 후 출력하는 함수입니다. 이 동작은 어떤 과정으로 이루어질까요? 다시 말해 countUpAndPrint는 어떻게 자신이 가지고 있지 않은 count라는 외부 변수를 알 수 있을까요? countUpAndPrint가 만들어질 때 외부의 count에 참조합니다. 그러면 countUpAndPrint가 선언되면서 count에 대해 렉시컬 환경이 만들어집니다. 이 렉시컬 환경은 countUpAndPrint의 외부 렉시컬 환경으로써 내부 숨김 연관 객체(internal hidden associated object)를 가지고 있습니다.

 

렉시컬 환경은 자바스크립트 엔진이 변수, 함수 선언 및 다른 렉시컬 환경과의 상호작용을 관리하는데 사용하는 내부 데이터 구조로 함수가 정의된 위치(및 스코프)와 변수에 대한 참조가 저장된 객체입니다. 이 렉시컬 환경 객체에서 변수에 대한 참조는 환경 레코드(Environment Record)와 외부 렉시컬 환경(Outer Lexical Environment)로 구분할 수 있습니다. 환경 레코드는 렉시컬 환경을 가지는 함수 또는 코드 블록의 모든 지역 변수(this를 포함)를 프로퍼티로 저장하고 있습니다. 외부 렉시컬 환경은 현재 함수가 정의된 위치에서의 렉시컬 환경을 가리키며, 클로저를 형성하거나 변수를 검색할 때 사용됩니다. 이 외부 렉시컬 환경을 저장할 때는 외부 렉시컬 환경의 환경 레코드를 직접 저장하는 것이 아닌, 외부 렉시컬 환경에 대한 참조를 [[Environment]]라는 숨겨진 프로퍼티에 저장합니다.

렉시컬 환경과 변수 검색

그렇다면 위의 코드에서 innerFunction은 count를 어떻게 찾아갈지 어느정도 감이 잡힙니다. 먼저 innerFunction을 살펴보면 localVariable이라는 지역변수를 가지고 있습니다. 이 지역변수는 환경 레코드에 속하게 됩니다. console.log(`${localVariable}, count의 값: ${count}`);가 실행될 때 자바스크립트 엔진은 innerFunction의 환경 레코드에서 localVariable을 찾습니다. 환경 레코드에서 count가 없기 때문에 자바스크립트 엔진은 innerFunction의 외부 렉시컬 환경으로 올라가 count를 찾기 시작합니다. 그리고 여기에서 count를 찾아 사용합니다. 그 다음 줄의 count++;도 마찬가지로 외부 렉시컬 환경에서 해당 변수를 찾아 사용합니다.

'렉시컬 환경’은 명세서에서 자바스크립트가 어떻게 동작하는지 설명하는 데 쓰이는 ‘이론상의’ 객체입니다. 따라서 코드를 사용해 직접 렉시컬 환경을 얻거나 조작하는 것은 불가능합니다.

렉시컬 환경의 생성 시기와 호이스팅

결국 자바스크립트에서 변수를 찾을 때는 가장 안쪽의 렉시컬 환경에서 시작해 가장 바깥의 렉시컬 환경, 즉 전역의 렉시컬 환경까지 타고 올라갑니다. 그런데 이 렉시컬 환경은 함수가 생성되는 시점에 새롭게 만들어집니다. 다시 말해, 함수가 생성되는 시점에 해당 함수의 모든 지역 변수는 해당 함수의 렉시컬 환경(환경 레코드)에 저장됩니다. 그리고 하나의 자바스크립트 파일은 하나의 코드 블록과 같습니다. 즉, 함수가 생성되는 시점에 자바스크립트의 모든 변수는 렉시컬 환경에 저장됩니다. 이때 변수와 달리 함수는 초기화된 상태로 렉시컬 환경에 저장됩니다. 이 과정이 전역 스코프에서 실행된다면 우리에게 익숙한 호이스팅입니다.

 

렉시컬 환경에서 const나 let 키워드로 선언된 변수는 해당 변수가 초기화되기 전까지는 'uninitialized’라는 특별한 상태가 되어, 해당 상태일 때는 각 변수가 있다는 사실은 알지만 참조가 불가능합니다. 이런 방식으로 호이스팅을 막을 수 있습니다.


클로저

이제 렉시컬 환경에 대한 정리가 되었습니다. 그럼 클로저에 대해 알아볼까요? 다음 코드는 처음에 봤던 코드와 동일한 코드에 단 3 줄이 더 추가되었습니다.

function outerFunction() {
  let count = 0;

  function innerFunction() {
    count++;
    const localVariable = "innerFunction 실행";
    console.log(`${localVariable}, count의 값: ${count}`);
  }

  return innerFunction;
}

const closureFunction = outerFunction();
closureFunction(); // innerFunction 실행, count의 값: 0
closureFunction(); // innerFunction 실행, count의 값: 1

여기서 outerFunction을 호출할 때마다 새로운 렉시컬 환경이 만들어집니다. 왜냐하면 outerFunction을 호출하면 innerFunction이 새롭게 생성되기 때문입니다. 앞서 이야기했듯 외부 렉시컬 환경은 [[Environment]]라는 숨겨진 프로퍼티에 저장됩니다. const closureFunction = outerFunction();가 실행된 시점에는 closureFunction.[[Environment]]에는 {count: 0}이라는 외부 렉시컬 환경(outerFunction의 환경 레코드)에 대한 참조가 저장됩니다. 실행한 위치와 상관없이 함수가 자신이 생성된 곳을 기억할 수 있는 건 바로 이 [[Environment]] 프로퍼티 덕분입니다.

 

코드의 실행이 innerFunction의 본문으로 넘어가면 innerFunction의 렉시컬 환경이 참조하는 외부 렉시컬 환경에서 count를 찾습니다. 이때 변숫값의 갱신 즉, count++;의 연산 결과는 변수(여기선 count)가 저장된 렉시컬 환경에서 이뤄집니다. 그래서 closureFunction을 여러 번 호출하면 count의 값이 계속 증가하게 됩니다.

참고
[[Environment]] 프로퍼티로 인해 자바스크립트의 모든 함수는 클로저가 됩니다.

클로저와 메모리

클로저라는 특성으로 인해 함수는 실행이 완료된 이후에서 렉시컬 환경이 메모리에 유지될 수 있습니다. 만약 외부 렉시컬 환경을 참조하고 있다면 해당 렉시컬 환경은 접근이 가능한 상태이므로 메모리에서 삭제되지 않습니다. 해당 렉시컬 환경이 가비지 컬렉션에 의해 메모리에서 삭제되기 위해선 해당 렉시컬 환경을 참조하는 모든 클로저가 삭제되어야 합니다. 물론 가비지 컬렉션의 작동은 각 엔진마다 다르고 V8 엔진의 경우 외부 렉시컬 함수를 참조하지 않는다면 메모리에서 삭제할 수 있습니다.

참고 자료

모던 JavaScript 튜토리얼, 변수의 유효범위와 클로저

+ Recent posts