[ 살펴보기 ] Javascript - Execution Context와 Closure

[ 살펴보기 ] Javascript - Execution Context와 Closure

·

4 min read

해당 포스트에선 Execution Context와 Closure에 대해 살펴본다. Javascript에선 변수를 var로 선언하느냐 const / let으로 선언하느냐에 따라 Execution Context 내부에서 처리가 다소 달라진다. 해당 포스트에서는 가능하면 심플하게 Execution Context 동작을 살펴보기 위해 변수는 모두 const / let으로 제한하도록 하자.

Execution Context

Execution Context란 간단히 말해 우리가 작성한 Javascript code가 실행되는 환경을 말한다. 다음 코드를 예제로 살펴보자

const globalName = "Jane"

const printPersonName = (newName:string) => {
    const originalName = globalName
    return `originalName is ${originalName} and newName is ${newName}`
}

printPersonName('Jack')

Javascript Engine이 우리가 작성한 코드를 실행하기 위해선 우리가 선언한 변수나 함수와 같은 식별자 정보와 실행하고 있는 코드의 스코프와 같은 정보가 필요하며 이를 Execution Context로 관리한다.

그리고 LexicalEnvironment라는 Execution Context를 구성하는 component가 있는데 LexicalEnvironment는 EnvironmentRecord와 OuterLexicalEnvironmentReference로 구성된다

현재 실행되고 있는 Execution Context에서 선언된 변수나 함수와 같은 식별자와 식별자에 대한 값은 EnvironmentRecord에서 관리되고 현재 Execution Context 상위 Execution Context 정보 ( 상위 스코프 정보 )는 OuterLexicalEnvironmentReference에서 관리된다.

그리고 모든 Execution Context는 다음 두 단계를 거친다.

  • 생성 단계

  • 실행 단계

생성 단계에선 현재 실행 중인 Execution Context에 존재하는 변수의 식별자와 EnvironmentRecord에 추가하고 값을 초기화 하진 않는다.

현재 Execution Context에 선언문으로 정의된 함수가 있다면 EnvironmentRecord에 추가하며 값을 생성 단계에서 초기화 한다 ( 그렇기에 선언문으로 정의된 함수는 실제 정의된 부분보다 상단에서 호출할 수 있다 )

현재 Execution Context의 생성 단계가 종료되면 실행 단계에서 실제 변수의 값이 할당되며 코드가 처리된다.

여기서 흔히 혼동하는 부분이 있다. ( 필자 역시 혼동을 했었다 ) 흔히들 let과 const로 선언한 변수는 호이스팅이 발생하지 않다고 생각하는 점인데 var, let, const 모두 호이스팅이 발생한다. 하지만 let과 const로 선언한 변수는 위에서 본 Execution Context의 실행 단계가 종료될 때 까지 해당 식별자에 대한 접근이 불가능할 뿐이며 이를 Temporal Dead Zone이라고 한다

이제 위의 예제를 다시끔 가져와 살펴보자

const globalName = "Jane"

const printPersonName = (newName:string) => {
    const originalName = globalName
    return `originalName is ${originalName} and newName is ${newName}`
}

printPersonName('Jack')

우리가 Javascirpt를 처음 load하여 실행할 때 Javascript engine은 우선 global scope에 존재하는 코드를 평가하여 Global Execution Context를 생성하여 Call Stack에 Push한다.

그리고 Global Scope에 존재하는 식별자를 Global Execution Context의 EnvironmentRecord에, 보다 정확하게는 ReclarativeEnvironmentRecord에 저장되고 초기화 되진 않는다 ( Global Execution Context는 built-in javascript 객체와 같이 다른 부분도 제공하므로 일반 함수 Execution Context 보다 구성하는 component가 많다 )

그 다음 실행 단계에서 globalName 변수에 대한 값 할당이 이루어지고 printPersonName을 실행하며 새로운 함수 Execution Context가 Call Stack에 추가되어 실행된다

함수 Execution Context 역시 생성 단계, 실행 단계를 거쳐서 처리된다. 생성 단계에선 함수 내부의 변수 식별자가 EnvironmentRecord 저장되고 실행 단계에서 해당 변수에 대한 값이 할당된다.

하지만 위의 예제에서 originalName의 값은 상위 스코프에 있는 globalName 변수를 할당하고 있다. 이 때 실행해야 하는 변수 정보가 현재 Execution Context에 없으면 Execution Context의 OuterLexicalEnvironmentReference 통해 상위 Execution Context를 참조하여 값을 찾는다. 이렇듯 Execution Context는 상위 Execution Context 참조를 통해 Scope Chain을 형성한다

Closure

위에서 살펴 본 Execution Context에 대한 이해가 있다면 Closure를 이해하기가 훨씬 수월해 진다. 흔히 Closure란 자신이 선언된 환경을 기억하고 있는 함수라고 말한다. 조금 더 자세한 이해를 위해 다음 예제를 살펴보자

const testFn = () => {
  const country = "korea";
  const testNestedFn = (city: string) => {
    return `country : ${country}, city : ${city}`;
  };
  return testNestedFn;
};

const printAddress = testFn();

const withBusan = printAddress("busan");
const withSeoul = printAddress("seoul");

console.log({ withBusan, withSeoul });
// withBusan - country : korea, city : busan
// withSeoul - country : korea, city : seoul

위의 예제에서 testFn이라는 함수안에 testNestedFn이라는 내부함수가 존재하고 testFn 함수는 해당 내부함수를 return한다

그리고 testFn을 실행하여 printAddress라는 변수에 할당하며 printAddress는 testFn 함수가 return한 testNestedFn 함수가 된다. 이 때 testFn 함수는 Execution Context의 실행 단계가 끝나 Call Stack에서 제거되었을 텐데 printAddress를 실행하니 testFn에 정의 되었던 country라는 변수가 korea라는 값을 그대로 유지하고 있다.

이렇듯 Closure는 자신이 선언된 상위 Execution Context의 실행 단계가 끝나 Call Stack에서 제거 되더라도 자신이 정의된 상위 Execution Context 환경을 기억하고 있다.

그리고 흔히 함수안에 있는 모든 내부 함수를 클로저라고 오해하기 쉽다. 개인적으로는 모던 자바스크립트 Deep Dive를 집필하신 이웅모님의 정의를 좋아한다

"클로저는 중첩 함수가 상위 스코프의 식별자를 참조하고 있고 중첩 함수가 외부 함수보다 더 오래 유지되는 경우에 한정하는 것이 일반적이다"

위의 예제를 조금 고쳐보자

const testFn = () => {
  const country = "korea";
  const testNestedFn = (city: string) => {
    return `city : ${city}`; // 상위 스코프의 변수를 참조하지 않는다
  };
  return testNestedFn;
};

const printAddress = testFn();

testNestedFn이라는 내부함수가 상위 스코프에서 아무것도 참조하지 않도록 코드를 조금 수정했다. 위의 정의에 따르면 testNestedFn은 이제 상위 스코프의 어느 값도 참조하고 있지 않으므로 closure라고 할 수 없다

다음과 같은 경우는 어떨까?

const testFn = () => {
  const country = "korea";
  const testNestedFn = (city: string) => {
    return `country : ${country}, city : ${city}`;
  };
  return testNestedFn('busan'); // 곧 바로 실행
};

const printAddress = testFn();

코드를 다시 고쳐 testNestedFn이 다시 상위 스코프의 변수를 참고하고 있다. 하지만 위의 예제에서는 testNestedFn을 return하는 것이 아닌 실행하여 그 값을 return한다. 따지자면 testNestedFn 함수를 closure였다라고 할 수 있지만 testNestFn은 상위 스코프인 testFn 내부에서 실행되어 상위 스코프 보다 빨리 소멸되므로 온전한 의미에서 closure라고 하기는 힘들 것이다.

우리가 제일 처음봤던 예제와 같이 상위 Execution Context 환경의 특정한 식별자를 참조하고 있고 상위 Execution Context 생명주기 보다 오래 유지 되는 함수를 Closure라고 할 수 있다.

이전에는 Closure 사용 시 메모리 낭비와 관련된 문제 목소리가 있었던 것 같으나 요즘은 대부분의 Javascript Engine들이 Closure 사용에 대해 최적화 작업이 잘 되어 있기에 Closure의 적극적인 활용을 권장하는 것을 여기 저기서 볼 수 있다 ( 상위 스코프의 모든 식별자를 기억하고 있지 않고 Closure 함수에서 참조하고 있는 식별자만 기억하고 있는다 )