자바스크립트 딥다이브 - 비동기 프로그래밍

ChoiYongHyeun·2024년 1월 1일
0
post-thumbnail

동기 처리와 비동기 처리

동기 처리

이전 실행 컨텍스트 부분에서 이야기 하였듯 자바스크립트는 싱글 스레드 처리 방식으로

실행 컨텍스트에서 호출된 코드를 콜스택에 담고 처리 후에는 콜스택 에서 제거한다고 하였다.

function foo() {}
function bar() {}

foo();
bar();

만일 다음과 같이 존재할 경우 콜스택의 변화는 다음과 같다.

  • 전역 실행 컨텍스트
  • 전역 실행 컨텍스트 -> foo
  • 전역 실행 컨텍스트 (foo는 실행 된 후 컨텍스트에서 제거 된다)
  • 전역 실행 컨텍스트 -> bar
  • 전역 실행 컨텍스트 (bar는 실행 된 후 컨텍스트에서 제거 된다)

그럼 함수 내에서 함수를 호출하는 중첩 함수의 경우는 어떻게 될까 ?

function foo() {
  console.log('foo!');
}
function bar() {
  foo();
  console.log('bar!');
}
function cha() {
  console.log('cha!');
}

bar();
cha();

다음 같은 경우에서 콜스택의 변화는 다음과 같다.

  • 전역 실행 컨텍스트
  • 전역 실행 컨텍스트 -> bar
  • 전역 실행 컨텍스트 -> bar -> foo
  • 전역 실행 컨텍스트 -> bar
  • 전역 실행 컨텍스트
  • 전역 실행 컨텍스트 -> cha
  • 전역 실행 컨텍스트

bar 함수가 콜스택에 담겨 실행 되었을 때 bar 함수가 foo 함수를 호출하여 콜스택에 foo 함수가 담겼다.

이처럼 자바스크립트 엔진은 싱글 스레드 방식이기 때문에 함수가 호출된 순서에 따라 함수의 컨텍스트를 모두 실행 한 후 콜스택에서 제거하고, 콜스택이 비어야 다음 함수를 담을 수 있다.

그로 인해 콜스택에 담긴 특정 함수가 콜스택에서 처리 기간이 긴 경우, 다음 콜스택에 담긴 함수는 실행되지 않는 블로킹이 일어나곤 한다.

설명에서 계속하여 함수라고 이야기 하였으나 , 꼭 함수일 필요는 없다. 실행 컨텍스트를 갖는 코드 문이면 모두 콜스택에 담긴다.
다만 원활한 이해를 위해 함수라고 표현하였다.

예를 들어 다음과 같은 예시를 살펴보자

const foo = (delay) => {
  const threshold = Date.now() + delay;
  while (Date.now() < threshold);
  console.log('foo!');
};

const bar = () => {
  console.log('bar!');
};

foo(3 * 1000); // 3초가 지난 후 .. foo!
bar(); // bar!

foo 함수는 실행 되면 콜스택에 담겨 함수의 컨텍스트가 종료 될 때 까지 delay 만큼의 시간이 걸린다.

threshold 로 선언된 시간이 오기 전까지는 while 문을 끊임없이 돈다.

그런 이유로 콜스택에서 foo 가 담긴 후 foo 의 모든 실행 컨텍스트가 실행되어 제거되기 전까지 bar 는 콜스택에 담기지 못하고 대기한다.

이처럼 이전 콜스택에 담긴 함수가 제거되기 전까지 콜스택에 올라가지 못하는 행위를 블로킹 당하였다고 한다 .

이런 자바스크립트 엔진의 처리 방식을 동기 처리(syncronous processing) 라고 한다.

동기 처리란 특정 행위가 완료 될 때까지 모든 행동을 중단한다는 것이다. 이런 동기 처리 방식은 실행된 순에 따라 처리되기 때문에 예측이 쉽다는 장점이 존재하지만

반대로 하나의 작업이 오래 걸리는 경우 모든 행위들이 막힌다는 단점이 존재한다.

특히 네트워크 요청이나 입출력 등과 같은 I/O 요청이 있을 때 더 치명적이다.


비동기 처리

그럼 위의 코드를 setTimeout 을 이용해서 실행해보자

const foo = () => console.log('foo!');
const bar = () => console.log('bar!');

setTimeout(foo, 3 * 1000);
bar(); // bar!
// 3초가 흐른 후 .. foo!

setTimeout 으로 함수를 실행하니 위에서 설명했던 자바스크립트 엔진의 처리 방식을 무시하는 듯 foo 의 호출이 완료 되기 전에도 bar 이 호출되었다.

이처럼 setTImeout 과 같이 특정 실행이 완료 되기 전에도 다른 함수 호출이 가능한 방식을 비동기 처리 (asyncronous process) 라고 한다.

그럼 setTImeout 은 자바스크립트 엔진의 동기 처리 방식을 비동기 처리 방식으로 변하게 만들어버리는걸까 ?

반은 맞고 반은 틀립니다. 마치 비동기 처리처럼 보이게 합니다.

태스트 큐이벤트 루프 를 이용해용

테스크큐 & 이벤트 루프

이전 챕터에서 말하였지만 setTimeout , setInterval 은 자바스크립트에서 내장된 빌트인 객체가 아닌 브라우저 , Node.js 에서 제공하는 호스트 객체라고 하였다.

브라우저에서 소스 코드가 평가되어 실행 될 때

자바스크립트 엔진은 소스코드에서 선언된 변수들을 저장할 힙 공간 과 실행 컨텍스트들을 관리 할 콜스택 을 갖는다.

콜스택 에서 실행 컨텍스트 별로 소스코드를 평가 및 실행하고 , 그 안에서 발생한 변수들을 힙공간 에서 관리한다.

이 때 콜스택동기 처리 방식을 엄격히 지킨다.

setTImeout , setInterval 등과 같이 브라우저에서 제공하는 호스트 객체 혹은 호스트 메소드 처럼 윕에서 제공하는 web API 를 관리하는 영역 또한 존재한다.

비동기 함수로 호출된 콜백 함수들을 FIFO 형식으로 관리하는 태스크 큐 (Task Queue)

비동기 함수에서 선언된 콜백함수 별 딜레이 기간과 , 콜스택이 비었는지 확인하는 이벤트 루프가 존재한다.

브라우저의 이벤트 루프는 비동기 함수에서 선언된 달레이 기간이 지나면 태스크 큐 에 저장되어 있는 콜백 함수 를 자바스크립트 엔진이 관리하는 콜스택 이 비었을 때 해당 콜백 함수를 콜스택에 푸쉬한다.

이 때 콜스택 이 아직 비어있지 않다면 (콜스택에 담긴 함수가 실행중이라면) 대기하다가 콜스택에 담긴다.

태스크큐는 정말 FIFO 인가요

좀 더 엄밀히 말하면 우선순위 큐 에 해당한다고 생각한다. 비동기 함수에서 설정된 딜레이 기간이 가장 높은 우선순위를 가지고 , 딜레이 기간이 짧은 콜백 함수부터 우선적으로 실행 된다.

예시를 통해 이해해보자

const foo = () => console.log('foo!');
const bar = () => console.log('bar!');

setTimeout(foo, 1.5 * 1000);
bar(); //bar! 
// 3초 후 .. foo ! 

시간의 흐름에 따른 변화를 살펴보자 , 예시로 쓰인 시간의 흐름은 설명을 위해 간편하게 쓴 내용이므로 정확한 시간은 맞지 않다. 그냥 흐름에 따라 이렇게 된다는 것만 이해해보자

  • 0초
    콜스택 : 전역 실행 컨텍스트
  • 1초
    콜스택 : 전역 실행 컨텍스트 -> setTImeout(foo,3 * 1000)
  • 1.1초
    setTImeout(foo,3 * 1000) 가 평가되어 브라우저가 관리하는 태스크 큐foo 함수를 담는다. 그리고 이벤트 루프 에서는 foo 함수의 딜레이가 3 * 1000 임을 기억한다.
    비동기 함수가 실행 되었을 때는 병렬적으로 처리된다. 일하는 놈이 두명이니까
    콜스택 : 전역 실행 컨텍스트 [자바스크립트 엔진이 관리함]
    태스크 큐 : foo [브라우저가 관리함]
  • 1.2초
    콜스택 : 전역 실행 컨텍스트 -> bar
    태스크 큐 : foo [이벤트 루프가 foo 의 딜레이 기간을 기다리는 중 ..]
  • 1.3초
    콜스택 : 전역 실행 컨텍스트 (bar 가 실행됨 , bar!) 태스크 큐:foo[이벤트 루프가foo` 의 딜레이 기간을 기다리는 중 ..]
    ...
  • 1.5초
    콜스택 : 전역 실행 컨텍스트
    태스크 큐 : foo [이벤트 루프가 foo 의 딜레이 기간이 끝난 걸 확인 , 콜스택도 끝난걸 확인]
  • 1.51초
    콜스택 : 전역 실행 컨텍스트 -> foo
    태스크 큐 : 비었음
  • 1.52초
    콜스택 : 전역 실행 컨텍스트 (foo 가 실행되어 콜스택에서 사라짐)

이처럼 비동기 함수로 선언된 콜백 함수들은 브라우저가 관리하는 태스크 큐에 담기고

브라우저가 관리하는 이벤트 루프 를 통해 콜스택 으로 이동하여 딜레이 기간 이후 실행된다.

자바스크립트 엔진 측에서는 비동기 함수 (setTImeout) 만을 평가 한 후 콜스택에서 제거해버리기 때문에 기다릴 필요 없이 다음 함수들을 실행을 할 수 있는 것이다.

힙 공간 , 콜스택 을 관리하는 자바스크립트 엔진과 태스크 큐 , 이벤트 루프 를 관리하는 브라우저의 병렬적인 협업을 통해 우리는 싱글 스레드 엔진인 자바스크립트 엔진에서 마치 비동기 처리를 흉내 낼 수 있다.

그런데 비동기 함수는 항상 완벽하게 시간을 지키지는 못한다.

내가 setTimeout 에서 딜레이를 1초로 설정했다고 해서 무조건적으로 1초 후에 해당 콜백 함수가 실행되는 것이 아니다.

이벤트 루프 는 해당 콜백 함수의 딜레이 기간을 기다리지만 딜레이 기간이 종료되어도 콜스택 이 비어있지 않는다면 대기한다.

const foo = () => console.log('foo!');
const bar = (delay) => {
  const threshold = Date.now() + delay;
  while (Date.now() < threshold);
  console.log('bar!');
};

setTimeout(foo, 1 * 1000);
bar(2 * 1000);
//bar!
//foo!

비동기 함수로 foo 는 실행되면 태스크 큐에 옮겨졌다가 1초 이후에는 다시 콜스택으로 옮겨지기를 기대하였으나

현재 콜스택에는 bar 가 2초동안 블로킹 하고 있기 때문에 foo 는 담기지 못하고 대기한다.

그로 인해 foobar 가 종료 된 후에나 콜스택에 담길 수 있었다.

자바스크립트에서의 비동기 처리는 자바스크립트 엔진브라우저 의 병렬적인 협업을 통해 비동기 처리를 지원하는구나

구우웃~~!

profile
빨리 가는 유일한 방법은 제대로 가는 것이다

0개의 댓글