이벤트 루프란? [ 자바스크립트 동작원리 ]
(이번 포스팅은 아래의 이벤트 루프 영상을 보고 정리한 포스팅입니다.)
자세한 설명을 원하시는 분은 위의 영상을 참고해주세요! :)
자바스크립트는 기본적으로 싱글스레드 프로그래밍 언어이다.
여기서 싱글스레드라는 것은 한 번에 하나의 작업만이 가능하다는 의미이다.
one thread == one call stack == one thing at a time
여기서 호출 스택(call stack)이라는 것은 프로그램 상에서
우리가 어떤 순서로 작업을 수행하는지 기록하는 작업 스케줄링과 관련된 자료구조이다.
우리가 어떤 함수를 실행하게 되면 우리는 그 함수를 stack(스택)의 맨 위에 놓는데, 이것을 push라고 한다.
만약에 함수가 어떤 값을 리턴하거나 실행을 종료하면 우리는 다시 그 함수를 스택의 맨 위에서부터 꺼내는데 이것을 pop이라고 한다.
호출 스택(call stack)이 어떤 식으로 작동하는지 위의 그림으로 확인 할수 있다.
[PUSH]
위 코드를 실행하면 일단 저 함수들을 감싸고 있는 main() 함수를 스택에 넣는다.
그리고 printSquare(4) 함수가 실행되므로 printSquare(4)를 스택에 넣는다.
printSquare() 함수 내부에서 square(n) 함수를 실행하므로 스택에 square(n)을 넣는다.
square(n) 함수 내부에서 실행되는 multiply(a, b)를 스택에 넣는다.
[POP]
드디어 처음으로 리턴이 되었다. multiply(a, b)가 값을 리턴하며 스택에서 나온다.
square(n)도 값을 리턴하고 나온다.
printSquare(n)에서 console.log(squared)를 스택에 push하고 실행한 후 다시 빠져나온다.
printSquare(n) 함수가 종료되었으므로 stack에서 나온다.
main() 함수가 종료되었으므로 스택에서 나온다.
자바스크립트는 이렇게 싱글 스레드 언어이기 때문에 함수를 실행하면 함수 호출이 스택에 순차적으로 쌓이고
스택의 맨 위에서부터 차례대로 한 번에 하나의 함수만 처리할 수 있다.
간단한 프로그램이면 상관 없지만, 만약에 매우 복잡한 프로그램을 구동한다면
시간이 매우 오래 걸리는 작업이 스택에 쌓이고 실행되면 그 다음 작업은 무한정 대기 할 수 밖에 없다.
예를 들어서, 우리가 화장실에서 차례를 기다리는 경우를 상상해보자.
우리는 앞사람의 볼일을 보고 나올 때까지 아무것도 할 수 없고, 가만히 서서 기다려야한다.
이렇게 다른 작업을 실행하기 위해서 이전 작업이 완료될 때까지 기다려야만 하는 상황을 블로킹(blocking)이라고 한다.
이것이 문제가 되는 이유는 어떤 특정한 작업이 실행되는 동작하는 동안에는
브라우저가 다른 일을 전혀 할 수 없기 때문에 잠시 먹통 되기 때문이다.
우리가 어떤 사이트에서 문서를 작성하고 제출 버튼을 눌렀을 때,
제출이 완료되어 페이지가 새로고침되기 전에 모래시계가 돌아가는 순간을 생각해보자.
우리는 얼른 페이지가 새로고침되서 다시 작업을 할 수 있을 때까지 가만히 기다려야하고,
브라우저는 그 시간 동안 잠시 렌더링을 멈춘다.
만약에 이렇게 브라우저가 블락(block)되는 순간이 잦다면 사용자의 불만은 점점 커질 것이다.
이러한 점을 극복하기 위한 해결 방안이 바로 Asynchronous Callbacks(비동기 콜백)이다!
자바스크립트가 싱글 스레드 언어임에도 불구하고 우리가 웹 사이트에서 끊김없이
여러 작업을 동시에 할 수 있는 것은 바로 브라우저가 Web APIs 같은 것들을 제공하여
비동기 작업을 가능하게 해주기 때문이다.
함수를 동기로 호출하게 되면 call stack에 차곡차곡 쌓여 순차적으로 실행된다.
이때 만약에 우리가 AJAX나 setTimeout 혹은 DOM event 함수를 실행하면 자바스크립트 엔진은 call stack에서 Web APIs로 보내고 정해진 시간 혹은 이벤트가 발생한 순간에 순차적으로 Callback queue에 쌓인다.
Callback queue에 줄을 선 함수들은 call stack에 쌓여있던 것들이 모두 없어지면 차례대로 스택에 쌓여서 실행된다.
**여기서 큐란 스택(stack)과 같이 자료구조의 일종이다. 한쪽에서만 삽입과 삭제가 이루어졌단 스택과는 다르게
한쪽에서는 삽입이 되고 다른 한쪽에서는 삭제 작업이 이루어지는 자료구조이다.
가장 먼저 삽입된 자료가 가장 먼저 삭제되는 구조이므로 선입선출(FIFO)이라고도 부른다.
이 구조는 계산대나 매표소에서 줄을 서서 기다리는 장면을 생각해보면 이해가 쉽다.
아래 그림과 같이 뒤에서 부터 차례대로 줄을 서고 가장 먼저 줄을 선 사람이 가장먼저 작업을 하고 빠지는 구조이기 때문이다.
아래 코드로 어떤 식으로 동작하는지 살펴보자.
위의 예제에서 setTimeout()을 사용했을 때, 어떤 식으로 동작하는지 그림으로 나타낸 것이다.
먼저 main() 함수가 실행되고, console.log(1)이 스택에 쌓인다.
console.log(1)이 실행되어 콘솔 창에 1이 출력되고 setTimeout의 콜백 함수인 cb가 스택에 쌓이는데,
setTimeout은 브라우저에 의해 제공된 API로 자바스크립트 엔진에서 처리하지 않고 바로 web APIs로 넘긴다.
그러면 브라우저는 마치 setTimeout 함수가 완료된 것처럼
스택에서 pop하고 다음 작업을 진행하므로 console.log(3)이 실행되어 콘솔 창에 3이 출력된다.
모든 코드가 실행되었으므로 main() 함수가 스택에서 제거되고,
5초 동안 대기하고 있던 cb 함수가 5초가 지난 시점에 task queue에 들어온다.
stack이 비어있으므로 cb 함수를 stack에 적재하고 console.log(2)를 실행하게 된다.
지금 이 코드는 setTimeout이 5초 후에 실행하는 코드이기 때문에 명백하게 1 - 3 - 2의 순서로 출력된다.
그렇다면 만약에 setTimeout 함수를 0초 후에 실행하도록 코드를 변경하면 어떤 결과가 일어날까?
이 경우에도 결과는 크게 다르지 않다.
그 이유는 task queue에 줄 서 있는 callback 함수들은 stack이 비어있을 때만 stack으로 이동할 수 있기 때문이다.
위 경우 setTimeout으로 설정한 cb 함수는 web APIs로 이동하는 즉시 task queue로 이동하게 되는데,
stack이 비어있지 않기 때문에 대기 상태로 있게 되고 console.log(3)이 출력되고
스택이 클리어되면 이동하여 console.log(2)가 실행된다.
모든 web APIs는 위와 같은 방식으로 작동된다. AJAX나 DOM 이벤트도 동일하다.
document.querySelector('button').addEventListener('click', function () {
console.log('clicked');
});
만약 이런 코드를 실행한다고 해보자.
그러면 브라우저는 일단 click event 함수를 call stack에 저장하고 이는 즉시 web APIs로 옮겨진다.
그 상태로 무한 대기하고 있다가 사용자가 버튼을 클릭하는 순간
click 이벤트의 콜백 함수는 callback queue로 이동한다.
그리고 stack이 비는 순간에 stack으로 이동하여 함수를 실행한다.
만약에 사용자가 버튼을 10번 누른다면 callback queue에 10개의 콜백 함수가 쌓일 것이고
먼저 들어온 콜백부터 순차적으로 스택으로 이동하여 실행되고 없어지고를 반복할 것이다.
setTimeout(function timeout () {
console.log(1);
}, 1000);
setTimeout(function timeout () {
console.log(1);
}, 1000);
setTimeout(function timeout () {
console.log(1);
}, 1000);
setTimeout(function timeout () {
console.log(1);
}, 1000);
만약에 우리가 1초 후에 콘솔 창에 1을 출력하는 setTimeout()을 4번 썼다고 생각해보자.
이 경우 각각의 함수들은 stack -> web APIs로 이동하고 차례대로 callback queue에 쌓이게 된다.
이 때 가장 먼저 적재된 timeout 함수부터 순차적으로 stack에 "이동 -> 실행 -> 제거"를 반복하게 된다.
즉, 4개의 함수를 1초 후에 실행하라고 설정했다고 해서 모든 이벤트가 동시에 실행되는 것이 아니다.
이 점을 항상 유의해야한다. AJAX나 setTimeout() 등을 이용하면 마치 자바스크립트가 여러 가지 일을 동시에 수행하는 것처럼 보이지만 그것은 일종의 눈속임일 뿐이고 자바스크립트는 오직 한 번에 하나의 작업만을 수행한다.
그렇기 때문에 위의 timeout 이벤트는 queue에 줄 서서 하나씩 차례대로 스택으로 이동하여 실행된다.
그러므로 setTimeout()과 같은 메소드로 어떤 시간을 설정한다고 해서
아주 정밀하고 정확한 시간이 보장되는 것은 아니다.
모두 미세한 오차가 존재하며 브라우저는 단지 그 오차를 최소한으로 줄여줄 뿐이다.
window.addEventListener('scroll', function () {
console.log('hello');
});
만약에 우리가 스크롤 이벤트를 발생시키는 함수를 만들었다고 해보자.
스크롤 이벤트는 실행해보면 느끼겠지만 아주 약간의 움직임에도 엄청나게 많은 이벤트가 실행된다.
이 경우 브라우저는 아주 많은 콜백을 callback queue에 적재하게 된다.
만약 위와 같이 아주 간단한 코드 한 줄이라면 상관없겠지만
매우 복잡한 이벤트가 일어나야한다면 프로그램 성능에 좋지 않은 영향을 줄 것이다.
그래서 우리는 디바운싱(debouncing)을 통해 이벤트가 큐에 적재되는 속도를
느리게 만든다거나 하는 방법을 사용할 수 있다.
긴글 읽어주셔서 감사합니다.
질문과 지적은 댓글로 환영합니다.
오늘도 즐코하세요!! :)
참고자료
https://www.youtube.com/watch?v=8aGhZQkoFbQ
https://im-developer.tistory.com/113
https://ko.javascript.info/event-loop
https://seungtaek-overflow.tistory.com/18
https://developer.mozilla.org/ko/docs/Web/JavaScript/EventLoop