⏱ 비동기 함수의 동작 원리,Web API & 이벤트 루프 & 태스크 큐

김철준·2023년 2월 23일
5

Javascript

목록 보기
12/16

🤔 자바스크립트에서 비동기 함수가 동작할 수 있는 원리는 무엇일까?

우리가 자바스크립트로 웹 프로그래밍을 할 때 이벤트 핸들러(ex onClick,onScroll),Http 요청(XMLHttpRequest,fetch,axios),타이머(setTimeout,setInterval) 와 같은 Web API를 대부분 많이 사용할 것이다.

이들은 모두 비동기로 동작하는 함수이며 동시에 실행될 수 있는 함수이다.

예를 들어 클릭 이벤트, 데이터를 요청하고 응답받아오기,타이머 설정과 만료와 같은 동작들이 동시에 이루어질 수 있다.

하지만 위 동작들이 동시에 이루어진다고 해서 동작들에 대한 콜백함수들은 동시에 실행된다는 것은 아니다.
이에 대한 자세한 내용은 뒤에서 설명하겠다.

하지만 아시다시피 자바스크립트만 따지고 본다면 싱글쓰레드 언어이기때문에 동기적으로 작동한다.

싱글쓰레드 : 한 번에 하나의 태스크만을 실행할 수 있는 방식
멀티쓰레드 : 한 번에 여러개의 태스크를 실행할 수 있는 방식

때문에 브라우저와 node.js환경과 상호작용하지 않고 자바스크립트만 따로 실행한다면 비동기적으로 동작할 수 없다.

하지만 위 경우의 수는 성립될 수 없다.
브라우저와 node.js 환경이 아니라면 자바스크립트는 실행되지않기 때문이다.

그렇다면 자바스크립트에서 실행되는 비동기 함수들은 어떻게 동작하길래 비동기로 동작하며 Web API와 같은 비동기 함수는 어떻게 동시에 실행될 수 있는것일까?

이는 브라우저와 Node.js라는 환경이 있기에 가능한 것이고 이 환경에는 WebAPI&Node.js API,이벤트 루프,태스크 큐라는 기능이 내장되어있다.

브라우저와 Node.js는 자바스크립트 엔진을 구동할 수 있는 환경이다.
여기서는 브라우저 관점에서의 기능으로써 web API,이벤트 루프,태스크 큐를 알아보겠다.

우선 비동기 함수가 무엇인지 이해하기위해 동기 함수와 비동기 함수를 먼저 정의하여 비교해보도록 하고 그 다음에는 비동기가 함수가 동작하는 원리를 Web API,이벤트 루프, 태스크 큐 중심으로 자세히 살펴보도록 하자.

집중해야할 포인트!
비동기 함수의 동작 원리

동기 함수

동기 함수란 말 그대로 동기적으로 실행되는 함수이다.
동기적으로 실행된다라는 것은 위에서 아래 방향으로 순서대로 실행된다라는 뜻이다.

let result = 0;

function a() {
  console.time();
  for (let i = 0; i < 100; i++) {
    result += i;
  }

  console.timeEnd();
}

function b() {
  console.time();

  for (let i = 0; i < 100; i++) {
    result += i;
  }
  console.timeEnd();
}

function c() {
  console.time();

  for (let i = 0; i < 100; i++) {
    result += i;
  }
  console.timeEnd();
}

function d() {
  console.time();

  for (let i = 0; i < 100; i++) {
    result += i;
  }
  console.timeEnd();
}

function e() {
  console.time();

  for (let i = 0; i < 100; i++) {
    result += i;
  }
  console.timeEnd();
}

a();
b();
c();
d();
e();

자바스크립트는 인터프리터 언어이고 위에서부터 아래로 실행된다.

위처럼 함수를 호출한대로 a,b,c,d,e 순서로 호출이 된다.

하지만 위 방식은 문제의 가능성이 있다.

위 a,b,c,d,e라는 함수는 눈 깜짝할 사이에 실행되는 간단한 함수이지만 c라는 함수가 오래걸리는 함수라면 어떨까?

d,e함수는 실행되기위해서 오랜 시간을 기다려야한다.

동기 함수의 문제점 : c라는 진상 함수..


기차표 티켓팅을 하기 위해 사람들이 역창구에서 줄을 서있다고 가정해보자.

이 동기 처리 방식 라인 창구는 진상 손님이 있더라도 해당 손님을 해결하고 나서 다음 손님을 받을 수 있는 다소 유도리가 없는 처리 방식을 가지고 있다.

이 라인에서 a,b,c,d,e라는 사람이 줄을 서있는데 c라는 손님은 진상 손님이다.

c 손님의 티켓 요금은 10000원인데 이를 10원짜리로 주려하며 자기 차례가 오자 창구 앞에서 10원을 일일히 세고 있다고 해보자.

a,b라는 손님은 괜찮지만 d,e 손님은 아주 죽을 맛일 것이다.

코드로 표현하면 다음과 같을 것이다.

다음 예제를 보자.

let consumption_time = 0;

function person_a() {
  console.time();
  for (let i = 0; i < 100; i++) {
    consumption_time += i;
  }
  console.log("a가 티켓팅하는데 걸린 소요 시간:", consumption_time);
  console.timeEnd();
}

function person_b() {
  console.time();

  for (let i = 0; i < 100; i++) {
    consumption_time += i;
  }
  console.log("b가 티켓팅하는데 걸린 소요 시간:", consumption_time);

  console.timeEnd();
}

function fuckin_person_c() {
  console.time();

  for (let i = 0; i < 1000000; i++) {
    consumption_time += i;
  }
  console.log("c가 티켓팅하는데 걸린 소요 시간 시간:", consumption_time);

  console.timeEnd();
}

function person_d() {
  console.time();

  for (let i = 0; i < 100; i++) {
    consumption_time += i;
  }
  console.log("d가 티켓팅하는데 걸린 소요 시간:", consumption_time);

  console.timeEnd();
}

function person_e() {
  console.time();

  for (let i = 0; i < 100; i++) {
    consumption_time += i;
  }
  console.log("e가 티켓팅하는데 걸린 소요 시간:", consumption_time);

  console.timeEnd();
}

person_a();
person_b();
fuckin_person_c();
person_d();
person_e();

이 c라는 진상 손님 때문에 d,e 손님은 엄청난 시간을 기다려야한다.
차라리 d,e 손님을 티켓팅해주고 c손님을 처리하면 좋을텐데 말이다.

이처럼 자바스크립트에서는 실행 중간에 처리 시간이 오래 걸리는 함수가 있으면 금방 실행될 수 있는 뒷 함수들도 오랜 시간을 기다려야한다.

그렇기에 이 d,e 손님을 위해 비동기 처리 방식이라는 나이스한 시스템이 마련되어있다.

비동기

비동기는 위와 같은 상황을 해결해줄 수 있는 시스템이다.

비동기 함수는 동기 함수와 달리 순서대로 실행되지 않고 비동기적으로 실행된다.

c라는 진상 손님이 있어도 d,e 손님은 피해를 보지 않고 기존대로 티켓팅을 진행할 수 있는 것이다.


이를 코드로 살펴보자.

let consumption_time = 0;

function person_a() {
  console.time();
  for (let i = 0; i < 100; i++) {
    consumption_time += i;
  }
  console.log("a가 티켓팅하는데 걸린 소요 시간:", consumption_time);
  console.timeEnd();
}

function person_b() {
  console.time();

  for (let i = 0; i < 100; i++) {
    consumption_time += i;
  }
  console.log("b가 티켓팅하는데 걸린 소요 시간:", consumption_time);

  console.timeEnd();
}

function fuckin_person_c() {
  console.time();

  for (let i = 0; i < 1000000; i++) {
    consumption_time += i;
  }
  console.log("c가 티켓팅하는데 걸린 소요 시간 시간:", consumption_time);

  console.timeEnd();
}

function person_d() {
  console.time();

  for (let i = 0; i < 100; i++) {
    consumption_time += i;
  }
  console.log("d가 티켓팅하는데 걸린 소요 시간:", consumption_time);

  console.timeEnd();
}

function person_e() {
  console.time();

  for (let i = 0; i < 100; i++) {
    consumption_time += i;
  }
  console.log("가 티켓팅하는데 걸린 소요 시간:", consumption_time);

  console.timeEnd();
}

person_a();
person_b();
setTimeout(() => {
  fuckin_person_c();
}, 3000);
person_d();
person_e();

동기 처리 섹션 예시와 다른 부분은 c 진상 함수에게 setTimeout를 적용해줬다.

c 진상 함수는 무조건 a,b,d,e 함수가 실행된 이후에 실행될 수 있다.

setTimeout은 web API로써 타이머 설정을 할 수 있는 비동기 함수이다.

위와 같이 처리함으로써 d,e 손님은 c라는 진상 손님때문에 기다릴 필요없다.

이처럼 비동기 함수를 사용하면 처리 시간이 오래걸리는 함수를 미뤄 금방 해결할 수 있는 함수부터 처리할 수 있다.

다만 알아둬야할 것은 비동기 함수는 처리 순서를 뒤로 미루는 것이지 함수 실행 시간을 줄여주는 것은 아니다.
진상 c는 어차피 맨 뒷차례에서 10원을 계속 세며 오랜 시간을 소요할 것이다.

비동기 함수가 동작하는 원리

앞에서 비동기 함수는 순서를 뒤로 미뤄줄 수 있도록 한다하였다.

그렇다면 자바스크립트는 어떻게 비동기 함수를 뒤로 미뤄서 처리할 수 있는 것일까?

그 이유는 자바스크립트 혼자만이 처리하는 것이 아니기 때문이다.
이면에는 브라우저라는 조력자가 있다.

브라우저는 이 진상 c와 같은 함수들을 골라주어 비동기 처리하여 잠시 데리고 있기도 하고 처리할 부분이 있으면 처리해주기도 한다.
그리고 브라우저라는 조력자는 필요한 부분은 동시에도 처리가 가능하다.

무조건 동시에 처리되는 것은 아니다.

구체적으로 알아보기 위해 자바스크립트의 동작과 브라우저의 기능을 표현한 그림으로 살펴보자.

콜스택,Web API(feat.백그라운드), 이벤트 루프, 태스크 큐

내가 만든 것이므로 그림이 뭔가 이상해도 양해를 부탁한다.

위 그림에 있는 용어들은 자바스크립트의 동작과 브라우저의 기능과 관련된 용어이다.

위 그림에 있는 용어들을 하나씩 알아보자.

콜스택(실행 컨텍스트 스택)

콜 스택이란 실행 컨택스트 스택이라고도 불린다.

이 콜스택은 자바스크립트에서 중요한 개념이라 나중에 따로 포스팅하겠지만 일단 간단히 설명하자면 자바스크립트의 함수 실행 순서를 결정해주는 공간이라고 생각하면 된다.

함수를 호출하면 해당 함수에 대한 실행 컨텍스트가 콜스택으로 들어가고(push) 함수가 종료되었을 경우 실행 컨택스트는 사라진다(pop).

콜스택은 스택 자료구조를 가지고 있다.

아까 처음에 살펴봤던 동기 처리 방식을 떠올려보자.
아래 코드는 동기 처리 방식의 코드이다.


let result = 0;

function a() {
  console.time();
  for (let i = 0; i < 100; i++) {
    result += i;
  }

  console.timeEnd();
}

function b() {
  console.time();

  for (let i = 0; i < 100; i++) {
    result += i;
  }
  console.timeEnd();
}

function c() {
  console.time();

  for (let i = 0; i < 100; i++) {
    result += i;
  }
  console.timeEnd();
}

function d() {
  console.time();

  for (let i = 0; i < 100; i++) {
    result += i;
  }
  console.timeEnd();
}

function e() {
  console.time();

  for (let i = 0; i < 100; i++) {
    result += i;
  }
  console.timeEnd();
}

a();
b();
c();
d();
e();

위와 같이 코드가 구성되어 해당 파일이 브라우저 위에서 실행된다면 콜스택에서 다음과 같은 일들이 일어난다.

콜 스택이라는 장소에 a라는 함수의 실행 컨택스트가 들어가고(push) 실행된 뒤에는 제거(pop)된다.

또 다음 함수 b라는 함수의 실행 컨택스트가 콜 스택에 들어가고 제거된다.

이러한 반복으로 e함수까지 push-pop되며 콜스택에서 마무리가 된다.

동기 처리 방식일 때는 위와 같이 순차적으로 push-pop이 진행된다.

하지만 비동기 처리 방식일때는 위 방식과 다르다.
브라우저의 기능들이 추가된다.

이 브라우저의 기능이란 Web AP, 이벤트 루프, 태스크 큐이다.

처음 봤던 그림을 다시 한번 보자.

그리고 비동기 함수가 실행되었을 때 위 그림이 어떻게 변경되는지 살펴보자.

일단 아래는 비동기 함수 코드이다.

let consumption_time = 0;

function person_a() {
  console.time();
  for (let i = 0; i < 100; i++) {
    consumption_time += i;
  }
  console.log("a가 티켓팅하는데 걸린 기다린 시간:", consumption_time);
  console.timeEnd();
}

function person_b() {
  console.time();

  for (let i = 0; i < 100; i++) {
    consumption_time += i;
  }
  console.log("b가 티켓팅하는데 걸린 기다린 시간:", consumption_time);

  console.timeEnd();
}

function fuckin_person_c() {
  console.time();

  for (let i = 0; i < 1000000; i++) {
    consumption_time += i;
  }
  console.log("c가 티켓팅하는데 걸린 기다린 시간 시간:", consumption_time);

  console.timeEnd();
}

function person_d() {
  console.time();

  for (let i = 0; i < 100; i++) {
    consumption_time += i;
  }
  console.log("d가 티켓팅하는데 걸린 기다린 시간:", consumption_time);

  console.timeEnd();
}

function person_e() {
  console.time();

  for (let i = 0; i < 100; i++) {
    consumption_time += i;
  }
  console.log("e가 티켓팅하는데 걸린 기다린 시간:", consumption_time);

  console.timeEnd();
}

person_a();
person_b();
setTimeout(() => fuckin_person_c(), 3000);
person_d();
person_e();

c라는 함수만 setTimeout을 적용함으로써 비동기로 실행되어 맨 마지막에 실행된다.

setTimeout은 web API로써 비동기로 실행되는 함수이다.

비동기 함수의 실행 과정(자바스크립트 with 브라우저)

위 비동기 함수 실행 순서를 설명하면 다음과 같다.

  1. a 함수,b 함수, setTimeout 함수,d 함수,e 함수가 차례로 콜스택에 push-pop 과정이 반복된다.
  1. 이 과정에서 setTimeout 함수가 실행되면서 콜백함수와 타이머는 백그라운드로 이동하게 된다.

    setTimeout 함수가 이동하는 것이 아니다.

  1. 백그라운드는 콜백 함수에 대하여 3초의 타이머 설정해주고 만료가 되면 태스크 큐라는 장소로 보내준다.

  1. 그리고 이벤트 루프라는 기능은 a,b,d,e 함수가 콜스택에서 모두 제거되었는지 확인하고 아무것도 없다면 c의 콜백함수를 태스크 큐에서 꺼내어 콜스택으로 가져다준다.

  1. 그리고 콜스택에 있는 c의 콜백 함수가 실행된다.

이처럼 비동기 함수가 실행되면 위처럼 자바스크립트 엔진과 브라우저의 기능이 쿵짝을 맞춰 콜백 함수를 관리한다.

그렇다면 쿵짝을 맞추는 위 브라우저 기능 3가지에 대하여 알아보자.

Web API(feat.백그라운드)

web API란 브라우저에서 제공하는 API로써 setTimeout,이벤트 핸들러,ajax 요청과 같은 함수이다.이들은 모두 비동기 함수이다.

  • 그리고 이들은 백그라운드내에서 동시에 실행될 수 있다.

동시에 실행될 수 있다는 말은 위 비동기 함수들의 콜백 함수가 동시에 실행된다라는 말이 아니다.

setTimeout의 타이머 설정과 만료,클릭이라는 이벤트 처리, 서버로 네트워크 요청과 같은 동작들이 동시에 실행될 수 있는 것이다.

또한 위와 같은 동작들은 자바스크립트 엔진에서 해결해줄수 있는 부분이 아니며 브라우저가 있기에 실행될 수 있는 부분들이다.

그렇기 때문에 포스팅 초반에 web API가 동시에 실행될 수 있다고 언급한 것이다.

  • 뿐만 아니라 이들은 타이머가 만료되었을 때의 콜백함수,클릭이란 이벤트가 발생했을 때의 콜백 함수, 네트워크에 대한 응답에 대한 콜백 함수를 태스크큐로 보내주는 역할도 한다.

태스크 큐

태스크 큐는 비동기 함수의 콜백 함수를 보관해주는 역할을 한다.
백그라운드에서 web API에 대한 콜백 함수를 전달해주면 태스크 큐는 이 콜백 함수들을 보관해주며 이벤트 루프가 콜백 함수를 가져가기를 기다린다.

이벤트 루프

이벤트 루프는 콜스택에 실행 컨텍스트가 아무것도 없다면 태스크큐에서 콜백함수들을 꺼내어 콜스택으로 가져다주는 역할을 한다.

즉, 아래와 같은 역할들을 한다.

  • 콜스택에 함수들이 있는지 확인하는 역할
  • 태스크큐에 콜백함수가 있는지 확인하는 역할
  • 태스크큐로부터 콜스택에 콜백함수를 가져다주는 역할

마무리

이처럼 비동기 함수는 자바스크립트의 콜스택과 브라우저 기능들이 협업하여 처리시간이 오래 걸릴 수 있는 함수를 뒷 순서로 미뤄 처리할 수 있다.

프로젝트를 진행하면서 실질적으로 이벤트 처리 및 ajax 통신을 많이 사용하게 된다는 것을 알 것이다.

그러므로 이러한 동작 원리를 아는 것은 중요하다고 생각되어 이렇게 정리하는 포스팅 로그를 남겨봤다.

직접 남에게 설명해준다 생각하고 그림을 그려 작성하니 무엇보다 내가 제가 잘 이해가 가게 되었다.

그림을 첨부하는 것이 시간은 기존보다 오래 걸리지만 그만큼 좋은 효과를 낸다는 경험을 하였다.

Q&A,참고자료(shout out to chat GPT)

Web API가 아닌 비동기 함수는 무엇이 있을까?

Promise 객체와 async 함수가 있다.
하지만 Promise 객체는 후속 처리 메서드를 왠만하면 사용하게 되는데 후속 처리메서드는 web API를 통해 처리가 된다.

async 함수 또한 프로미스 기반으로 작동하기 때문에 사실상 web API를 사용하고 있다고 할 수 있다.

하지만 promise 객체와 async함수 자체는 web API를 사용하지는 않는다.

모든 비동기 함수는 동시에 동작하는 것인가?

모든 비동기 함수는 동시에 동작하지않고 실행 순서만 뒤로 미뤄줄 수 있다.

다만, 이벤트 처리,타이머 설정과 만료,네크워크 요청과 응답은 동시에 이뤄질 수 있다.

콜백 함수가 동시에 이루어진다는 것은 아니다.

web API는 동시에 실행되는가?

이벤트 처리,타이머 설정과 만료,네크워크 요청과 응답은 동시에 이뤄질 수 있다.

다만 이에 대한 콜백 함수는 동시에 이뤄지진 않는다.

Web API,이벤트 루프,태스크큐는 브라우저의 기능인가?

Yes.

모든 비동기 함수는 백그라운드,이벤트 루프와 태스크 큐를 이용하나요?

Yes.

브라우저에서 web API와 백그라운드는 무슨 관계일까?

브라우저에서 백그라운드란 무엇일까?

profile
FE DEVELOPER

0개의 댓글