Skip to content

자바스크립트 콜스택, 이벤트루프의 이해

Dia edited this page Apr 2, 2019 · 1 revision

요즘 웹 개발자나 프론트엔드 개발자는 브라우저에서 컴퓨터 게임, 데스크톱 위젯, 멀티 플랫폼 모바일 앱등 모든 것들이 제대로 동작하도록 해낸다. 더 나아가 이러한 작업을 DB에 연결하여 서버 측(Node.js와 사용됨)에서 동작하게 함으로써 스크립트 언어를 어디에서도 발견할 수 있게 되었다. 따라서, Javascript를 잘 이해하고 더 효과적으로 사용하는 것이 중요하다.

Javascript 생태계는 그 어느 때 보다 복잡해졌으며 계속해서 더 복잡해질 것이다. 모던 웹 앱을 만드는 데 필요한 도구는 Webpack, Babel, ESLint, Mocha, Karma, Grunt 등등 압도적이게 많다. 어떤 것을 사용해야하며 무슨 툴이 어느 동작을 수행할까? 오늘 날의 웹 개발자들의 투쟁을 완벽하게 보여주는 만화를 발견했다.

위에 나온 것들을 다 제쳐두고, 모든 자바스크립트 개발자는 프레임워크나 라이브러리를 사용하기 전에 먼저 루트 레벨에서 내부적으로 어떻게 동작하는지 알아야한다. 대부분의 JS 개발자는 크롬의 런타임 엔진인 V8이라는 용어를 들었을 지 모르지만 어떤 이들은 이 단어가 무엇을 의미하고 어떤 동작을 하는지 모를 수 있다. 나 또한 처음 개발 경력 1년 동안엔 V8을 비롯한 다른 멋진 용어에 대해 잘 알지 못했고, 작업을 완료하는 것에만 몰두했었다. 그 이후에 Javascript가 어떻게 이런 일을 할 수 있는지에 대한 호기심이 생겼고 깊게 파고 들기로 결심했다. 구글을 뒤적거리며Philip Roberts가 쓴 블로그 포스팅을 포함한 몇가지의 좋은 글들과 great talk at JSConf on the event loop를 발견했다. 그리고 내가 배운 것들을 요약하여 공유해야겠다고 결심했다. 알아야 할 것들이 많기 때문에 글을 두 부분으로 나누었다. 첫 부분은 일반적으로 사용되는 용어를 소개하고, 두 번째 부분에서는 모든 용어 간의 연결을 소개할 예정이다.

Javascript는 단일 스레드 언어이다. 즉, 한 번에 하나의 작업이나 하나의 코드 조각만 처리 할 수 ​​있다는 뜻이다. 자바스크립트는 하나의 콜 스택과 동시성 모델(Concurrency Model)로 불려지는 힙과 큐 같은 부가 요소를 가지고 있다. 먼저 각 용어를 살펴보자.

콜 스택 (Call Stack)

콜 스택은 우리가 프로그램 상에서 어디에 있는지를 기록하는 데이터 구조이다. 함수를 호출하면, 콜 스택에 함수를 밀어 넣고 함수가 종료(return)되면 스택의 맨 위에서 꺼낸다.

함수를 호출하면 그 함수의 사용을 위한 스택 프레임(Stack Frame)이 생성된다.
호출 스택의 각 단계를 스택 프레임이라고 한다.

function foo(b) {	// 세번째 프레임
  var a = 10;
  return a + b + 11;
}
	
function bar(x) {	// 두번쨰 프레임
  var y = 3;
  return foo(x * y);
}
	
console.log(bar(7)); // 첫 프레임

위의 경우, console.log(bar(7))에서 시작하며 프레임이 생성되어 스택에 push된다. bar를 호출할 때, bar의 인자와 지역 변수를 포함하는 두번째 프레임이 생성되고 스택에 pusheh된다. bar가 foo를 호출하면 세번째 프레임이 만들어져 foo의 인자와 지역 변수가 들어있는 프레임이 맨 위에 push된다. foo가 반환(return)되면, 최상위 프레임 요소(foo)는 스택 밖으로 빠져나오고 그 다음 프레임(bar)이 실행(pop)된다. bar까지 pop되어 종료된다면 마지막 console 프레임이 실행되면서 결과를 뿌린다. 이 모든 것들은 한번에 한개씩 수행된다.

여러분은 브라우저의 콘솔에서 빨간색으로 된 긴 error stack trace를 보았을 것이다. errro stack trace는 기본적으로 콜 스택의 현재 상태와 어느 함수에서 에러가 났는지 스택처럼 위에서 아래방향으로 표시하여 가르켜준다. (아래 이미지 참조)

// foo.js
function foo() {
	throw new Error('Opps!');
}
	
function bar() {
	foo()
}
	
function baz() {
	bar()
}
	
baz();

때때로 무한 루프 처럼 함수 호출 횟수가 너무 많아 콜 스택의 최대 허용치를 넘어가게 되면 Max Stack Error가 발생된다.(Chrome 브라우저의 경우 스택의 크기가 16,000 프레임). 아래 이미지를 참고하자.

힙 (Heap)

힙은 구조화되지 않은 넓은 메모리 영역을 지칭한다. 변수와 객체에 대한 모든 할당은 힙 영역에서 이루어 진다.

큐 (Queue)

자바스크립트 런타임엔 메시지 큐도 있다. 각 메시지에는 메시지를 처리하기 위해 호출되는 관련 함수가 있다. 스택에 충분한 여유공간이 있을 때, 런타임은 큐에서 메시지를 꺼내어 처리한다. 메시지는 관련 함수를 호출한다.(따라서 스택 프레임을 생성한다.) 스택이 다시 비게 되면 메시지 처리가 종료된다. 기본적으로, 이러한 메시지들은 마우스를 클릭하거나 HTTP 요청에 대한 응답을 수신 것과 같은 외부 비동기 이벤트에 대한 응답으로 큐에 존재한다. 예를 들어 사용자가 버튼을 클릭했는데 콜백 함수가 없다면 아무런 메시지도 큐에 추가되지 않는다.

이벤트 루프

일반적으로 JS 코드의 성능을 평가하려고 할때, 콜 스택에 들어있는 함수가 성능을 느리거나 빠르게 만들 수 있다. console.log() 는 빠르지만 forwhile를 이용하여 수천번, 수맥만번을 호출 한다면 느려질 것이고 그렇게 되면 스택이 꽉차있는 상태가 되거나 차단될 것이다. 이러한 현상을 블로킹 스크립트라 한다. (추가적인 Javascript의 실행을 위해 이전 작업이 완료될 때까지 기다려야만 하는 상황)

네트워크 요청은 느릴 수 있고, 이미지 요청은 느릴 수 있다. 그러나 고맙게도 서버 요청은 비동기 함수인 AJAX를 통해 수행된다. 이러한 네트워크 요청이 동기함수를 통해 이루어진다고 가정하면 어떻게 될까? 네트워크 요청은 기본적으로 다른 컴퓨터/기계로 이루어진 어떤 서버로 전송된다. 서버는 요청에 대한 응답의 회신을 천천히 할 수도 있다. 때문에 그동안 사용자가 어떤 버튼을 클릭하거나 다른 렌더링을 완료 해야 할 경우엔 아무 일도 일어나지 않는다. 스택이 차단되기 때문이다. Ruby와 같은 다중 스레드 언어에서는 처리할 수 있지만, 자바스크립트와 같은 단일 스레드 언어에서는 스택 내부의 함수가 값을 반환하지 않는 한 다른 작업을 하는 것은 불가능하다. 따라서 브라우저는 아무것도 할 수 없기 때문에 웹페이지가 완전히 망가질 것이다. 사용자를 위한 유동적인 UI를 원한다면 이러한 현상은 올지 않다. 어떻게 처리해야할까?

JS에서의 동시성(Concurrency) - 한번에 한 작업만! 단, 비동기 콜백 제외

가장 쉬운 해결책은 비동기 콜백을 사용하는 것이다. 즉, 코드의 일부를 실행하고 나중에 실행할 콜백 (함수)을 제공하면 된다. 우리는 AJAX 요청과 같은$.get(),setTimeout(),setInterval(), Promises, 등을 사용함으로 비동기 콜백을 접했을 것이다. Node는 비동기 함수 실행에 관한 것이다. 모든 비동기 콜백은 즉시 실행되지 않고 얼마 후 실행된다. 따라서 console.log(), 수학 연산과 같은 동기 함수와 달리 스택에 즉시 넣을 수 없다. 그럼 비동기 함수는 어디로 가고 어떻게 처리될까?

위 코드 처럼 Javascript에서 네트워크 요청을 실행할 경우:

  1. loadDoc 함수를 실행하면, onreadystatechange 이벤트에서 응답이 가능할 때 실행하기 위한 콜백으로 익명 함수를 전달한다.
  2. "Script call done!"이 콘솔에 즉시 출력된다.
  3. 언젠가 미래에, 응답이 돌아오고 콜백이 실행되어 responseText가 콘솔에 출력된다.

응답에서 호출자를 분리하면, 비동기 동작이 완료되고 콜백이 실행되기를 기다리는 동안 Javascript 런타임이 다른 작업을 수행할 수 있다. 여기서 브라우저 웹API가 사용되고 DOM 이벤트, http 요청, setTimeout 등과 같은 비동기 이벤트를 처리한다.

브라우저 웹 API - 브라우저가 비동기 이벤트를 처리하기 위해 만든 스레드. DOM 이벤트, http 요청,setTimeout 등과 같은 비동기 이벤트를 처리한다.

이러한 WebAPI들은 스택 상에 실행 코드로 집어 넣을 수 없다. 만약 이게 가능하다면 코드 중간에 무작위로 나타나게 될 것이다. 위에서 이야기한 메시지 콜백 큐는 방법을 알려준다. WebAPI는 실행이 끝나면 콜백을 큐에 밀어 넣는다. 이벤트 루프는 큐에서 콜백을 실행하고 스택이 비어있을때 스택에 넣는다. 이벤트 루프의 기본 작업은 스택과 작업 큐를 모두 보고 스택이 비어있을때 큐의 첫번째 작업을 스택에 넣는 것이다. 각 메시지 또는 콜백은 다른 메시지가 처리되기 전에 완전히 처리된다.

while (queue.waitForMessage()) {
  queue.processNextMessage();
}

웹 브라우저에서는 이벤트가 발생하면 메시지가 추가되고 해당 메시지를 위한 이벤트 리스너가 추가된다. 만약 이벤트 리스너가 없으면 이벤트는 소실된다. 클릭 이벤트 핸들러가 등록된 요소를 클릭하면 메시지가 추가된다. (다른 이벤트와도 마찬가지). 이 콜백 함수의 호출은 호출 스택의 초기 프레임으로 사용되며 JavaScript가 단일 스레드이므로 스택의 모든 호출이 반환 될 때까지 추가 메시지 폴링 및 처리가 중단됩니다. 후속 (동기식) 함수 호출은 스택에 새로운 호출 프레임을 추가합니다.

콜백 함수의 호출은 콜 스택의 초기 프레임으로 사용되며. Javascript가 싱글 스드 이므로 스택에 있는 모든 호출이 반횐 될때 까지 메시지 폴링과 처리가 중단된다.

통신에서, "폴링"은 한 프로그램이나 장치에서 다른 프로그램이나 장치들이 어떤 상태에 있는지를 지속적으로 체크하는 전송제어 방식으로서, 대체로 그들이 아직도 접속되어 있는 지와 데이터 전송을 원하는지 등을 확인한다.

다음 챕터에서는 코드 실행의 시각적 애니메이션을 보여 주며, task, micro-task와 같은 다양한 유형의 비동기 함수에 대해 설명하고, 큐에서 우선 순위가 높은 비동기 함수를 설명할 것이다. 또한 일부 기능을 수행하는 데 사용되는 zero delay과 같은 팁도 설명할 예정이다.

번역하며 느낀점

글 내용보다 번역이 더 어려웠다....

Clone this wiki locally