자바스크립트 초심자든 전문가든, 면접이건 일상적인 개발 작업이건, 우리는 종종 다음과 같은 상황을 맞닥뜨립니다: 몇 줄의 코드가 주어졌을 때, 그들이 무엇을 출력하며 어떤 순서로 실행되는지 알아야 합니다. 🧑💻 자바스크립트는 싱글 스레드 언어이기 때문에 우리는 다음 결론을 내릴 수 있습니다:
자바스크립트는 문장을 나타난 순서대로 실행합니다. 📜
이 시점에서, "나는 JS가 한 줄씩 실행된다는 것을 알아, 왜 강조하는 거지?"라고 하실 수 있습니다. 🤔 우리는 JS가 한 줄씩 실행된다고 가정하기 때문에 이런 식으로 모든 JS가 동작한다고 가정합니다.
let a = '1';
console.log(a);
let b = '2';
console.log(b);
하지만 실제로 JavaScript는 다음과 같이 실행됩니다:
setTimeout(function(){
console.log('Timer started')
});
new Promise(function(resolve){
console.log('For loop is about to be executed');
for(var i = 0; i < 10000; i++){
i == 99 && resolve();
}
}).then(function(){
console.log('Execute then function')
});
console.log('Code execution ends');
JavaScript가 문장을 순서대로 실행한다는 개념을 따라, 예상 출력을 자신 있게 적었습니다:
- 타이머 시작. ⏱️
- for 루프가 실행됩니다. 🔄
- then 함수를 실행합니다. ➡️
- 코드 실행 종료. 🛑
그러나 Chrome에서 확인했을 때 결과가 완전히 잘못되어 순간적으로 혼란스러웠어요. 😕 예상대로 줄 단위로 실행되어야 하는 것이 아니었나요? 🤨
이 혼란을 해소하기 위해 JavaScript의 실행 메커니즘을 완전히 이해해야 합니다.
JavaScript에 대해
JavaScript는 단일 스레드 언어입니다. 🧵 최신 HTML5에서 Web Workers가 소개되었지만, JavaScript의 단일 스레드 코어는 변하지 않았습니다. 따라서 JavaScript의 모든 "다중 스레딩"은 단일 스레드를 사용하여 모의되며, 모든 다중 스레딩은 속임수입니다!
자바스크립트 이벤트 루프
자바스크립트는 한 번에 하나의 작업만 처리하기 때문에, 한 창만 있는 은행과 같아요; 고객들은 차례로 업무를 처리하기 위해 줄을 서야 합니다. 🏦 마찬가지로, 자바스크립트 작업들은 하나씩 실행되어야 해요. 만약 어떤 작업이 너무 오래 걸린다면, 다음 작업은 기다려야 해요.
그래서 질문이 생깁니다: 우리가 뉴스를 보려고 하는데, 뉴스에 있는 고화질 사진들이 느리게 로드된다면, 사진이 완전히 표시될 때까지 웹 페이지가 멈춰 있어야 할까요?
똑똑한 프로그래머들은 이 문제를 두 가지 범주로 작업을 나누어 해결합니다:
- 동기 작업 🕒
- 비동기 작업 ⏩
웹사이트를 열 때, 렌더링 프로세스는 페이지 골격과 요소를 렌더링하는 등 많은 동기 작업으로 구성됩니다. 🖥️ 사진이나 음악 파일과 같이 많은 리소스를 사용하고 오랜 시간이 걸리는 작업들은 비동기 작업입니다.
더 쉽게 이해하기 위해 마인드 맵을 사용하여 설명하겠습니다:
마음의 맵 콘텐츠를 설명하는 단어를 사용하면:
- 동기 작업과 비동기 작업은 서로 다른 실행 "장소"로 들어갑니다. 🕒⏩
- 동기 작업은 주 스레드로 들어가고, 비동기 작업은 이벤트 테이블에 들어가서 함수를 등록합니다. 🧵📋
- 지정된 작업이 완료되면, 이벤트 테이블은 이 함수를 이벤트 큐로 이동시킵니다. 📤
- 주 스레드의 작업이 실행되고 나면, 해당 함수는 이벤트 큐에서 읽혀 주 스레드에서 실행됩니다. 🔄
- 이 프로세스는 계속해서 반복되며, 이를 이벤트 루프라고 합니다. 🔁
여러분은 아마 궁금해 할지도 모릅니다. 주 스레드 실행 스택이 비어있는지 어떻게 알 수 있을까요? 🤔 자바스크립트 엔진은 주 스레드 실행 스택이 비어있는지 계속 확인하는 모니터링 프로세스가 있습니다. 한 번 비어지면, 이벤트 큐로 이동하여 호출을 기다리는 함수가 있는지 확인할 것입니다.
위 설명 이후에, 코드 한 줄이 더 직관적일지 모릅니다:
let data = [];
$.ajax({
url: 'www.javascript.com',
data: data,
success: () => {
console.log('Send successful');
}
})
console.log('Code execution completed');
여기에 간단한 AJAX 요청 코드 예시가 있어요:
- AJAX가 이벤트 테이블에 들어가며 콜백 함수 success를 등록해. 📋
- console.log(
Code execution ended
)를 실행해. 🖨️ - AJAX 이벤트가 완료되고 콜백 함수 success가 이벤트 대기열에 들어갔어. 📤
- 메인 스레드가 이벤트 대기열에서 콜백 함수 success를 읽고 실행해. 🔄
위의 텍스트와 코드를 통해 JavaScript의 실행 순서에 대해 간단히 이해했을 거라고 믿어.
🧑💻 다음으로, 고급 주제인 setTimeout을 공부해 봅시다.
setTimeout에 대한 사랑과 미움
우리가 모두 알다시피, setTimeout에 대해 자세한 소개는 필요하지 않습니다. ⏳ 우리의 첫인상은 지연 후 비동기적으로 실행될 수 있다는 것입니다. 우리는 종종 3초 지연 실행을 구현하기 위해 사용합니다:
setTimeout(() => {
task();
}, 3000);
console.log('콘솔 실행');
그러나 setTimeout을 계속 사용할수록 문제가 발생합니다. 때로는 코드에서 3초의 지연을 지정해도 함수가 5 또는 6초 이후에 실행되기도 합니다. 🤯 그 이유가 무엇일까요? 🤔
예시를 살펴봅시다:
setTimeout(() => {
task();
}, 3000)
console.log('콘솔 실행');
이전 결론에 따르면, setTimeout은 비동기적이므로 동기 작업인 console.log이 먼저 실행되어야 합니다. 따라서 우리의 결론은 다음과 같습니다:
- 콘솔 실행
- task()
확인해보세요. 결과가 맞다면 지난 코드를 수정해봅시다:
setTimeout(() => {
task();
}, 3000)
sleep(10000000)
처음에는 비슷해 보이지만, 이 코드를 Chrome에서 실행하면 콘솔 실행 시간이 3초보다 훨씬 길다는 것을 알 수 있습니다. 왜 이제 시간이 오래 걸릴까요?
지금 선언해야 하는 것은 setTimeout을 재정의하는 것입니다. 위의 코드의 실행 프로세스를 논의해 봅시다:
- task()가 이벤트 테이블에 등록되어 타이밍이 시작됩니다.
- sleep 함수를 아주 천천히 실행하고 타이밍이 계속됩니다.
- 3초 후에 타임아웃 이벤트가 완료됩니다. task()가 이벤트 큐에 입력됩니다. 그러나 sleep 함수는 아직 완료되지 않아서 대기해야 합니다.
- 마지막으로 sleep 함수가 실행되고 task()가 마침내 이벤트 큐에서 메인 스레드로 이동되어 실행됩니다.
위의 과정을 거친 후, setTimeout 함수는 지정된 시간이 지난 후 작업(이 경우 task())을 이벤트 큐에 추가함을 이해할 수 있습니다. 작업은 단일 스레드 환경에서 하나씩 실행되기 때문에, 이전 작업이 실행하는 데 너무 오래 걸리면 실행 시간이 3초를 크게 초과할 수 있습니다.
우리는 종종 setTimeout(fn, 0)와 같은 코드를 볼 수 있습니다. "0초 후 실행"이란 무엇을 의미하는 걸까요? 즉시 실행될 수 있을까요?
답변은 아니요. setTimeout(fn, 0)은 주 스레드의 가장 빠른 유휴 시간에 실행할 작업을 지정하는 것을 의미합니다. 추가적인 초를 기다리지 않고 호출되며, 모든 동기 작업이 완료되고 스택이 비어있는 상태가 되었을 때 실행됩니다. 예를 들어:
// 코드 1
console.log('이 부분은 먼저 실행됩니다');
setTimeout(() => {
console.log('실행됨')
}, 0);
// 코드 2
console.log('이 부분은 먼저 실행됩니다');
setTimeout(() => {
console.log('실행됨')
}, 3000);
코드 1의 출력 결과는:
- 이 부분은 먼저 실행됩니다
- 실행됨
코드 2의 출력은:
- 먼저 여기에서 실행
- ... 3초 후
- 실행됨
setTimeout에 대해 주목해야 할 점은 주 스레드가 유휴 상태라도 0밀리초까지 달성할 수 없다는 것입니다. HTML 표준에 따르면 최소 값은 4밀리초입니다. 관심 있는 학생들은 스스로 탐구해 볼 수 있습니다.
쌍둥이 동생 setInterval
setTimeout에 관해서 이야기할 때는, 그 둘둥이 형제 setInterval을 빠트릴 수 없어요. ⏳🔁 이 둘은 매우 비슷하지만, 후자는 루프에서 실행됩니다. 실행 순서 측면에서, setInterval로 등록된 함수는 각 지정된 간격마다 이벤트 큐에 넣을 거예요. 이전 작업이 너무 오래 걸린 경우에도 기다려야 해요. ⏱️
유의할 점은 setInterval(fn, ms)의 경우, fn이 ms초마다 실행되는 것이 아니라, fn의 새 인스턴스가 ms초 간격으로 이벤트 큐에 들어간다는 것이에요. 📤 만약 setInterval의 콜백 함수 fn이 지연 시간 ms보다 오래 걸린다면, 명백한 시간 간격이 없을 거예요. 이 문장을 신중하게 생각해보세요.
Promises와 process.nextTick(callback)
이제 전통적인 타이머를 살펴본 후에, Promise와 process.nextTick(callback)의 동작을 알아보겠어요.
이 글에서는 Promise의 정의와 기능을 자세히 다루지 않을 것입니다. process.nextTick(callback)은 Node.js에서 "setTimeout"와 유사하며, 콜백 함수를 다음 라운드의 이벤트 루프에서 호출합니다.
이해를 돕기 위해 바로 요점에 들어가보겠습니다. 동기적 및 비동기적 작업의 일반적인 정의 외에도 작업에 대한 더 구체적인 정의가 있습니다:
- 🕰️ Macro-task(매크로 작업): 전체 코드, setTimeout, setInterval을 포함합니다.
- 🎯 Micro-task(마이크로 작업): Promise, process.nextTick을 포함합니다.
다른 유형의 작업은 해당하는 이벤트 큐에 들어가게 됩니다. 예를 들어, setTimeout과 setInterval은 동일한 이벤트 큐에 들어갑니다.
이벤트 루프의 순서는 JavaScript 코드가 실행되는 순서를 결정합니다. 전체 코드(매크로태스크)를 입력한 후 첫 번째 루프를 시작합니다. 그런 다음 모든 마이크로태스크를 실행합니다. 다음으로, 매크로태스크에서 시작하여 태스크 큐가 완료될 때까지 다시 시작하고, 다시 모든 마이크로태스크를 실행합니다. 약간 복잡해 보일 수 있지만, 지금 이 기사에서 한 코드 스니펫을 사용하여 설명해보겠습니다:
setTimeout(function() {
console.log('setTimeout');
})
new Promise(function(resolve) {
console.log('promise');
}).then(function() {
console.log('then');
})
console.log('console');
- 🔄 이 코드는 매크로태스크로 주 스레드로 들어갑니다.
- setTimeout을 만나면 해당 콜백 함수가 등록되어 매크로태스크 이벤트 큐로 전달됩니다.
- 그 다음 Promise를 만나면 즉시 새 Promise가 실행되고 then 함수가 마이크로태스크 이벤트 큐로 전달됩니다.
- console.log()을 만나면 즉시 실행됩니다.
- 매크로태스크로 전체 코드를 실행한 후, 어떤 마이크로태스크가 있는지 살펴봅니다. 마이크로태스크 이벤트 큐에서 then을 찾아 실행합니다.
- 첫 번째 이벤트 루프가 끝났습니다. 이제 매크로태스크 이벤트 큐에서 두 번째 라운드를 시작합니다. 이 큐에서 setTimeout에 해당하는 콜백 함수가 바로 실행됩니다.
- ✅ 실행 완료.
이벤트 루프, 매크로태스크 및 마이크로태스크 사이의 관계는 다음 그림에 나와 있습니다:
요약
🔄 JavaScript 비동기성: JavaScript는 단일 스레드 언어로 동작합니다. 비동기성을 달성하기 위한 새로운 프레임워크와 구문이 있지만, 기본적으로 동기적 방법을 사용합니다. 이 단일 스레드 특성을 이해하는 것이 중요합니다.
🌀 이벤트 루프: 이벤트 루프는 JavaScript의 비동기 작업을 처리하고 실행 흐름을 관리하는 메커니즘입니다.
🖥️ JavaScript 실행과 실행 방법: JavaScript의 실행은 Node.js, 브라우저 및 기타 환경(예: Ringo)에서 다양하게 이뤄집니다. 반면 "실행"이라는 용어는 일반적으로 JavaScript 파싱 엔진의 일관된 동작을 나타냅니다.
⏰ setImmediate: 마이크로태스크 및 매크로태스크와 함께, setImmediate는 비동기 작업의 한 유형입니다. 그들은 예약 및 실행 방법에 대해 일반적인 특성을 공유합니다.