들어가며

JavaScript를 공부하는 사람들이라면 한번 쯤 보게되는 단어. 실행 컨텍스트다.
영어로는 Execution Context라고 한다.

특이하게도 이 친구는 호이스팅을 검색해도 나오고, 클로저를 검색해도 나오며, 스코프, 스코프체이닝 등 다방면을 알아볼 때 등장한다.

이건 무슨 뜻일까?

당연히 이는 실행 컨텍스트가 굉장히 중요하다는 뜻이겠다.

이번에는 실행 컨텍스트가 대체 뭔지, 그리고 실제 JavaScript에서 어떻게 동작을 하는지 알아보고자 한다.

실행 컨텍스트

이 글을 모두 이해하면 우리는 아래의 것들을 얻을 수 있다.

  1. 실행 컨텍스트가 뭔지
  2. JavaScript에서 실행 컨텍스트가 어떻게 동작하는지
  3. Scope Chaining이 뭔지, 왜 발생하는지
  4. 기타 호이스팅, 클로저 등등을 나중에 공부할 때 훨신 수월함
  5. 아는 척 가능

자 그럼 이제 같이 알아보자!

실행 컨텍스트는 무엇인가?

실행 컨텍스트(이하 Execution Context)는 JavaScript 엔진이 코드를 실행하기 위해 필요한 여러가지 정보를 가지고 있고, 제공 하는 객체 이다.

시작부터 그런 사전같은 설명이라뇨.. 😕

당장의 이해가 어려울 것 같아 요리 를 예로 들어보았다.

요리에서 Execution Context

예를 들어 카레를 만든다고 가정해보자.
어떤게 필요할까?

우선적으로 생각나는 건 카레를 만드는 재료들이다.
아마도 당근, 양파, 감자, 고기, 카레(강황)가루, 우유, 등이 있을 것 같다.

그럼 재료만 있으면 카레를 만들 수 있을까?

그렇지 않다.

재료 이외에도 재료를 손질할 수 있는 도구가 있어야 할 것 같다.
지금 필요한 도구로는 냄비, 도마, 등이 있을 것 같다.

자 그럼 이제 진짜 카레를 만들 수 있을까?

아쉽게도 아직 아니다.

우리는 요리를 할 공간 도 필요하다.
공간 이란 집 부엌이 될 수도 있고 음식점 이 될 수도 있겠다.

지금까지의 내용을 정리해보면 아래와 같다.

카레레시피를 보고 요리하기 위해서는 재료도구, 공간이 필요하다. (제공되어야 한다)

이걸 JavaScript에 빗대어 말하면 아래와 같다.

내가 작성한 JavaScript Code를 보고 JavaScript Engine이 실행하기 위해서는 변수, 스코프 등의 정보가 필요하다. (제공되어야 한다)

내(JavaScript Engine)카레 레시피(코드)를 보고 요리(실행)할 수 있게 재료, 도구, 공간을 제공해주는 것이 바로 Execution Context라는 것이다.

조금은 이해가 더 되었을까?
그럼 지금 한번 함께 알아보도록 하자.


JavaScript에서 소스코드의 종류

Execution Context에 대해 알아보기 전에 먼저 소스코드 종류 부터 알아야 한다.

종류에 따라 생성되는 Execution Context가 약간 다르기 때문이다.

본 게시글에서는 크게 Global Code(전역코드)Function Code(함수코드)로 나누도록 하겠다.

실제로는 이외에도 다른 종류가 있기 때문에 나중에 직접 공부해보자.

Global Code(전역 코드)

let 전역변수 = '안뇽';

전역에 존재하는 코드. 단, 전역에 정의되어 있는 함수나 클래스 등의 내부 코드는 포함되지 않는다.

Function Code(함수 코드)

function 음식하기(){
  let 음식이름 = '카레';
}

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



JavaScript Engine은 어떻게 동작하는가?

소스코드 종류를 알았으니 이제 JavaScript Engine이 어떻게 우리의 소스코드를 실행시키는지 알아보자.

먼저 JavaScript Engine소스코드 평가, 소스코드 실행 두 가지의 단계로 나누어 우리의 코드를 실행해준다.

소스코드 평가

소스코드 평가 과정에서는 Execution Context를 생성하고, 코드의 시작부터 끝까지 확인한다. 이 때 변수, 함수 등의 선언문'만'을 먼저 실행하여 식별자를 Execution Context에 저장해둔다.

예를 들어 아래와 같은 코드가 있다고 가정해보자.

var 음식재료;
음식재료 = '양파';

소스코드 평가 과정에서는 변수 선언문인 var 음식재료;만 실행되어 Execution Context에 선언된다.


소스코드 실행

소스코드 평가가 완료되면 소스코드 실행이 진행된다.

아까 그 코드를 다시 보자면 var 음식재료;는 이미 실행되었기 때문에 다시 실행하지 않고 음식재료 = '양파'; 만 실행한다.

이 때 JavaScript Engine음식재료 라는 변수가 선언되어 있는지 여부를 Execution Context에서 확인한다. (정확히는 음식재료라는 식별자가 등록되어 있는지 확인)



Execution Context Stack, Call Stack

JavaScript는 하나의 CallStack을 사용하고 있으며 EventLoop 으로 동작한다.

아마 JavaScript에 대해 조금 공부해 본 사람이라면 알 법 한 내용이다.

그럼 만약 아래와 같은 코드가 있다고 생각해보자.


function 함수1 () {
 	console.log('안녕하세요1'); 
}

function 함수2 () {
  함수1()
  console.log('안녕하세요2');		// 여기까지 실행되었다고 가정
}

함수2();

이 코드의 Call Stack이 아래 사진과 같다고 생각 할 것이다.

(편의상 console.log() 생략)

하지만 실제 Call Stack은 아래와 같다.

Call Stack에는 함수 가 아닌, Execution Context가 들어간다는 말이다.

실제로 Execution Context에 대해 검색하다 보면 Execution Context Stack 이라는 표현도 종종 보이는데, Call StackExecution Context Stack은 동일한 것을 다르게 부르는 것 뿐이다. 참고질문

따라서 JavaScriptCall Stack은 실제로는 Execution Context를 저장한다.

그리고 우리의 코드가 실행 될 때 이 Execution Context에 저장 된 정보를 사용해서 실행한다.

이 때 Stack의 가장 위에 있는 Execution ContextRunning Execution Context라고 칭한다.

그럼 이제 Execution Context에 대해 더 자세히 알아보자.

본 게시물에서는 Call Stack으로 용어를 통일한다.



Execution Context의 구조 (ES6 기준)

executionContext = {
  lexicalEnvironment: {...},
  variableEnvironment: {...},
}

Execution Context에는 LexicalEnvironment, VariableEnvironment가 존재한다. 각각 하나씩 살펴보자.

Lexical Environment

혹시 방금 뒤로가기 버튼을 누르려고 했다면 조금만 더 읽어보자.
복잡해보여도 생각보다 간단한 내용은 아니지만 읽어 볼 만 할 것이다.

좀 더 우리가 아는 친숙한 트리 구조로 표현하자면 위와 같은 구조이다.

그럼 Environment Record 부터 알아보자.


Environment Record

여기서는 변수함수식별자(Identifier)를 등록하고, 해당 식별자에 바인딩 된 값을 관리한다.
쉽게 말해서 아래와 같은 코드의 변수들을 저장해두고 필요할 때 꺼내쓰는 곳이다.

let 변수당 = '안녕하세용';
const 안뇽 = '하세용';

여기서 Environment Record는 다시 Object Environment RecordDeclarative Environment Record로 나눠진다.


- Declarative Environment Record
const 변수 = 'hello';
let 변수둘째 = 5;
var 안녕 = 'haseyo';

변수, 함수 등의 Identifier을 저장할 때 사용한다.
기본적으로 이 Record가 사용된다.


- Object Environment Record
var window  = {
  scrollHeight:100,
  scrollWidth:500,
};

record를 객체 형식으로 저장할 때 사용한다.
전역 객체를 선언할 때 사용된다.

다만 객체 특성 상 불변성이 깨질 수 있어 전역객체 이외에 사용을 지양된다.

withStatement문에서도 사용된다고 한다.


This Binding

this = '이것';

우리가 아는 그 this의 값이다.
어디에서 어떻게 호출되느냐에 따라 값이 다르다.
this에 관한 자세한 내용은 이 게시글에서 다루지 않는다.



Outer Environment Reference

❗ 중요한 내용, 아래 다시 나올예정

Outer Environment Reference를 직역하자면 외부 환경 참조 로, 이름이 상당히 직관적이다.

이는 자신이 아닌 외부의 Lexical Environment를 참조 할 때 (그 값을 사용할 때) 사용한다.

Scope Chaining, Closer를 이해하는 데 필수적인 내용이다.
내용이 길고 중요하기 때문에 조금 아래에서 본격적으로 설명하도록 하겠다.


Variable Environment

Lexical Environment와 동일한 내부 구조를 가지고 있다.
하지만 여기에는 var 키워드로 선언 된 값만 사용하게 된다.

크게 중요하지는 않기 때문에, 이 게시글에서는 Lexical Environment를 기준하여 설명하도록 한다.



그래서 실제로 어떻게 동작하는데요

이론적인 내용으로만 공부하다 보면 길을 잃을 때가 종종 있다.
이번에는 실제 코드의 동작을 보며 실질적인 Execution Context의 동작에 대해 알아보자.

var 전역변수_1 = 'Himprover';
const 전역변수_2 = '일 수 있지?';

function 함수1 (함수1_파라미터) {
  var 전역변수_1 = '엄준식';
  const 전역변수_2 = '이냐';
  
  function 함수2 (함수2_파라미터) {
    const 함수2_지역변수 = 'ㅋㅋ';
    
    console.log(함수1_파라미터, 함수2_파라미터, 전역변수_1, 전역변수_2, 함수2_지역변수);
  }
  함수2('사람이름이');
}

함수1('어떻게');

이 코드를 실행하여 console에 찍히는 값은 뭘까?
잠시 생각해보고 아래로 내려보자.













정답은 어떻게 사람이름이 엄준식이냐 ㅋㅋ 이다.

근데 보자 뭔가 이상하지 않은가?

함수2에서 console.log하는 코드를 보면 전역변수_1, 전역변수_2 식별자를 사용하고있다.

그런데 예제 코드 내에서는 전역함수1에서 같은 식별자로 선언된 변수가 존재한다.

과연 JavaScript는 어떤 방법으로 같은 식별자의 변수 중에서 선택하는 걸까?

비밀은 바로 Execution Context, 그리고 그 내부의 Outer Environment Reference에 숨겨져 있다.

한번 단계 별 로 분석해보며 동작 원리를 알아보자.


전역 코드 평가

※ 전역 객체는 전연 코드 평가 이전에 생성된다. 자세한 내용은 구글 참고

JavaScript Engine은 가장 먼저 전역 코드를 평가한다.
그리고 Global Execution Context를 생성 해 Execution Context Stackpush 해 준다.

Global Execution Context (GEC)

var 전역변수_1;
const 전역변수_2;
function 함수1;

예제 코드에서 전역 코드는 전역변수_1, 전역변수_2, 함수1 이다.

JavaScript EngineGlobal Execution Conext를 생성 하고 그 내부에 아래 사진과 같이Lexical Environment를 설정한다.

오잉? 우리가 할당 한 값들이 들어있지 않아요 🙄

현재 var, const, function 3개의 방식으로 선언하였는데, 각각 다른 값이 보여진다. (초기화안됨, undefined, Function)

이는 각 선언 방식의 특징과 변수가 선언되는 방식 때문인데, 이곳에서 설명하기엔 너무 많으니 생략하고 진행한다. (검색 키워드 : "자바스크립트 선언 초기화 할당")


전역 코드 실행

Global Execution Context (GEC)

이제 전역 코드를 실행해준다.
비로소 모든 변수들에 올바른 값이 할당 된 모습이다.

잠깐! Outer Environment Reference는 왜 null일까?

JavaScriptLexical Scope를 사용한다.
이는 함수가 코드 상에서 선언되었을 때를 기준으로 Scope가 결정된다는 의미이다.

Outer Environment Reference는 나보다 상위의 Execution Context를 참조하게 되는데, Global Context는 이미 최상위에 위치하고 있다.

Global Context의 상위에는 아무것도 존재하지 때문에 null이 들어가게 된다.

다음 함수1()을 분석해보며 더 자세히 알아보자.


함수1 코드 평가

function 함수1 (함수1_파라미터='어떻게') {
  var 전역변수_1 = '엄준식';
  const 전역변수_2 = '이냐';
  
  function 함수2 (함수2_파라미터) {
   ~~~
  }
  함수2('사람이름이');
}

전역 코드와 마찬가지로 함수1() 또한 코드 평가코드 실행으로 나누어 진행된다.

또한 Call Stack에는 함수1()Execution Context를 생성해 넣어준다.

this

여기서 this의 값은 전역객체를 참조한다.

Outer Environment Reference

Outer Environment Reference는 선언 된 코드 상에서 상위 Execution Context를 참조한다.
여기서는 Global함수()이 선언되어 있었으므로 Global Execution Context가 된다.

Function Environment Record

Function Environment Record(환경레코드)는 arguments가 추가된 Record이다.
해당 record에 전달 받은 argument가 추가로 저장된다.

함수1 코드 실행

전역 코드 실행 때와 마찬가지로, 함수1의 코드가 실행되면 실제 값들이 변수에 할당된다.


함수2 코드 평가

function 함수2 (함수2_파라미터='사람이름이') {
    const 함수2_지역변수 = 'ㅋㅋ';
    
    console.log(함수1_파라미터, 함수2_파라미터, 전역변수_1, 전역변수_2, 함수2_지역변수);
}

이제 마지막으로 함수2의 코드 평가가 진행된다.
동일하게 Execution Context를 생성해서 Call Stack에 넣어준다.

const로 선언한 함수2_지역변수는 아직 초기화되지 않았고,
함수1에서 파라미터로 전달받은 '사람이름이'는 정상적으로 arguments에 들어가 있다.

Outer Environment Reference함수1Execution Context를 참조하고 있다.


함수2 코드 실행

const 함수2_지역변수 = 'ㅋㅋ';
console.log(함수1_파라미터, 함수2_파라미터, 전역변수_1, 전역변수_2, 함수2_지역변수);

자 이제 함수2_지역변수에도 값이 잘 할당되었다.

위에서 보았듯이 console에 찍힌 실행결과는 아래와 같다.

실행 결과 : 어떻게 사람이름이 엄준식 이냐 ㅋㅋ

자 이제 우리가 위에서 궁금해했던 JavaScript는 어떻게 같은 이름의 변수 중 하나를 선택하는가 에 대한 비밀을 해결 할 차례이다.

이를 유식한 말로 식별자 결정 이라고 한다.

우선 지금까지 있었던 CallStack을 조금 단순화 하여 표현해보겠다.


함수1_파라미터 값 결정

모든 값의 결정은 Running Execution Context 부터 시작된다.
JavaScript는 먼저 Running Execution Context에서 함수1_파라미터라는 식별자가 등록되어 있는지 확인한다.

하지만 함수2Execution Context에는 함수1_파라미터라는 식별자가 등록되어 있지 않았다.

이 때 JavaScriptOuter Environment Reference를 타고가서 함수1_파라미터를 찾거나 이 보일 때 까지 탐색을 시작한다.

이런 과정을 거쳐서 함수1_파라미터 값을 찾아냈다.

이렇게 Outer Environment Reference를 통해 다른 Lexical Environment를 확인하는 것을 스코프 체이닝 (Scope Chaining) 이라고 부른다.

원하는 것을 찾기 위해 Scope Chain을 타고타고 간다고 해서 이렇게 부르는 것 같다.


함수2_파라미터 값 결정

이제 나머지 과정은 모두 동일하게 진행된다.
함수2_파라미터의 값 또한 Running Execution Context에서 부터 확인을 시작한다.

함수2_파라미터Running Execution Context함수2 Execution Context에 있으므로 바로 사용하고, Outer Environment Reference를 타고 내려가지 않는다.


전역변수_1 값 결정

이제 우리는 궁금했던 전역변수_1의 값 결정 방법을 알 수 있다.
이전 변수들과 마찬가지로 Running Execution Context부터 확인하며 차례차레 Scope Chaining을 진행한다.

이렇듯 Global Execution Context에도 전역변수_1이라는 변수가 선언되어 있지만, Running Execution Context부터 내려가다가 전역변수_1을 찾으면 Global Execution Context까지 도달하지 못한다.


전역변수_2 값 결정


함수2_지역변수 값 결정


모든 값 결정 완료

지금 까지 알아본 과정을 거쳐 위 처럼 값이 결정된 것이었다.

결국 Global Execution Context에도 전역변수_1전역변수_2가 존재했지만, 그전에 값을 찾았기 때문에 사용되지 않았다.


정리

이렇듯 Execution ContextJavaScript의 동작원리를 이해하는 데 매우 중요한 지식이다.

호이스팅, 클로저 등을 그냥 외우는 것이 아닌, 먼저 Execution Context를 이해하고 공부하면 훨신 더 수월할 것이다.


  • Execution ContextJavaScript 코드가 실행되는 데 필요한 환경 제공
  • CallStack에는 Execution Context가 쌓이게 됨. 이 떄 가장 위에는 Running Execution Context가 됨.
  • Execution Context 하나하나가 Scope라고 생각할 수 있음.
  • JavaScript식별자 결정을 위해 Outer Environment Reference를 통하여 다른 Lexical Environment를 참조하는데, 이 과정을 Scope Chaining이라고 함.
  • 호이스팅, 클로저 공부하기전에 Execution Context 먼저 공부하자.

Reference

+ 읽어주셔서 감사합니다.
+ 오타, 내용 지적, 피드백을 환영합니다. 많이 해주실 수록 제 성장의 밑거름이 됩니다.
profile
반갑습니다. 프론트엔드 개발자 황주현 입니다. 🤗

2개의 댓글

comment-user-thumbnail
2024년 10월 19일

안녕하세요, 포스팅 잘 읽었습니다! 실행 컨텍스트에 대해 이해하기 쉽게 설명해주셔서 감사합니다.

한 가지 궁금한 점이 있어 질문드립니다.

포스팅에서 "그래서 실제로 어떻게 동작하는데요" 부분의 "함수1 코드 평가" 예시에서 나온 Execution Context 그림에 대해 문의드립니다.

function 함수1(함수1_파라미터 = '어떻게') {
  var 전역변수_1 = '엄준식';
  const 전역변수_2 = '이냐';
  
  function 함수2(함수2_파라미터) {
    ~~~
  }
  
  함수2('사람이름이');
}

제가 이해하기로, 함수1 스코프 안의 전역변수_1은 var로 선언되었으니 Variable Environment에 저장되지 않을까요?
각 변수가 저장되는 환경을 아래와 같이 생각했습니다.

함수1 Execution Context
├── Lexical Environment
│     ├── Environment Record
│         ├── Declarative Environment Record {전역변수_2, 함수2, arguments}
│
├── Variable Environment {전역변수_1}

Variable Environment를 함수 스코프에서 var 변수를 저장하는 환경으로 이해하고 있는데, 혹시 제가 잘못 이해하고 있는 부분이 있다면 조언 부탁드립니다.

감사합니다!

1개의 답글