안녕하세요! 이번 글에서는 웹 개발에선 빠질 수 없는 Javascript라는 언어의 동작 방식에 대한 글을 써보려고 합니다.
저는 2달 전에 졸업하신 선배님께 프론트엔드 모의 면접을 봤습니다! 이때를 기준으로 여러 기술의 동작원리와 내가 왜 쓰는지에 대한 생각들이 많이 들었고 또 공부했습니다. 저는 Javascript라는 언어로 개발을 합니다.
그런데 Call Stack, Event Loop 등의 단어를 들어도 그게 무엇인지 전혀 몰랐습니다.. 반성해야 하는 부분이죠.
Call Stack, Task Queue, Event Loop 등의 단어들은 JS 동작원리 그 자체라고도 할 수 있기 때문에 잘 읽어주세요!
❓JS는 싱글 스레드
Javascript는 싱글 스레드입니다. 스레드라는 단어가 무엇인지 알고 넘어가자면
스레드는 프로세스 내에서 실행되는 가장 작은 실행 단위입니다.
각 스레드는 자체적인 Stack을 가지고 있습니다.
프로세스는 단순히 현재 실행 중인 프로그램을 의미합니다.
스레드에 대한 감이 잡히시나요? 이런 스레드는 두 종류로 나뉩니다.
1. 싱글 스레드
말 그대로 하나의 스레드만을 사용해 작업을 처리하는 것을 말합니다.
이 말은 순차적으로 한 번에 하나의 작업만 처리할 수 있다는 것을 의미합니다.
멀티 스레드보다 작업처리의 효율이 떨어질 순 있지만 안정성과 복잡성, 리소스 효율성이 좋습니다.
2. 멀티 스레드
멀티 스레드는 여러 개의 스레드를 사용하여 병렬로 작업을 처리합니다. 각각의 스레드는
독립적으로 실행되며 서로 다른 작업을 수행할 수 있습니다. 멀티 스레드를 사용하면
여러 작업을 동시에 처리할 수 있어 효율적입니다.
JS는 싱글스레드라 한 번에 하나의 작업만 처리한다...
이상하지 않나요?
console.log("hello!");
setTiemout(() => {
console.log("3 seconds!");
}, 3000);
console.log("world!");
이 말대로라면 위 코드는 첫 번째 console.log가 실행되고 3초 기다린 후에
두 번째 console.log가 실행되고 마지막 console.log가 실행되어야 합니다. 하지만 실제는 어떤가요?
3초를 기다리지 않고
hello! ➡️ world! ➡️ 3 seconds! 순으로 실행됩니다. 어떤 비밀이 숨겨져 있단 겁니다!
이미 아시는 분들도 계시겠지만 모른다면 좀 더 봐주세요!
Blocking
그럼 블로킹 현상에 대해 알아보겠습니다. JS에서 블로킹이란 그저 느리게 동작하는 코드를 뜻하기도 하는데.
느리게 동작하는 게 와닿지 않다면 이렇게 생각해보세요. console.log 같은 실행문이 반복문 안에서
정말 많은 횟수 실행된다면 이것은 느린겁니다. 실행을 지연시키니까요.
네트워크 요청도 마찬가지입니다. 이렇게 보통 느린 동작이 Call Stack에 남아있는 것을 블로킹이라고 합니다.
저희는 비동기를 사용할 수 있습니다! 하지만 이런 callback 같은 비동기 코드를 안 쓴다고 가정하고
다음 코드를 보겠습니다.
var foo = $.getSync("//example1.com");
var bar = $.getSync("//example2.com");
var qux = $.getSync("//example3.com");
console.log(foo);
console.log(bar);
console.log(qux);
저희가 알고있는 내용대로 생각해볼까요? 첫 번째 네트워크 요청 코드가 실행되고
응답이 올 때까지 기다린 다음 Stack에서 사라지고.. 두 번째 요청을 보내고 응답을 기다리고..
세 번째도 같은 동작을 합니다. 그 후에 아래 cosole.log들이 실행되죠.
그리고 Call Stack을 지울 수 있습니다.
JS는 싱글스레드입니다. 위에서 말했죠? 멀티 스레드가 아니기에 네트워크 요청을 하면
무작정 기다려야 합니다. 달리 방법이 없으니까요.
이런 문제는 JS가 브라우저에서 실행되기 때문이기도 합니다.
위의 문제를 해결하기 위해선 어떻게 해야되는지 저희는 알고 있습니다. 비동기 callback이요!
비동기는 작업이 실행되는 것에 영향을 받지않고 자신의 일을 처리하니까요.
하지만 그거 아시나요? V8엔진의 코드를 보면 setTimeout, HTTP request 같은 대표적인 비동기를
관리하는 코드를 찾을 수 없다고 합니다. 그럼 이런 비동기 작업은 어디서 처리하는 걸까요?
❓JS가 비동기 작업을 처리하는 방법
console.log("hello!");
setTiemout(() => {
console.log("3 seconds!");
}, 3000);
console.log("world!");
위에서 있었던 궁금증들을 해결해보겠습니다!
❓Call Stack
Chrom V8같은 엔진의 JS의 런타임을 시각화 했습니다.
메모리 할당이 일어나는 Memory Heap과 함수 프레임이 쌓이는 Call Stack이란 것이 보입니다.
이제 위에서 그렇게 언급했던 Call Stack을 살펴보겠습니다.
Call Stack이란 함수를 실행했을 때 이 함수의 실행 즉, 프레임이 쌓이는 공간입니다.
Stack 자료구조이기 때문에 LIFO(Last In First Out)을 준수하고요.
먼저 실행한 것이 먼저 쌓이고 나중에 실행한 것이 먼저 빠지는 구조입니다.
이 Call Stack은 당연히 한계량이 있습니다. 다음 코드를 통해 Call Stack을 확인해볼 수 있습니다.
function foo() {
foo();
}
foo();
위 코드를 실행시키면 어떻게 될까요? 바로 Call Stack에 foo함수의 프레임이 무한으로 쌓이면서
다음과 같은 오류를 던집니다.
RangeError: Maximum call stack size exceeded
Call Stack의 용량을 초과했단 소리죠?
이렇게 Call Stack이라는 것이 실제로 존재한단 사실을 알 수 있습니다.
Call Stack이 어떤 것인지 알 것 같나요?
다시 돌아와서
먼저 hello!를 출력하는 console.log는 stack에 추가되고 실행을 마친 후 사라집니다.
그럼 setTimeout이 새로 stack에 추가되겠죠?
❓Web APIs
setTimeout은 V8엔진에서는 존재하지 않고 Web APIs에서 제공해주는 API입니다.
우리의 JS 코드는 브라우저에서 돌아갑니다.
그렇기에 드라우저에서 이 timer를 실행시킬 수 있습니다.
그럼 타이머를 끝낸 callback은 어디로 갈까요를
Web API는 갑자기 작성된 코드에 끼어들 수 없습니다.
갑자기 Stack에 함수 프레임을 추가할 수 없단거죠.
Task Queue
Web APIs에서 비동기 작업이 처리되고 남은 callback은 위 사진에 있는 Callback Queue.. Task Queue라는
표현이 맞는 표현이라고 합니다. Task Queue라는 공간에 들어간 후 stack에서는
setTimeout이 사라지죠. 그 다음 world!를 출력하는 console을 실행이되고 사라집니다.
Task Queue는 Call Stack과 달리 Queue 자료구조를 가지기에 FIFO(First In First Out) 구조를 가집니다.
여기서 Evnet Loop가 동작합니다. Evnet Loop의 역할은 Call Stack과 Task Queue를 주시하고
만약 Call Stack이 비어있으면 첫 번째 callback을 stack에 쌓아서 실행할 수 있도록 해줍니다.
아래와 같은 상황이라고 하면 될까요?
잠깐
여기서 Task Queue는 MacroTask Queue, MicroTask Queue 2개의 공간이 있습니다.
1. MacroTask Queue
- setTimeout, setInterval, I/O, 이벤트 핸들러 등의 작업이 처리되는 공간
2. MicroTask Queue
- Promise, async/await, then, catch, finally 등의 작업이 처리되는 공간
두 공간 중에서 MicroTask Queue가 우선순위가 더 높아 Call Stack이 비어있을 때 MicroTask Queue가 먼저 callback이
처리됩니다.
따라서 다음과 같은 코드가 있을 때
console.log('start');
setTimeout(() => {
console.log('timeout');
}, 0);
Promise.resolve().then(() => {
console.log('promise');
});
console.log('end');
start ➡️ end➡️ promise➡️ timeout
순서로 console이 찍히게 됩니다.
🚨Event Loop도 주체구나!
하지만 여기서 주의! Event Loop는 Call Stack이나 Task Queue같이 어떤 주체가 아닙니다.
Task Queue에 추가된 callback을 Call Stack에 추가하는 일련의 과정 그 자체를 뜻합니다.
저도 착각했던 부분이더라구요..
이제 callback이 추가됐습니다. Call Stack은 V8의 영역입니다. 따라서 3 seconds!를 출력하는
console.log를 실행시키죠. 이제 JS가 싱글 스레드이면서 비동기적인 작업들을 어떻게 처리하는 지
아실 것 같나요?? 전 공부하면서 되게 신기하고 재밌단 느낌을 받았는데 어떨 지 모르겠네요..
예시를 setTiemout으로 든 것이지 HTTP 요청도 동일하게 동작합니다!
📖 마무리
이번 글을 정리하면서 잘 못 기억했던 부분도 바로잡고 확실하지 않았던 정보들이
정리된 느낌을 받았습니다. 많은 자료들을 참고했지만 잘못 된 부분이나 보충 설명이
필요할 것 같은 부분들은 피드백 남겨주시면 빠르게 수정하겠습니다!
이번 글도 읽어주셔서 감사합니다 ㅎㅎ
만약 직접 이 과정을 보시고 싶다면 아래 링크를 추천드립니다!
https://github.com/latentflip/loupe
🔗Ref
https://medium.com/@gemma.croad/understanding-the-javascript-runtime-environment-4dd8f52f6fca
Understanding the JavaScript runtime environment
What‘s going on “under the covers” of JavaScript and the runtime environment
medium.com
'Javascript' 카테고리의 다른 글
비동기 공부에 동기를 부여해줄게 - JS에서의 비동기 처리 (1) | 2024.04.27 |
---|---|
this? 이것? (0) | 2024.04.15 |
ES6? 다른 숫자도 있는건가? (0) | 2024.03.31 |
🦴 JS 타입 종류 (원시 타입, 참조 타입?) (0) | 2024.02.05 |
Closer ❌ Closure ⭕ (2) | 2024.02.02 |