클릭 이벤트는 비동기 이벤트이다. 그런데 그 안에 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
풀이
- 호출스택:
anonymous
->setTimeout
- 백그라운드: 타이머1(0초)
- 테스크큐:
- 콘솔:
- 호출스택:
anonymous
->->setTimeout
setTimeout
- 백그라운드: 타이머1(0초) 타이머2(0초)
- 테스크큐:
- 콘솔:
- 호출스택:
->anonymous
->setTimeout
setTimeout
- 백그라운드: 타이머1(0초) 타이머2(0초)
- 테스크큐:
- 콘솔:
- 호출스택:
- 백그라운드:
타이머1(0초)타이머2(0초)- 테스크큐: 타이머1(0초) 타이머2(0초)
- 콘솔:
- 호출스택: 타이머1(0초) ->
console.log('a')
- 백그라운드:
- 테스크큐:
타이머1(0초)타이머2(0초)- 콘솔: a
- 호출스택: 타이머1(0초) ->
->console.log('a')
aaa
- 백그라운드:
- 테스크큐: 타이머2(0초) 타이머3(0초)
- 콘솔: a
- 호출스택: 타이머1(0초) ->
aaa
->->setTimeout
console.log('c')
- 백그라운드:
- 테스크큐: 타이머2(0초) 타이머3(0초)
- 콘솔: a, c
- 호출스택:
타이머1(0초)->->aaa
console.log('c')
- 백그라운드:
- 테스크큐: 타이머2(0초) 타이머3(0초)
- 콘솔: a, c
- 호출스택: 타이머2(0초)
- 백그라운드:
- 테스크큐:
타이머2(0초)타이머3(0초)- 콘솔: a, c
- 호출스택: 타이머2(0초) ->
aaa
->->setTimeout
console.log(c)
- 백그라운드:
- 테스크큐:
타이머2(0초)타이머3(0초)- 콘솔: a, c
- 호출스택: 타이머2(0초) ->
aaa
->console.log(c)
- 백그라운드:
- 테스크큐: 타이머3(0초) 타이머3(0초)
- 콘솔: a, c, c
- 호출스택: 타이머2(0초) ->
->aaa
->console.log(c)
console.log(b)
- 백그라운드:
- 테스크큐: 타이머3(0초) 타이머3(0초)
- 콘솔: a, c, c, b
- 호출스택:
타이머2(0초)- 백그라운드:
- 테스크큐: 타이머3(0초) 타이머3(0초)
- 콘솔: a, c, c, b
14.호출스택이 비워졌으므로 테스크큐에 저장되어 있는 것들 중 첫번째를 호출스택으로 가져온다. 즉, 타이머3을 가져온다.
- 호출스택: 타이머3(0초)
- 백그라운드:
- 테스크큐: 타이머3(0초)
- 콘솔: a, c, c, b
- 호출스택: 타이머3(0초) ->
console.log('d')
- 백그라운드:
- 테스크큐: 타이머3(0초)
- 콘솔: a, c, c, b, d
- 호출스택:
타이머3(0초)- 백그라운드:
- 테스크큐: 타이머3(0초)
- 콘솔: a, c, c, b, d
- 호출스택: 타이머3(0초) ->
console.log('d')
- 백그라운드:
- 테스크큐:
- 콘솔: a, c, c, b, d, d
- 호출스택:
- 백그라운드:
- 테스크큐:
- 콘솔: a, c, c, b, d, d