Published on

JavaScript는 코드를 어떻게 실행할까?

Authors
  • avatar
    Name
    GW

개요

JavaScript를 공부하다 보면 한 번쯤 이런 경험이 있습니다.

console.log(name);

const name = '철수';

분명 name은 아래에서 선언했는데 왜 에러가 발생할까요?

또 다음 코드는 어떨까요?

const sayHello = () => {
  console.log(name);
};

const name = '철수';

sayHello();

이번에는 정상적으로 철수가 출력됩니다.

둘 다 name을 사용했는데 왜 결과가 다를까요?

이 질문은 호이스팅, 스코프, 클로저를 공부하다 보면 계속 등장합니다.

그리고 이 모든 질문의 공통된 답은 바로 실행 컨텍스트(Execution Context) 입니다.

JavaScript는 코드를 한 줄씩 무작정 실행하지 않습니다.

코드를 실행하기 전에 실행에 필요한 정보를 미리 준비하고, 그 이후에 실제 코드를 실행합니다.

이번 글에서는

  • JavaScript가 코드를 어떤 순서로 실행하는지
  • 실행 컨텍스트는 무엇이고 언제 생성되는지
  • 실행 컨텍스트에는 무엇이 들어 있고, 어떻게 실행되는지

함께 살펴보겠습니다.

실행 컨텍스트란 무엇일까?

실행 컨텍스트는 JavaScript가 코드를 실행하기 위해 만드는 실행 환경입니다.

조금 쉽게 표현하면

현재 실행 중인 코드에 필요한 정보를 저장하는 공간

이라고 생각하면 됩니다.

예를 들어 다음 코드를 실행한다고 해보겠습니다.

const name = '철수';

const sayHello = () => {
  const age = 20;

  console.log(name, age);
};

sayHello();

JavaScript는 이 코드를 실행하기 전에

  • 어떤 변수가 있는지
  • 어떤 함수가 있는지
  • 현재 this는 무엇인지
  • 현재 스코프는 어떻게 되는지

등의 정보를 먼저 준비합니다.

즉, 실행 컨텍스트는 현재 코드 실행에 필요한 정보를 저장하는 역할을 하는 것입니다.

실행 컨텍스트는 언제 생성될까?

실행 컨텍스트는 크게 두 가지 경우에 생성됩니다.

1. 전역 코드가 실행될 때

프로그램이 시작되면 가장 먼저 전역 실행 컨텍스트(Global Execution Context) 가 생성됩니다.

예를 들어

const name = '철수';
const age = 20;

를 실행하면 전역 실행 컨텍스트가 먼저 만들어집니다.

그리고 이 전역 실행 컨텍스트 안에서 전역 변수와 함수들이 관리됩니다.

2. 함수가 호출될 때

함수가 호출되면 새로운 실행 컨텍스트가 생성됩니다.

const sayHello = () => {
  console.log('안녕하세요');
};

sayHello();

sayHello()를 호출하는 순간 함수 실행 컨텍스트(Function Execution Context) 가 새롭게 생성됩니다.

즉, 함수마다 자신의 실행 환경을 하나씩 가지게 되는 것입니다.

그래서 함수 안에서 선언한 변수는 다른 함수에서 접근할 수 없습니다.

실행 컨텍스트에는 무엇이 들어 있을까?

그렇다면 실행 컨텍스트 안에는 무엇이 저장될까요?

대표적으로 다음과 같은 정보가 들어 있습니다.

실행 컨텍스트 구조

렉시컬 환경 (Lexical Environment)

현재 스코프의 변수와 함수 정보를 저장합니다.

예를 들어

const name = '철수';
const age = 20;

라면

렉시컬 환경

name → '철수'
age → 20

처럼 현재 스코프에서 사용할 수 있는 식별자들이 저장됩니다.

변수 환경 (Variable Environment)

실행 컨텍스트에는 변수 환경도 포함됩니다.

변수 환경은 var로 선언된 변수와 함수 선언문의 바인딩 정보를 저장하는 실행 컨텍스트의 내부 구성 요소입니다.

주로 var로 선언한 변수와 함수 선언문이 이곳과 관련됩니다.

예를 들어

var count = 10;

function hello() {}

라면 실행 컨텍스트가 생성되는 과정에서 이러한 정보들이 먼저 준비됩니다.

실행 컨텍스트가 생성되는 시점에는 변수 환경과 렉시컬 환경이 같은 정보를 가리키지만, 이후 블록{}에 진입할 때마다 렉시컬 환경만 새로 생성됩니다.

이 차이 때문에 var는 블록 스코프의 영향을 받지 않고, let과 const는 블록마다 새로운 스코프를 갖게 됩니다.

대부분의 상황에서는 렉시컬 환경만 이해해도 충분하며, 변수 환경은 주로 var의 호이스팅이나 블록 스코프와 관련된 동작을 설명할 때 등장하는 개념이라고 생각하면 됩니다.

this

실행 컨텍스트는 현재 코드에서 사용할 this도 함께 관리합니다.

예를 들어

person.sayHello()와 sayHello()는 같은 함수라도 this가 달라질 수 있습니다.

실행 컨텍스트는 현재 함수가 어떤 방식으로 호출되었는지에 따라 적절한 this를 결정합니다.

그래서 this는 함수를 선언할 때가 아니라 호출할 때 결정된다고 설명하는 것입니다.

기타 실행 정보

실행 컨텍스트는 변수나 this 외에도 코드를 실행하기 위한 다양한 내부 정보를 함께 관리합니다.

예를 들어 현재 코드가 어디까지 실행되었는지, 어떤 실행 컨텍스트에서 실행 중인지 등의 정보가 포함됩니다.

이러한 정보들은 JavaScript 엔진이 함수 호출과 종료를 관리하고, 실행을 이어 나가는 데 사용됩니다.

다만 이러한 내부 정보는 JavaScript 코드에서 직접 접근하거나 사용할 수는 없으며, 엔진 내부에서만 사용됩니다.

실행 컨텍스트는 어떻게 실행될까?

실행 컨텍스트는 단순히 만들어지기만 하는 것이 아닙니다.

JavaScript 엔진은 실행 컨텍스트를 콜 스택(Call Stack, 호출 스택) 이라는 공간에서 관리하며 코드를 실행합니다.

예를 들어 다음 코드를 살펴보겠습니다.

const first = () => {
  second();
};

const second = () => {
  console.log('Hello');
};

first();

코드가 실행되는 과정을 순서대로 살펴보겠습니다.

1. 전역 실행 컨텍스트 생성

프로그램이 시작되면 가장 먼저 전역 실행 컨텍스트가 생성됩니다.

┌─────────────────────────┐
│ 전역 실행 컨텍스트           │
└─────────────────────────┘
        콜 스택

이 시점에는 아직 어떤 함수도 실행되지 않았습니다.

전역 코드만 실행되고 있는 상태입니다.

2. first() 호출

다음으로 first()가 호출됩니다.

그러면 JavaScript는 first()를 실행하기 위한 새로운 실행 컨텍스트를 생성하고 콜 스택에 추가(Push)합니다.

┌─────────────────────────┐
│ first() 실행 컨텍스트       │
├─────────────────────────┤
│ 전역 실행 컨텍스트           │
└─────────────────────────┘
        콜 스택

콜 스택은 가장 마지막에 들어온 실행 컨텍스트부터 실행합니다.

즉, 현재는 first()가 실행되고 있습니다.

3. second() 호출

first() 안에서는 다시 second()를 호출합니다.

그러면 또 하나의 실행 컨텍스트가 생성됩니다.

┌─────────────────────────┐
│ second() 실행 컨텍스트      │
├─────────────────────────┤
│ first() 실행 컨텍스트       │
├─────────────────────────┤
│ 전역 실행 컨텍스트           │
└─────────────────────────┘
        콜 스택

현재 가장 위에는 second()가 있으므로 JavaScript는 second()부터 실행합니다.

4. 함수 실행 종료

second()의 실행이 끝나면 실행 컨텍스트가 콜 스택에서 제거(Pop)됩니다.

┌─────────────────────────┐
│ first() 실행 컨텍스트       │
├─────────────────────────┤
│ 전역 실행 컨텍스트           │
└─────────────────────────┘
        콜 스택

그리고 다시 first()의 실행이 이어집니다.

┌─────────────────────────┐
│ 전역 실행 컨텍스트           │
└─────────────────────────┘
        콜 스택

first()도 종료되면 제거되고, 마지막으로 전역 실행 컨텍스트가 남게 됩니다.

마지막으로 전역 코드까지 모두 실행되면 프로그램이 종료됩니다.

콜 스택(Call Stack)은 왜 Stack일까?

여기서 한 가지 궁금증이 생길 수 있습니다.

왜 이름이 콜 스택일까?

콜 스택은 이름 그대로 Stack(스택) 자료구조를 사용합니다.

스택은 가장 마지막에 들어온 데이터가 가장 먼저 나가는 LIFO(Last In, First Out) 구조입니다.

예를 들어 접시를 쌓는 모습을 떠올려 보겠습니다.

접시 C        ← 가장 먼저 꺼냄
────────────
접시 B
────────────
접시 A

마지막에 올린 접시부터 꺼낼 수 있습니다.

콜 스택도 같은 방식으로 동작합니다.

second()
────────────
first()
────────────
Global

second()가 가장 마지막에 들어왔기 때문에 가장 먼저 실행을 마치고 제거됩니다.

이 과정을 반복하면서 JavaScript는 함수 호출 순서를 관리합니다.

실행 컨텍스트는 왜 중요할까?

실행 컨텍스트를 이해하면 지금까지 배운 여러 개념들이 하나로 연결됩니다.

예를 들어 호이스팅을 떠올려 보겠습니다.

왜 선언보다 먼저 변수에 접근할 수 있었을까요?

실행 컨텍스트가 생성될 때 현재 스코프에 어떤 식별자가 있는지 미리 준비하기 때문입니다.

클로저는 왜 외부 변수를 기억할 수 있었을까요?

실행 컨텍스트 안에 있는 렉시컬 환경을 함수가 계속 참조하고 있기 때문입니다.

this는 왜 호출할 때 결정될까요?

함수가 호출되어 새로운 실행 컨텍스트가 생성되는 시점에 현재 호출 방식에 맞는 this가 결정되기 때문입니다.

지금까지 배운 여러 개념들이 모두 실행 컨텍스트를 중심으로 연결됩니다.

그래서 실행 컨텍스트는 JavaScript를 이해하는 핵심 개념 중 하나라고 할 수 있습니다.

왜 JavaScript는 실행 컨텍스트를 만들었을까?

함수가 호출될 때마다 JavaScript는 새로운 실행 환경이 필요합니다.

왜냐하면 함수마다

  • 지역 변수
  • 매개변수
  • this
  • 외부 스코프

가 모두 다를 수 있기 때문입니다.

예를 들어 같은 함수를 여러 번 호출해도 각 호출은 서로 영향을 주지 않아야 합니다.

const add = (a, b) => {
  return a + b;
};

add(1, 2);
add(10, 20);

첫 번째 호출과 두 번째 호출이 같은 실행 환경을 공유한다면 a와 b의 값이 서로 섞여 버릴 것입니다.

그래서 JavaScript는 함수가 호출될 때마다 새로운 실행 컨텍스트를 생성하여 각 함수가 독립적인 실행 환경을 가지도록 설계했습니다.

덕분에 여러 함수가 서로 영향을 주지 않고 안전하게 실행될 수 있습니다.

자주 하는 오해

실행 컨텍스트는 함수가 실행될 때만 생성된다?

아닙니다.

함수가 호출될 때마다 새로운 실행 컨텍스트가 생성되는 것은 맞지만, 프로그램이 시작될 때도 전역 실행 컨텍스트 가 가장 먼저 생성됩니다.

즉, 실행 컨텍스트는 크게 두 종류가 있습니다.

  • 전역 실행 컨텍스트
  • 함수 실행 컨텍스트

프로그램은 항상 전역 실행 컨텍스트에서 시작해 함수가 호출될 때마다 새로운 실행 컨텍스트가 추가됩니다.

실행 컨텍스트와 콜 스택은 같은 것일까?

실행 컨텍스트와 콜 스택은 서로 다른 개념입니다.

실행 컨텍스트는 코드를 실행하기 위한 실행 환경이고, 콜 스택은 실행 컨텍스트를 관리하는 공간입니다.

실행 컨텍스트는 개발자가 직접 사용할 수 있을까?

실행 컨텍스트는 JavaScript 엔진 내부에서 코드를 평가하고 실행하기 위해 관리하는 내부 동작 메커니즘입니다.

우리가 new ExecutionContext()처럼 직접 생성하거나 접근할 수 있는 객체는 아닙니다.

정리

JavaScript는 코드를 한 줄씩 무작정 실행하지 않습니다.

먼저 실행 컨텍스트를 생성하여 현재 코드에 필요한 정보를 준비한 뒤, 콜 스택을 이용해 실행 순서를 관리합니다.

실행 컨텍스트 안에는 현재 스코프의 변수와 함수 정보, this 등 코드 실행에 필요한 정보가 저장됩니다.

또한 함수가 호출될 때마다 새로운 실행 컨텍스트가 생성되기 때문에 각 함수는 서로 독립적인 실행 환경을 가집니다.

핵심 내용을 정리하면 다음과 같습니다.

  • 실행 컨텍스트는 JavaScript가 코드를 실행하기 위해 만드는 실행 환경입니다.
  • 프로그램이 시작되면 전역 실행 컨텍스트가 가장 먼저 생성됩니다.
  • 함수가 호출될 때마다 새로운 실행 컨텍스트가 생성됩니다.
  • 실행 컨텍스트는 콜 스택에서 관리됩니다.
  • 실행 컨텍스트에는 렉시컬 환경, this 등 실행에 필요한 정보가 저장됩니다.
  • 호이스팅, 스코프, 클로저, this는 모두 실행 컨텍스트와 밀접한 관련이 있습니다.

실행 컨텍스트는 JavaScript를 공부할 때 가장 어렵게 느껴지는 개념 중 하나입니다.

하지만 실행 컨텍스트는 여러 개념을 새로 배우는 것이 아니라, 이미 알고 있는 개념들을 하나로 연결해 주는 개념이라고 생각하면 훨씬 이해하기 쉽습니다.

  • 호이스팅이 왜 발생하는지,
  • 클로저가 왜 외부 변수를 기억하는지,
  • this가 왜 호출 시점에 결정되는지,

이 모든 질문의 출발점에는 실행 컨텍스트가 있습니다.

참고 자료