본문 바로가기

JavaScript

[JavaScript] 콜스택

1. JavaScript 

자바스크립트는 하나의 스레드로 단 1개의 동시성만 다루는 언어이다. 

즉, 한 번에 한 개의 작업만 할 수 있다는 뜻이다. 단, 비동기 콜백들은 제외하고 말이다.

 

자바스크립트는 힙, 큐와 함께 구성하는 단일 콜스택을 가진다. 

2. 콜스택 

원시 타입의 데이터가 저장되거나 함수의 호출을 기록하는 자료구조이다. 기본적으로 프로그램 안에 위치해서

우리가 어떤 함수를 실행시키면 스택 위에 작업을 추가(Push)하고,

함수로부터 반환받을 때 스택의 맨 위에서부터 가져온다(Pop).

function foo(b) {
	let a = 5;
    return a * b + 10;
}

function bar(x) {
	let y = 3; 
    return foo(x * y);
}

console.log(bar(b));

코드를 실행시키면 

1. 처음하는 작업은 메인 함수를 찾는다. 즉, console.log(bar(b)); 가 먼저 콜스택에 올라가게 된다.

2. 다음으로 bar 함수가 매개변수들과 같이 콜스택에 올라가고,

3. foo 함수가 스택에 올라갔다가,

4. 값을 계산하고 빠지게 된다.

5. 이어서 bar 함수가 빠져나오고

6. console.log(bar(6));가 차례대로 빠지게 된다. 

 

JS Stack Visualization (GIF)

브라우저 콘솔창에 붉은색으로 에러 스택이 나오는 것을 확인할 수 있다. 

일반적으로 그것은 콜스택의 현재 상태를 나태내고, 실패한 함수를 스택의 Top부터 Bottom까지 나타낸다.

 

Error Stack Trace (credits[https://www.youtube.com/watch?v=8aGhZQkoFbQ])

3. 힙 

오브젝트 ( 객체 )들은 힙 내부에 할당된다. 거의 구조화되지 않은 영역의 메모리로 

변수와 객체들의 모든 메모리 할당이 여기서 일어나게 된다. 

 

4. 큐 

자바스크립트 런타임은 메시지 큐를 가지고 있다. 

메시지 큐는 실행될 콜백 함수나 실행될 메시지들에 대한 리스트로, 스택이 충분한 공간을 가지고 있을 때, 

메시지는 큐 밖으로 나오게 되며, 메시지가 가지고 있던 함수 목록들이 실행된다. 

 

이렇게 초기 스택 프레임이 만들어지고, 스택이 빌 때, 메시지 수행도 끝나게 된다. 

이벤트들에 대한 콜백 함수가 제공되었다고 가정했을 때,

메시지들은 외부 비동기 이벤트들에 대한 응답으로 큐에 쌓이게 된다. 

 

외부 비동기 이벤트들이란 마우스 클릭, HTTP 요청들을 말한다. 

하지만 사용자가 버튼을 눌었는데, 아무런 콜백 함수도 등록되어 있지 않다면 어떤 메시지도 큐에 들어가지 않는다. 

 

function main(){
  console.log('A');

  setTimeout(
    function exec(){ 
      console.log('B'); 
    }, 0);
  
  console.log('C');
}

main();  // A C B

이해하기 쉽게 코드로 나타내보았다. 

  1. 1-1. main() 함수스택에 Push 된다. 
    1-2. 그 후, 브라우저는 main() 함수의 첫 번째 명령문인 console.log('A');스택에 Push 한다.
    1-3. 함수가 실행되어 A가 콘솔에 출력되고 console.log('A')스택에서 Pop된다. 
  2. 2-1. setTimeout()스택에 Push 되고 실행된다. 
    2-2. setTimeout() 함수는 브라우저의 WebAPI를 사용해 콜백을 지연시킨다. 
  3. exec()의 콜백 타이머가 API에서 실행되는 동안 console.log('C')스택에 Push 된다. 
  4. 4-1. console.log('C')가 실행되어 콘솔에 C가 출력되고 console.log('C')는 Pop 된다. 
    4-2. 모든 코드를 실행했기 때문에 main() 함수스택에서 Pop 된다. 
    그리고 타이머가 끝나서, exec() 함수는 메시지큐(이벤트 큐)에 추가된다. 
  5. 현재 콜스택이 비어있는 상태이므로, exec()스택으로 Push 되고 console.log('B')가 실행된다. 

5. 이벤트 루프 

일반적으로 자바스크립트 코드를 작업하다 보면, 스택 안에 있는 함수는 성능을 느리게도 빠르게도 만든다. 

만약, console.log() 한 줄만 있다면 코드는 빠르겠지만,

수천 수백만개의 반복문을 수행한다면, 코드는 느릴 것이며, 코드들이 스택을 계속 차지할 것이다. 

 

이러한 것을 가리켜, 'Blocking Script'라고 부른다. 

 

네트워크 요청이나 이미지 요청이 느릴 수 있다. 하지만 우리는 서버 요청들은 비동기 함수인 AJAX를 통해

처리할 수 있다. 

 

만약, 이런 요청들을 동기 함수를 통해서 처리한다면 응답 상황에 따라 아주 많이 느려질 수 있다. 

그동안은 버튼이나, 렌더링 등 무언가 이벤트를 발생시켜도 스택이 막혀있어 어떤 반응도 일어나지 않을 것이다. 

 

멀티 스레드 언어라면 잘 해결할 수 있지만, 싱글 스레드 언어인 자바스크립트는 스택에 쌓인 함수들이 모두 

처리되기 전까지는 불가능하다.  

 

해결방법 

그렇다면 싱글 스레드로 웹사이트를 사용하는데 어떻게 해야 이상적인 환경을 만들 수 있을까? 

 

비동기 함수 이용

가장 쉬운 해결책으로, 당연히 비동기 함수를 이용하는 것이다. 

비동기 함수를 이용한다는 것은 코드의 일정 부분을 실행시키고 나중에 실행될 콜백 함수를 스택에 넣는 것을 말한다.

 

개발을 하다보면 한 번쯤은 AJAX와 같은 비동기 함수( setTimeout(), setInterval(), Promises... )를 사용하게 된다. 

모든 비동기 함수들은 코드를 읽자마자 바로 실행되지 않고 잠시 후에 실행된다. 그래서 동기 함수들과는 다르게 바로 

스택 내부로 Push될 수 없다. 

자바스크립트의 네트워크 액션 요청을 보자면, 

  1. 요청 함수(loadDoc)가 실행된다. 
  2. onreadystatechange 이벤트 안에 익명의 함수를 넣어 요청이 들어온다면 실행될 콜백을 정의합니다. 
  3. open()과 send()를 통해 요청을 보냅니다. 
  4. console.log("Script call done");은 동기 함수이므로, 먼저 output에 들어간다. 
  5. 비동기 함수가 서버로부터 응답을 받고 익명 함수를 실행시킨다. 

자바스크립트 런타임비동기 명령이 완료되고 콜백이 호출될 때까지 기다리는 동안 다른 일을 하는 것을 허용한다. 

 

그리고 DOM 이벤트, http 요청, setTimeout과 같은 비동기 이벤트를 다루는 Web API들은 스스로 실행 코드를 

스택에 넣을 수 없다. 

만약 이게 가능하다면 코드 중간중간에 랜덤으로 비동기 함수의 실행 결과가 나오게 될 것이다. 

이때 사용하는 것이 메시지 큐로 실행 중인 코드가 모두 끝났다면 Web API 중 하나가 콜백을 큐에 넣는다. 

그리고 이벤트 루프는 큐 안의 콜백들을 스택이 비었을 때 넣는 일을 담당한다.

 

6. 정리

자바스크립트힙, 콜스택, 메시지 큐가 있다. 

변수와 객체를 메모리에 할당하는 구조화되지 않은 메모리이다. 

콜스택함수의 호출을 기록하는 자료구조이다. 

메시지 큐비동기 작업을 저장하는 자료구조로, 콜스택이 비었을 때 사용된다. 

이벤트 루프콜스택이 비었는 것을 확인하면, 메시지 큐의 함수를 콜스택으로 이동시키는 일을 담당한다. 

 

비동기 함수는 콜스택에서 호출되면, Web API에서 응답을 기다리는 동안 다음 작업을 콜스택에 넣고, 

응답을 받으면 콜백 함수를 메시지 큐에 넣어 현재 준비된 작업이 모두 끝나 콜스택이 빈다면 이벤트 루프로부터 메시지 큐에서 콜스택으로 이동하게 된다. 

반응형