[JS] 실행 컨텍스트

Yunhye Park·2023년 10월 15일
0

Back To Basic

목록 보기
1/10
post-thumbnail

실행 컨텍스트는 자바스크립트 엔진이 소스코드를 평가하고 동작하는 작동 원리와 밀접하게 연관되었다. 스코프*와 코드 실행 순서 관리를 구현한 내부 메커니즘으로, 모든 코드가 실행 컨텍스트를 통해 실행되고 관리된다.

스코프(scope)?
식별자(변수 이름, 함수 이름, 클래스 이름 등)가 유효한 범위. 식별자는 선언된 위치에 따라 다른 코드를 참조할 수 있는 범위가 정해진다.
예를 들어, 전역에 선언한 변수는 어디에서나 참조 가능하다.
반면, 함수의 매개변수나 함수 내에 선언한 변수는 지역 스코프를 가져서 그외의 공간(함수 바깥)에서 다른 코드가 해당 식별자를 참조할 수 없다.

실행 컨텍스트는 실행할 코드에 제공할 환경 정보를 모아둔 객체다.

실행 컨텍스트에는 두 가지 환경이 있다. 하나는 식별자와 스코프를 관리하는 렉시컬 환경(LexicalEnvironment), 다른 하나는 코드 실행 순서에 관여하는 실행 컨텍스트 스택이다.

우선 후자부터 알아보자.

코드 실행 순서 : 실행 컨텍스트 스택

'스택'은 자료구조 개념이라서 '큐'와 비교해보면 좋다.

스택은 출입구가 하나인 데이터 구조다. 비어있는 스택에 데이터 1, 2, 3, 4를 순차적으로 저장하면 가장 나중에 있는 4부터 역순으로 꺼낼 수있다. LIFO (Last-in First-out) 방식이다.
이와 달리 큐는 출입구가 양쪽에 있어서 가장 먼저 들어온 것을 가장 먼저 내보낼 수 있다. FIFO(Fist-in First-out) 방식이다.

자바스크립트는 스코프마다 실행 컨텍스트를 생성하고, 그렇게 생성된 실행 컨텍스트는 스택 자료 구조로 관리된다. 이를 실행 컨텍스트 스택 혹은 콜 스택이라고 부른다.

텍스트로는 어려울 수 있으니 예제를 통해 콜 스택을 도식화하고, 자바스크립트 엔진이 작동하는 순서를 파악해보자.

// ----------------------------- (1)
const x = 1;

function outer () {
	const y = 2; // ------------ (2)

  	function inner () {
    const z = 3;
    console.log(x + y + z);
    }
  inner(); // ------------------ (3)
}
outer(); // -------------------- (4)

자바스크립트는 위에서부터 아래로 순차적으로 코드를 평가해 나가는 인터프리터 언어이다. 하지만 런타임 실행 전에 하는 일이 있다. 바로 변수를 비롯한 식별자 선언문만 먼저 실행하는 것이다. 이렇게 생성된 식별자는 실행 컨텍스트가 관리하는 스코프(렉시컬 환경의 환경 레코드)에 등록된다. 여기서 호이스팅(hoisting)* 개념도 나온 것이다.

호이스팅(hoisting)?
코드를 실행하기 전 함수 내 모든 선언문(함수, 변수 등)을 각 선언문의 스코프 최상단으로 끌어올리는 것으로 간주함을 의미. 실제로 끌어올리는 건 아니지만 이해의 편의상 그렇게 여긴다.

이렇게 선언문을 확인하고 메모리를 할당한 후, 런타임을 시작한다.

  1. 최상단 공간은 별도의 실행 명령 없이 브라우저에서 자동으로 실행(1)한다. 콜 스택에 전역 컨텍스트가 들어서고, 변수 x에 1을 할당한다.

  2. 순차적으로 코드를 읽어가다가 그 다음으로 전역 컨텍스트에 있는 outer 함수를 호출(2)한다. 이때 콜 스택은 전역 컨텍스트 위에 outer 함수가 생긴다.

  3. outer 함수가 최상단을 차지했으므로, 전역 컨텍스트의 실행을 중단하고 outer 함수부터 한 줄씩 읽어간다. 변수 y에 2를 할당한다. 그러다 inner 함수를 호출(3)한다.

  4. 이제 콜 스택은 outer 함수 위에 inner 함수가 담겼다. 변수 z에 3을 할당하고, 세 변수의 합을 콘솔에 찍는다. 함수의 모든 코드가 완료되었으므로 inner 함수는 종료되고, 이는 곧 콜 스택에서의 제거를 말한다. 그럼 outer 함수가 최상단을 차지한다.

  5. outer 함수의 실행이 멈춘 시점으로 돌아가 다시 동작한다. 그 뒤에 남아있는 코드가 없어서 이 함수도 종료되어 콜 스택에서 제거된다. 이제 전역 컨텍스트가 남았다.

  6. 마찬가지로 전역 컨텍스트 실행 중단 시점부터 코드를 읽어간다. 남아있는 내용이 없어서 함수 실행이 종료되고, 콜 스택에서 제거된다.

예시를 통해 알아본 것처럼 스택의 최상단에 있는 컨텍스트는 언제나 '현재 실행 중인 실행 컨텍스트'다. 실행 컨텍스트를 실행하는 데에 필요한 환경 정보들을 수집해서 저장한 객체는 어떻게 구성되었을까?

식별자와 스코프 관리 : 렉시컬 환경

실행 컨텍스트는 LexicalEnvironment 컴포넌트와 VariableEnvironment 컴포넌트로 구성되었다. 두 컴포넌트는 생성 초기엔 하나의 동일한 렉시컬 환경을 참조해서 완전히 동일하다. 이후에 코드 진행에 따라 차이가 생기기도 하지만 공통 부분이 있기에 렉시컬 환경을 위주로 살펴보려고 한다.

렉시컬 환경은 <식별자와 식별자에 바인딩된 값>, <상위 스코프에 대한 참조>를 기록하는 자료구조라고 했다. 역할에 맞춰 영역도 2개로 나뉜다.

  1. 환경 레코드(Enviornment Record)
    현재 컨텍스트와 관련된 코드의 식별자를 등록하고, 등록된 식별자에 바인딩된 값을 관리하는 저장소.

  2. 외부 렉시컬 환경에 대한 참조(Outerlexical Enviornment Reference)
    해당 실행 컨텍스트를 생성한 소스코드를 포함하는 상위 스코프를 말한다.

예제를 통해 스코프마다 각 컴포넌트가 어떤 과정으로 진행되는지 살펴보자.

예제

var x = 1;
const y = 2;

function foo(a) {
  var x = 3;
  const y = 4;

  function bar(b) {
  	const z = 5;
  	console.log( a + b + x + y + z);
  }
  bar(10);
}

foo(20);

전역 코드

전역 코드 평가 순서는 다음과 같다.

  1. 전역 실행 컨텍스트 생성 : 콜 스택의 최상단이 된다.
  2. 전역 렉시컬 환경 생성
    • 2.1. 전역 환경 레코드 생성
      • 2.1.1. 객체 환경 레코드 생성
      • 2.1.2 선언적 환경 레코드 생성
    • 2.2 this 바인딩
    • 2.3 외부 렉시컬 환경에 대한 참조 결정

이 과정을 거친 후, 드디어 코드가 실행된다.
변수 할당문이 실행되어 전역 변수 x, y에 값이 할당되고, foo함수가 호출된다. 그런데 동일한 이름의 식별자가 서로 다른 스코프에 여럿 존재할 수 있다. 이럴 때 어느 스코프의 식별자를 참조하면 될지 자바스크립트 엔진이 결정해야 한다.

이렇게 식별자 결정을 위해 검색할 때엔 실행 중인 실행 컨텍스트에서 식별자를 검색한다. 즉 전역 렉시컬 환경에서 식별자 x, y, foo함수를 찾는 것이다. 여기서 검색할 수 없으면 상위 스코프로 이동한다. 하지만 전역 스코프는 스코프 체인의 종점이라서 더는 참조할 범위가 없다. 이럴 땐 참조 에러가 발생한다.

foo 함수

foo 함수가 호출되면 전역 코드의 실행은 일시 중단하고 foo 함수의 코드를 평가하기 시작한다. 순서는 아래와 같다.

  1. 함수 실행 컨텍스트 생성 : 콜 스택의 최상단이 된다.
  2. 함수 렉시컬 환경 생성
    • 2.1. 함수 환경 레코드 생성 : 매개변수, arguments 객체, 지역 변수, 중첩 함수 등
    • 2.2 this 바인딩 : 전역 객체 🔍
    • 2.3 외부 렉시컬 환경에 대한 참조 결정 : 전역 렉시컬 환경 ⚙️

🔍 함수는 호출 방식에 따라 this에 바인딩 될 값이 달라진다. 일반 함수로 호출하면 전역 객체를 가리키고, 메서드로 호출하면 해당 메서드의 객체가 this이다.
⚙️ 자바스크립트에서 함수는 어디서 호출했는지가 아니라 어디에 정의했는지에 따라 상위 스코프를 결정한다. foo 함수는 전역 코드에 정의된 전역 함수라서 전역 렉시컬 환경이 할당된다.

bar 함수

foo함수 내부의 bar 함수가 호출되면, foo함수 실행이 중단되고 실행 컨텍스트의 제어권이 bar 함수로 넘어온다.

함수이기 때문에 실행 컨텍스트 생성과 렉시컬 환경 생성 과정은 동일하다. 스코프 체인 과정만 살펴보자.

먼저 변수 z에 값 5를 할당하고, console 식별자를 찾는다. 스코프 체인은 현재 실행 중인 실행 컨텍스트의 렉시컬 환경에서 외부 렉시컬 환경에 대한 참조로 이어진다. bar 함수 실행 컨텍스트의 렉시컬 환경에서 검색을 시작했는데 식별자를 찾을 수 없다.

상위 스코프인 foo 함수의 렉시컬 환경으로 이동한다. 이곳에서도 발견할 수 없어서, 종점인 전역 렉시컬 환경으로 이동한다. 비로소 console 식별자를 찾았다.

콘솔 내부의 식별자들 중 x는 전역 스코프와 foo 함수 스코프 두 곳에 존재한다. 하지만 스코프는 내부에서 외부로 확장되므로 foo 함수 렉시컬 환경에서 x를 발견하면 바로 식별자를 결정하기에 전역으로 나아가지 않는다.

이후

더 실행할 코드가 없으면 순차적으로 함수 코드가 종료되고 실행 컨텍스트가 콜 스택에서 제거된다. 단, 실행 컨텍스트가 소멸되어도 함수 렉시컬 환경을 누군가 참조하면 해당 렉시컬 환경은 사라지지 않는다.

블록 레벨 스코프

var 키워드로 선언한 변수는 함수의 코드 블록만 지역 스코프로 인정한다. 즉 함수 레벨 스코프를 따른다. 반면 ES6에 추가된 let, const 키워드는 조건문, 반복문 등 모든 코드 블록을 지역 스코프로 인정한다. 블록 레벨 스코프를 따른다고 말할 수 있다.


참고

📚 자바스크립트 Deep Dive
📚 코어 자바스크립트


생각

  • 렉시컬 환경은 컴포넌트 단어부터 되게 복잡하게 생겨서 낯설었다. 그래서 코어 자바스크립트 한 권으로는 부족하다는 느낌을 받아 딥 다이브까지 참조해가며 정리해봤다.
  • 호이스팅, 스코프, 블록 레벨 스코프는 면접에서도 자주 물어보는 개념이라고 들었다. 이전에도 이해를 몇 번 시도했는데 서너 번 반복하니까 이제야 좀 안다고 할 수 있을 것 같다.
  • 자바스크립트의 동작 원리는 개발 스킬이 쌓일수록 도움이 크게 와 닿을 것 같다. 지금 내가 체감하는 정도 : 코드를 읽고 해석할 때 순서가 덜 헷갈리고, 순서의 이유를 일부는 대략적으로라도 말할 수 있다.
  • 프로그래밍 언어도 결국 인간의 언어이기에 외국어 배울 때와 자세가 같아야 한다고 본다. 스페인어를 배우려면 스페인의 문화나 역사를 알면 알수록 그들의 언어 습관을 쉽게 인지할 수 있듯이. 자바스크립트 엔진이 어떤 식으로 움직이는지를 알아야 이 언어를 더 잘 쓸 수 있을 것이다. 물론 초장부터 너무 깊게 들어가면 지엽적으로 빠질 것을 알기에... 오묘한 줄타기. ⚖️
profile
일단 해보는 편

1개의 댓글

comment-user-thumbnail
2023년 10월 19일

호이스팅이랑 스코프 개념을 이번 기회에 알게 되었습니다.

답글 달기

관련 채용 정보