이벤트 루프란?

Sheryl Yun·2021년 12월 28일
1

Javascript 정복

목록 보기
2/9
post-thumbnail

출처: [우아테크-10분 테코톡] 피터의 이벤트 루프 https://www.youtube.com/watch?v=wcxWlyps4Vg&list=RDCMUC-mOekGSesms0agFntnQang&index=1

이벤트 루프에 대해 얘기하기 전에 사전에 짚고 넘어가야 할 몇 가지 개념들이 있다.
다음의 개념들에 대해 먼저 이해하고 이후에 이벤트루프에 대해 알아보자.

  1. 콜백함수
  2. 동기/비동기
  3. 자바스크립트 엔진
  4. 브라우저 구조

콜백 함수

특정 함수의 인자로 들어가는 함수이다.
비동기 콜백이냐 동기 콜백이냐에 따라 콜백 함수의 실행 시점이 달라진다.

동기 콜백

호출 즉시 실행

비동기 콜백

동기 콜백 실행 이후 조건을 만족하면 실행

1) 이벤트 리스너 (addEventListener)

특정 이벤트 발생 시

2) 타이머 (setTimeout, setInterval)

일정 시간 경과 후

3) XMLHttpRequest 요청

async/await로 axios 요청 시

퀴즈 1

코드 순서 예상하기

console.log('배고프다');

setTimeout(function() {
  console.log('저기 어때?');
}, 2000);

console.log('저기 가 봤어?');

scenario 1. 동기적 예상

위에서부터 순서대로 실행

// 배고프다

// (2초 후)
// 저기 어때?

// 저기 가 봤어?

scenario 2. 비동기적 예상

코드를 순서대로 실행하다가 setTimeout과 같은 비동기 함수를 만나면
콜백 함수를 자바스크립트의 뒤편(backstage)으로 보내고 타이머를 작동시킨다.

나머지 동기 코드들이 실행된 후에
타이머의 시간이 지나면 대기하고 있던 콜백 함수를 실행한다.

// 배고프다

// 저기 가 봤어?

// (2초 후)
// 저기 어때?

자바스크립트 엔진

자바스크립트 엔진은 자바스크립트 코드를 해석하고 실행하는 ‘번역기’이다.
각 브라우저마다 엔진의 종류가 다르며, (예: 사파리는 Webkit, 크롬은 V8 등)
크게 힙(Heap)과 호출스택(Call Stack)으로 구성된다.

변수나 객체 등이 저장되는 일종의 '창고'같은 개념이다.
이번 포스트에서 크게 중요한 개념은 아니다.

호출 스택

함수를 실행하고 제거하는 스택이다.

호출 스택의 동작 과정

  1. 자바스크립트 코드에서 함수의 실행이 시작되면 호출 스택에 해당 함수를 집어넣는다.

  2. 함수의 실행이 끝나면 호출 스택의 맨 위에 있는 실행된 함수를 꺼내서 제거한다.

즉, 흔히 '호출 스택이 비어 있다'는 말은 '실행할 함수가 남아있지 않다'와 동일하다.

예시 코드

아래 예시를 보기 전 컵 모양의 호출 스택을 먼저 상상해보자.
코드의 실행이 시작되면 호출 스택 안에 함수를 블록처럼 쌓은 뒤
함수의 실행이 끝나면 호출 스택에서 하나씩 차례로 제거한다.

처음에 호출 스택 가장 밑바닥에는 전역 변수가 존재하는 Global context(전역 문맥)가 먼저 쌓여 있다. 전역 문맥은 호출 스택의 밑바닥에 항상 존재하며 호출 스택의 작업이 끝난 후 가장 나중에 사라진다.

function second() {
	setTimeout(function() {
		console.log('세상에..');
	}, 2000);
}

function first() {
	console.log('디저트를 안 먹는다고?');
	second();
	console.log('어떻게 디저트를 안 먹을 수 있지?');
}

first();

실행 순서

(전역 문맥은 제외)

  1. first 함수가 호출 스택에 쌓임
  2. first 함수가 실행되면서 첫 번째 콘솔이 그 위에 쌓임
  3. 첫 번째 콘솔이 출력된 후 제거됨
  4. 이후 second 함수가 쌓이고
  5. 그 안에 있는 setTimeout 함수가 쌓인 뒤 실행됨
  6. setTimeout이 실행되면 콜백함수와 타이머를 자바스크립트 뒤편(Web API)으로 보냄
  7. setTimeout의 실행이 끝나서 스택에서 제거됨
  8. setTimeout밖에 없었던 second 함수도 제거됨(return)
  9. first 함수의 남은 두 번째 콘솔이 쌓인 뒤 출력(실행)되고
  10. 실행이 모두 끝난 first 함수도 리턴
  11. 호출 스택이 비게 되면 자바스크립트 뒤편에 있던 타이머 시간인 2초가 지나고 나서 Web API에 있던 콜백함수를 콜백 큐로 보냄
  12. 이벤트 루프가 호출 스택이 비어있는 것을 확인한 후 콜백 큐의 콜백함수를 호출스택에 읏차 하고 넣음
  13. 콜백 함수의 콘솔이 마저 출력된 뒤에 제거됨 (모든 과정 완료!)

콘솔결과
// 디저트를 안 먹는다고?
// 어떻게 디저트를 안 먹을 수 있지?
// (2초 뒤) 세상에..

브라우저의 구조

자바스크립트는 ‘싱글 스레드 언어’이다. 즉, 호출 스택을 1개만 사용한다는 뜻이다.
이는 자바스크립트가 기본적으로 한 번에 한 가지 일만 처리할 수 있다는 것을 의미한다. (= 동시에 여러 작업을 진행할 수 없다)

하지만 브라우저는 자바스크립트 엔진 외에도 Web API, 이벤트 루프, 콜백 큐 등을 사용하여 여러 작업을 동시에 진행하는 멀티 스레드로 동작하는데, 이는 자바스크립트의 '비동기'적 특성에 기인한다.

Web API는 DOM 조작(addEventListener), AJAX(Fetch API), setTimeout과 같은 비동기 메소드들을 자바스크립트에 제공한다. 이 비동기 메서드들의 역할은 실행 후 인자로 받은 콜백 함수(실제 실행문)를 Web API로 보내는 것이다. (이걸로 비동기 메서드 자체의 역할은 끝나고 호출 스택에서 사라진다. 이후 실제 콜백 함수를 실행하는 것은 이벤트 루프의 역할이다)

이벤트 루프

이벤트 루프는 호출 스택과 콜백 큐를 계속 주시하고 있다가
다음의 2가지 조건이 만족되면 콜백 큐에 대기하고 있던 콜백 함수들을 순서대로 호출 스택에 넣는다.

조건

  1. 콜백 큐에 콜백 함수가 들어 있다.
  2. 호출 스택이 비어 있다. (중요!)
    (▶ 반드시 호출 스택이 비어있어야 콜백 큐의 콜백 함수가 호출 스택으로 이동 가능)

콜백 큐의 종류

콜백 큐에는 매크로 태스크 큐와 마이크로 태스크 큐가 있다.

매크로 태스크 큐에는 동기적으로 실행되는 코드들이,
마이크로 태스크 큐에는 비동기적으로 실행되는 코드들이 들어 있다.

이벤트 루프는 먼저 매크로 태스크 큐에 있는 동기적 코드들을 호출 스택에 넣어 모두 실행한 다음,
호출 스택이 비워지면 이후에 마이크로 태스크 큐에 있는 코드들을 실행한다.

다음의 과정을 반복한다.

  1. 매크로 태스크 큐에서 가장 오래된 태스크를 꺼내서 실행시킨다.
  2. 마이크로 태스크 큐에 있는 모든 태스크를 실행시킨다.
  3. 렌더링 작업을 실행한다.
  4. 매크로 태스크 큐에 새로운 매크로 태스크가 나타날 때까지 대기한다.
  5. 1번으로 돌아가서 반복

퀴즈 2

다음 코드를 보고 결과를 예상해보자.

console.log('script start'); // A

setTimeout(function () { // B
  console.log('setTimeout');
}, 0);

Promise.resolve() 
  .then(function () { // C
    console.log('promise1');
  })
  .then(function () { // D
    console.log('promise2');
  });

console.log('script end'); // E

정답

script start
script end
promise1
promise2
setTimeout

비동기 에러상황

비동기 함수를 실행하다 보면 try-catch가 에러를 제대로 잡지 못하는 경우가 생길 수 있다.

예를 들어,

btn.addEventListener('click', function() { // (A)
  try {
    axios.get(url, function(res) { // (B)
      // 여기서 에러 발생
    });
  } catch (e) {
    console.log(e.message);
  }
});

위의 코드에서 try-catch문은 (B)에서 발생하는 에러를 잡아내지 못한다.

왜?

  1. 이벤트 리스너에서 click 이벤트 발생
  2. 콜백 함수(try-catch문)인 (A)가 콜백 큐로 이동
  3. 호출 스택이 비어있고, 콜백 큐에 콜백 함수가 있으므로
    이벤트 루프가 콜백 큐의 (A) 콜백 함수를 호출 스택으로 보냄
  4. (A) 함수의 try-catch문이 실행되면서 axios의 get 함수가 호출 스택에 쌓임
  5. get 함수가 실행되면 url과 (B) 콜백 함수를 Web API에 보내고 자신은 return
  6. try문 실행이 끝난 (A) 콜백 함수 리턴
  7. Web API에 있던 url 정보로 HTTP 요청을 보내고 나면 (B) 콜백함수가 콜백 큐로 들어감
  8. 호출 스택이 비어있고, 콜백 큐에 콜백 함수가 있으므로
    이벤트 루프가 콜백 큐의 (B) 콜백 함수를 호출 스택으로 보냄
  9. (B) 콜백 함수가 실행 중 에러 발생
    하지만 try-catch문이 있는 (A) 콜백 함수가 이미 리턴(호출 스택에서 제거)되었으므로 해당 에러를 감지하지 못함
    => 에러 처리가 되지 않음

해결방법?

(B) 콜백 함수에도 똑같이 try-catch문을 넣어주면 된다.
(=> (B) 함수 자체 내에서 에러를 처리할 수 있도록)

setTimeout의 타이머가 0초인 경우

대기 시간이 0초인 setTimeout 함수는 0초 후에 실행(= 즉시 실행)이라는 의미가 아니다.

console.log('피카츄!');

setTimeout(function() {
	console.log('삐까삐까!');
}, 0);

console.log('백만볼트!');

대기 시간이 0초라 해도 비동기 함수들은 콜백 큐에서 대기하고 있다가 동기적 코드가 모두 실행되고 난 뒤에 실행된다.
따라서 위의 코드에서 '삐까삐까!'는 코드 작성 순서로는 2번째이지만 실제 출력 순서는 가장 마지막이 된다.

profile
데이터 분석가 준비 중입니다 (티스토리에 기록: https://cherylog.tistory.com/)

0개의 댓글