[JavaScript] 비동기 처리

D uuu·2023년 12월 15일
0

JavaScript

목록 보기
3/8

자바스크립트는 싱글 스레드

자바스크립트는 싱글 스레드(single thread) 입니다. 싱글 스레드란 엔진이 단 한개만 있어서 한 번에 하나의 일만 처리할수 있다는 뜻입니다. 여기서 말하는 자바스크립트 엔진은 실행 컨텍스트 스택을 의미합니다.

자바스크립트는 함수를 호출하면 함수 실행 컨텍스트가 생성됩니다. 그리고 호출 된 순서대로 실행 컨텍스트 스택에 푸시되어 쌓이기 시작하는데, 실행 컨텍스트의 최상위 요소인 현재 실행중인 실행 컨텍스트를 제외한 모든 실행 컨텍스트는 모두 대기중인 상태입니다. 다시 말해 제일 최상위에 있는 실행 중인 태스크가 실행이 끝나 스택에서 제거 되면 그 다음 태스크가 실행되기 시작합니다.

const foo = () => {
	console.log("foo함수")
}

const bar = () => {
	console.log("bar함수")
}

foo()
bar()

위 예제의 foo 함수와 bar 함수는 호출된 순서대로 실행 컨텍스트 스택(호출 스택)에 푸시되어 차례대로 실행 됩니다.

  1. 일단 처음 코드를 실행 하는 순간 모든 것을 포함하는 전역 실행 컨텍스트가 생깁니다. 코드를 실행하는데 필요한 환경을 제공하고 코드의 실행 결과까지 관리하는 모든 것을 관리하는 환경이라고 생각하시면 됩니다.

  2. 그리고 순차적으로 foo 함수가 실행 됩니다.
    foo 함수가 실행 되어 콘솔에 "foo함수" 가 찍히고 스택에서 제거 됩니다.

  3. 그 다음 bar 함수가 실행 되어 콘솔에 "bar함수" 가 찍히고 스택에서 제거 됩니다.

  4. 모든 함수가 실행 되고 종료되어 전역 컨텍스트만 남았을때 할 일을 다한 자바스크립트 엔진은 전역 컨텍스트 역시 사라지고 종료됩니다.

이처럼 자바스크립트는 싱글 스레드이기 때문에 동시에 2개 이상의 함수를 실행할 수 없습니다. 그렇기 때문에 처리에 시간이 걸리는 무언가를 실행할 경우 그 작업이 끝날때까지 마냥 기다리는 수 밖에 없습니다. 만약 앞선 foo 함수가 엄청 복잡한 계산을 수행해야 하는 함수라면, 계산이 끝날때까지 bar 함수는 계속 대기중인 상태가 된다는 뜻입니다. 그럼 아래 예제를 한번 봐볼까요 ?

function sleep(func, delay) {
  const delayUntil = Date.now() + delay

  while(Date.now() < delayUntil) {
    func()
  }
}

function foo () {
  console.log("foo")
}

function bar() {
  console.log("bar")
}

sleep(foo, 1*1000)
bar()

sleep 라는 함수는 함수(func)와 delay 라는 매개변수를 받습니다. 이때, Date.now() 라는 현재 시간을 숫자로 반환하는 메서드를 이용해 현재 시간(Date.now()) 에 delay 를 더한 delayUntil 를 구한 다음 현재 시간이 delayUntil 이 될때까지 함수(func)를 호출하도록 했습니다. (while문)

sleep(foo, 1*1000) 으로 sleep 함수를 호출했더니 sleep 함수 내부의 로직이 실행 되면서 foo 함수가 20295 번 호출된 다음에 bar 함수가 호출 되었습니다. bar 함수는 sleep 함수의 실행이 끝날때까지 계속 대기 중인 상태였다가 sleep 함수가 끝나 실행 컨텍스트 스택에서 사라지자 bar 함수가 실행 될 수 있었습니다.

이처럼 현재 실행 중인 태스트(task) 가 종료할 때까지 다음에 실행 될 태스크가 대기하는 방식을 동기 처리라고 합니다.

동기 처리 방식은 태스크를 순서대로 하나씩 처리하므로 순서가 보장된다는 장점이 있지만, 앞선 태스크가 종료할때까지 이후 태스크들은 블로킹(작업중단) 된다는 단점이 있습니다.

function foo() {
  console.log('foo')
}

function bar() {
  console.log("bar")
}

setTimeout(foo, 3* 1000)
bar()

이번에 setTimeout 함수를 사용했을 땐 결과가 어떻게 나올까요 ? setTimeout 를 이용해 3*1000 (3초) 가 경과한 이후 foo 함수가 실행 되도록 했습니다.

그랬더니 bar 함수가 먼저 실행 되어 콘솔에 bar 가 찍히고 나서 3초 후에 foo 함수가 실행 되어 콘솔에 foo 가 찍혔습니다. 사진 상으로는 bar, foo 가 연달아 나와보이지만, 실제로 콘솔에 찍오보면 bar 가 찍힌 이후 3초 뒤에 foo 가 찍힙니다.

순서상 setTimeout 이 bar 보다 먼저 호출 되었기 때문에 3초 뒤에 foo 함수가 실행 되고 그 다음 bar 함수가 실행 될 것 처럼 보이지만, 이번에는 bar 함수는 앞선 실행을 기다리지 않고 먼저 호출 되어 실행 되었습니다.

이처럼 현재 실행 중인 태스크가 종료 되지 않은 상태라해도 다음 태스크를 바로 실행 하는 방식을 비동기 처리라고 합니다.

비동기 처리 방식은 현재 실행 중인 태스크가 종료되지 않은 상태라 해도 다음 태스크를 곧바로 실행 하므로 블로킹(작업 중단)이 발생하지 않는다는 장점이 있지만, 태스크의 실행 순서가 보장되지 않는다는 단점이 있습니다.

setTimeout, setInterval, HTTP 요청, 이벤트 핸드러는 비동기 처리 방식으로 동작합니다. 따라서 위에 예제도 setTimeout 를 사용했기 때문에 비동기 처리로 동작했던 것 입니다.


다시 한번 자바스크립트는 싱글 스레드 이다.

다시 한번 자바스크립트는 싱글 스레드이기 때문에 자바스크립트 엔진은 하나의 호출 스택만을 사용합니다. 이는 한번에 한가지 일만 처리할 수 있음을 의미하며 이런 방식을 동기 처리 방식이라 합니다.

그럼 이제 비동기 처리 방식에 대해 생각해봅시다. 앞서 setTimeout, setInterval, HTTP 요청, 이벤트 핸드러 요청은 비동기 처리 방식으로 동작한다고 했었는데요. 비동기 요청은 어떤 식으로 동작하는 걸까요 ?

비동기 처리를 위한 브라우저와 Node.js 의 구조

아래 그림은 브라우저 환경을 간단하게 표현 한 것입니다. 그림에서 볼 수 있듯이 자바스크립트 엔진에는 Heap, Call Stack 이 있고, setTimeout 이나 http 통신 등 비동기 처리와 관련된 것들은 자바스크립트 엔진이 아닌 Web API 영역에 따로 정의 되어 있는 것을 알 수 있습니다.

이벤트 루프`콜백 큐와 같은 장치도 자바스크립트 엔진 외부에 구현되어 있습니다. 지금까지 자바스크립트는 싱글 스레드라고 여러번 강조를 했었는데요. 더 정확하게 표현하자면, 자바스크립트는 단 하나의 호출 스택(Call Stack) 을 사용한다는 점입니다. 실제로 자바스크립트가 구동되는 환경(브라우저, Node.js) 에서는 주로 여러개의 스레드가 사용되며, 이러한 구동 환경이 단일 호출 스택을 사용하는 자바스크립트 엔진과 연동 하기 위해 사용하는 장치가 바로 이벤트 루프 입니다.

비동기 처리 방식 이해하기

앞에 예제를 다시 살펴봅시다. setTimeout 함수가 먼저 호출되었음에도 불구하고 bar 함수 내부에 콘솔이 먼저 찍히고 3초 뒤에 foo 함수가 실행 되었는데요.

function foo() {
  console.log('foo')
}

function bar() {
  console.log("bar")
}

setTimeout(foo, 3* 1000)
bar()

실행 순서를 나타내면 아래와 같습니다.

  1. 처음 코드를 실행 하는 순간 모든 것을 포함하는 전역 실행 컨텍스트가 생깁니다.

  1. 그러고 나서 setTimeout 함수가 먼저 실행이 됩니다.
    그런데 setTimeout 은 비동기 함수이므로 호출 스택에 쌓이는 것이 아니라Web API(노드인 경우 백그라운드) 를 호출 하게 됩니다.

Web API는 DOM 이벤트, setTimeout, HTTP 통신 등 비동기 작업 등을 수행하며, 콜백 함수를 태스크 큐에 밀어 넣는 역할을 합니다.

  1. 그 다음 bar 함수가 실행 되어 호출 스택에 쌓이게 됩니다. bar 함수가 실행 되어 콘솔에 "bar" 가 찍히고 나면 호출 스택에서 제거 됩니다.

  1. bar 함수가 호출 스택에서 쌓이고 제거 되는 동안에도 Web API 에서 setTimeout 은 3초를 세고 있습니다. 그리고 setTimeout 의 콜백함수를 태스크 큐로 밀어 넣습니다.

  1. 호출 스택에 어떠한 함수도 존재하지 않을때 이벤트 루프는 태스크 큐 에 대기중인 함수가 있는지를 살펴봅니다. 아까 setTimeout 함수의 콜백 함수를 태스크 큐에 넣었기 때문에 태스크 큐에 setTimeout 함수가 있겠네요. 그럼 이벤트 루프는 태스크 큐에 있는 함수를 순차적으로 호출 스택으로 올려 보냅니다.

  1. 호출 스택으로 이동한 setTimeout 함수는 실행이 되어 콘솔에 'foo' 가 찍히고 호출 스택에서 제거 됩니다.

  2. 더이상 호출 스택과 태스크 큐에 대기중인 함수가 없을때 비로소 모든 함수가 실행이 되었다고 판단된 자바스크립트 엔진 역시 종료됩니다.


동기와 비동기 처리 방식 - 정리

지금까지 동기 처리 방식을 설명할때는 호출 스택에 함수가 차례대로 쌓이고 하나씩 실행 되고 제거 되는 방식으로 순서가 보장 되었다면, 비동기 함수는 이벤트루프와 태스크 큐라는 브라우저와 Node.js 환경에서 자바스크립트의 엔진(호 출스택) 과 어떻게 연동하여 작동하는지까지 살펴봐야 했습니다.

비동기 함수인 setTimeout 은 호출 스택에 바로 쌓이지 않고 Web API 로 이동된 후에 태스크 큐에 쌓이게 됩니다. 호출 스택에 모든 함수들이 실행 되고 종료되어 아무것도 남지 않았을때 이벤트 루프가 이를 감지하고 태스크큐에서 대기중인 함수를 하나씩 호출 스택으로 끌어 올리기 시작합니다. setTimeoue 함수는 호출 스택으로 이동이 된 후에야 비로소 함수가 실행 되므로 우리가 코드를 작성했을때와는 다른 순서로 실행 되는 것입니다.
이는 순서를 보장 받지 못한다는 단점이기도 하지만, setTimeoue 처럼 무언가를 기다렸다가 함수를 실행해야 할때 모든 처리가 끝날때까지 기다리지 않아도 된다는 장점이 있습니다.





도움 받은 곳

profile
배우고 느낀 걸 기록하는 공간

0개의 댓글