본문 바로가기
언어/자바스크립트

실행 컨텍스트(렉시컬 스코프, 렉시컬 환경, 스코프 체인, 호이스팅) 이 글로 끝내버리기

by SeungYn 2023. 5. 13.

이 글은 실행 컨텍스트와 렉시컬 스코프, 렉시컬 환경, 호이스팅, 스코프 체인을 이야기 진행 방식으로 작성하였으니 천천히 커피 한잔 마시면서 이야기를 들어주시면 감사하겠습니다.

실행 컨텍스트

실행 컨텍스트는 식별자를 등록하고 관리하는 스코프와 실행 순서 관리를 구현한 내부 메커니즘으로, 모든 코드는 실행 컨텍스트를 통해 실행되고 관리된다.

흠.. 말은 어렵지만 한번 풀어보면 코드를 실행하는데 재료를 준비해 주고 실행 순서를 관리해 주는 것 같네요.

이 실행 컨텍스트를 이해하면 호이스팅, 렉시컬 환경, 스코프 체인을 알게 됩니다.

자바스크립트에는 전역, 함수, eval, 모듈로 4개의 소스 코드 타입이 있습니다.

저는 여기서 전역, 함수 소스 코드로 이야기를 풀어 볼 건데, 일단 전역 소스 코드와 함수 소스 코드의 차이는 전역과 지역의 차이로 보고 나중에 딥하게 들어가 볼게요.

시작해 봅시다.

 

자바스크립트는 소스 코드를 실행하기 전에 평가 과정을 거칩니다. 소스코드의 평가 과정에는 실행 컨텍스트를 생성하고 변수, 함수의 선언만 먼저 실행하여 컨텍스트에 등록을 합니다. 그리고 이 평가 과정이 끝나면 자바스크립트는 순차적으로 코드를 실행하게 됩니다.

 

여기 ”변수,함수의 선언만 먼저 실행하여 컨텍스트에 등록을 합니다.” 이 부분이 중요한데요. (변수 선언이란 말은 var a = 1 이 부분에서 var a 요 부분을 말하는 것이고 함수 선언이란 말은 function test(){} 이런 식으로 작성한 함수 선언을 말합니다.)

위에 언급한 문장이 바로 호이스팅이 되는 이유입니다. 첫 번째 산 호이스팅 산을 넘어가 봅시다.

 

호이스팅

호이스팅은 이렇게 알고 계실 텐데요. “함수를 호출 부분 밑에서 작성해도 위로 끌어당겨져서 실행할 수 있도록 하는 메커니즘” 사실 제가 이렇게 알고 있었어요 ㅋㅋ.. 호이스팅이란 산을 부수어버립시다. 예시를 보여드릴게요.

밑에 작성한 코드에서 과연_내가_실행이_될까요라는 함수는 과연 실행이 될까요?

 

과연_내가_실행이_될까요();

function 과연_내가_실행이_될까요() {
  console.log('두근두근');
}

정답은 됩니다.

위 코드 실행 결과

 

당연하게 보이실 수도 있지만 여러분들은 여기서 의문점을 가져야 해요.

왜냐하면 자바스크립트는 한 줄 한줄 코드를 해석하면서 실행시키기 때문이죠. (자바처럼 코드를 한꺼번에 해석해서 실행 시키는 것이 아니라)

자바스크립트는 한줄 한줄 해석하며 실행시키니깐 당연히 에러가 나야 정상입니다. 하지만 이게 가능한 이유가 호이스팅 때문이에요! 아까 호이스팅은 뭐라 그랬죠? 함수를 호출 밑에서 작성해도 실행할 수 있도록 하는 메커니즘이라고 했는데 이걸 전문적으로 설명을 하면 소스코드는 실행되기 전에 평가과정이 일어나는데 이때 실행 컨텍스트를 생성해요.

이 실행 컨텍스트는 코드를 실행 시킬 재료들을 준비 시키는데 이 재료 준비 과정에서 호이스팅이 일어납니다. 이 실행 컨텍스트는 변수, 함수의 선언 부분을 자신한테 등록합니다. 이 등록은 실행 컨텍스트의 렉시컬 환경의 환경 레코드 라는 곳에서 등록을 해요. 일단 렉시컬 환경, 환경 레코드는 밑에서 설명하고 실행 컨텍스트에 등록한다고 알아 두시면 됩니다.

실행 컨텍스트의 변수나 함수의 선언 부분을 등록하기 때문에 우리는 함수를 작성한 부분 위에서도 호출할 수 있던 거예요!

우리는 지금 호이스팅의 50% 정도를 부숴버렸습니다. (짝짝) 이제 나머지 50% 부시러 가봅시다.

지금까지 함수로만 예시를 들었어요. 이제 변수도 봐 볼까요?

아래 예제를 우리가 앞에서 배웠던 호이스팅을 적용하면

 

나는 var!
나는 let!
나는 const

가 출력 돼야 될 거예요. 한번 봐볼까요?

아래는 출력 결과예요. 역시 현실은 다르네요 ㅠㅠ

 

출력된 결과를 보니 undefined에 에러에 대환장 파티네요.

걱정 마세요. 호이스팅 개념은 틀린 게 아니에요. let, const, var 특성 때문이니 헤쳐나가 봅시다.

 

아까 함수 호이스팅 예제를 봐볼게요.

아래 예제에서 자바스크립트는 코드를 실행하면 실행 키기 전에 재료를 준비 한다했죠(평가 과정)? 그 재료를 등록한다는 것은 실행 컨텍스트의 변수, 함수의 선언 부분을 등록한다라고도 위에서 설명했었어요.

 

그럼 위의 예제에서 자바스크립트는 코드를 실행하기 전 전역 소스코드의 실행컨텍스트에 아래 표처럼 등록이 됩니다. 함수의 식별자를 키로 값을 등록하는데요. 함수는 함수 내용이 바로 등록됩니다. 이런 이유 떄문에 함수는 바로 호출이 가능한거에요! 이제 변수 부분을 볼게요.

 

전역 실행 컨텍스트
과연_내가_실행이_될까요 : <function object>

여기서 잠깐!

정확히는 렉시컬 환경의 환경 레코드에 등록이 되는데 전역 컨텍스트의 렉시컬 환경글로벌 렉시컬 환경이라 부르고 환경 레코드글로벌 환경 레코드라 이 글로벌 환경 레코드의 전역 환경 객체에 var, 함수 선언문으로 작성한 함수를 등록하고, let, const 키워드로 작성된 변수를 선언적 환경 레코드에 등록을 합니다. 지금은 이렇구나 알아두고 컨텍스트의 등록과정에 집중합시다.

 

아까 봤던 변수 삼총사(var, let, const) 예제를 봅시다. 이 삼총사들은 전역 컨텍스트에 어떻게 등록이 되냐면

아래 표처럼 등록이 됩니다! 이것은 var, let, const 키워드의 특성이기 때문이에요. var 키워드는 undefined,

letconst 키워드는 값을 할당하기 전에 <uninitialized>로 등록되기 때문입니다. var는 선언과 동시에 undefined 값이 할당이 되지만, let,const는 선언을 하면 uninitialized로 등록이 돼서 값을 할당하기 전에 접근하려 하면 에러가 출력됩니다! 이러한 이유 때문에 let, const는 에러가 발생하며 이것을 TDZ(일시적 사각지대)라 불러요!

(이 부분은 나중에 따로 자세히 다루겠습니다.)

전역 실행 컨텍스트
var변수: undefined
let변수: <unintialized>
const변수: <unintialized>
 

그래서 처음 실행 전 실행컨텍스트에 등록을 하고 코드가 실행되면 값이 할당되는 부분을 만나면 실행 컨텍스트에는 아래 표처럼 등록이 되는거에요. 위의 사진에서 코드가 실행하다 var var변수 = '나는 var!'; 이 부분을 만나면 실행 컨텍스트에 등록된 var변수는 아래 표처럼 var변수: ‘나는 var!’ 이렇게 등록이 됩니다.

 

전역 실행 컨텍스트
var변수: ‘나는 var!’
let변수: <unintialized>
const변수: <unintialized>
 

나머지 부분을 자바스크립트가 실행시키면 최종적으로는 아래 표처럼 값이 다 등록이 돼요!

 

전역 실행 컨텍스트
var변수: ‘나는 var!’
let변수: ‘나는 let!’
const변수: ‘나는 const’
 

어때요? 호이스팅을 부셔버렸습니다. (짝짝) 이제 아까 언급한 렉시컬 환경에 대해 봐 볼까요?

 

렉시컬 환경

실행 컨텍스트식별자를 등록하는데 렉시컬 환경환경 레코드에 등록한다고 했습니다.

그 중 우리는 전역 렉시컬 환경을 봤어요! 함수를 실행하면 함수 함수 렉시컬 환경이라 부릅니다. 둘의 차이가 조금 있어요. 전역 렉시컬 환경var, 선언식으로 작성한 함수를 객체 환경 레코드에 저장하고 const, let선언적 환경 레코드에 저장되지만 함수 렉시컬 환경에서는 몽땅 함수 환경 레코드에 저장되어요. 이 부분은 가볍게 읽어주시고 나중에 찾아 보시면 될 것 같아요. 일단 그렇구나 하고 넘어갑시다!

다시 본론으로 돌아옵시다.

 

렉시컬 환경식별자들이 저장되었는데 도대체 이 렉시컬 환경이 뭘까요? 두 번째 산 렉시컬 환경을 부셔버립시다.

그전에 동적 스코프, 정적 스코프를 알고 넘어가야 됩니다.

 

여기서 잠깐!

상위 스코프가 결정된다고 했는데 내가 필요한 재료를 찾을 바깥 렉시컬 환경이라 생각해봅시다. 간단한 예시를 보면

만약 2층 짜리 집이 있는데 1층은 부엌 2층은 음식 저장소라 해볼게요. 만약 1층에서 요리를 하다 레몬이 급하게 필요해졌어요. 근데 아쉽게도 1층 냉장고에 없네요? 그럼 레몬을 어디서 구해야 할까요? (집 밖으로는 나갈 수 없다고 가정합시다.) 2층 음식 저장소에서 구해야겠죠? 이 2층 음식 저장소를 상위 스코프라고 억지로 생각해봅시다 ㅋㅋ.. 다시 볼론으로 가죠...

 

동적 스코프란? 함수가 실행될 때 해당 함수의 상위 스코프가 결정 돼요.

만약 아래 예시 코드를 실행시키면 결과는 2가 나와야 합니다. 동적 스코프인 경우에 말이죠

위에 했던 레몬 예시처럼 들어보면, 레몬을 찾을려 하는데 음식 저장소는 어떤 음식 저장소인지 알 수가 없는거에요. 직접 올라가지 않는 이상. (음식 저장소가 랜덤으로 바뀐다고 생각하고 넘어가죠.)

동적 스코프인 경우

정적 스코프란? 함수가 실행되기 전에 해당 함수의 상위 스코프가 결정 돼요! 실행되기 전에 상위 스코프가 결정되므로 아래 결과는 3이 나옵니다. 자바스크립트는 정적 스코프이기 때문에 3이 나오는 거예요. 

즉 음식 저장소는 이 2층 건물이 지어질 때 만들어진 거에요. 랜덤으로 바뀌는게 아니라!

정적 스코프인 경우

 

이 정적 스코프를 다른 말로는 렉시컬 스코프라 부르며 자바스크립트는 렉시컬 스코프를 따르기 때문에 코드 평가 과정에서 상위 스코프를 결정해 등록을 해주는 겁니다! 이런 상위 스코프가 결정 되는 것을 스코프 체인이라고도 말합니다.

 

이제 다시 렉시컬 환경으로 돌아와 볼게요. 이 글 초반 부분에 실행 컨텍스트“식별자를 등록하고 관리하는 스코프와 실행 순서 관리를 구현한 내부 메커니즘” 이라고 설명했습니다. 실행 컨텍스트식별자를 등록하는데 이 등록을 실행 컨텍스트렉시컬 환경에 등록된다고 말했었어요. 그리고 이 과정에서 호이스팅이 일어나는 이유를 알게 됐고요! 이 렉시컬 환경은 이 식별자들 관리 말고도 자신의 상위 스코프도 등록을 해요. 아주 엄청난 녀석이죠?

 

위에서 자바스크립트는 렉시컬 스코프(정적 스코프) 따른다 했죠? 렉시컬 스코프란 자신의 상위 함수를 함수가 호출 되는 시점에 정하는게 아니라 함수가 작성된 곳에 따라 상위 스코프를 결정하는 것 입니다. 이 상위 스코프를 저장하는 곳이 렉시컬 환경OuterLexicalEnvironmentReference 곳이에요. 애매하죠? 한번 예제로 볼게요!

(앞으로는 OuterLexicalEnvironmentReference 를 편의상 외부 렉시컬 환경참조라고 부르겠습니다)

 

 

아래 예제를 보면 test() 를 호출하면 xx 가 출력이 됩니다. 너무 당연한가요? 근데 너무 당연한데 이게 어떻게 일어나는 걸까요? test 라는 함수안에는 x 라는 변수는 선언이 안돼있는데 알아서 상위 스코프에 있는 x 를 가져다가 출력하네요.

 

이 마법이 가능한 이유가 무엇일까요? 그 이유는 test함수가 실행 되기전 실행 컨텍스트함수 렉시컬 환경외부 렉시컬 환경 참조에 자신의 렉시컬 스코프를 저장해 두었기 때문이에요! 이해를 위해 흐름을 살펴봅시다.

 

위의 코드가 실행되기전 자바스크립트는 전역 실행 컨텍스트가 생성되고 전역 실행 컨텍스트 안에는 전역 렉시컬 환경이 구성돼요. 맞죠? (다시 말하지만 전역 렉시컬 환경객체 환경 레코드, 선언적 환경 레코드로 구성 되어 있지만 이해를 위해 일단 통틀어서 전역 렉시컬 환경에 저장 되는 것으로 설명하겠습니다.)

 

렉시컬 환경 흐름

 

1. 전역 코드가 평가가 되면 전역 실행 컨텍스트의 전역 렉시컬 환경은 아래 표처럼 구성됩니다. 이제 OuterLexicalEnvironmentReference 라는 키가 추가 되었네요! (전역 렉시컬 환경에서 외부 렉시컬 환경은 null 입니다. 최상단에 있기 떄문에 참조할 렉시컬 환경이 없어요)

전역 렉시컬 환경
x: <unintialized>
test: <function object>
OuterLexicalEnvironmentReference: null
 

2. 이제 자바스크립트가 const x = 'xx' 를 만나면 전역 렉시컬 환경은 아래 표처럼 바뀝니다. 앞에서 해봤죠?

전역 렉시컬 환경
x: xx
test: <function object>
OuterLexicalEnvironmentReference: null
 

3. 자바스크립트가 test() 를 실행시키기 전에 이번에는 함수 실행 컨텍스트를 생성해요. 함수 실행 컨텍스트를 생성한다는 말은 함수 렉시컬 환경도 생성하는 거겠죠? 그러면 이제 함수 렉시컬 환경이 생성됩니다. 여기서 외부 렉시컬 환경전역 렉시컬 환경을 가리키게 돼요! 렉시컬 스코프란 함수가 정의된 곳의 함수 스코프이기 때문에 test 함수의 외부 렉시컬 환경은 전역 렉시컬 환경을 가리키게 되죠. 맞죠?

 

함수(test) 렉시컬 환경
arguments: {}
OuterLexicalEnvironmentReference: 전역 렉시컬 환경
 

 

test 함수의 외부 렉시컬 환경참조는 아래 그림처럼 가르키게 됩니다. 다시 말하지만 이런 외부 렉시컬 환경 참조값이 상위 스코프로 정해지는 것을 정적 스코프(렉시컬 스코프)라고도 부릅니다.

 

4. 함수 실행 컨텍스트함수 렉시컬 환경이 생성되면 자바스크립트는 test 함수 안에있는 코드를 실행 시킵니다. 하지만 아쉽게도 test 함수 안에는 x 라는 변수가 없어요. 그럼 이 변수를 찾기 위해 자신의 외부 렉시컬 환경전역 렉시컬 환경에서 찾게 됩니다. 이런 이유 때문에 아래 코드 결과 처럼 xx 라는 값이 출력 되는 거에요!

 

우리는 지금 자신의 영역(스코프)안에서 선언한 변수도 아닌데 접근할 수 있던 마법의 원리를 알게 됐어요! 

 

 

실행 컨텍스트 스택(콜 스택)

우리는 지금까지 실행 컨텍스트의 정의(식별자를 등록하고 관리하는 스코프와 실행 순서 관리를 구현한 내부 메커니즘) 부분에서 식별자 등록과 스코프를 알아 보았습니다. 이제 마지막인 실행 순서 관리에 대해 알아봅시다.

 

우리는 아래 사진에 나와있는 코드에서 test 함수가 실행 되고 어떻게 x 변수를 찾고 실행하는 지를 배웠습니다.

그리고 test 함수가 실행이 끝나면 자바스크립트 엔진은 종료가 되죠. 이 당연한 흐름을 실행 컨텍스트 스택(앞으로 콜 스택이라 부르겠습니다.)이 관리를 해줍니다.

아래 예시 코드를 보겠습니다. 아래 코드를 보면 나는_첫_번째_함수 라는 코드를 실행 시킨다음 나는_두_번째_함수 라는 함수를 실행 시킨 후 전역 코드의 나머지 부분을 실행시킨다음 끝이 나겠네요. 이런 코드의 흐름을 관리해주는 것이 콜 스택입니다. 실행과정을 자세히 봐볼게요.

 

콜 스택 예시

콜 스택 실행과정

  1. 자바스크립트는 콜 스택을 생성합니다. 아래 사진은 비어있는 콜 스택이예요.

1단계 사진

2. 전역 코드가 평가되고 실행되면 콜 스택에 전역 실행 컨텍스트를 넣고 실행시킵니다.

2단계 사진

 

3. 자바스크립트 엔진은 나는_첫_번째_함수() 라는 코드를 만나면 나는_첫_번째_함수 실행 컨텍스트를 생성 후 실행 시킵니다. 그렇다는 말은 콜 스택에 나는_첫_번째_함수 라는 실행 컨텍스트를 쌓고 실행시킨다는 거죠?

3단계 사진

 

4. 나는_첫_번째_함수 라는 함수를 실행 시킨 후 나는_두_번째_함수() 라는 코드를 만나면 실행 컨텍스트를 쌓은 후 실행 시킵니다.

4단계 사진

5. 자바스크립트 엔진은 나는_두_번째_함수 라는 함수의 실행이 끝나면 콜 스택에서 pop 을 하여 제거하게 됩니다. 그러면 자바스크립트 엔진은 나는_첫_번째_함수 로 돌아와서 나머지 부분을 실행시키게 됩니다.

5단계 사진

 

6. 이제 또 나는_첫_번째_함수 실행이 끝나면 콜 스택에서 pop 이 되어 나는_첫_번째_함수 는 콜 스택에서 제거되고 자바스크립트 엔진은 전역 코드로 복귀합니다.

6단계 사진

 

7. 이제 전역 코드의 나머지 부분을 실행시키고 끝이 나면 전역 실행 컨텍스트로 pop 이 발생하여 제거된 후 종료가 됩니다.

 

실행 컨텍스트 스택(콜 스택)의 실행과정을 보았는데요. 이제 우리는 자바스크립트가 실행 컨텍스트 스택(콜 스택)을 이용하여 함수의 실행 순서를 관리해 주는 것을 알게 되었습니다.! (짝짝)

 

자바스크립트 실행 컨텍스트는 대단하지 않나요? (자바를 했으면 계속 자바를 해야겠다는 것도 느끼게 되었습니다.)는 장난입니다. 하하

고생하셨습니다. 클로저는 다음글에서 찾아뵙겠습니다.

 

 

'언어 > 자바스크립트' 카테고리의 다른 글

클로저(Closures)  (1) 2024.02.21
BigInt 큰 숫자 다루기  (0) 2023.04.12
자바스크립트 This 이걸로 끝내기  (0) 2023.03.28
Iteration Protocol(for of, spread)  (0) 2023.03.21
이벤트 캡처링과 버블링  (0) 2023.03.06