실행 컨텍스트란?

이종경·2025년 5월 17일
0

자바스크립트

목록 보기
11/11

실행 컨텍스트 개요

실행 컨텍스트(Execution Context)란 자바스크립트 코드가 실행되는 환경 정보들의 모음입니다. 자바스크립트 엔진은 실행 가능한 코드 블록을 만날 때 마다 해당 코드 실행에 필요한 환경을 담은 실행 컨텍스트를 만들고 관리합니다.

이 컨텍스트에는 현재 사용할 수 있는 식별자(변수, 함수 등)의 정보, 상위 스코프(outer scope)에 대한 참조, 그리고 this 키워드가 가리키는 대상 등이 포함됩니다.

실행 컨텍스트의 구성 요소

실행 컨텍스트

자바스크립트 엔진의 실행 컨텍스트는 세 가지로 구성됩니다.

Variable Environment

현재 컨텍스트의 식별자(변**수)들에 대한 환경으로, var로 **선언된 변수와 함수 선언문으로 생성된 함수 정보를 담 있으며, 컨텍스트 생성 시 초기 스냅샷으로 저장됩니다. 이 스냅샷은 실행 도중 변화되지 않고, 새 변수 선언(var)은 이 환경에 추가됩니다.

Lexical Environment

Lexical Scope

현재 컨텍스트의 렉시컬 환경으로, 변수 정보와 더불어 외부 렉시컬 환경에 대한 참조(outer environment reference)를 포함합니다. 컨텍스트 생성 시에는 Variable Environment와 동일한 내용을 가지지만, 실행 중 새로운 렉시컬 환경이 생성되면 Lexical Environment는 해당 환경을 가리키도록 변경됩니다.

Lexical Environment 내부에는 환경 레코드(Environment Record)와 외부 환경 참조 정보가 있어, 현재 스코프에 없는 식별자를 찾을 때 스코프 체인(Scope Chain)을 따라 외부 스코프로 접근할 수 있게 합니다.

환경 레코드(Environment Record)

  • Environment Record는 “식별자와 그에 대응하는 값을 저장·관리하는 추상 레코드”를 의미합니다.
  • 이 레코드는 다양한 타입(Declarative, Object, Global, Module)으로 구현되어, 스코프별 바인딩을 정확히 처리합니다.
  • 렉시컬 환경은 이 레코드와 외부 참조(outer)를 통해 식별자 해석(Identifier Resolution)을 수행합니다.

This Binding

해당 컨텍스트에서의 this 키워드가 가리키는 객체에 대한 참조입니다.

위 구성 요소들은 컨텍스트 생성 단계(Creation Phase)에서 초기화되고 역할을 수행합니다.

일반적으로 Variable Environment와 Lexical Environment는 컨텍스트 생성 시 동일한 환경으로 시작하지만, 특정 상황에서 Lexical Environment가 새로운 환경을 가리키게 되어 두 환경이 달라질 수 있습니다.

이렇게 하는 이유는 예를 들어 블록 스코프의 let / const 변수가 생길 때 새 렉시컬 환경을 만들어도, var 선언은 여전히 기존 함수 레벨 환경에 추가하기 위함입니다. 따라서 스코프 체인을 따라 식별자를 검색할 때는 Lexical Environment 기준으로 하고, 새로운 변수 선언(특히 var)은 Variable Environment에 추가되어 호이스팅 등 동작을 결정합니다.

Variable Environment vs Lexical Envrionment

아래 코드는 함수 내부의 블록에서 varlet으로 변수를 선언하는 경우를 비교합니다.

function scopeTest() {
  var x = 1;              // 함수 스코프 변수 (VariableEnvironment에 등록)
  if (true) {
    var x = 2;            // 같은 함수 스코프의 변수 x를 다시 선언 (기존 var x를 변경)
    let y = 3;            // 블록 스코프 변수 (새 LexicalEnvironment에 등록)
    console.log(x);       // 2  -> var는 함수 전체에서 하나의 변수로 취급됩니다.
    console.log(y);       // 3  -> let은 블록 내에서만 유효합니다.
  }
  console.log(x);         // 2  -> 블록에서 var로 변경된 값이 유지됩니다.
//console.log(y);         // ReferenceError -> y는 블록을 벗어나면 접근 불가
}
scopeTest();

Step 1: 함수 실행 컨텍스트 생성

  • scopeTest()가 호출되면 새로운 함수 실행 컨텍스트(Function Execution Context)가 생성됩니다.
  • 이 컨텍스트는 두 가지 환경을 가지고 있습니다.
    • Variable Environment: 함수 내 var 선언 변수가 저장되는 환경
    • Lexical Environment: 블록 스코프(let, const) 변수가 저장되는 환경

Step 2: 함수 스코프 (var)

  • 처음 var x = 1;이 선언될 때, 변수 xVariable Environment(함수 스코프)에 저장됩니다.
  • 이 변수는 함수 전체에서 유효하며, 함수 범위 내 어디에서든 접근 가능합니다.

Step 3: 블록 스코프 진입

  • if (true) 블록을 만나면, 자바스크립트 엔진은 새로운 블록 전용 Lexical Environment를 생성합니다.
  • 이 환경은 블록 스코프(let, const)로 선언된 변수만 저장합니다.

Step 4: 블록 내 변수 선언 (var vs let)

  • 블록 내에서 var x = 2;는 기존 함수의 Variable Environment에서 선언된 변수 x를 재사용하여 값을 변경합니다.
    • 따라서 블록 밖에서도 값 변경이 유지됩니다.
  • 블록 내에서 let y = 3;는 블록 전용 Lexical Environment에 새롭게 등록됩니다.
    • 이 변수는 블록 내에서만 유효하며, 블록 밖에서는 접근이 불가능합니다.

Step 5: 변수 참조 결과

  • 블록 내:
    • console.log(x); // 2 : 기존의 함수 스코프 변수 x가 변경된 값으로 출력됩니다.
    • console.log(y); // 3 : 블록 스코프 변수 y가 출력됩니다.
  • 블록 밖:
    • console.log(x); // 2 : 블록 내에서 변경된 함수 스코프 변수 x의 값이 유지됩니다.
    • console.log(y); // ReferenceError : 블록이 끝난 후 Lexical Environment에서 사라지므로 접근 불가능합니다.

This 바인딩과 실행 컨텍스트

자바스크립트의 this 키워드는 현재 실행중인 컨텍스트와 호출 방식에 따라 결정되는 값입니다. 실행 컨텍스트 객체의 구성 요소 중 This Binding 에 해당 컨텍스트에서의 this 가 저장되며, 함수가 어떻게 호출되었느냐에 따라 this 에 바인딩될 대상이 동적으로 정해집니다.

This 바인딩 규칙

  • 전역 컨텍스트
    • 전역에서의 this는 기본적으로 전역 객체를 가리킵니다.
    • 전역에서 선언한 변수나 함수는 전역 객체의 프로퍼티로 접근되는데, 이는 전역 실행 컨텍스트의 Lexical Environment가 전역 객체를 활용하기 때문입니다.
    • 단, ES 모듈이나 ‘use strict’ 모드의 전역 코드에서는 this가 undefined가 될 수 있습니다.
  • 일반 함수 호출
    • 어떤 함수가 독립적으로 호출될 때 그 함수 내부의 this는 전역 객체로 설정됩니다.
    • 함수가 특정 객체의 메서드로 호출되지 않았다면 기본적으로 전역 컨텍스트처럼 동작합니다.
    • 단, ‘use strict’ 모드에서는 undefined로 설정됩니다.
  • 메서드 호출
    • 객체의 프로퍼티로 함수를 호출할 경우, 해당 함수 내부의 this는 호출한 객체 인스턴스를 가리킵니다.
  • 생성자 호출
    • 함수를 new 키워드를 사용하여 new Func() 와 같이 호출하면 새로운 빈 객체가 만들어지고, 함수 내부의 this 는 그 신규 객체를 가리키게 됩니다.
    • 이때 함수가 별도로 객체를 반환하지 않으면 new 호출 표현식의 결과로 그 새로운 객체가 반환됩니다.
  • 명시적 바인딩
    • 함수 객체의 메서드인 call, apply, bind 를 사용하면 함수 호출 시 임의의 객체를 this로 지정할 수 있습니다.
  • 화살표 함수
    • 화살표 함수 내에서 this를 참조하면 자신을 감싼 외부 컨텍스트의 this를 그대로 사용합니다.
    • 화살표 함수는 정의될 때 this 바인딩이 이미 정적으로 결정되며, 이후 어떤 객체의 메서드로서 호출되어도 this값이 변하지 않습니다.
// 1. 전역 컨텍스트
console.log(this);          // window 객체 (브라우저 환경 가정)

// 2. 일반 함수 호출
function independent() {
  console.log(this);
}
independent();              // window (비엄격 모드), undefined (엄격 모드)

// 3. 메서드 호출
const obj = {
  value: 42,
  getValue: function() {
    console.log(this);
    return this.value;
  }
};
obj.getValue();             // obj 객체 (this가 obj를 가리킴)

// 4. 생성자 호출
function Person(name) {
  this.name = name;
}
const alice = new Person('Alice');
console.log(alice.name);    // "Alice" (`this`가 새로 생성된 alice 객체)

// 5. 화살표 함수
const arrowFunc = () => console.log(this);
arrowFunc();                // window (자신의 상위 컨텍스트가 전역인 경우)
// 화살표 함수를 객체 메서드로 설정해도 여전히 상위 스코프(this)는 전역
obj.arrowMethod = arrowFunc;
obj.arrowMethod();          // window (화살표 함수는 obj에 바인딩되지 않음)

// 6. 명시적 바인딩
function showThis() {
  console.log(this);
}
showThis.call(obj);         // obj (call로 명시적으로 this를 obj로 설정)

클로저와 실행 컨텍스트

클로저란 어떤 함수가 자신이 정의된 환경(렉시컬 스코프)의 변수들을 계속해서 접근할 수 있는 현상을 말합니다.

클로저는 함수가 중첩되어 정의될 때 내부 함수가 외부 함수의 변수에 접근하는 것을 가능하게 하며, 외부 함수 실행이 종료된 이후에도 내부 함수가 그 변수를 기억하고 사용할 수 있도록 해줍니다. 이러한 동작이 가능한 이유는 자바스크립트가 렉시컬 스코프를 따르는 언어이기 떄문입니다. 함수가 선언될 때 자신의 상위 스코프에 대한 참조를 내부 슬롯에 저장하고, 함수가 실행될 때 그 참조를 토대로 상위 Lexical Environment를 연결하여 스코프 체인을 구성합니다.

일반적으로 함수 실행이 끝나면 그 실행 컨텍스트와 지역 변수들은 스택에서 제거되어 가비지 컬렉터에 의해 메모리가 회수됩니다. 그러나 클로저를 형성하는 경우, 이미 종료된 외부 함수의 변수들도 내부 함수에서 계속 참조되고 있으므로 메모리에 유지됩니다.

내부 함수가 살아있는 한, 내부 함수가 참조하는 외부 Lexical Environment에 있는 변수들도 가비지 컬렉션이 되지 않고 유지되는 것 입니다. 이는 의도한 동작으로서 우리가 클로저를 통해 값을 보존할 수 있는 이유지만, 동시에 불필요하게 오래 참조가 남아있는 변수들은 메모리를 점유하는 부작용도 있습니다.

따라서 클로저를 사용할 때는 필요 없어진 참조를 제거하여 메모리 누수를 방지하는 것이 중요합니다.

function createCounter() {
  let count = 0;            // 외부 함수의 지역 변수 (createCounter 실행 컨텍스트 내)
  console.log("Counter 생성, 초기 count =", count);
  return function() {       // 내부 함수 반환 (클로저 형성)
    count++;                // 외부 함수의 지역 변수에 접근하여 증가
    console.log(`현재 count 값: ${count}`);
  };
}

const counterA = createCounter();  // counterA는 내부 함수에 대한 참조
counterA();  // 출력: "현재 count 값: 1"
counterA();  // 출력: "현재 count 값: 2"

const counterB = createCounter();  // 새로운 카운터 생성 (별도 렉시컬 환경)
counterB();  // 출력: "현재 count 값: 1" (counterA의 count와 독립적인 값)

참고
https://www.youtube.com/watch?v=zdGfo6I1yrA&t=95s
https://meetup.nhncloud.com/posts/86
https://junilhwang.github.io/TIL/Javascript/Domain/Execution-Context/#summary

profile
작은 성취들이 모여 큰 결과를 만든다고 믿으며, 꾸준함을 바탕으로 개발 역량을 키워가고 있습니다

0개의 댓글