자바스크립트의 동작 원리에 대해서 알고 싶은 분들과 저의 학습을 위해 정리해 보려 합니다😀
자바스크립트는 기본적으로 싱글 스레드로 돌아가는 언어입니다.
이를 위해, 먼저 Call stack, Heap, Web APIs, Queue들이 어떤 역할을 하는지 알아보겠습니다.
기능 호출을 기록하는 데이터 구조로 프로그램에서 어떤 순서로 작업이 수행되는지 기록하는 작업 스케줄링과 관련된 자료 구조이다.
Call Stack은 기본적으로 자바스크립트를 한 줄씩 읽어가며 우리의 코드가 순서대로 돌 수 있도록 보장해주는 데이터 구조이다. 스택을 사용하기 때문에 후입 선출(LIFO, Last-In-First-Out)의 구조를 갖는다.
코드는 console.log(a) 같은 한 줄 코드 그리고 function fn() 같은 함수도 될 수 있습니다.
call stack은 한 동작에 하나의 일만 수행하게 되는데 이러한 동작 방식을 싱글 스레드 또는 단일 스레드라고 합니다. 이것은 동기식 언어라고도 할 수 있습니다. (Single Call Stack)
만약 스택을 초과하게 된다면 어떻게 될까요? 무한 루프를 도는 코드를 돌리면 아래 처럼 Maximum call stack size 에러가 발생하게 됩니다.
무한 루프가 될 때를 그림으로 표현하면 밑의 그림처럼 표현할 수 있습니다.
Call Stack은 정해진 스택 사이즈가 존재하고, 하나씩 쌓이기 때문에 정해진 용량을 초과하게 되면 에러가 발생하게 됩니다. 흔히 이것을 Stack Overflow라고 말합니다.
자바스크립트는 이러한 특성 때문에 무한 루프는 발생할 수 있지만 동기화 문제인 교착상태(DeadLock)는 발생할 수 없습니다!
메모리를 할당받는 영역
자바스크립트는 기본적으로 동기적 언어입니다.
위 그림을 보면 비동기적 코드는 call stack에 들어올 시 Web APIs로 보내지고 여기서 비동기적 로직이 수행된 다음 결과값이 Callback Queue에 쌓아뒀다가 call stack이 비어있을 때 값들이 call stack에 들어가게 됩니다.
예시를 보며 정리해 보겠습니다.
// 예시 코드
function goLunch(){
console.log(“식사시간입니다.”);
}
function work(){
// 5초 이상 걸리는 작업
for(var i=0; i < 5000000000; i++);
console.log(“work완료”);
}
function example(){
var SECOND = 1000;
setTimeout(goLunch,3*SECOND);
work ();
...
}
위 코드의 출력 순서는?
아래 그림은 자바스크립트의 실행 환경을 나타낸 그림입니다.
다시 간단하게 설명해 보자면, 메서드가 호출될 때마다 스택에 쌓이고 힙 영역은 객체를 생성할 때 메모리 할당이 일어나는 곳입니다. 자바스크립트는 싱글스레드로 동작하기 때문에 단 하나의 스택이 존재합니다.
그럼 그림에서 이벤트 큐의 역할은 무엇일까요?
이벤트 큐란 이벤트가 발생했을 때 수행해야 할 콜백 함수를 대기시키는 곳입니다.
이벤트 큐에 대기 중인 콜백은 스택이 비어있을 때 처리됩니다.
앞서 보여준 예시 코드를 수행하는 환경을 나타낸 그림을 보며 더 구체적으로 살펴보겠습니다.
setTimeout은 파라미터로 지정된 시간이 지나면 콜백 메서드를 수행시키는 기능을 제공합니다.
이때 누군가 시간을 계산해야 지정된 시간 후에 메서드를 수행할 텐데 자바스크립트 엔진이 한다면 setTimeout 뒤에 있는 코드가 지정된 시간이 지나야 수행될 수 있어 비동기 처리를 할 수 없습니다.
다행히 이 작업은 브라우저가 대신 맡아 수행하게 됩니다.
코드는 자바스크립트 모습이지만, 자바스크립트가 아니라 브라우저에 기능을 위임하고 다음 코드를 수행하는 것입니다. 지정된 시간이 지나면 Timeout 이벤트가 발생해서 이벤트 큐에 콜백이 추가됩니다.
스택과 큐의 상태를 확인하면서 스택이 비어있을 때마다 큐에 있는 콜백을 하나씩 실행시킨다.
큐에 추가된 콜백은 반복되는 과정을 통해 최종적으로 자바스크립트 엔진에 의해 실행됩니다.
이러한 과정을 이벤트 루프라고 합니다.
만약, 스택에 작업이 계속 실행 중이라면 이벤트 큐에 계속해서 작업이 대기할 수밖에 없습니다.
즉, 스택과 이벤트 큐의 상황에 따라 의도한 대로 콜백이 실행되지 않을 수도 있습니다.
위 그림은 예시 코드의 실행 중 스택, 이벤트 큐, 브라우저 상태를 시간 흐름도로 표현한 것입니다.
이제는 앞서 봤던 예시 코드의 결과가 왜 2번인지 어떻게 수행되는지 알게 되었습니다.
goLunch가 3초 후에 이벤트 큐에 추가되고 실행을 기다리고 있지만 이벤트 루프가 work가 종료된 후에야 goLunch를 스택에 올릴 수 있기 때문에 3초가 지나도 “식사시간입니다.”가 출력되지 않는 것이었습니다.
전역변수: 제일 바깥 범위 최상단 범위에 만든 변수를 뜻한다.
지역변수: 함수 안에 들어있는 변수를 뜻한다.
// 예시 코드
var x = 'a'; // 전역변수
function ex() {
var x = 'b'; // 지역변수
x = 'c';
}
ex(); // x를 바꿔본다.
alert(x); // 바뀌지 않고 그대로 'a'
위에서 전역변수는 x이고 지역변수는 ex함수 내의 x변수가 해당됩니다.
위 코드에서 지역변수는 전역변수에 영향을 끼칠 수 없는데 그 이유는 함수 스코프 때문이다.
스코프는 범위란 뜻으로 변수들의 범위를 결정하는 것을 뜻한다. 함수 안에서 선언된 변수는 함수 안에서만 쓸 수 있다.
그렇기에 위 예시에서 ex함수 내의 var x= 'b'는 함수 안에서 선언된 변수이므로 함수 내에서만 적용됩니다. 따라서 아래 x = 'c'가 ex안에서의 x만 바꿔 밖에서는 그대로 a인 것입니다.
하지만 아래 예시 코드는 위 예와 달리 ex함수 안에서 변수를 선언하지 않았기 때문에 전역변수인 x = 'a' 가 c로 바뀌게 됩니다.
var x = 'a';
function ex() {
x = 'c';
}
ex();
alert(x); // 'c'로 바뀜
위 결과가 나오는 이유는 자바스크립트가 변수의 범위를 호출한 함수의 지역 스코프부터 전역 스코프까지 점차 넓혀가며 해당 변수를 찾기 때문이다. 이 개념이 scope chain(스코프 체인)이다.
자바스크립트에서 내부 함수는 외부 함수의 변수에 접근 가능하지만 외부 함수에서는 내부 함수의 변수에 접근할 수 없습니다.
var name = '제로';
function outer() {
console.log('외부', name); // 외부 제로
function inner() {
var enemy = '네로';
console.log('내부', name); // 내부 제로
}
inner();
}
outer();
console.log(enemy); // 접근 못함 undefined
위의 예는 outher() -> inner() ...으로 진행되는데 inner함수는 name 변수를 찾기 위해 자기 자신의 스코프에서 찾고 없다면 상위 스코프에서 그래도 없으면 그 상위인 전역 스코프까지 접근하는 과정을 반복한다. 결국 전역 스코프에서 name 변수를 찾아 '제로' 값을 얻게 된다.
하지만 console.log(enemy)는 외부에서 내부로 접근하지 못하기 때문에 inner 안에 enemy 값을 찾지 못하고 undefined가 발생한다.
이렇게 꼬리를 물고 계속 범위를 넓혀가면서 찾는 것을 Scope chain(스코프 체인)이라 한다.
함수를 어디서 호출하는지가 아니라 어디에 선언하였는지에 따라 결정되는 것을 말한다.
즉, 함수를 어디서 선언하였는지에 따라 상위 스코프를 결정한다는 뜻이며, 가장 중요한 점은 함수의 호출이 아니라 함수의 선언에 따라 결정된다는 점입니다.
함수가 선언됐을 때 형성되는 스코프를 다른 말로 정적 스코프(Static scope) 라 부르기도 합니다.
var name = '제로';
function log() {
console.log(name); // 네로
}
function wrapper() {
name = '네로';
log();
}
wrapper();
var name = '제로';
function log() {
console.log(name);
}
function wrapper() {
var name = '네로';
log();
}
wrapper();
동적 언어 (Dynamically Typed Language)
정적 언어 (Statically Typed Language)
자바스크립트 언어는 동적언어입니다, 하지만 유효범위는 코드가 작성되는 순간 정해지는 정적인 특성을 가집니다.
다시 한번 정리하면, 함수 안에서 정의된 변수는 함수 밖으로 빠져나갈 수 없다는 것이 핵심입니다.
먼저 first() 함수가 Call Stack에 쌓이게 됩니다.
그 다음 first() 함수 안에 있는 console.log()가 Call Stack에 쌓이게 됩니다.
콘솔 창에 1을 출력합니다.
first() 함수는 종료되었으니 Call Stack에서 빠지게 되고, setTimeout()이 Call Stack으로 들어옵니다.
setTimeOut()은 비동기 함수입니다. 그렇다면 이걸 Web API에서 처리하도록 보냅니다.
만약 nodejs 같은 경우에는 백그라운드에서 처리하도록 보냅니다.
그리고 그다음 함수인 second() 함수를 불러옵니다.
second() 함수 안에 있는 console.log()가 Call Stack에 쌓이게 됩니다.
콘솔 창에 2를 출력합니다.
second() 함수는 종료되었으니 Call Stack에서 빠지게 됩니다.
이제 들어올 함수가 없는 거 같지만 아직 비동기 함수를 처리하고 있습니다.
setTimeOut()의 시간을 2초로 설정해 두었으니 2초간은 Wab API에서 처리하게 됩니다.
2초가 지난 후에는 setTimeOut()의 콜백 함수를 Callback Queue로 보내게 됩니다.
❗ 여기서 헷갈리면 안되는 게 Call Stack에 있던 second() 함수가 종료되고 나서야 Web API를 처리하는 게 아니고 setTimeOut()이 Web API로 넘어간 시점부터 처리되고 있는 것입니다.
즉, Call Stack에서는 second() 함수를 처리하고 있었고, Web API에서는 setTimeOut()을 2초 동안 돌리고 있었다는 것입니다.
이제 Event Loop가 등장하게 됩니다. Event Loop는 Callback Queue에 있는 콜백 함수를 Call Stack으로 보내서 처리하기 위해 Call Stack이 비어있는지를 검사합니다.
만약 Call Stack이 비어 있다면 Callback Queue에 있던 함수를 Call Stack으로 보내서 처리하게 됩니다.
마지막으로 Call Stack에 있던 console.log()를 콘솔에 출력하는 것으로 프로그램이 종료됩니다.
이 글을 읽고 도움이 되셨으면 좋겠습니다. 감사합니다 😊