JavaScript
를 공부하는 사람들이라면 한번 쯤 보게되는 단어. 실행 컨텍스트
다.
영어로는 Execution Context
라고 한다.
특이하게도 이 친구는 호이스팅
을 검색해도 나오고, 클로저
를 검색해도 나오며, 스코프
, 스코프체이닝
등 다방면을 알아볼 때 등장한다.
이건 무슨 뜻일까?
당연히 이는 실행 컨텍스트
가 굉장히 중요하다는 뜻이겠다.
이번에는 실행 컨텍스트
가 대체 뭔지, 그리고 실제 JavaScript
에서 어떻게 동작을 하는지 알아보고자 한다.
이 글을 모두 이해하면 우리는 아래의 것들을 얻을 수 있다.
- 실행 컨텍스트가 뭔지
- JavaScript에서 실행 컨텍스트가 어떻게 동작하는지
- Scope Chaining이 뭔지, 왜 발생하는지
- 기타 호이스팅, 클로저 등등을 나중에 공부할 때 훨신 수월함
아는 척 가능
자 그럼 이제 같이 알아보자!
실행 컨텍스트(이하 Execution Context)는 JavaScript 엔진이 코드를 실행하기 위해 필요한 여러가지 정보를 가지고 있고, 제공 하는 객체 이다.
시작부터 그런 사전같은 설명이라뇨.. 😕
당장의 이해가 어려울 것 같아 요리 를 예로 들어보았다.
예를 들어 카레를 만든다고 가정해보자.
어떤게 필요할까?
우선적으로 생각나는 건 카레를 만드는 재료들이다.
아마도 당근
, 양파
, 감자
, 고기
, 카레(강황)가루
, 우유
, 물
등이 있을 것 같다.
그럼 재료만 있으면 카레를 만들 수 있을까?
그렇지 않다.
재료 이외에도 재료를 손질할 수 있는 도구가 있어야 할 것 같다.
지금 필요한 도구로는 냄비
, 도마
, 칼
등이 있을 것 같다.
자 그럼 이제 진짜 카레를 만들 수 있을까?
아쉽게도 아직 아니다.
우리는 요리를 할 공간 도 필요하다.
그 공간 이란 집 부엌
이 될 수도 있고 음식점
이 될 수도 있겠다.
지금까지의 내용을 정리해보면 아래와 같다.
카레레시피
를 보고내
가요리
하기 위해서는재료
와도구
,공간
이 필요하다. (제공되어야 한다)
이걸 JavaScript
에 빗대어 말하면 아래와 같다.
내가 작성한 JavaScript Code
를 보고JavaScript Engine
이 실행하기 위해서는변수
,스코프
등의 정보가 필요하다. (제공되어야 한다)
즉 내(JavaScript Engine)
가 카레 레시피(코드)
를 보고 요리(실행)
할 수 있게 재료
, 도구
, 공간
을 제공해주는 것이 바로 Execution Context
라는 것이다.
조금은 이해가 더 되었을까?
그럼 지금 한번 함께 알아보도록 하자.
Execution Context
에 대해 알아보기 전에 먼저 소스코드 종류
부터 알아야 한다.
종류에 따라 생성되는 Execution Context
가 약간 다르기 때문이다.
본 게시글에서는 크게 Global Code(전역코드)
와 Function Code(함수코드)
로 나누도록 하겠다.
실제로는 이외에도 다른 종류가 있기 때문에 나중에 직접 공부해보자.
let 전역변수 = '안뇽';
전역에 존재하는 코드. 단, 전역에 정의되어 있는 함수나 클래스 등의 내부 코드는 포함되지 않는다.
function 음식하기(){
let 음식이름 = '카레';
}
함수 내부에 존재하는 코드. 단, 함수 내부에 중첩된 함수나 클래스 등의 내부 코드는 포함되지 않는다.
소스코드 종류
를 알았으니 이제 JavaScript Engine
이 어떻게 우리의 소스코드
를 실행시키는지 알아보자.
먼저 JavaScript Engine
은 소스코드 평가
, 소스코드 실행
두 가지의 단계로 나누어 우리의 코드를 실행해준다.
소스코드 평가
과정에서는 Execution Context
를 생성하고, 코드의 시작부터 끝까지 확인한다. 이 때 변수
, 함수
등의 선언문'만'을 먼저 실행하여 식별자를 Execution Context
에 저장해둔다.
예를 들어 아래와 같은 코드가 있다고 가정해보자.
var 음식재료;
음식재료 = '양파';
소스코드 평가 과정에서는 변수 선언문인 var 음식재료;
만 실행되어 Execution Context
에 선언된다.
소스코드 평가
가 완료되면 소스코드 실행
이 진행된다.
아까 그 코드를 다시 보자면 var 음식재료;
는 이미 실행되었기 때문에 다시 실행하지 않고 음식재료 = '양파';
만 실행한다.
이 때 JavaScript Engine
은 음식재료
라는 변수가 선언되어 있는지 여부를 Execution Context
에서 확인한다. (정확히는 음식재료
라는 식별자가 등록되어 있는지 확인)
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 Stack
과 Execution Context Stack
은 동일한 것을 다르게 부르는 것 뿐이다. 참고질문
따라서 JavaScript
의 Call Stack
은 실제로는 Execution Context
를 저장한다.
그리고 우리의 코드가 실행 될 때 이 Execution Context
에 저장 된 정보를 사용해서 실행한다.
이 때 Stack
의 가장 위에 있는 Execution Context
를 Running Execution Context
라고 칭한다.
그럼 이제 Execution Context
에 대해 더 자세히 알아보자.
본 게시물에서는
Call Stack
으로 용어를 통일한다.
executionContext = {
lexicalEnvironment: {...},
variableEnvironment: {...},
}
Execution Context
에는 LexicalEnvironment
, VariableEnvironment
가 존재한다. 각각 하나씩 살펴보자.
혹시 방금 뒤로가기 버튼을 누르려고 했다면 조금만 더 읽어보자.
복잡해보여도 생각보다 간단한 내용은 아니지만 읽어 볼 만 할 것이다.
좀 더 우리가 아는 친숙한 트리 구조로 표현하자면 위와 같은 구조이다.
그럼 Environment Record
부터 알아보자.
여기서는 변수
와 함수
의 식별자(Identifier)
를 등록하고, 해당 식별자에 바인딩 된 값을 관리한다.
쉽게 말해서 아래와 같은 코드의 변수들을 저장해두고 필요할 때 꺼내쓰는 곳이다.
let 변수당 = '안녕하세용';
const 안뇽 = '하세용';
여기서 Environment Record
는 다시 Object Environment Record
와 Declarative Environment Record
로 나눠진다.
const 변수 = 'hello';
let 변수둘째 = 5;
var 안녕 = 'haseyo';
변수
, 함수
등의 Identifier
와 값
을 저장할 때 사용한다.
기본적으로 이 Record
가 사용된다.
var window = {
scrollHeight:100,
scrollWidth:500,
};
record
를 객체 형식으로 저장할 때 사용한다.
전역 객체를 선언할 때 사용된다.
다만 객체 특성 상 불변성이 깨질 수 있어 전역객체 이외에 사용을 지양된다.
withStatement
문에서도 사용된다고 한다.
this = '이것';
우리가 아는 그 this
의 값이다.
어디에서 어떻게 호출되느냐에 따라 값이 다르다.
this
에 관한 자세한 내용은 이 게시글에서 다루지 않는다.
❗ 중요한 내용, 아래 다시 나올예정
Outer Environment Reference
를 직역하자면 외부 환경 참조
로, 이름이 상당히 직관적이다.
이는 자신이 아닌 외부의 Lexical Environment
를 참조 할 때 (그 값을 사용할 때) 사용한다.
Scope Chaining
, Closer
를 이해하는 데 필수적인 내용이다.
내용이 길고 중요하기 때문에 조금 아래에서 본격적으로 설명하도록 하겠다.
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 Stack
에 push
해 준다.
var 전역변수_1;
const 전역변수_2;
function 함수1;
예제 코드에서 전역 코드는 전역변수_1, 전역변수_2, 함수1
이다.
JavaScript Engine
은 Global Execution Conext
를 생성 하고 그 내부에 아래 사진과 같이Lexical Environment
를 설정한다.
오잉? 우리가 할당 한 값들이 들어있지 않아요 🙄
현재 var
, const
, function
3개의 방식으로 선언하였는데, 각각 다른 값이 보여진다. (초기화안됨
, undefined
, Function
)
이는 각 선언 방식의 특징과 변수가 선언되는 방식 때문인데, 이곳에서 설명하기엔 너무 많으니 생략하고 진행한다. (검색 키워드 : "자바스크립트 선언 초기화 할당")
이제 전역 코드를 실행해준다.
비로소 모든 변수
들에 올바른 값이 할당 된 모습이다.
잠깐!
Outer Environment Reference
는 왜null
일까?
JavaScript
는 Lexical Scope
를 사용한다.
이는 함수가 코드 상에서 선언
되었을 때를 기준으로 Scope
가 결정된다는 의미이다.
Outer Environment Reference
는 나보다 상위의 Execution Context
를 참조하게 되는데, Global Context
는 이미 최상위에 위치하고 있다.
즉 Global Context
의 상위에는 아무것도 존재하지 때문에 null
이 들어가게 된다.
다음 함수1()
을 분석해보며 더 자세히 알아보자.
function 함수1 (함수1_파라미터='어떻게') {
var 전역변수_1 = '엄준식';
const 전역변수_2 = '이냐';
function 함수2 (함수2_파라미터) {
~~~
}
함수2('사람이름이');
}
전역 코드와 마찬가지로 함수1()
또한 코드 평가
와 코드 실행
으로 나누어 진행된다.
또한 Call Stack
에는 함수1()
의 Execution Context
를 생성해 넣어준다.
여기서 this
의 값은 전역객체
를 참조한다.
Outer Environment Reference
는 선언 된 코드 상에서 상위 Execution Context
를 참조한다.
여기서는 Global
에 함수()
이 선언되어 있었으므로 Global Execution Context
가 된다.
Function Environment Record
(환경레코드)는 arguments
가 추가된 Record
이다.
해당 record
에 전달 받은 argument
가 추가로 저장된다.
전역 코드 실행 때와 마찬가지로, 함수1의 코드가 실행되면 실제 값들이 변수
에 할당된다.
function 함수2 (함수2_파라미터='사람이름이') {
const 함수2_지역변수 = 'ㅋㅋ';
console.log(함수1_파라미터, 함수2_파라미터, 전역변수_1, 전역변수_2, 함수2_지역변수);
}
이제 마지막으로 함수2의 코드 평가가 진행된다.
동일하게 Execution Context
를 생성해서 Call Stack
에 넣어준다.
const
로 선언한 함수2_지역변수
는 아직 초기화되지 않았고,
함수1
에서 파라미터로 전달받은 '사람이름이'
는 정상적으로 arguments
에 들어가 있다.
Outer Environment Reference
는 함수1
의 Execution Context
를 참조하고 있다.
const 함수2_지역변수 = 'ㅋㅋ';
console.log(함수1_파라미터, 함수2_파라미터, 전역변수_1, 전역변수_2, 함수2_지역변수);
자 이제 함수2_지역변수
에도 값이 잘 할당되었다.
위에서 보았듯이 console
에 찍힌 실행결과는 아래와 같다.
실행 결과 : 어떻게 사람이름이 엄준식 이냐 ㅋㅋ
자 이제 우리가 위에서 궁금해했던 JavaScript는 어떻게 같은 이름의 변수 중 하나를 선택하는가 에 대한 비밀을 해결 할 차례이다.
이를 유식한 말로 식별자 결정
이라고 한다.
우선 지금까지 있었던 CallStack
을 조금 단순화 하여 표현해보겠다.
모든 값의 결정은 Running Execution Context
부터 시작된다.
JavaScript
는 먼저 Running Execution Context
에서 함수1_파라미터
라는 식별자가 등록되어 있는지 확인한다.
하지만 함수2
의 Execution Context
에는 함수1_파라미터
라는 식별자가 등록되어 있지 않았다.
이 때 JavaScript
는 Outer Environment Reference
를 타고가서 함수1_파라미터
를 찾거나 끝
이 보일 때 까지 탐색을 시작한다.
이런 과정을 거쳐서 함수1_파라미터
값을 찾아냈다.
이렇게 Outer Environment Reference
를 통해 다른 Lexical Environment
를 확인하는 것을 스코프 체이닝 (Scope Chaining)
이라고 부른다.
원하는 것을 찾기 위해 Scope Chain
을 타고타고 간다고 해서 이렇게 부르는 것 같다.
이제 나머지 과정은 모두 동일하게 진행된다.
함수2_파라미터
의 값 또한 Running Execution Context
에서 부터 확인을 시작한다.
함수2_파라미터
는 Running Execution Context
인 함수2 Execution Context
에 있으므로 바로 사용하고, Outer Environment Reference
를 타고 내려가지 않는다.
이제 우리는 궁금했던 전역변수_1
의 값 결정 방법을 알 수 있다.
이전 변수들과 마찬가지로 Running Execution Context
부터 확인하며 차례차레 Scope Chaining
을 진행한다.
이렇듯 Global Execution Context
에도 전역변수_1
이라는 변수가 선언되어 있지만, Running Execution Context
부터 내려가다가 전역변수_1
을 찾으면 Global Execution Context
까지 도달하지 못한다.
지금 까지 알아본 과정을 거쳐 위 처럼 값이 결정된 것이었다.
결국 Global Execution Context
에도 전역변수_1
과 전역변수_2
가 존재했지만, 그전에 값을 찾았기 때문에 사용되지 않았다.
이렇듯 Execution Context
는 JavaScript
의 동작원리를 이해하는 데 매우 중요한 지식이다.
호이스팅
, 클로저
등을 그냥 외우는 것이 아닌, 먼저 Execution Context
를 이해하고 공부하면 훨신 더 수월할 것이다.
Execution Context
는 JavaScript
코드가 실행되는 데 필요한 환경 제공CallStack
에는 Execution Context
가 쌓이게 됨. 이 떄 가장 위에는 Running Execution Context
가 됨. Execution Context
하나하나가 Scope
라고 생각할 수 있음.JavaScript
는 식별자 결정
을 위해 Outer Environment Reference
를 통하여 다른 Lexical Environment
를 참조하는데, 이 과정을 Scope Chaining
이라고 함.호이스팅
, 클로저
공부하기전에 Execution Context
먼저 공부하자.Reference
+ 읽어주셔서 감사합니다.
+ 오타, 내용 지적, 피드백을 환영합니다. 많이 해주실 수록 제 성장의 밑거름이 됩니다.
안녕하세요, 포스팅 잘 읽었습니다! 실행 컨텍스트에 대해 이해하기 쉽게 설명해주셔서 감사합니다.
한 가지 궁금한 점이 있어 질문드립니다.
포스팅에서 "그래서 실제로 어떻게 동작하는데요" 부분의 "함수1 코드 평가" 예시에서 나온 Execution Context 그림에 대해 문의드립니다.
제가 이해하기로, 함수1 스코프 안의 전역변수_1은 var로 선언되었으니 Variable Environment에 저장되지 않을까요?
각 변수가 저장되는 환경을 아래와 같이 생각했습니다.
Variable Environment를 함수 스코프에서 var 변수를 저장하는 환경으로 이해하고 있는데, 혹시 제가 잘못 이해하고 있는 부분이 있다면 조언 부탁드립니다.
감사합니다!