본문 바로가기

개발공부/Javascript

[JS] 실행 컨텍스트

실행 컨텍스트란?

자바스크립트 엔진이 코드를 실행하는데 사용하는 내부 매커니즘이다. 이 컨텍스트는 실행되는 코드에 대한 환경 정보를 포함한다. 변수, 함수 선언, this의 값 등의 스코프 값 정보가 포함된다. 실행 컨텍스트가 활성화 되는 시점에 호이스팅, 외부환경, this값 등을 설정한다.

전역 실행 컨텍스트

js 코드가 실행될 때 생성되는 가장 기본적인 실행 컨텍스트로 전역 함수와 변수를 포함한다. 글로벌 실행 컨텍스트에 대한 예시 코드이다.

let globalLet = 'Hello World'; // 전역 변수
function globalFunction() { // 전역 함
    return 'Hello Global Function';
}

객체 형식으로 글로벌 실행 컨텍스트 구조를 표현해 보면 다음과 같다.

globalExcecutionContext = {
    VariableEnvironment :  {
        globalLet: 'Hello World',
        globalFunction: <function reference>,
    },
    this: window
}

 

위 예시에서 VariableEnvironment는 전역 함수와 변수를 포함하는 객체이다. 이런 방식으로 전역 실행 컨텍스트는 전역 스코프에 선언된 모든 변수와 함수를 관리하고, 자바스크립트 프로그램이 실행될 때 생성된다. VariableEnvironment 실행 컨텍스트 내에서 변수와 함수 선언을 저장하고 이는 해당 컨텍스트에서 선언된 모든 식별자 (ex 변수이름, 함수이름 등 값, 함수 참조)를 포함한다.

실행 컨텍스트가 함수 호출에 의해 생성 되었을 경우에는 Variable.Environment는 해당 함수의 매개변수, 지역변수, 함수 선언 등을 포함한다. 이는 해당 함수의 스코프 내에서 유효한 함수와 변수를 정의한다.

this 키워드

this는 실행 컨텍스트에서 수행되는 작업을 지시한다. 실행 컨텍스트가 생성될 때 this 가 바인딩 된다. (자동으로 정해짐) 전역 공간에서 this는 전역 객체를 가리킨다. 브라우저에서는 윈도우, node.js에서는 글로벌을 가리킨다. 객체의 메서드를 함수로 호출할 때 this는 메서드를 호출한 객체가 되고 생성자 함수로 (new로 객체를 만들 때는) 새로 만들어진 객체를 가리킨다. call, apply, bind 같은 메서드를 사용해서 원하는 대로 지정할 수도 있다.

실행 컨텍스트로 Closure 설명하기

내부 함수가 선언된 외부 함수의 실행 컨텍스트에 대한 참조를 유지할 때 발생한다. 외부 함수의 실행 컨텍스트가 종료되어 콜 스택에서 제거되더라도, 내부 함수는 외부 함수의 LexicalEnvironment에 대한 참조를 유지한다. 즉, 내부 함수는 외부 함수의 변수 및 함수에 계속 접근 가능하다. 클로저는 외부 함수의 lexical environment가 가비지 컬렉터에 의해 수집되지 않고 유지 되도록 하며, 이는 내부 함수가 외부 함수의 변수에 접근할 수 있도록 한다. 이 때, 실행 컨텍스트의 구조와 동작방식이 중요하다.

실행 컨텍스트 관점에서 클로저가 실행되는 원리에 대한 코드이다.

const outer = function () {
    let a = 1;
    const inner = function () {
        return ++a;
    }
    return inner;
}

 

outer 함수가 호출될 때 클로저 생성을 위해 inner 를 만들고, inner 함수 내부에서 외부함수의 변수 a를 참조하고 inner를 리턴한다. 그런 다음 outer 함수를 count 변수에 할당한다.

const count = outer();
console.log(count()); // 2
console.log(count()); // 3

 

함수가 호출될 때마다 콜스택에 쌓인다. outer 함수가 호출되면 콜스택에 먼저 쌓이고, outer 함수 내부에서 inner 함수가 호출되면, inner 함수도 콜스택에 쌓인다. 각 함수가 실행을 마치면 콜스택에서 제거된다. 일반적으로 함수가 실행되면, 그 함수의 실행 컨텍스트가 콜스택에 쌓이고 실행이 끝나면 콜스택에서 제거가 된다. 그 때 함수 내부의 지역변수는 일반적으로 사라지지만, 이 예제에서는 클로저 때문에 outer 함수가 종료되고, 콜스택에서 제거가 되더라도 inner 함수는 여전히 변수 a에 접근 가능하다.

내부 구현 원리는 활성 객체 (Activation Object)이다. LexicalEnvironment를 통해 Activation Object 구현한다. LexicalEnvironment는 실행 컨텍스트와 관련된 스코프와 식별자에 대한 정보를 저장한다. 클로저가 생성될 때 내부 함수는 외부 함수의 Lexica lEnvironment에 대한 참조를 유지한다. 이를 통해 외부 함수가 실행을 마치고 컨텍스트가 종료된 이후에도 외부 함수의 변수에 접근 가능한다. 상세 이유는 Lexical Environment는 Environment Record와 외부 렉시컬 환경 참조 (Outer Lexical Environment Reference)로 구성이 되어있다. 이 두개 중에 외부 렉시컬 환경 참조 때문에 가능하다. 클로저는 내부 함수가 자신이 선언된 시점의 Lexical 환경을 기억하는 현상이다. 함수가 선언이 되고 내부 함수의 Lexical 환경은 외부 함수의 Lexical 환경을 외부 렉시컬 환경 참조로 포함한다. 이것이 내부 함수가 외부 함수의 변수에 접근할 수 있게 한다. 외부 함수가 실행을 마치고 종료된 이후에도 내부 함수는 외부 함수의 Lexical 환경에 대한 참조를 유지하여 외부 함수의 변수에 계속 접근할 수 있게 되고 이러한 상태는 클로저를 형성한다.

  • outer 함수의 LexicalEnvironment
const outerExecutionContext = {
    "outer 함수의 LexicalEnvironment": {
        EnvironmentRecord: {
            a: 1,
            inner: "/* inner 함수의 참조 */ "
        },
        OuterLexicalEnvironment: "/* 전역 환경의 LexicalEnvironment 참조*/"
    }
}

 

lexical environment는 함수가 실행될 때 해당 함수의 스코프와 관련된 정보를 담고있음. lexical environment는 두 가지 구성요소로 이루어져 있다. EnvironmentRecord와 OuterLexicalEnvironment이다. EnvironmentRecord는 함수 내에서 선언된 변수, 함수 선언, 매개 변수를 저장하며 함수가 실행되는 동안 사용된다. OuterLexicalEnvironment는 현재 함수의 외부 또는 부모 스코프에 대한 참조이다. 이 참조를 통해 함수는 자신의 외부 환경, 상위 스코프에 접근 가능하다. outer 함수 내에는 지역변수 a와 내부 함수 inner가 선언되어 있다. 여기서 EnvironmentRecord는 outer의 지역변수 a와 inner 를 포함하고 있다. OuterLexicalEnvironment는 전역 환경의 렉시컬 환경을 참조하여 outer 함수는 전역 변수에 접근 가능하다. (평소에 사용하던 함수 스코프, 함수 내부에서 전역 변수 사용 가능!)

  • inner 함수의 LexicalEnvironment
const innerExecutionContext = {
    "inner 함수의 LexicalEnvironment": {
        EnvironmentRecord: {
            // inner 함수 내부의 지역 변수들이 여기 포함 됩니다. (이 경우 비어 있음)
        },
        OuterLexicalEnvironment: 'outer 함수의 LexicalEnvironment'
    }
}

 

inner 함수가 호출되면 inner 의 실행 컨텍스트가 생성이 된다. inner의 실행 컨텍스트에 outer lexical environment는 outer 함수의 lexical environment를 참조한다. 이 때문에 inner 함수가 outer 함수의 변수 a에 접근 가능하다. 이부분에서 클로저가 형성된다. inner의 실행 컨텍스트에 outer lexical environment가 outer 실행 컨텍스트의 lexical environment를 참조하는 것은 inner 함수가 outer 함수의 변수와 환경에 접근할 수 있도록 한다. (클로저의 핵심!)

실행 컨텍스트로 스코프 설명하기

let globalLet = 'Hello World';

function outer() {
    let localLet = '지역 변수';
    console.log(globalLet);
    console.log(localLet);
}

outer();

 

outer 함수 내에서 globalLet 전역변수에 어떻게 접근 하였을까? 바로 실행 컨텍스트의 스코프 체인 덕분이다.

const outerContext = {
    'outer 컨텍스트': {
        'VariableEnvironment': {
            localLet: "지역변수"
        },
        'scopeChain': ['outer 변수객체', '전역 변수객체']
    }
}

 

함수가 호출이 되면 자바스크립트 엔진은 해당 함수에 대한 새로운 실행 컨텍스트를 생성한다. outer 함수가 실행될 때 outer 컨텍스트가 실행이 된다. 변수 객체에는 localLet이 선언이 된다. scopeChain 속성이 추가 되어서 자기 자신의 변수 객체와 전역 변수 객체를 참조한다. 스코프 체인은 현재 함수의 스코프 뿐만 아니라 외부 함수의 스코프도 포함한다. 함수 내에서 변수를 참조할 때 자바스크립트 엔진은 outer 함수의 지역변수 객체에서 해당 변수를 찾아보 없으면 스코프 체인을 따라 상위 스코프로 가서 찾게 된다. 스코프 체인과 실행 컨텍스트는 함수가 자신의 지역 변수 뿐 아니라 전역 변수에도 접근할 수 있도록 한다.


Q. 전역 변수는 왜 메모리 누수를 일으킬까요?

 

글로벌 실행 컨텍스트는 프로그램이 종료될 때까지 호출 스택에 남아있는다. 따라서 전역 변수들도 프로그램이 실행되는 동안 메모리에 계속 유지가 된다. 이런 특성 때문에 전역 변수를 과도하게 사용하는 것은 메모리 사용량을 증가 시키고 성능 문제나 메모리 누수가 될 수 있음. 큰 객체나 배열의 경우 메모리 사용량이 더욱 증가한다. 전역 변수는 자동적으로 가비지 컬렉터의 대상에서 제외가 된다. 따라서 더 이상 필요하지 않은 데이터를 전역변수로 유지하면 메모리 누수가 발생할 위험이 크다. 가능한 함수 내부에서 지역 변수를 사용하고 전역 변수 사용은 최소화해야 한다. 또는 모듈 패턴이나 자바스크립트의 최신 모듈 시스템을 사용하여 전역 네임스페이스의 오염을 방지한다. 또한, 관련된 여러 전역 변수를 객체 속성으로 그룹화하여 관리할 수도 있다. 코드를 구조화하고, 스코프 관리를 적절히 하여 안정적이고 효율적인 애플리케이션 개발을 해야 한다.

함수 호출과 실행 컨텍스트 스택

실행 컨텍스트를 구성하는 방법은 함수를 실행하는 것이다. 다음 내용은 여러 함수가 실행될 때 실행 컨텍스트가 관리되는 방식에 대한 코드이다.

let globalVar = '전역 변수';
function outer() {
    console.log(globalVar);
    function inner() {
        var innerVar = '내부 함수 변수';
        console.log(innerVar);
    }
    inner();
}
outer();

 

outer 함수 호출 → outer 함수의 실행 컨텍스트가 생성되어 호출 스택에 push → 그 안에서 inner 함수 호출 → inner 함수의 실행 컨텍스트가 생성되어 스택에 추가 → inner 함수가 실행 완료되고 return 이 되면 inner 의 실행 컨텍스트는 호출 스택에서 pop 된다. → outer 함수가 실행 완료되고 return 이 되면 outer 의 실행 컨텍스트는 호출 스택에서 pop 된다. 함수의 실행 컨텍스트는 함수가 실행 완료 되면 호출 스택에서 제거가 된다. 하지만 글로벌 실행 컨텍스트는 프로그램이 종료될 때까지 호출 스택에 남아있어 전역 환경이 유지 된다. 자바스크립트 프로그램이 실행될 때 가장 먼저 글로벌 실행 컨텍스트가 생성된다. 이 컨텍스트는 호출 스택의 가장 바닥에 위치하고 프로그램 실행 전체 기간 동안 유지하 전역 함수의 변수와 함수를 관리하고 전역 환경을 설정한다.

실행 컨텍스트는 함수가 호출될 때마다 형성되고 콜스택에 쌓이게 된다. 전역 실행 컨텍스트는 프로그램이 시작될 때 형성되고 전역 함수와 변수를 관리한다. 실행 컨텍스트의 스코프 체인은 클로저의 구현과 밀접한 관련이 있다. 클로저는 내부 함수가 외부함수의 실행 컨텍스트에 대한 참조를 유지할 때 발생한다. 이 참조는 내부 함수가 외부함수의 변수와 함수에 접근할 수 있게 한다.

 

실행 컨텍스트의 핵심 구성 중 하나는 다음과 같다.

LexicalEnvironment = EnvironmentRecord + OuterLexiclaEnvironment

  • EnvironmentRecord: 현재 컨텍스트 내의 식별자와 변수, 함수 선언을 저장
  • OuterLexiclaEnvironment: 상위 컨텍스트의 LexicalEnvironment를 참조한다. 이 참조 덕분에 함수는 상위 스코프 변수에 접근할 수 있고 이는 함수 스코프와 클로저 구현에 필수적인 요소이다.

실행 컨텍스트로 함수 스코프 설명하기

함수 스코프와 클로저는 실행 컨텍스트와 밀접한 연관이 있다. Function scope 는 함수가 선언된 위치에 따라 결정된다. (lexical scoping)

실행 컨텍스트가 생성될 때, 각 함수는 자신의 Lexical Environment(함수 내부에서 선언된 변수, 함수선언, 매개변수가 포함)를 가진다. 여기에는 OuterLexicalEnvironment를 포함하는데, 이는 함수가 선언된 외부 환경이다. (상위 스코프 참조)

함수가 호출되면 새로운 실행 컨텍스트가 스택에 push되고 이 컨텍스트는 해당 함수의 스코프를 결정한다.

const a = 1;
const outer = () => {
    // 전역변수 a에 접근
    console.log('a: ', a);
}

outer();

 

함수 스코프가 형성되는 방식은 LexicalEnvironment와 그 안에 있는 OuterLexical Environment가 구성이 된다. OuterLexicalEnvironment는 outer 함수의 외부 환경 (전역 환경의) LexicalEnvironment를 참조한다.

outer 함수 내에서 변수 a를 참조하려고 할 때 outer함수의 내부 EnvironmentRecord에서 먼저 변수 a를 찾는다. 없다면 OuterLexicalEnvironment를 통해 상위 스코프의 전역 환경에서 a를 찾게 된다. 함수 내에서 자신의 스코프 뿐 아니라 상위 스코프의 변수에 접근할 수 있게 되고 클로저가 가능하게 된다.

 

 

👉🏻 정리

 

실행 컨텍스트는 함수가 실행될 때 필요한 모든 정보를 포함하고 있다. 코드가 실행되는데 필요한 모든 정보와 자원을 담고 있는 가상의 작업 공간이며, 자바스크립트 엔진에 장착이 되어있다. 프로그램 실행 흐름을 제어하는데 필수적이다. 현재 실행되는 코드에 대한 환경과 상태 정보를 담고 있으며, 이것은 변수, 객체, 함수 호출과 같은 코드에 필요한 여러 세부 사항을 포함한다.

실행 컨텍스트는 변수 환경, this, scope chain으로 이루어져 있다.

  • 변수 환경: 함수 또는 전역 코드 내에서 선언된 변수와 함수 선언을 포함되며 환경은 코드 실행 동안 지속적으로 갱신 된다.
  • 실행 컨텍스트는 this의 값을 결정함. this는 함수가 어떻게 호출되었는지에 따라 다르게 결정된다.
  • 실행 컨텍스트는 하위 스코프 체인을 형성한다. 현재 컨텍스트의 변수 환경과 상위 변수 환경을 연결한다.

 

[참고]

패스트캠퍼스 강의 (내 생에 마지막 JavaScript : 기초 문법부터 실무 웹 개발까지 한 번에 끝내기 초격차 패키지 Online) 를 기반으로 작성하였고, 추가 서치 내용을 정리하였습니다.