Execution Context & Closure

GY·2021년 10월 25일
0

[JS] 개념 정리

목록 보기
20/32
post-thumbnail

실행 컨텍스트는 함수가 '선언'될 때 생성된다.

왜 실행이 아닌 선언될 때 생성될까?
이에 대해 알기 위해서는 우리가 작성한 코드가 어떤 원리로 실행되는지 알아야 한다.



🌟 코드 실행 과정

모든 코드는 실행되기 전 준비단계가 필요한데, 이 단계를 '평가' 단계라고 한다.

💨 소스코드 평가

코드를 평가하는 단계에서는 변수, 함수 등의 선언문을 먼저 실행한다. 그 다음 실행 컨텍스트를 생성하여 선언된 변수나 함수 식별자를 여기에 등록한다.
(정확히는 실행컨텍스트가 관리하는 스코프(렉시컬 환경의 환경 레코드)에 등록한다.)

예시를 보자.

let x;
x = 1;

이 코드를 실행할 때 평가단계에서 let x 변수 선언문을 먼저 실행한다. 생성된 변수 x를 실행 컨텍스트에 등록하는데, 이 때 x의 값은 초기화된 undefined 이다.

💨 소스코드 실행

준비가 끝나고 본격적인 실행 단계에서는,
선언된 변수와 함수가 선언되었는지 확인 후 값을 할당한다.

위의 예시를 이어 살펴보면,
x의 값에 1이 할당되어 x=1이 된다.

그렇다면 실행 컨텍스트에서는 어떻게 코드를 등록하고 관리할까?



🌟 실행컨텍스트

실행 컨텍스트는 식별자를 등록하고 관리하는 스코프와 코드 실행순서 관리를 구현한 내부 메커니즘으로,
쉽게 말하면 자바스크립트 코드가 실행되고 있는 컨텍스트, 즉 환경을 제공하는 객체이다.

  • 함수를 실행할 때마다 그 함수에 대한 실행 컨텍스트를 생성한다.
  • 이 컨텍스트에 실행할 코드에 제공할 환경정보(변수 정보)를 모아놓는다.
  • 소스코드를 실행하는 데 필요한 환경을 제공하고 코드의 실행결과를 실제로 관리한다.

정확히 어떤 환경 정보를 갖고 있다는 걸까?

  • 어떤 변수가 있는지
  • 어떤 변수를 호이스팅할지
  • 스코프와 스코프 체인
  • this는 어디에 어떻게 바인딩되는지 등등.....

사실, this binding, scope, hoising, closure 등의 동작원리를 모두 포함하고 있어 이 개념들을 모두 잘 이해하고 있어야 한다.

💨 실행 컨텍스트의 종류

  • Global Context (전역 컨텍스트)
    - 자바스크립트 엔진이 코드를 실행할 때 처음으로 생성되는 첫 실행 컨텍스트이다.

  • Functional Context (함수 컨텍스트)
    - 선언된 함수가 호출될 때를 기점으로 생성이 되고, 함수의 모든 동작이 종료되면 소멸된다. (js 엔진의 콜 스택에서 제거된다.)

    • 각각의 함수들은 각각의 함수 실행 컨텍스트를 가지며, 함수가 호출이 되어야만 이 실행컨텍스트가 생성된다.

💨 실행컨텍스트의 역할

실행컨텍스트가 하는 일은 더 자세하게 다음과 같다.

  • 선언되어 생성된 식별자(함수, 클래스, 변수 등)를 스코프를 구분하여 등록하고 상태변화를 지속적으로 관리할 수 있도록 한다.
  • 스코프 체인을 형성해 상위 스코프로 이동하며 식별자를 검색할 수 있도록 한다.
  • 현재 실행중인 코드의 실행 순서를 변경하거나 되돌아갈 수 있도록 한다..

💨 스택 & 렉시컬 환경

이것을 실행컨텍스트의 구성으로 구분지어서 보면,

  • 실행 컨텍스트 스택 : 코드실행순서 관리
  • 실행컨텍스트의 렉시컬 환경 : 식별자와 스코프 관리

실행 컨텍스트 스택이 어떻게 코드의 실행순서를 관리하는지부터 정리해보자.



🌟 실행컨텍스트 스택

💨 코드 실행순서 관리

(실행 컨텍스트 스택을 콜 스택이라고 부르기도 한다.)

실행 컨텍스트는 스택의 형태로 코드의 실행순서를 관리한다.

콜스택 (실행 컨텍스트 스택 Execution Stack)

function outer() {
	function inner() {
    	return 'inner 함수';
    }
  
  	inner();
}

outer();

이 예시를 보자.

함수 각각마다 어떻게 실행 컨텍스트가 생성되어 스택에 쌓일까?

출처: 실행컨텍스트의 시각화

  1. 첫번째로 전역 컨텍스트가 생성된다.
  2. outer함수가 호출되면서 outer함수 컨텍스트가 생성되어 스택에 쌓인다.
  3. inner함수가 호출되면서 마찬가지로 컨텍스트가 스택에 쌓인다.
  4. 차례대로 실행되며 스택에서 지워진다.

소스코드(실행가능한 코드)의 타입에 따라 실행 컨텍스트를 생성하는 과정과 관리 내용이 다르기 때문에, 이 소스코드의 종류부터 알아보자.


💨 소스코드의 종류

전역코드

  • 전역에 존재하는 소스코드
  • 전역에 정의된 함수, 클래스 등의 내부 코드는 포함되지 않는다.
  • 전역에 존재하는 코드는 전역 컨텍스트에서 관리하며, 콜 스택에 전역 컨텍스트는 하나 뿐이다.
  • 자바스크립트 파일을 실행하면 전역 컨텍스트가 가장 먼저 활성화 된다.

함수코드

함수 내부에 존재하는 소스코드
함수 내부에 중첩된 함수, 클래스 등의 내부 코드는 포함되지 않는다.

eval 코드

빌트인 전역 함수인 eval함수에 인수로 전달되어 실행되는 소스코드

요즘에는 거의 쓰이지 않으므로 자세히 알 필요는 없다.

모듈코드

모듈 내부에 존재하는 소스코드
모듈 내부의 함수, 클래스 등의 내부 코드는 포함되지 않는다.



컨텍스트에 담기는 정보

실행 컨텍스트는 다음과 같은 구조의 객체이다.

ExecutionContext = {
	ThisBinding: <this>,
  	LexicalEnvironment: { ... },
  	VariableEnvironment: { ... },
}

크게 보았을 때 ThisBinding, Variable Environment와 LexicalEnvironment라는 환경정보를 저장하는 객체로 구성되어 있다.

1️⃣ ThisBinding

현재 컨텍스트의 this가 어디를 가리키는지에 대한 정보를 저장한다.

2️⃣ VariableEnvironment

Virable Object 변수 객체라고도 부른다.

3️⃣ LexicalEnvironment

💨 스코프와 식별자 관리

렉시컬 환경(Lexical Environment)은 식별자와 식별자에 바인딩된 값, 그리고 상위 스코프에 대한 참조를 기록하는 자료구조로 실행 컨텍스트를 구성하는 컴포넌트다.


렉시컬 환경은 다시 아래 2 개의 컴포넌트로 구성된다.

1. EnvironmentRecord

환경 레코드
스코프에 포함된 식별자를 등록하고 등록된 식별자에 바인딩된 값을 관리하는 저장소

조금 더 풀어서 이해해보자.

  • 실행컨텍스트의 LexicalEnvironment는 Environment Record라는 곳에 선언된 변수와 함수 등을 저장한다.
  • 호이스팅: 코드가 실행되기 전에 변수, 함수등의 정보를 모두 저장한다. 즉, 자바스크립트 엔진은 코드를 실행하기 전 코드의 변수명을 모두 알고 있는 것이다. (단, 변수에 할당된 값이 무엇인지는 알지 못한다.)

2. OuterLexicalEnvironmentReference

외부 렉시컬 환경에 대한 참조

  • 렉시컬 환경은 이 외부 렉시컬 환경에 대한 참조를 통해 상위 렉시컬 환경과 연결되어 스코프 체인을 형성한다.

무슨 말이지?

  • 렉시컬 환경에서 외부 렉시컬 환경에 대한 참조에 상위 렉시컬 환경에 대한 참조값을 저장한다. 그리고 이 상위 렉시컬 환경에 대한 참조값이 상위 스코프이다.
  • 다시 말하면, 함수 정의가 평가되는 시점에 렉시컬환경에서 외부 렉시컬 환경에 대한 참조컴포넌트에 상위 스코프에 대한 참조값을 넣는데, 이 상위 스코프는 함수가 정의된 위치에 따라 결정한다. 이것을 렉시컬 스코프라고 한다.

💨 렉시컬 스코프

자바스크립트 엔진은 함수를 정의한 위치에 따라 상위 스코프를 결정하는데, 이를 렉시컬 스코프라고 한다.

그렇다면, 어떻게 호출되는 위치와 상관없이 함수가 정의된 위치인 상위 스코프를 기억할까?



🌟 함수 내부 슬롯 [[Environment]]

함수 객체에는 [[Environment]]라는 내부 슬롯이 있다. 이곳에 자신이 정의된 환경, 즉 상위 스코프의 참조를 저장한다.

왜?

렉시컬 스코프가 가능하려면 함수가 호출된 위치와 상관없이 정의된 위치인 상위 스코프를 기억해야 하기 때문이다.

  • 함수 정의가 평가되어 함수 객체를 생성할 때 자신이 정의된 위치에 의해 결정된 상위 스코프의 참조를 객체 자신의 내부 슬롯 [[Environment]]에 저장한다. 이 참조값은 함수가 호출되면 렉시컬 환경의 외부 렉시컬 환경에 대한 참조에 저장된다.

이 부분이 헷갈릴 수 있다. 내부슬롯과 외부렉시컬 환경에 대한 참조에 각 상위스코프 참조값이 저장되는 시점이 다소 헷갈린다면, 아래 클로저의 예를 보면서 과정을 짚어보자.



🌜 클로저

클로저는 일종의 '현상'과 같은 개념으로 생각한다.
MDN의 정의는 함수와 그 함수가 선언된 렉시컬 환경과의 조합이라고 한다.


💨 클로저의 조건

클로저 : 외부함수보다 중첩함수가 더 오래 유지되는 경우
중첩함수는 이미 생명 주기가 종료한 외부함수의 변수를 참조할 수 있다.

즉, 클로저의 일반적인 조건은 다음과 같다.

  • 중첩함수가 상위 스코프의 식별자를 참조하고 있고
  • 중첩함수가 외부 함수보다 더 오래 유지되는 경우

어떻게 클로저가 가능할까?


위에서 알아본 렉시컬스코프와 관련해 클로저의 한 예를 함께 살펴보자.

아래 예시 코드가 실행되는 과정과 클로저가 형성되는 원리를 뜯어보자.

const x = 1;

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

const innerFunc = outer();
innerFunc(); //10

아 코드에서 outer가 inner를 반환하고 종료되었음에도 inner함수에서 변수 x를 참조하여 10이라는 숫자를 출력한다.

  1. outer함수 평가 : 함수가 평가되어 함수 객체를 생성할 때
    • outer함수 객체의 [[Environment]] 내부 슬롯 : 상위 스코프는 전역렉시컬 환경로 저장
    • (실행 컨텍스트 스택에서 현재 실행중인 컨텍스트의 렉시컬 환경을 저장하기 떄문)

  2. outer함수 호출 : outer함수 렉시컬 환경 생성
    • [[Environment]]내부 슬롯에 저장되 전역 렉시컬 환경을 outer함수 렉시컬 환경의 외부 렉시컬 환경에 대한 참조에 할당

  3. inner 함수 평가 : 자신의 [[Environment]]내부 슬롯에 outer함수의 렉시컬 환경을 상위스코프로 저장

  4. inner함수 호출 : inner함수의 실행 컨텍스트가 생성되고 실행 컨텍스트 스택에 푸시된다.
    • inner 함수 객체의 [[Environment]]슬롯에 저장된 참조값이 렉시컬 환경의 외부 렉시컬 환경에 대한 참조로 할당된다.

outer함수는 inner함수를 반환하면서 종료하기 떄문에 실행컨텍스트가 실행 컨텍스트 스택에서 제거된다. 그러나 inner함수에 저장된 outer함수의 렉시컬 환경은 유지된다.

가비지 컬렉션의 대상이 되지 않는다.


Garbage Collection

Garbage Collection이란, 자바스크립트 엔진이 내부적으로 사용하는 메모리 관리 시스템이다. 사용하지 않는 값들에 대한 메모리 영역을 주기적으로 정리하고 불필요한 메모리 사용이 없도록 관리한다.

가비지 컬렉터는 누군가가 참조하고 있는 메모리공간을 해제하지 않는다.

  • inner함수는 전역변수 innerFunc에 의해 참조되고 있다.
  • outer함수의 렉시컬 환경은 inner함수의 [[Environment]]내부 슬롯에 의해 참조되고 있다.

때문에 inner함수의 내부에서 상위 스코프를 참조할 수 있고 식별자의 값을 변경할 수도 있다.



🌜 Closure 예제로 이해하기

💨 예제 1

func()를 실행했을 때, say()의 외부에서 어떻게 a,b 변수에 접근할 수 있었을까?

function say() {
  const a = 1;
  const b = 2;

  function log() {
    console.log(a + b);
  }

  return log;
}

const func = say();

func(); //3

모든 자바스크립트 함수는 선언될 당시에 클로저가 형성되어 주변환경을 기억할 수 있게 된다.
따라서 say 함수가 선언될 때 클로저가 생성되었다.

func() 호출로 인해 say 함수가 실행될 때 함수 실행 컨텍스트가 생성되어
변수와 내부의 함수를 선언하게 되고, 이 때 클로저가 생성되어 주변환경을 기억하게 된다.
따라서 변수 a, b, log함수 정보에 컴퓨터의 메모리에 저장되어, 함수 선언 당시의 환경에 대한 접근이 가능하게 된다.


💨 예제 2

function carrot() {
  const food = "jjajang";

  function potato() {
    console.log(food);
  }

  mushroom(potato);
}

function mushroom(fn) {
  fn();
}

carrot();

carrot 함수가 실행되면 실행컨텍스트에 변수와 함수 정보가 저장된다.
carrot 함수내부의 food 변수와 potato 함수가 선언되어 클로저가 형성되었다.
이후 mushroom함수로 인해 potato함수가 실행되는데, potato함수는 mushroom함수 내부에서 실행되었지만 carrot함수 내부의 food 변수에 접근하여 콘솔에 출력할 수 있다.


💨 예제 3 (클로저는 지속적으로 변화를 추적한다)

function carrot() {
  let potatoCount = 0;

  function potato() {
    potatoCount++;
    console.log(potatoCount);
  }

  return potato;
}

const veggie = carrot();

veggie(); // 1
veggie(); // 2
veggie(); // 3

carrot함수를 실행시키면 potatoCount와 potato함수 정보를 기억하는 클로저가 형성되었다.
veggie함수로 인해 carrot함수가 3번 실행되는데, 지속적으로 변화를 추적하여 potatoCount++된다.


💨 예제 4 (매개변수 2개)


function addCurry(x) {
  return function add(y) {
    return x + y;
  };
}

const addFive = addCurry(5);
const result = addFive(5);

console.log(result);

addCurry함수를 실행하면 add함수가 생성되어 클로저가 형성된다. (매개변수 x=5라는 정보를 기억한다.)
addCurry(5)는

function add(y) {
  return 5 + y;
}

를 리턴하여 addFive에 할당된다.
addFive(5)를 실행하면 5+5를 연산하여 10을 리턴하고 result에 할당된다.


💨 예제 5 (호출 시마다 다른 실행컨텍스트 생성)

function addCurry(x) {
  return function add(y) {
    return x + y;
  };
}

const addFive = addCurry(5);
const addTen = addCurry(10);

const result1 = addTen(20);
const result2 = addFive(5);
const result3 = addTen(10);

console.log(result1); //30
console.log(result2); //10
console.log(result3); //20

addCurry함수는 한번 선언되었지만 2번 각기 다른 매개변수와 함께 호출되었다.
즉, 실제로 코드가 실행되며 생성된 add함수는 2개이다.
이에 따라 각각 2개의 add함수가 각각의 x매개변수의 값을 기억하고 있다.



🌜 클로저 활용

언뜻 보면 모호하면서도 단순한 개념 같다.
클로저는 왜 필요한걸까? 어디에 활용되는 걸까?

💨 상태유지

상태(state)를 안전하게 변경하고 유지하기 위해 클로저를 사용한다.
의도치 않게 상태가 변경되지 않도록 방지하고, 특정 함수에서만 가능하도록 만들 때 활용한다.

💨 전역 변수의 사용 억제

💨 캡슐화와 정보의 은닉

캡슐화 (encapsulation)는 프로퍼티와 메서드를 하나로 묶는 것을 말한다.

  • 프로퍼티 (객체의 상태를 나타냄)
  • 메소드 (프로퍼티를 참조하고 상태변경)

정보 은닉은 이 캡슐화로 특정 프로퍼티/메서드를 감추는 것이다.

클로저 활용 사례에 대해서는 별도로 다시 정리할 예정이다.



🌜 클로저의 단점

클로저는 성능 관련 이슈나 메모리 누수의 가장 흔한 원인이다.
생성된 함수가 주변 환경에 대한 값을 지속적으로 사용하고 있을 수 있기 때문에,
Garbage Collection에 의해 정리되지 않는다.



업데이트:
2022.02.23

Reference

profile
Why?에서 시작해 How를 찾는 과정을 좋아합니다. 그 고민과 성장의 과정을 꾸준히 기록하고자 합니다.

0개의 댓글