JavaScript-호출 스택과 이벤트 루프

hannah·2023년 8월 4일
0

JavaScript

목록 보기
60/121

클릭 이벤트는 비동기 이벤트이다. 그런데 그 안에 setTimeout 같은 비동기 함수들이 또 들어있으니 코드 실행 순서가 헷갈릴 수밖에 없다. 코드의 실행 순서를 명확하게 알고 있어야 정확하게 코드를 설계할 수 있다. 코드의 실행 순서를 파악하려면 호출 스택(call stack)과 이벤트 루프(event loop)라는 개념을 알아야 한다.

호출 스택

동기 코드를 담당

동기 함수만 있을 때는 호출 스택만 생각하면 된다. 함수가 호출될 때 호출 스택에 들어가고, 실행이 완료되면 호출 스택에서 빠져나간다. 기존 함수의 실행이 완료되지 않았는데 다음 함수가 호출되면 새로 호출된 함수는 기존 함수 위에 쌓인다.
처음 파일을 실행할 때는 anonymous(크롬 브라우저 기준)라는 익명 함수가 실행된다는 점을 유의해야한다.

이벤트 루프

비동기 코드를 담당

비동기 함수가 실행될 때는 호출 스택뿐만 아니라 이벤트 루프까지 동원해 실행 순서를 파악해야 한다. 타이머나 이벤트 리스너 같은 비동기 함수들은 콜백 함수를 백그라운드에서 테스크 큐로 보낸다. 이벤트 루프는 호출 스택이 비어 있으면 테스크 큐에서 하나씩 함수를 꺼내 호출 스택으로 보내 실행한다. 반대로 말하면 호출 스택이 비어 있지 않으면 테스크 큐에 있는 함수는 실행되지 않는다.

추가로 비동기 코드 실행에는 백그라운드(background)테스크 큐(tsak queue)라는 개념도 등장한다.


백그라운드

타이머를 처리하고 이벤트 리스너를 저장하는 공간

setTimeout같은 함수가 실행되면 백그라운드에서 시간을 재고 시간이 되면 setTimeout의 콜백 함수를 테스크 큐로 보낸다. 또한, addEventListener로 추가한 이벤트를 저장했다가 이벤트가 발생하면 콜백 함수를 테스크 큐로 보낸다. 백그라운드에서 코드를 실행하는 것이 아니라 실행될 콜백 함수들이 테스크 큐로 들어간다.


테스크 큐

실행돼야 할 콜백 함수들이 줄을 서서 대기하고 있는 공간

테스크 큐에 먼저 들어온 함수부터 실행된다. 다만, 테스크 큐도 함수를 직접 실행하지 않는다. 함수는 모두 호출 스택에서만 실행된다. 호출 스택에 들어온 함수가 호출(실행)된다고 생각하면 된다.


테스크 큐에서 호출 스택으로 함수를 이동시키는 존재가 바로 이벤트 루프이다. 호출 스택이 비어 있으면 이벤트 루프는 테스크 큐에서 함수를 하나씩 꺼내(들어온 순서대로) 호출 스택으로 옮긴다. 호출 스택으로 이동한 함수는 그제서야 실행된다. 실행이 완료된 함수는 호출 스택에서 빠져나가게 되고, 호출 스택이 비면 이벤트 루프는 테스크 큐에 있는 다음 함수를 호출 스택으로 옮긴다.


예시

-카드의 앞면-
카드 앞면
-카드의 뒷면-
카드 뒷면
-버그가 발생했을 때의 화면-

발생 버그 분석
카드의 뒷면일 때, 임의의 색이 다른 네 장의 카드(2, 5, 8, 9)를 연속해서 뒤집으면 색이 일치하지 않는 카드 두 장(8, 9)이 뒤집어진다.


위의 그림에서 2, 5, 8, 9번 카드를 클릭했다고 가정하자. 여기 네 카드는 색이 다른 카드이다. 클릭 이벤트가 네 번 발생하므로 백그라운드에서 클릭 이벤트의 콜백 함수 네 개를 태스크 큐로 보낸다. 태스크 큐에는 실행된 순서로 콜백 함수 네 개가 줄을 서서 대기하게 된다.



8번 카드의 클릭 콜백 함수가 실행되면 clicked 배열에 [2, 5, 8] 세 개 카드가 들어있게 된다. clicked 배열에는 최대 2개 카드만 존재한다고 예상했으나(2개가 되면 같은 색이든 다른 색이든 clicked가 비워져야 하므로) clicked 배열에 3개의 카드가 들어 있는 상황이 발생한다.


여기서 9번 카드의 클릭 콜백 함수까지 실행되면 clicked 배열에는 {2, 5, 8, 9}가 들어 있게 된다.


9번 카드의 클릭 콜백 함수가 끝나고 호출 스택이 비어 있으니 0.5초 타이머의 콜백 함수 세 개가 연달아 실행되는데, 여기서 clicke[0]과 clicked[1]인 2번과 5번 카드를 뒷면으로 뒤집고 clicked 배열을 [ ]로 초기화한다. clicked[2], clicked[3]인 8번과 9번 카드는 뒤집히지 않은 채(앞면인 채) 남아 있게 된다.

카드를 뒷면으로 뒤집고 clicked를 [ ]로 초기화하기 전에 8, 9번 카드의 클릭 이벤트 콜백 함수가 실행되는 것이 문제이다. 사실, 코드가 실행되는 순서는 정해져 있으므로 실행 자체를 막을 수는 없다. 그래서 실행되더라도 아무 일도 하지 않게 만들면 된다. 카드가 2장이 될 때 clickable을 false로 만들어서 세 번째 카드부터는 클릭해도 아무것도 하지 않고 끝나게 하면 된다.


문제
다음 코드를 실행할 때 콘솔에 어떤 순서로 알파벳이 찍히는지 호출 스택과 이벤트 루프를 통해 설명하라. 참고로 setTimeout에 0초가 들어 있는데, 거의 즉시 실행되는 타이머라고 보면 된다. '거의'가 붙은 이유는 setTimeout이 비동기 타이머이므로 진짜로 즉시 실행되지는 않기 때문이다.

function aaa(){
	setTimeout(()=>{
    	console.log('d');
    }, 0);
  	console.log('c');
}

setTimeout(()=>{
	console.log('a');
  	aaa();
}, 0);

setTimeout(()=>{
	aaa();
  	console.log('b');
}, 0);











정답
a, c, c, b, d, d




풀이

  1. 크롬브라우저 위에 anonymous가 맨처음 깔리게 되고, setTimeout 함수를 호출한다. setTimeout을 호출하면 타이머(0초)가 백그라운드로 가서 저장되어 있다.
  • 호출스택: anonymous -> setTimeout
  • 백그라운드:                   타이머1(0초)
  • 테스크큐:
  • 콘솔:
  1. 1번에서의 setTimeout은 실행이 되었으니 호출스택에서 지워지고 그 다음 setTimeout이 호출된다. 이에 따라 타이머(0초)가 백그라운드에 가서 저장된다.
  • 호출스택: anonymous -> setTimeout -> setTimeout
  • 백그라운드:                   타이머1(0초)     타이머2(0초)
  • 테스크큐:
  • 콘솔:
  1. 2번에서의 setTimeout 또한 실행이 되었으니 지워지고 코드가 끝난다. 이에 따라 anonymous에서 빠져나온다.
  • 호출스택: anonymous -> setTimeout -> setTimeout
  • 백그라운드:                   타이머1(0초)     타이머2(0초)
  • 테스크큐:
  • 콘솔:
  1. 이벤트 루프는 호출스택이 비워져 있으면 테스크큐에 있는 것을 호출스택으로 가져온다. 근데 호출스택 또한 비워져 있으니 백그라운드에 있는 타이머들을 순차적으로 테스크큐로 가져온다.
  • 호출스택:
  • 백그라운드:                   타이머1(0초)     타이머2(0초)
  • 테스크큐:   타이머1(0초)   타이머2(0초)
  • 콘솔:
  1. 호출스택이 비워져 있으니 태스크큐에 있는 타이머1을 호출스택으로 가져오고 이 타이머는 바로 실행이 되어 console.log(a)가 실행된다.
  • 호출스택:   타이머1(0초) -> console.log('a')
  • 백그라운드:
  • 테스크큐:   타이머1(0초)   타이머2(0초)
  • 콘솔:         a
  1. console.log(a)가 실행된 후 호출스택에서 바로 지워지고 그 다음 함수인 aaa가 호출되어 실행되어 타이머3(0초)가 테스크큐에 저장된다.
  • 호출스택:   타이머1(0초) -> console.log('a') -> aaa
  • 백그라운드:
  • 테스크큐:   타이머2(0초)   타이머3(0초)
  • 콘솔:         a
  1. 타이머를 테스크큐에 넣고 setTimeout은 지워지고 console.log('c')가 호출스택에 들어온다. 호툴스택에 들어모녀서 바로 콘솔에 c가 뜬다.
  • 호출스택:   타이머1(0초) -> aaa -> setTimeout ->console.log('c')
  • 백그라운드:
  • 테스크큐:   타이머2(0초)   타이머3(0초)
  • 콘솔:         a, c
  1. console.log('c')가 역할을 수행하면서 aaa함수에 대한 부분을 다 마쳤다.
  • 호출스택:   타이머1(0초) -> aaa -> console.log('c')
  • 백그라운드:
  • 테스크큐:   타이머2(0초)   타이머3(0초)
  • 콘솔:         a, c
  1. 이전에 aaa함수에 대한 실행이 다 끝남으로써 호출스택이 비워지게 되고 테스크큐에 첫번째로 저장되어 있는 타이머가 호출스택으로 간다.
  • 호출스택:   타이머2(0초)
  • 백그라운드:
  • 테스크큐:   타이머2(0초)   타이머3(0초)
  • 콘솔:         a, c
  1. 타이머2가 호출되어 aaa함수가 실행되고 이에 따라 setTimeout이 실행된다.
  • 호출스택:   타이머2(0초) -> aaa -> setTimeout ->console.log(c)
  • 백그라운드:
  • 테스크큐:   타이머2(0초)   타이머3(0초)
  • 콘솔:         a, c
  1. setTimeout이 실행됨에 따라 타이머3가 테스크큐에 저장되고 이어서 console.log('c')가 호출스텍에 들어온다. console.log('c')에 따라 콘솔에 c가 찍힌다.
  • 호출스택:   타이머2(0초) -> aaa ->console.log(c)
  • 백그라운드:
  • 테스크큐:   타이머3(0초)   타이머3(0초)
  • 콘솔:         a, c, c
  1. console.log('c')가 끝나면서 aaa에서 해야 할 일이 끝난다. 다음으로 console.log('b')가 들어온다.
  • 호출스택:   타이머2(0초) -> aaa ->console.log(c) -> console.log(b)
  • 백그라운드:
  • 테스크큐:   타이머3(0초)   타이머3(0초)
  • 콘솔:         a, c, c, b
  1. condole.log('b')가 실행되면서 타이머2에 대한 일이 다 끝난다.
  • 호출스택:   타이머2(0초)
  • 백그라운드:
  • 테스크큐:   타이머3(0초)   타이머3(0초)
  • 콘솔:         a, c, c, b

14.호출스택이 비워졌으므로 테스크큐에 저장되어 있는 것들 중 첫번째를 호출스택으로 가져온다. 즉, 타이머3을 가져온다.

  • 호출스택:   타이머3(0초)
  • 백그라운드:
  • 테스크큐:   타이머3(0초)
  • 콘솔:         a, c, c, b
  1. 타이머3이 실행됨에 따라 console.log('d')가 호출스택에 들어가고 콘솔에 d가 찍힌다.
  • 호출스택:   타이머3(0초) -> console.log('d')
  • 백그라운드:
  • 테스크큐:   타이머3(0초)
  • 콘솔:         a, c, c, b, d
  1. console.log('d')가 실행됨에 따라 타이머3이 지워진다.
  • 호출스택:   타이머3(0초)
  • 백그라운드:
  • 테스크큐:   타이머3(0초)
  • 콘솔:         a, c, c, b, d
  1. 테스크큐에 저장되어 있던 타이머3가 호출스택으로 오고 이에 따라 console.log('d')가 호출스택으로 들어오면서 콘솔에 d가 찍힌다.
  • 호출스택:   타이머3(0초) -> console.log('d')
  • 백그라운드:
  • 테스크큐:
  • 콘솔:         a, c, c, b, d, d
  1. console.log('d')가 실행됨에 따라 지워지고 이로인해 타이머3 또한 지워진다.
    호출스택, 백그라운드, 테스크큐가 모두 끝나면 해야할 일이 모두 끝난 것이다.
  • 호출스택:
  • 백그라운드:
  • 테스크큐:
  • 콘솔:         a, c, c, b, d, d

0개의 댓글