자바스크립트 이벤트 루프와 콜 스택
이 글은 원문을 바탕으로 쓴 글입니다.
이 글의 목적은 브라우저에서 자바스크립트가 어떻게 동작하는지 알려주는 것입니다.
자바스크립의 브라우저 동작 과정
각 토픽으로 들어가기 전에, 대략적인 개요를 보여주겠습니다. 이 개요엔 자바스크립트가 어떻게 브라우저와 동작하는 지 보여주고 있습니다.
콜 스택
자바스크립트가 싱글 스레드라는 말은 들어봤을 수 있습니다. 하지만 싱글 스레드의 진짜 의미는 무엇일까요?
자바스크립트에는 콜스택이 하나만 존재하기 때문에 한번에 하나씩 할 수 있습니다.
콜 스택은 자바스크립트가 함수의 호출지를 찾아갈 수 있도록 하는 매커니즘입니다.
스크립트나 함수가 함수를 호출하면, 이 함수가 콜 스택의 가장 위에 쌓입니다. 함수가 종료되면 인터프리터가 콜스택에서 이를 다시 제거합니다.
함수가 종료되는 경우는 return
문을 만나거나 스코프의 끝에 다다랐을 경우입니다.
콜스택은 가장 상단에 쌓이기 때문에 LIFO(Last In, First Out)
특성을 가집니다.
1 | const addOne = (value) => value + 1; |
만약 위와 같은 코드를 실행시킨다고 하면 다음과 같은 과정을 따릅니다.
- 파일이 로드되고
main
함수가 실행됩니다. 이main
함수는 전체 파일을 실행시킨다는 것을 의미합니다. 이 함수가 콜 스택에 추가됩니다. main
은calculation()
을 호출하고 콜 스택에 쌓입니다.calculation()
은addThree()
를 호출합니다. 그리고 이 함수는 콜스택에 쌓입니다.addThree
는addTwo
를 호출합니다. 이 함수도 콜스택에 쌓입니다.addOne
도 위와 동일한 과정을 거칩니다.addOne
이 다른 함수를 호출하지 않기 때문에 함수가 종료되면 콜 스택에서 제거됩니다.addOne
이 종료됨에 따라addTwo
도 종료되고 콜스택에서 사라집니다.- 마찬가지로
addThree
도 종료됩니다. calculation
에서addTwo
를 부르게 되고 콜스택에 추가됩니다.addTwo
는addOne
을 호출하고 콜 스택에 쌓입니다.addOne
이 콜 스택에서 제거됩니다.addTwo
가 콜 스택에서 제거됩니다.addThree
와addTwo
가 모두 평가되었으므로calculation
은 결과값을 계산할 수 있다.calculation
이 콜 스택에서 제거됩니다.- 더 이상 존재하는 코드가 없기 때문에
main
도 콜 스택에서 제거됩니다.
이 글에서
main
이라 작성하긴 했지만 공식적으로 이에 대한 이름은 존재하지 않습니다. 브라우저 콘솔에서는 이 함수를anonymous
라 부릅니다.
Uncaught RangeError: Maximum call stack size exceeded
이 에러 메시지에 따르면, 서로 순환 호출을 하기 때문에 콜스택의 적재 횟수를 초과하여서 생긴 에러입니다. 최대 콜 스택의 범위는 최소 만 부터 5만까지이다. 따라서 이 에러가 발생한 경우에는 코드가 무한 루프에 빠지고 있습니다.
요약하자면 콜 스택은 함수의 호출지를 추적할 수 있도록 하고 LIFO(Last In, First Out) 특성을 가지고 있습니다. 다시 말해서 스택의 가장 상단에 있는 부분이 먼저 실행됩니다.
Heap
자바스크립트의 힙은 함수나 변수를 정의할 때 객체가 정의되는 곳입니다. 이 주제는 콜 스택이나 이벤트 루프와 관련은 없지만, 자바스크립트의 메모리 할당과 관련하여 알고 싶은 사람은 이 링크를 참고하길 바랍니다.
Web APIs
자바스크립트는 싱글 스레드 기반이지만 브라우저와 동시에 실행될 수 있습니다. 이유는 브라우저가 제공하는 Web API가 있기 때문입니다.
예를 들어, 자바스크립트 인터프리터를 통해 코드를 실행시키기 위해서는 서버에서 이에 대한 응답을 받아야만 합니다. 이 때문에 웹 애플리케이션의 사용이 불가능할 수 있습니다.
이에 대한 해결책으로 웹 브라우저가 자바사크립트 코드에서 실행시킬 수 있는 API를 제공합니다. 이를 실행하는 건 브라우저가 실행합니다. 이 때문에 콜스택을 막지 않습니다.
web APIs의 또다른 장점으로는 C와 같은 낮은 레벨의 코드로 쓰여진 점입니다. 따라서 자바스크립트만으로 하기 어려운 일들을 해줄 수 있습니다.
이들은 AJAX 요청이나 DOM관리를 할 수 있습니다. 이뿐만 아니라, geo-tracking, 로컬 스토리지 접근 등이 가능합니다.
콜백 큐
web APIs의 특성 덕분에 자바스크립트 인터프리터 밖에서도 무언가를 할 수 있습니다. 하지만 예를 들어 Web API나 AJAX 요청에 대한 응답에 대해 처리를 자바스크립트로 하고 싶다면 어떻게 해야할까요?
이 때문에 콜백이 등장합니다. 이들을 활용해서, web API가 API 실행 뒤에 코드를 실행시킬 수 있습니다.
콜백이 무엇인가요? 콜백이란 다른 함수에 전해지는 함수를 말합니다. 콜백은 보통 어떤 코드가 실행된 이후에 실행됩니다. 함수를 매개변수로 해서 쉽게 콜백 함수를 만들 수 있습니다. 이들은 고차함수라고 불리기도 합니다. 콜백은 기본적으로 비동기가 아닌걸 기억하세요!
예제를 살펴봅시다.
1 | const a = () => console.log('a'); |
이미 나올 값을 알고 있는 사람도 있을 겁니다.
setTimeout
이 실행될 동안 JS 인터프리터도 다음 구문을 해석해 나갑니다. 만약 setTimeout에서 설정된 시간이 지나고 콜 스택이 전부 비게 되면 setTimeout
의 인자로 넘긴 콜백 함수가 실행됩니다.
따라서 결과는 a c b
가 됩니다.
setTimeout
가 종료되더라도 콜백 함수는 바로 실행되지 않습니다. 이 이유는 자바스크립트가 한번에 하나의 일만 하기 때문입니다.
자바스크립트로 작성된 콜백함수는 setTimeout
의 인수로 전해집니다. 그러므로 자바스크립트 인터프리터가 이를 해석(=콜스택에 추가)해야 합니다. 이를 다시 말하면, 콜백을 실행하기 위해서는 콜 스택이 전부 빌 때까지 기다려야 합니다.
setTimeout
이 web APIs를 부릅니다. API는 콜백을 콜백 큐로 이동시켜줍니다. 그리고 이벤트 루프가 콜 스택이 빌 때 콜백 큐에서 콜 스택으로 콜백 함수를 추가합니다.
콜 스택과 달리 콜백 큐는 FIFO(First In, First Out)를 따릅니다. 이는 큐에 쌓인 순서대로 실행되는 것을 의미합니다.
이벤트 루프
자바스크립트 이벤트 루프는 콜 스택이 빌 때, 콜백 큐에 있는 가장 첫번째 일을 콜 스택에 추가합니다.
다르게 말하자면 콜 스택이 비지 않는 한, 콜백 큐에 있는 어떠한 일도 실행되지 않습니다.
따라서 너무 많은 코드를 실행시키거나 콜백 큐를 가로막으면, 새로운 자바스크립트 코드가 실행되지 못하기 때문에 웹 사이트가 반응하지 않을 수 있습니다.
onscroll
과 같은 이벤트 헨들러들은 이벤트가 실행될 때, 콜백 큐에 일들을 추가합니다. 이 때문에 이러한 콜백들엔 디바운스가 필요합니다. 디바운스란 매 x밀리초마다 실행되도록 하는 것입니다.
실제로 해보세요 이 코드를 브라우저 콘솔에 입력해보세요. 스크롤시 얼마나 많은 콜백이 나오는지 관찰해보세요.
1
window.onscroll = () => console.log('scroll');
setTimeout(fn, 0)
위처럼 코드를 작성하는 것의 이점은 메인 스레드를 오랫동안 막아두지 않으면서 어떠한 일을 할 수 있는 것입니다.
비동기 코드를 콜백에 넣고 setTimeout
을 0ms로 지정하면 브라우저가 콜백 실행을 하기 전에 DOM업데이트와 같은 작업을 수행할 수 있습니다.
잡(job) 큐와 비동기 코드
콜백 큐에 더불어 프로미스들을 독립적으로 관리하는 큐가 있는데, 그것이 바로 잡 큐입니다.
프로미스
프로미스는 EcmaScript 2015 (or ES6)에서 처음 소개되었습니다. 바벨을 통해 사양을 낮출 수도 있습니다.
프로미스는 콜백 대신 비동기 코드를 관리할 수 있는 방법입니다. 프로미스를 통해 비동기 함수를 체이닝 함으로써 콜백헬이나 운명의 피라미드에 빠지는 것을 방지할 수 있습니다.
1 | setTimeout(() => { |
이런식으로 코드를 작성하게 되면 옆구리가 점점 더 들어가버릴거에요😞
프로미스를 활용하면 가독성을 더 높일 수 있습니다.
1 | // A promise wrapper for setTimeout |
여기서 async
/await
를 활용하면 더 가독성이 좋아집니다.
1 | const logDelayedMessages = async () => { |
여기서는 프로미스의 대략적인 부분만 다뤘기 때문에 더 세부적인 내용이 궁금하신 분은 MDN을 참고하세요
프로미스의 올바른 사용법
프로미스는 콜백과 다르게 그들만의 큐를 가지고 있습니다. 잡 큐는 프로미스 큐라고도 알려져있습니다. 그리고 이 큐는 콜백 큐보다 더 높은 우선 순위를 가지고 있습니다. 따라서 콜백 큐보다 우선적으로 콜 스택에 쌓습니다.
예시를 보겠습니다.
1 | console.log('a'); |
이 예제를 보면 프로미스 큐가 콜백 큐보다 더 높은 우선순위를 가지기 때문에 출력은 a d c b
로 출력됩니다.