웹 개발자, 프론트엔드 개발자는 브라우저 내에서 상호작용하는 액션부터 컴퓨터 게임, 데스크탑 위젯, 크로스 플랫폼 모바일 어플리케이션, DB와 연결하는 서버 사이드 코딩 등 모든 것을 스크립팅 언어로 구현한다. 따라서, 자바스크립트를 더욱 효율적으로 사용하기 위해서는 자바스크립트의 내부 동작을 이해해야 한다. 프레임워크나 라이브러리를 사용하기 전에 기초적인 토대에 대해 알아야 할 필요가 있다.
자바스크립트는 하나의 스레드(thread, 어떠한 프로그램 내에서, 프로세스 내에서 실행되는 흐름의 단위)로 단 1개의 동시성만 다루는 언어이다. 즉, 자바스크립트가 한 번에 1개의 작업만 다룰 수 있다는 의미이다. 자바스크립트는 힙, 큐와 함께 구성하는 단일 콜스택을 가진다.
함수의 호출을 기록하는 자료구조이다. 기본적으로 우리가 프로그램 안에 위치한 곳이다. 만약 어떤 함수를 실행시킨다면, 스택 위에 무언가를 올리는(push) 행위를 하는 것이다. 그리고 함수로부터 반환을 받을 때, 스택의 맨 위를 가져오는(pop) 것이다.
위의 파일을 실행시키면서 처음으로 하는 일은 모든 일이 시작되는 메인 함수를 찾는 일이다. 위에서는 console.log(bar(6));
가 먼저 콜스텍에 올라가게 된다. 그 후에는 bar
함수가 매개변수들과 같이 스택의 top으로 올라가게 되고 foo
함수는 스택의 top으로 올라가다가 곧장 값을 반환하고 빠지게(pop) 된다. 그 후에는 bar
함수와 console.log(bar(6));
구문이 차례로 빠지게 된다. 그리고 마침내 console.log(bar(6));
구문은 값을 출력하게 된다. 이 모든 것들을 짧은 시간(jiffy time, ms[마이크로 세컨드]) 안에 처리하게 된다.
브라우저 콘솔에서 가끔 긴 빨간색 에러 스택들을 볼 수 있다. 보통 이 에러들은 콜스택의 현재 상태를 나타낸다. 그리고 실패한 함수를 스택처럼 top부터 bottom까지 나타내는 것이다.
함수를 재귀적으로 여러 번 부르다가 무한 루프에 빠지는 상황이 발생한다. 크롬 브라우저는 16000 프레임의 제한된 스택을 가지고 있어서 이 범위를 넘어서면 Max Stack Error Reached라는 상태가 되고 실행 중이던 것을 죽인다.
오브젝트(객체)들은 힙 내부에 할당된다. 힙은 거의 구조화되지 않은 영역(unstructured)의 메모리이다. 변수와 객체들의 모든 메모리 할당이 여기서 일어나게 된다.
자바스크립트 런타임은 메시지 큐를 갖고 있다. 메시지 큐는 실행될 콜백함수나 실행될 메시지들에 대한 리스트이다.
스택이 충분한 공간(capacity)을 갖고 있을 때, 메시지는 큐 밖으로 나오게 되고 메시지가 가지고 있던 함수 목록들이 실행된다. 이렇게 초기 스택 프레임이 만들어진다.
스택이 다시 빌 때 메시지 수행도 끝나게 된다. 이벤트에 대한 콜백 함수가 제공되었다고 가정했을 때 이 메시지들은 외부 비동기 이벤트들에 대한 응답으로 큐에 쌓인다. 여기서 외부 비동기 이벤트들이란 마우스 클릭, HTTP 요청 등을 말한다.
하지만, 만일 한 사용자가 버튼을 눌렀는데 아무런 콜백함수도 등록되어 있지 않다면 어떠한 메시지도 큐에 들어가지 않을 것이다.
일반적으로 자바스크립트 코드의 성능을 측정할 때, 스택 안에 있는 함수는 성능을 느리게도 빠르게도 만든다. 만일 console.log()
한 줄만 있다면 코드는 빠를 것이다. 하지만 수백만개가 넘는 for문, while문과 같은 반복문을 수행한다면 코드는 매우 느릴 것이다. 또 이 코드들은 스택을 계속 차지하고 있을 것이다. 이런 것들을 가리켜 Blocking Script
라고 한다.
(Blocking Script
는 Webpage Speed Insights
에 방문하면 쉽게 볼 수 있는 용어다.)
네트워크 요청이나 이미지 요청은 느릴 수 있다. 하지만 서버 요청들은 비동기 함수인 AJAX를 통해 가능하다. 만일 이러한 네트워크 요청들이 동기화 함수를 통해서 이뤄졌다면 어땠을까?
먼저 컴퓨터가 네트워크 요청을 받는다면 그 요청은 또 다른 컴퓨터나 기계와 같은 어떤 서버고 갈 것이다. 네트워크 응답은 응답자의 사정에 따라 아주 많이 느려질 수 있다. 그 동안, 만약 CTA(Call To Action)버튼이나 렌더링이 필요한 무언가를 클릭한다면 스택이 막혀있기 때문에 어떠한 반응도 일어나지 않을 것이다. 루비와 같은 멀티 스레드 언어에서는 이러한 일이 잘 해결될 수 있다. 하지만 싱글 스레드 언어인 자바스크립트는 스택에 쌓인 함수들에서 어떠한 값을 반환하기 전까지는 불가능하다. 따라서 해당 웹페이지는 브라우저가 아무 것도 할 수 없기 때문에 완전히 망가지게 된다.
자바스크립트에서의 동시성 - 한 번에 한 일만 한다. 단, 비동기 콜백들은 제외.
어떻게하면 싱글 스레드로 이상적인 환경을 만들 수 있을까?
가장 쉬운 방법은 비동기 함수를 이용하는 것이다.
비동기 콜백을 이용한다는 것은 코드의 일정 부분을 실행시키고 나중에 실행될 콜백함수를 스택에 넣는 것을 말한다.
개발을 하다보면 AJAX와 같은 비동기형 콜백($.get(), setTimeout(), setInterval(), Promises, ...
)을 마주하게 된다. 사실 노드는 비동기 함수 실행이 전부이다. 모든 비동기 콜백들(console.log(), mathematical operations
)은 코드에서 읽히자마자 바로 실행되지 않고 잠시 후에 실행된다. 그렇기 때문에 동기 함수들과는 달리 바로 스택의 내부로 push될 수 없다.
(😢...비동기 콜백들때문에 movie app에서 데이터가 원하는 순서대로 전달되지 않은 것이겠지... 비동기 콜백에 대해 다시 살펴보고 수정해야겠다...)
그렇다면 비동기 콜백은 어디로 가고 어떻게 다뤄지는 걸까?
위의 코드에서 자바스크립트의 네트워크 액션 요청(request in action)을 살펴보자.
1️⃣ 요청 함수가 실행된다. 요청이 들어온 때에 실행될 콜백으로 onreadystatechange
이벤트 안에 있는 익명의 함수를 넘긴다.
2️⃣ "Script call done"
은 동기 함수로 코딩되어 있기 때문에 바로 콘솔의 output에 들어간다.
3️⃣ 비동기 함수가 실행될 때가 됬을 때, 서버로부터 응답이 오고 body부분을 콘솔에 출력하며 콜백이 실행된다.
응답(response)에서의 호출자(caller)의 분리는 자바스크립트 런타임이 비동기 명령이 완료되고 콜백이 호출될 때까지 기다리는 동안 다른 일을 하는 것을 허용한다.
2️⃣에서는 브라우저 API들이 작동한다. DOM이벤트들, HTTP요청들, setTiemout과 같은 비동기 이벤트들을 다루기 위해 브라우저 내부 C++로 구현된 코드들에 의해 만들어진 기본적인 스레드들(threads)의 API를 호출한다.
(이것들을 이해했다면 앵귤러2에서 런타임 변화 감지를 일으키기 위한 이런 API들을 몽키패치(강제로 오버라이딩하여 프로그램의 행동을 바꾸는 것)하기 위해 Zones가 사용되었다는 것을 알게 된다.)
(😢...이해가 될 것 같으면서 아직 이해가 잘 안된다. 위의 설명이....🤯)
DOM events, HTTP request, setTimeout과 같은 비동기 이벤트들을 다루기 위한 브라우저의 웹 API 스레드들은 브라우저 내부에 C++로 구현되어 만들어졌다.
지금 이 웹 API들은 스스로 자신들의 실행코드를 스택에 넣을 수 없다. 만일 이런 일이 일어났다면 코드 중간에 랜덤하게 나타나게 될 것이다. 위에서 다뤄진 메시지 콜백 큐가 이것을 증명한다.
3️⃣에서 현재 실행 중인 코드가 끝난다면 웹 API 중 어느 하나가 콜백을 큐에 넣는다. 이벤트 루프는 큐 안의 콜백들을 스택이 비었을 때 큐의 첫 번째에 있는 콜백을 스택에 밀어넣는 일을 한다. 다른 메시지가 들어오기 전에 각각의 메시지 또는 콜백들은 작업을 완료한다.
while (queue.waitForMessage()) {
queue.processNextMessate();
}
계속하여 메시지를 기다리다가 다음 메시지를 진행시키는 역할을 한다.
메시지들은 웹 브라우저에서 언제든 이벤트가 발생했을 때 추가된다. 그리고 이벤트들에는 이벤트 리스너가 붙어있다. 만일 리스너가 없다면 발생한 이벤트는 그냥 사라진다. 언제든 웹 브라우저에서 어떤 요소를 클릭했을 때, 클릭 이벤트 핸들러는 큐에 메시지를 추가한다. 웹브라우저의 다른 이벤트들도 동일하다.
이러한 콜백 함수 호출은 콜스택 안에서 초기의 프레임 역할을 한다. 자바스크립트는 싱글스레드이기 때문에, 추가적인 폴링 중 메시지와 프로세싱은 잠시 중단되고 스택에 있는 모든 호출들의 return을 기다리게 된다. 그리고 동기 함수들은 스택에 새로운 콜 프레임들을 추가한다.