Home 자바스크립트는 동기적 언어인가요?
Post
Cancel

자바스크립트는 동기적 언어인가요?

DesktopView

자바스크립트는 동기적 언어인가요?

자바스크립트는 싱글스레드 언어다.

자바스크립트를 나타내는 가장 핵심적인 특징을 선택해야 한다면 한 시점에 한 개의 명령만 처리할 수 있는 싱글스레드라는 점이 아닐까 싶다. 싱글스레드라는 특징은 곧바로 동기적 언어라는 결론으로 이어진다.

하지만 실제 브라우저 위에서 동작하는 JS에서 비동기처리는 매우 빈번하게 발생하고, 심지어 setTimeout과 같은 비동기 함수도 존재한다!

그렇다면 동기적 언어인 자바스크립트에서는 어떻게 비동기처리가 가능한 것일까?
이를 가능하게 하는 구성 요소인 이벤트루프콜스택 그리고 태스크큐웹 API 을 천천히 살펴보자.

호출스택 ( Call Stack )


이벤트루프와 태스크큐 그리고 웹API를 알아보기 이전에 비교적 간단한 호출스택 (a.k.a.콜스택)에 대해서 알아보자.
프로그램의 의도하는 동작을 동기적으로 정확히 실행시키기 위한 데이터 구조라고 볼 수 있으며 수행하는 동작은 다음과 같이 매우 간단하다.

  1. 실행되는 함수와 함수가 필요로하는 실행 컨텍스트를 담는다.
  2. 해당 함수가 종료될 때 꺼낸다. ( return 혹은 종료 )

정말 별 게 없다. 실행중인 함수를 담고, 실행이 끝난 함수를 뺀다. 이게 시작이자 끝이다.
이것을 설명해주는 간단한 예시를 한번 보자.

1
2
3
4
5
6
7
8
function multiply(x, y) {  
    return x * y;  
}
function printSquare(x) {  
    var s = multiply(x, x);  
    console.log(s);  
}
printSquare(5);

Callstack

이미 너무나 간단하고 명확하게 그려진 좋은 설명이미지지만, 글로서 위 코드의 실행순서를 자세하게 설명하자면 다음과 같다.

  1. multiply와 printSquare 가 정의되고 printSquare(5) 가 실행된다.
  2. printSquare 함수가 실행되고 5와 같은 인자를 함께 담은 실행컨텍스트가 콜스택에 담긴다.
    => (PUSH printSquare(5))
  3. 실행되고있는 printSquare 함수 안에서 multiply(x,x) 함수가 호출되어 다시금 콜스택에 담긴다.
    => (PUSH multiply(x,x))
  4. multiply 함수가 실행되고 return문을 통해 x와 y 가 곱해진 값이 반환되어 콜스택에서 꺼내진다.
    => (POP multiply(x,x))
  5. multiply 함수에 의해 반환된 값이 s 에 할당되고 다음 줄로 이동한다.
  6. console.log 라는 함수가 실행되어 콜스택에 담긴다.
    => (PUSH console.log(s))
  7. s 라는 값을 log에 찍고 console.log 함수는 종료되어 콜스택에서 꺼내진다.
    => (POP console.log(s))
  8. 다음 줄로 이동하지만 더이상 실행할 값이 없다. printSquare라는 함수가 종료되며 콜스택에서 꺼내진다.
    => (POP printSquare(5))

콜스택만을 떼어놓고 보자면 이것이 끝이다.
동기적 프로그램을 정확하게 동작시키기 위해서라면, 매우 효율적인 시스템으로 보인다. 하지만 실행하는 데 1분이 걸리는 함수가 여기에 들어오게 된다면?
교착상태(Dead Lock)에 빠져 우리는 1분 동안 아무것도 할 수 없게 된다. 그럴 때 필요한 것이 바로 비동기 처리 이다.

간단하게 콜스택에 대해서 알아보았다. 하지만 콜스택 만으로는 아직 비동기 처리를 할 수 없다. 비동기 처리를 위해 앞서 얘기했던 다른 구성요소와의 상호작용이 필요하다.

다음으로 그 상호작용을 위해 여러가지 비동기처리를 담당하는 구성요소 중 하나인 웹API 에 대해서 알아보자

웹 API


지금 우리에게 필요한 것은 우리의 비동기함수를 처리하고, 완료하면 다시 알려줄 콜백 기능이 필요하다.
이러한 기능을 담당하는것이 브라우저에 내장되어있는 웹 API 이다. 그리고 이 웹 API를 호출하는 것으로 비동기 처리를 위탁할 수 있으며 웹 API는 메인스레드와 독립적으로 동작한다.

자바스크립트는 싱글스레드 언어라면서요!😡

사실 나도 이 부분에서 큰 혼란을 겪었다. 나를 헷갈리게 한 명제들을 정리하자면 다음과 같다.

  • 자바스크립트는 싱글스레드 언어이다. 이건 확실하다.
  • 하지만 웹 API도 분명히 독립적으로 동작한다. 경험을 통해 우린 이미 알고있다.
    이 두가지 완전히 상반되는 명제가 나를 너무 헷갈리게 만들었다.

하지만 역시나 개발자의 알렉산드리아 도서관 Stack Overflow에는 나의 질문을 해소해줄 수 있는 대답이 있었다.
Stack-Overflow 웹 API는 멀티쓰레드인가요? 🔗
질문자는 Web API를 콜스택과 같은 하나의 공간으로 이해하고, 콜스택에서 비동기 처리를 위해 Web API로 이동한 두 개의 함수가 Web API 내부에선 어떻게 비동기처리가 되는지가 궁금했던 모양이다.

Web API는 독립적으로 존재하는 것이 아닌 별도 API의 집합이다.

우선 다시 한번 정리하자면 Web API는 콜스택과 같은 하나의 공간을 점유하는 공간이 아니라 브라우저가 가지고있는 각각의 기능들이 가지고있는 API를 통칭하여 Web API라고 부르는 것이다.
예를 들자면 브라우저의 AJAX 기능이 가지고있는 fetch API, 타이머 기능이 가지고 있는 setTimeout API 와 같은 것이다. 어찌 보자면 API라는 것이 힌트였던 것이다. API처럼 브라우저에게 해당 기능을 요청하는 것이다.

이 정보를 가지고 다시 싱글스레드 논쟁의 두 가지 명제로 돌아가자면 다음과 같이 정리할 수 있다.

  • 자바스크립트는 여전히 싱글스레드 언어가 맞다.
    => 하지만 자바스크립트는 브라우저의 부분집합일 뿐이었다.
  • 웹 API도 독립적으로 동작한다.
    => 하지만 웹 API는 브라우저에서 동작하며, 브라우저는 싱글스레드가 아니다.

즉, 자바스크립트는 브라우저의 일부분인 하나의 스레드에서 동작하는 런타임이고, 브라우저는 멀티스레드이기 때문에 다른 기능들을 대신 처리해 줄 수 있다.

위 정리를 통해 처음에 우리가 필요로 했던 비동기함수 처리를 대신해주는 Web API에 대해서 알아보았다.
하지만 또 다시 새로운 기능이 필요하다. 브라우저가 완료한 비동기호출을 어떻게 콜스택으로 넣을 수 있을까?

완료된 비동기처리를 콜스택으로 옮겨주는 구성요소가 바로 이벤트루프태스크큐다.

이벤트루프와 태스크큐 ( Event Loop & Task Queue )


태스크 큐 - Task Queue

태스크큐를 간단히 요약하자면 앞선 Web API를 통해 처리된 비동기 호출 콜백이 담기는 공간이자 콜스택과 마찬가지로 프로그램을 동기적으로 의도하는 동작을 정확히 실행시키기 위한 데이터 구조이다.
또한 수행하는 동작도 큐 자료구조의 특징인 선입선출에 맞춰 매우 단순하다.

  1. 먼저 처리된 웹 API 비동기요청이 들어온다.
  2. 특정한 조건이 만족될 경우 꺼낸다.

어찌보면 태스크큐는 완료된 비동기 호출이 콜스택으로 이동하기 전 잠시 머무는 공간에 불과하다. 이어 나올 특정한 조건이 더욱 중요하지만, 그것을 알아보기 이전에 태스크 큐의 특징에 대해서 조금만 더 알아보자.
( 혹시 다 알고있고 딱히 궁금하지 않다면 즉시 이벤트루프로 넘어가도 무방하다. )

현재 Task Queue로 뭉뚱그려진 구성요소는 사실 여러종류의 작업을 처리하기 위한 다양한 큐의 집합이다. 가장 큰 분류로는 Macro Task Queue 와 Micro Task Queue 로 나눠 볼 수 있다.

  • Macro Task Queue
    • 주로 비교적 큰 단위의 작업이 여기에 담기며, 예시로 ‘setTimeout’, ‘setInterval’, ‘callback’ 등이 있다.
  • Micro Task Queue ( Job Queue )
    • Macro Task Queue 에 비해 작은 단위의 작업이 여기 담기며, 예시로 ‘Promise’, ‘async function’, ‘MutationObserver’ 등과 같은 작업이 있다.

여기에 추가적으로 굳이 분류를 하자면 Animation Frame에 대한 큐도 존재하는데, 주로 ‘requestAnimationFrame’ API 가 담긴다. 이것은 브라우저가 새롭게 렌더링을 진행할 지 정하는 호출이라고 보면 된다.

이 세가지 큐의 우선순위를 정리하자면 다음과 같다. ( 1번이 최우선순위 )

  1. Micro Task Queue ( Job Queue )
  2. Animation Frame ( Render Queue )
  3. Macro Task Queue

특정한 조건이 만족되면 최고 우선순위부터 순차적으로 큐에서 빠져나가 콜스택에 담기게 된다.

이벤트루프 - Event Loop

앞선 모든 설명이 존재하는 이유이자 이 글의 결론이기도 한 이벤트루프에 대해서 요약하자면 태스크큐와 콜스택을 모니터링하고 큐에서 나온 비동기호출을 콜스택으로 푸시하는 역할을 맡는 구성요소이다.
작성하고 보니 이벤트루프 또한 수행하는 동작은 매우 단순하다.

  1. 콜스택이 비어있는 지 모니터링한다. [ 특정한 조건 ]
  2. 콜스택이 빈다면, 태스크큐를 확인한다.
  3. 태스크큐에 완료된 호출이 있다면 큐의 우선순위에 따라 호출을 콜스택으로 푸시하고 1번으로 돌아간다.

이를 통해 우리가 경험한 모든 동기 및 비동기 작업이 실행된다.

  • 블록스코프의 경우엔 콜스택을 통해 구현되며,
  • 하나의 요청이 길게 콜스택에 쌓여있다면, Animation Frame ( Render Queue ) 에서 렌더링 호출이 나오지 못해 UI가 멈춰있게 되는 것이며,
  • 스크롤 이벤트의 콜백함수에 쓰로틀링을 적용하는 이유 또한 태스크큐에 의도하지 않은 너무 많은 작업이 쌓여버리기 때문이다.

콜스택과 Web API, 태스크큐 그리고 이벤트루프의 연쇄적인 동작을 통해 싱글스레드인 자바스크립트는 동기적인 언어지만 비동기적으로 동작할 수 있게 된다.

결론


EventLoop

사실 각각의 구성요소들을 떼어놓고 보니, 엄청 쉬운 개념인 것 같다.
내가 헷갈리게 된 가장 큰 이유 두가지는
첫째로, 브라우저와 자바스크립트를 동일선상에 두고 생각해서.
둘째로 Web API 를 하나의 독립적 공간으로 이해해서였던 것 같다.

예전에도 이벤트루프에 대해서 공부한 적은 있지만 이렇게 내가 이해한 것을 바탕으로 정리해본건 처음이어서 아마 쉽게 잊어버리지 않을 것 같다. 혹시 잊어버리더라도 이걸 다시 보면 되겠지!

This post is licensed under CC BY 4.0 by the author.