[JavaScript] 자바스크립트 동작 원리

서동경·2023년 2월 11일
1
post-thumbnail

🍀 이벤트 루프란 ? (feat. 싱글 스레드)

자바스크립트 엔진은 싱글 스레드로 동작한다. 즉 자바스크립트의 코드는 순차적으로 실행되며 한번에 하나의 작업만 처리할 수 있음을 의미한다. 이 특성은 비동기적 처리를 힘들게 만든다. 그렇다면 어떤 방식으로 비동기 처리를 가능하게 할까?

이벤트 루프란 자바스크립트의 코드의 실행을 처리, 관리하는 메커니즘이다. 이 이벤트 루프를 통해 자바스크립트는 비동기 처리가 가능해진다.

자바스크립트 엔진은 Memory Heap, Call Stack으로 구성되어 있다. 이 자바스크립트 엔진의 Call Stack과 브라우저가 제공하는 Web API, Task Queue(Callback Queue는 정식 명칭은 아니지만 많이들 사용...)가 상호작용하며 함수를 실행시키는데, 비동기 처리가 필요할 때 이벤트 루프를 이용한다.

함수가 호출되면, 해당 함수는 Call Stack에 쌓인다. Call Stack은 후입선출 방식(LIFO: Last In First Out)으로 동작하므로 가장 마지막에 들어간 함수가 먼저 실행 되고 실행을 마친 코드는 Call Stack에서 사라진다.
한편, 비동기 작업은 Call Stack이 아닌 Web API에 위임되어, 그 콜백함수가 Task Queue에서 대기한다. Call Stack이 비게 되면, Task Queue에서 대기중인 비동기 작업이 선입선출 방식(FIFO: First in Fisrt Out)으로 이벤트 루프를 통해 Call Stack으로 넘어가서 동작한다.

간단한 코드를 통해 이벤트 루프를 이해해보자.

const firstFunc = () => {
  console.log("First");
  setTimeout(function () {
    console.log("Timeout");
  }, 0);
  secondFunc();
};

const secondFunc = () => {
  console.log("Second");
};

firstFunc();

코드를 실행하면, Call Stack과 Web API, Task Queue에는 다음과 같은 변화가 일어난다.

  1. 호출한 함수의 정보를 담은 anonymous가 제일 먼저 Call Stack에 쌓인다.
  2. firstFunc() 함수가 Call Stack에 쌓인다.
  3. console.log("First")가 Call Stack에 쌓이고 실행된 후 제거된다. 즉 첫 번째 출력이 된다.
  4. setTimeout() 비동기 처리 함수는 Web API에 위임되어 콜백 함수인 console.log("Timeout")가 Task Queue에 대기하도록 한다.
  5. secondFunc() 함수가 Call Stack에 쌓인다.
  6. console.log("Second")가 쌓이고 실행된 후 제거된다. 즉 두 번째 출력이 된다.
  7. 할 일을 마친 secondFunc() 함수가 Call Stack에서 제거된다.
  8. 할 일을 마친 firstFunc() 함수가 Call Stack에서 제거된다. setTimeout의 콜백은 Task Queue에서 대기중이고, firstFunc()은 실행을 완료한 것으로 본다.
  9. anonymous까지 Call Stack에서 제거된다.
  10. Call Stack은 비어있고 쌓일 것도 없다 대기중인 console.log("Timeout")이 이벤트 루프를 통해 Call Stack에 쌓이고 실행된 후 제거된다. 즉 세 번째 출력이 된다.

👉 그럼 First → Second → Timeout 순으로 출력된다 !

🔎 대기열의 종류

위 예제에서는 Task Queue로 통칭해서 다뤘지만, 대기열 종류는 크게 Macrotask Queue( = Task Queue)와 Microtask Queue로 나뉜다. setTimeout, setInterval, XMLHttpRequest 등을 이용한 비동기 처리에서는 Task Queue 즉 Macrotask Queue를 사용하지만, Promise나 async/await를 이용한 비동기 처리에는 Microtask Queue를 사용한다.
(requestAnimationFrame을 이용한 비동기 처리에 사용되는 Animation Frames이라는 대기열도 존재하지만, 사용빈도도 적고 당장 쓸 일도 없으니 간결하게 다루기 위해 제외하겠다. )

Call Stack이 비어있고, Macrotask Queue와 Microtask Queue에 모두 함수가 실행을 대기중이라면 선입선출 방식이 아니라 우선순위에 따라서 먼저 Call Stack으로 들어갈 함수가 결정된다.

Queue의 종류별 우선순위

1순위 👉 Mircotask Queue( = Job Queue)
: Promise(⊃Fetch, Axios), async/await, process.nextTick, Object.observe 등

2순위 👉 Macrotask Queue( = Task Queue)
: setTimeout, setInterval, XMLHttpRequest 등

한편 addEventListener를 이용한 비동기 처리는 이벤트가 발생했을 경우에만 콜백함수를 실행시키므로 Event Queue라는 또 다른 개념을 사용해 콜백함수를 관리한다.

🍀 스코프란? (feat. var, let, const)

스코프(Scope)는 "범위"라는 뜻으로, 프로그래밍에서는 "변수에 접근할 수 있는 범위"라고 이해하면 된다.

스코프는 중첩이 가능하고 안쪽 스코프에서 바깥쪽 스코프로는 접근할 수 있지만 반대로는 불가능하다.

스코프는 어디서든 접근할 수 있는 전역 스코프(Global Scope)와 내부 영역인 지역 스코프(Local Scope)로 나뉜다. 여기서 지역 스코프는 함수 스코프(Function Scope)와 블록 스코프(Block Scope)로 나뉜다.

한편 "어떤 변수가 전역 스코프를 가진다"라고 함은 변수에 접근할 수 있는 범위가 전역이라는 의미이다. 코드상 가장 바깥쪽에 위치해야만 전역 스코프를 가지는 것은 아니고, 안쪽에 위치하더라도 전역 스코프가 될 수 있다. 마찬가지로 "함수 스코프 또는 블록 스코프를 가진다"라고 하면 각각 함수 안, 블록 안에서만 변수에 접근이 가능하다는 의미이다.

변수 선언 키워드인 var, let, const를 스코프와 연관지어 알아보자. 가장 바깥 쪽(Global)에서 선언했을 때, 함수 안(Function)에서 선언했을 때, 블록 안에서 선언했을 때(Block)를 비교하면 다음과 같다. (let과 const은 재선언 가능 여부선언 시 할당의 필요성에서 차이가 있고, 스코프 규칙은 동일하므로 구분하지 않겠다.)

varlet, const
global에서 선언Global ScopeScript Scope
function에서 선언Local Scope(Function Scope)Local Scope(Function Scope)
block에서 선언Global ScopeBlock Scope

특이한 점으로 let, const 로 만들어진 변수는 global에서 선언되더라도 Global Scope가 아닌 Script Scope에 할당된다. Script Scope는 스크립트 파일의 가장 최상위에 존재하므로 Global Scope와 기능적인 차이가 없다. 굳이 구분짓는 이유는 varlet, const를 구분하고, 이미 많은 데이터가 할당되어있는 Global Scope에 또 다시 할당하는 것을 방지하기 위함이라고 생각된다.

한편 var이나 let, const 모두 함수에서 선언되었을 때 Function Scope를 갖는다. 그러나 block에서 선언되었을 때는 차이가 있다. varGlobal Scope을 가지고 let, constBlock Scope를 가진다.

var는 함수의 변수는 지역 변수로 인정하지만 그 외 블록에서의 변수는 지역 변수로 인정하지 않으므로 "함수 레벨 스코프"를 가진다고 표현한다. 한편 let, const는 함수의 변수도 지역 변수로 인정하고, 블록의 변수도 지역 변수로 인정한다. 즉 블록을 가지고 있다면 해당 블록은 모두 스코프로 인정한다는 의미로 "블록 레벨 스코프"를 가진다고 표현한다.

💡 Local Scope? Function Scope?

함수 안에서 변수를 찍어보면 Function Scope가 아닌 Local Scope에 할당되던데, 왜이런지 모르겠어서 ChatGPT에게 질문해보았다.

🍀 실행 컨텍스트란 ?

실행 컨텍스트(Execution Context)는 자바스크립트 엔진이 코드를 읽고 실행할 때, 코드 실행에 영향을 주는 조건이나 상태를 모아둔 객체, 즉 코드 실행에 필요한 것을 모아둔 환경을 뜻한다.

실행 컨텍스트는 매우 어렵지만 자바스크립트가 실행되는 방식을 담고있는 핵심 원리라고 할 수 있기 때문에 반드시 이해해야 하는 개념이기도 하다.

실행 컨텍스트가 생성될 때 변수 객체(Variable Object / VO), 스코프 체인, this가 생성된다. 함수는 변수 객체에서 변수를 찾고, 없다면 스코프체인을 통해 변수를 찾아 나간다.

코드가 어디에서 실행되는지에 실행 컨텍스트가 다르고, 이에 따라 두 종류로 구분할 수 있다.

🌱 실행 컨텍스트의 종류

코드가 어디에서 실행되는지에 실행 컨텍스트가 다르고, 이에 따라 두 종류로 구분할 수 있다.

🧩 전역 실행 컨텍스트 (Global Execution Context / GEC)

가장 바깥에서 실행되는 코드에 대한 실행 컨텍스트로, 전역 공간에 존재하는 코드들의 실행에 필요한 것들이 담겨 있다.

프로그램이 실행될 때 한번만 생성되고, 모든 코드를 읽어들인 뒤 종료된다. 그러므로 전역 실행 컨텍스트는 단 하나만 존재한다. 이는 자바스크립트가 싱글 스레드 언어라는 점을 뒷받침한다.

GEC는 전역 객체(Gloval Object / GO)를 가리킨다. 전역 객체에는 window 객체와 this 객체, Number · String 등의 내장 객체, BOM, DOM, 전역변수 등이 담겨있다.

🧩 함수 실행 컨텍스트 (Function Excution Context / FEC)

함수 안에서 실행되는 코드에 대한 실행 컨텍스트로, 함수 공간에 존재하는 코드들의 실행에 필요한 것들이 담겨 있다.

함수가 호출될 때마다 새로운 FEC가 생성되고, 함수의 실행이 끝나면 종료된다.

FEC는 활성 객체(Activation Object / AO)를 가리킨다. 활성 객체에는 함수 내부의 this, parameter(매개변수)와 arguments(매겨변수의 인자), 지역 변수 등이 담겨있다.

🌱 실행 컨텍스트의 단계

함수의 실행 여부에 따라 생성 단계(Creation Phase)와 실행 단계(Excution Phase)로 나뉜다.

🧩 생성 단계 (Creation Phase)

자바스크립트 엔진이 함수를 호출했지만 실행이 시작되지 않은 단계이다.

이 단계에서 GO, AO, this, 그리고 변수들을 생성한다. 변수는 var로 선언된 경우에는 undefined로 초기화되고, let, const로 선언된 변수의 경우에는 초기화는 이루어지지 않고 선언만 이루어진다.

이 단계에서 스코프 체인이 구성된다.

🧩 실행 단계 (Excution Phase)

실행되지 않던 함수가 실행되는 단계이다.

이 단계에서 GO, AO, this, 변수들에 값을 할당한다. 그러므로 arguments에 접근이 가능해진다.

구성된 스코프 체인을 통해 GEC에서 생성된 환경에 접근 가능하다. 만약 다른 함수가 호출된다면 새로운 함수의 FEC가 생성되며 새로운 스코프 체인을 연결한다.

💡 호이스팅과 스코프 체인

🔍 Lexical Environment와 Variable Environment

호이스팅과 스코프 체인을 이해하기 이전에 Lexical Environment와 Variable Environment의 데이터 구조를 이해해야 한다.

실행 컨텍스트의 Creation Phase에서는 Lexical Environment와 Variable Environment라는 객체가 생성된다. Lexical Environments는 Lexical 중첩 구조를 기반으로 하는 특정 변수 및 함수에 대한 식별자의 연결을 정의한다. Variable Environment는 특정 Lexical Environment에서 변수의 값을 담은 객체이다.

Variable Environment도 Lexical Environment이기 때문에, 두 객체는 모두 Environment Record, Outer Environment Reference, ThisBinding이라는 동일한 프로퍼티를 가진다. 다만 여기서 두 객체의 Environment Record만 서로 다른 역할을 가진다.

💬 Environment Record

Environment Record는 현재 실행 컨텍스트의 식별자 정보, 그리고 변수 객체를 담고 있는 프로퍼티로, 호이스팅(Hoisting)과 관련이 있다.

  • Lexical Environment에서의 Environment Record
    선언만 이루어지고 초기화는 이뤄지지 않는 let, const, 함수표현식 을 저장한다.
  • Variable Environment의 Environment Record
    선언과 동시에 초기화가 이루어지는 var, 함수 선언식을 저장한다.

💬 Outer Environment Reference

Outer Environment Reference는 현재 Lexical Environment가 다른 Lexical Environment의 변수와 함수에 접근할 수 있도록 하는 프로퍼티로, 스코프 체인(Scope Chain)과 관련이 있다.

💬 ThisBinding

this에 값을 할당하는 프로퍼티이다. 함수 호출 방식과 Lexical Scope에 따라 결정된다.

🔍 호이스팅이란?

호이스팅이란 선언문이 마치 최상단으로 끌어올려지는 현상을 말한다. 그러므로 호이스팅이 일어나면 특정 변수의 값을 읽어들일 때 그 윗 라인에 선언문이 존재하지 않더라도 undefined가 출력될지언정 에러가 뜨지는 않는다.

호이스팅은 var함수 선언식에서 일어난다. var로 인한 호이스팅은 변수 호이스팅, 함수 선언식으로 인한 호이스팅은 함수 호이스팅이라고 부른다. var 혹은 함수 선언식으로 변수 또는 함수를 선언한다면 변수를 읽어올 때 또는 함수를 실행할 때, 해당 라인보다 아래에 선언문이 아래에 존재한다고 하더라도 undefined가 출력될지언정 에러가 뜨지는 않는다.

💬 변수 호이스팅

변수 호이스팅의 경우, var로 선언한 변수는 undefined로 초기화가 이루어져 Environment Record에 기록된다. 윗단에서 호출시 undefined가 출력된다.

이와 달리 let, const는 선언문만 실행되고 초기화는 이루어지지 않는다. 그러므로 윗단에서 호출 시 에러가 발생한다.

//var
console.log(legWorkout); // undefined
var legWorkout = "Squat";
console.log(legWorkout); // Squat

//const (let도 마찬가지!)
console.log(chestWorkout); // error
const chestWorkout = "Bench Press";
console.log(chestWorkout); // "Bench Press"

💬 함수 호이스팅

함수 호이스팅의 경우, 함수 선언식은 선언과 동시에 함수(f{})가 생성되어 이 함수로 초기화가 이뤄어지고 Environment Record에 기록된다. 윗단에서 호출이 가능하다.

한편 함수 표현식var를 사용하면 함수가 f{}가 아니라 undefined로 초기화되어 함수 실행시 Type Error가 뜨고, let, const를 사용하면 초기화가 되지 않아 함수 실행 시 Reference Error가 뜬다.

//함수 선언식
getLastName(); // Seo

function getLastName() {
  console.log("Seo");
}

//함수 표현식
getFirstName(); // error

var getFirstName = () => {
  console.log("Dong Kyeong");
};

🔍 스코프 체인이란?

스코프 체인이란 실행 컨텍스트 체인과 같은 말이다. 스코프 체인은 위에서 설명한 Lexical Environment와 연관이 있다.

Lexical Environment는 Lexical Scope와 같은 말이다. 자바스크립트는 Lexical Scope를 기반으로 하는 언어이다. Lexical Scope는 함수의 호출이 아닌 함수의 선언에 따라 스코프가 결정된다는 의미이다. (반대 용어는 Dynamic Scope.)

이 Lexical Environment 객체 안에 존재하는 Outer Environment Reference 프로퍼티는 안쪽과 바깥쪽 스코프를 연결하는 역할을 한다. 그러므로 Outer Environment Reference를 통해 안쪽 스코프는 바깥쪽 스코프의 프로퍼티를 참조할 수 있게 되는 것이다.

📌 스코프 체인과 프로토타입 체인의 차이

스코프 체인은 변수를 검색하는 매커니즘이고,
프로토타입 체인은 객체의 프로퍼티를 검색하는 메커니즘이다.

profile
개발 공부💪🏼

0개의 댓글