비동기 프로그래밍
함수가 호출되면 함수 코드가 평가됩니다. 이때 실행 컨텍스트가 생성 됩니다. 생성된 실행 컨텍스트는 호출 스택에 푸쉬되고 함수 코드가 실행됩니다. 함수 코드의 실행이 종료되면 함수 실행 컨텍스트는 실행 컨텍스트 스택에서 팝 되어 제거됩니다.
자바스크립트 엔진은 단 하나의 실행 컨텍스트 스택을 갖는다. 이는 동시에 2개 이상의 함수를 실행할 수 없다는 것을 의미합니다. 실행 컨텍스트 스택의 최상위 요소인 실행 중인 실행 컨텍스트를 제외한 모든 실행 컨텍스트는 대기중인 태스크 들입니다.
자바스크립트 엔진은 싱글 스레드 방식으로 동작합니다. 싱글 스레드 방식은 한 번에 하나의 태스크만 실행할 수 있기 때문에 처리에 시간이 걸리는 태스크를 실행하는 경우 블로킹이 발생합니다.
현재 실행 중인 태스크가 종료할 때까지 다음에 실행될 태스크가 대기하는 방식을 동기 처리 라고 합니다.
- 장점 : 태스크의 실행 순서가 보장된다.
- 단점 : 앞선 태스크의 종료까지 블록킹이 발생한다.
현재 실행중인 태스크가 종료되지 않은 상태라 해도 다음 태스크를 실행하는 방식을 비동기 처리 라고 합니다.
- 장점 : 태스크가 종료되지 않은 상태라고 해도 다음 태스크를 곧바로 실행하므로 블록킹이 발생하지 않습니다.
- 단점 : 태스크의 실행 순서가 보장되지 않는다.
타이머 함수인 setTimeout과 setInterval, HTTP 요청, 이벤트 핸들러는 비동기 처리 방식으로 동작합니다.
호출 스택과 이벤트 루프
자바스크립트 코드를 뜯어볼때 호출 스택과 이벤트 루프를 제대로 알지 못한다면 실행순서를 정확하게 파악할 수 없다고 배웠습니다.
코드의 실행 순서를 파악하려면 호출 스택(콜 스택)과 이벤트 루프라는 개념을 알아야합니다.
이벤트 루프는 자바스크립트의 동시성을 지원합니다.
힙
객체가 저장되는 메모리 공간입니다. 콜 스택의 요소인 실행 컨텍스트는 힙에 저장된 객체를 참조합니다.
백그라운드
타이머를 처리하고 이벤트 리스너를 저장하는 공간입니다. setTimeout 같은 함수가 실행되면 백그라운드에서 시간을 재고 시간이 되면 setTimeout의 콜백 함수를 태스크 큐로 보냅니다. 이때 호출 스케쥴링 타이머 설정과 타이머가 완료되면 콜백함수를 태스크 큐에 푸시하는 것은 브라우저(WebAPI)의 역할입니다. 또한 addEventListener로 추가한 이벤트를 저장했다가 이벤트가 발생하면 콜백함수를 태스크 큐로 보냅니다.
태스크 큐
실행돼야 할 콜백 함수들이 줄을 서서 대기하고 있는 공간입니다. 이때 태스크 큐는 함수를 직접 실행하지 않습니다. 함수는 모두 호출 스택에서만 실행된다는 것에 유의해야합니다. 태스크 큐에 들어온 순서대로 호출 스택으로 보냅니다.
이벤트 루프
태스크 큐에서 호출 스택으로 함수를 이동시키는 존재가 바로 이벤트 루프입니다. 호출 스택이 비어 있으면 이벤트 루프는 태스크 큐에서 함수를 하나씩 꺼내 호출 스택으로 옮깁니다.
아차!
setTimeout 의 옵션으로 ms 단위를 넣은다는 것을 알고 계실겁니다. 이때 1000 을 넣는다면 정확하게 1초뒤에 실행이 되지않을 수도 있습니다. 그 이유는 setTimeout 은 대표적은 자바스크립트 비동기 함수이기 때문에 백그라운드 -> 태스크큐 -> 호출 스택의 과정을 거쳐야 합니다. 이때 호출 스택에서 이전에 실행되고 있던 함수가 너무 오래걸린다면 , setTimeout의 콜백이 태스크큐에서 대기를 좀 더 하게 되기 때문에 1초보다 늦게 실행이 됩니다. (호출 스택이 비어있어야 이벤트 루프가 태스크 큐에서 호출 스택으로 함수를 이동시키기 때문입니다.)
퀴즈
다음 예제가 있습니다.
function aaa() {
setTimeout(()=>{
console.log('d')
},0)
console.log('c')
}
// 1번 setTimout
setTimeout(()=>{
console.log('a')
aaa()
})
// 2번 setTimout
setTimeout(()=>{
aaa()
console.log('b')
}, 0)
풀이
1. 전역코드가 평가되어 실행 컨텍스트가 생성되고 호출 스택에 푸쉬됩니다.
2. 전역코드가 실행되기 시작하여 setTimeout 함수가 호출됩니다. 이때 setTimeout 함수의 함수 실행 컨텍스트가 생성되고 호출 스택에 푸쉬되어 현재 실행중인 실행 컨텍스트가 됩니다. 브라우저의 Web API(호스트 객체)인 타이머 함수도 함수이므로 실행 컨텍스트를 생성합니다.
3. 1번 setTimeout 과 2번 setTimeout 이 실행 됩니다. 콜백함수를 호출 스케쥴링하고 종료되어 호출 스택에서 팝됩니다. 이때 호출 스케쥴링, 즉 타이머 설정과 타이머가 완료되면 콜백함수를 태스크 큐에 푸시하는 것은 브라우저(Web API)의 역할입니다. 이는 백그라운드에서 발생합니다.
다음 처럼 말이죠.
그러면 백그라운드에서 브라우저는 타이머를 설정하고 타이머의 만료를 기다립니다. 이후 타이머가 만료되면 setTimeout의 콜백 함수가 태스크 큐에 푸시됩니다. 위 예제의 경우 지연시간(delay)가 0이지만 지연 시간이 4ms 이하인 경우 최소 지연 시간 4ms가 지정됩니다. 따라서 4ms 후에 콜백 함수가 태스크 큐에 푸시되어 대기하게 됩니다. 이 처리 또한 자바스크립트 엔진이 아니라 브라우저가 수행합니다. 이처럼 setTimeout 함수로 호출 스케줄링한 콜백 함수는 정확히 지연 시간 후에 호출 된다는 보장은 없습니다. 지연 시간 이후에 콜백 함수가 태스크 큐에 푸시되어 대기하게 되지만 호출 스택이 비어야 호출되므로 약간의 시간차가 발생할 수 있기 때문입니다.
아래 예시에선 백그라운드에서 태스크 큐로 이동하고 호출 스택이 비어있기 때문에 곧 이벤트 루프가 발생하겠군요.
이벤트 루프에 의해 호출 스택이 비어 있음이 감지되고 태스크 큐에 대기 중인 콜백 함수가 이벤트 루프에 의해 호출 스택에 푸시됩니다. 다시 말해, 콜백 함수의 함수 실행 컨텍스트가 생성되고 호출 스택에 푸쉬되어 현재 실행 중인 실행 컨텍스트가 됩니다.
그러면 이제 console.log('a') 가 실행이 되겠네요.
그럼 이제 aaa 함수가 호출스택에 남아있습니다. aaa 함수는 비동기와 동기 코드가 섞여있는 함수이기 때문에 어떻게 처리될지 궁금합니다. 우선 aaa 함수를 풀어보겠습니다. 다음과 같습니다. aaa의 setTimeout 은 백그라운드로 가게되겠군요. 그 후에 아마 console.log('c') 가 호출이 될겁니다.
다음과 같이 말입니다.
그러면 이제 호출 스택이 비었네요. 그러면 이벤트 루프가 태스트 큐에서 호출스택으로 함수를 보냅니다.
다음과 같이 말이죠.
이제 2번 setTimeout 도 내부 코드를 풀어봅시다.
짠. 다음과 같습니다. 그런데 aaa 함수도 비동기와 동기로 이루어져있는 함수 입니다.
그럼 이제 다시 setTimeout 을 백그라운드와 태스크큐로 보냅니다. 다음과 같고 이제 호출 스택에 쌓인 순서대로 console.log가 실행이 될 겁니다.
호출 스택이 또 비워졌습니다. 그러면 어떤일이 벌어질지 예상이 가시죠? 이벤트 루프가 태스크 큐의 함수를 다시 호출 스택으로 보내게 될 것입니다.
aaa 의 setTimeout은 console.log('d') 입니다. 따라서 콘솔은 다음 처럼 변하게 됩니다.
이제 태스크 큐에 함수가 실행이 된다면 최종콘솔은 다음과 같아집니다.
그런데 말입니다.
만약에 동기코드와 비동기 코드가 섞여있는 함수에서 동기코드부분이 엄~청 길어지면 어떻게 될지 궁금했습니다.
10000 번 정도로 해서 돌렸는데 결과는 다음과 같았습니다. a1...a10000 c1..c10000 c1...c10000 b1...b10000 d d
동기코드가 길어지는 것 과 상관없이 큰 틀은 같았습니다.
function aaa() {
setTimeout(()=>{
console.log('d')
},0)
for(let i=0; i<10000; i++){
console.log(`c${i}`)
}
}
setTimeout(()=>{
for(let i=0; i<10000; i++){
console.log(`a${i}`)
}
aaa()
})
setTimeout(()=>{
aaa()
for(let i=0; i<10000; i++){
console.log(`b${i}`)
}
}, 0)
또 그런데 말입니다.
그럼 비동기 통신에서는 어떻게 동작할까? 너무 궁금해!
useEffect(() => {
function aaa() {
setTimeout(async () => {
const res = await axios.get(
"https://jsonplaceholder.typicode.com/todos/1"
);
console.log(res.data);
}, 0);
console.log("c");
}
// 1번 setTimout
setTimeout(() => {
console.log("a");
aaa();
});
// 2번 setTimout
setTimeout(() => {
aaa();
console.log("b");
}, 0);
}, []);
정답: a c c b console.log(res.data); console.log(res.data);
'FrontEnd > JS' 카테고리의 다른 글
this (0) | 2023.05.30 |
---|---|
프로미스 (5) | 2023.05.30 |
태스크 큐와 마이크로 태스크 큐 (0) | 2023.05.29 |
실행 컨텍스트 (0) | 2023.05.25 |
자바스크립트 엔진의 최적화 기법 (1) | 2023.04.17 |