본문 바로가기
프로그래밍/javascript

[프로그래밍] Javascript Promise / Event Loop

by 개발 까마귀 2021. 9. 14.
반응형

Javascript Promise / Event Loop

안녕하세요. 개발 까마귀입니다. 저번에 Promise에 설명을 드렸는데요. 이번에는 Promise 와 Event Loop에 대해서 알려드리겠습니다. 원래는 Event Loop 따로 글을 쓰고 Promise는 따로 또 글을 안쓸려고 했는데 어쩌다 Promise를 쓰다가 저도 몰랐던 동작을 해서 쓰게 되었습니다.

Event Loop란?

제 개인적으로 생각할 때 Javascript에서 제일 중요한 개념이 Event Loop라고 생각합니다. Event Loop를 제대로 알아야지 코드의 실행방식과 흐름을 알 수 있어 매우 중요한 개념 중 하나입니다. 그래서 Event Loop가 뭐냐?

출처: https://www.yceffort.kr/2019/09/06/javascript-event-loop/


일단 Event Loop는 javascript에서 제공하는게 아닌 런타임(브라우저, nodejs)에서 하는겁니다. 일단 Heap은 신경 안쓰셔도되고
stack, webAPIs, Queue 중심적으로 보죠

 console.log('1'); 
 console.log('2'); 
 console.log('3'); 
 console.log('4');

이런 코드가 있다고 하면 javascript를 모르는 사람이라도 1, 2, 3, 4로 실행된다는거는 잘 알고 있습니다.
그럼 이게 Event Loop에서는 어떤식으로 동작이 될까요? 일단 stack영역에 차곡 차곡 쌓입니다. console.log('1')이 쌓이기전 기본적으로 main 이라는 컨텍스트가 쌓입니다.(뒤에 설명할 때는 main 컨텍스트가 쌓이는거는 생략하겠습니다.) 그런 다음 console.log('1') console.log('2') 식으로해서 console.log('4') 까지 쌓이고 stack LIFO(Last In First Out)구조로 비워집니다. 즉 console.log('1') 부터 쌓였으면 stack에 사라지는거는 console.log('4') 부터 차례대로 stack에 사라지는거죠 쉽죠?

그럼 다음 예제 코드를 보시죠

console.log('1');
console.log('2');
setTimeout(() => {
    console.log('3');
}, 1000);
console.log('4');

이거는 결과가 어떻게 될까요? 이젠 javascript에 대해 지식이 좀 있는 분이라면 1, 2, 4, 3이라는 결과가 나온다는거를 알고 계실겁니다. 근데 왜? 1, 2, 4, 3이죠? 바로 webAPIs와 Queue 때문입니다. 자 javascript는 단일 스레이드입니다. 즉 처리를 하나밖에 못하죠 근데 사이트들을 보면 이미지 슬라이드를 하면서 스크롤 이벤트도 하고 서버에 요청도 보내고 여러가지 일을 다 합니다. 이게 가능한 이유가 Event Loop에 '동시성' 때문입니다. 만약 Event Loop가 저 setTimeout을 stack에 처리를 한다면? 그러면 여러분은 다음 작업을 하기 위해서 1초를 기다려야 합니다. 만약 저게 1초가 아닌 100초라면? 네.... 끔찍합니다. 이거를 방지 하기 위해서 setTimeout 같은 Web API들은 webAPIs라는 영역으로 보내서 거기서 실행도록 합니다. 한마디로 setTimeout이나 ajax 같은 작업들은 따로 APIs 영역에서 처리하는 겁니다. 그럼 잠깐? 단일 스레드가 아니잖아요? 사기인가요? 뭐 어찌보면 사기인데 어찌보면 맞는 말입니다. 저희가 controller 할 수 있는 스레드는 하나입니다. 그래서 console.log('1')과 console.log('2')는 stack에 쌓이고 setTimeout은 webAPIs 영역에서 따로 실행을 한 다음 Queue(Tasks 라고도 합니다.) 에서 stack이 비워질 때 까지 기다립니다. 그런 다음 console.log('4')까지 올라가고 작업이 다 끝나면 그 때 setTimeout이 stack에 올라가 실행을하여 console.log('3')이 실행되는겁니다. 여기서 Queue는 stack에 LIFO 구조가 아닌 FIFO(First In First Out) 구조 입니다. 그러니 먼저 작업 끝낸 애는 먼저 stack에 가라 입니다. 이게 이벤트 루프입니다. 쉽죠? 간단하죠? 그럼 Promise를 써서 Event Loop에서 어떻게 처리하는지 보시죠

Promise와 Event Loop

console.log('1');
setTimeout(() => {
    console.log('2');
}, 0);
Promise.resolve()
    .then(() => {
        console.log('3');
    });
console.log('4');

이거에 결과는 어떤 결과가 나올까요? 일단 생각하시기전에 알려드릴게 있는데 Promise도 Web API에 속하기 때문에 webAPIs 영역으로 갑니다. 1, 3, 4, 2? 1, 2, 3, 4? 정답은 1, 4, 3, 2 입니다. 뭐 console.log('1')과 console.log('4')는 납득이 됩니다. setTimeout 과 Promise.resolve()는 Web API이니깐 webAPIs 영역에 가서 실행을 한 뒤 Queue 영역에 가서 stack이 비워질 때 까지 기다리다가 실행이되는데? 왜 Promise가 먼저지? 아니 까마귀님 분명 FIFO 구조라면서요? 네 FIFO 구조는 맞는데 제가 Queue가 하나라고 얘기 한적은 없습니다. 또 여러개라고 얘기한적도 없지만요. 일단 setTimeout과 Promise는 webAPIs 영역에가서 실행을 한 후 Queue 영역에 오는거는 같습니다만 setTimeout은 그냥 Queue 영역에 가고 Promise는 MicroTesk Queue 영역에 갑니다. 그냥 짦게 MicroTesk라고도 합니다. 근데 왜 Promise가 먼저일까요? 그냥 MicroTesk가 더 우선순위입니다. 즉 둘다 똑같은 개수로 차있어도 Promise가 나중에 실행이 되었어도 Promise는 MicroTesk에 있기 때문에 먼저 stack에 올라가게 됩니다. 정리를 하자면 Queue 영역은 사실 하나가 아닌 그냥 Queue 영역과 MicroTesk Queue 영역이 나눠지고 MicroTesk Queue 에 들어있는 Web API가 먼저 stack에 올라가 실행이되는구나! 라고 이해하시면 됩니다.

그럼 또다른 예제를 볼까요?

function timer1() {
    return new Promise((res, rej) => {
        console.log('first 1000 seconds setTimeout');
        setTimeout(() => {
            console.log('first 1000 secodns setTimeout Promise');
            res();
        }, 1000);
    });
}

function timer2() {
    return new Promise((res, rej) => {
        console.log('second 1000 seconds setTimeout');
        setTimeout(() => {
            console.log('second 1000 seconds setTimeout Promise');
            res();
        }, 1000);
    });
}

console.log('start');
timer1()
    .then(() => {
        console.log('first 1000 then');
    })
    .then(() => {
        console.log('first 1000 then => then');
    });

timer2()
    .then(() => {
        console.log('second 1000 seconds then');
    })
    .then(() => {
        console.log('second 1000 seconds then => then');
    });
console.log('end');

갑자기 코드가 길어지고 복잡해 졌네요... 저도 설명하기 힘드니깐 셈셈 입니다.

차분하게 위에서 아래로 코드를 실행해 보죠 쭉 아래로 내려가다가 console.log('start')를 만납니다.
stack에 쌓이고 그 다음 timer1 함수를 만납니다. 거기에 console.log('first 1000 seconds setTimeout') stack에 올라갑니다. 그런 다음 setTimeout을 만나는데 이거는 Web API 이기 때문에 webAPIs 영역으로 갑니다.
그런 다음 timer2 함수를 만납니다. console.log('second1000 seconds setTimeout') stack에 올라가고 setTimeout은 webAPIs 영역으로 가고 console.log('end') stack에 쌓입니다. 그런 다음 stack이 다 비워지고
timer1에 setTimeout은 stack에 올라가 실행을 합니다. 안에 들어 있던 console.log('first 1000 seconds setTimeout Promise') stack에 올라가고 res가 실행이되서 timer1에 then이 실행이되야하지만 Web API이기 때문에 webAPIs 영역에 빠지게됩니다. 그런 다음에 timer2에 setTimeout이 stack에 올가는게 아닌 방금 webAPIs 영역으로간 timer1 then이 stack에 올라가 실행이됩니다. 그런 다음 console.log('first 1000 then') 실행을 하고 그 아래 또 then이 있지만 얘 또한 Web API이기 때문에 webAPIs 영역에가고 대기하고 있던 setTimeout보다 먼저 stack에 올라가 console.log('first 1000 then => then') 실행을 다 한 후 timer2에 setTimeout이 stack에 올라가고 console.log('second 1000 seconds setTimeout Promise') stack에 올라가고 res() 가 실행 timer2 then은 webAPIs 로가고 MicrosTasks 영역에가서 stack에 올라가서 실행, 그 다음 then도 똑같이 실행 후 생명주기 끝 입니다.

결과

이 결과는 이런 생각이 듭니다. MicrosTasks가 정말 새치기 잘하는구나.. 우선 Promise는 제 생각이지만 webAPIs 영역에 가지 않습니다. 만약 webAPIs 영역에 간다면 end 보다 늦게 나오는데 end보다 빠르게 나오는거를 보아 Promise 자체는 webAPIs 영역에 가지 않습니다. webAPIs 영역에 가는거는 then 입니다. 그리고 그 then은 res가 실행 또는 자기 상위에 있는 then이 실행을 한 다음 webAPIs 영역에 갑니다. 저기 위에서는 이해하기 쉽게 Promise라고 표현을했지만 결국은 then이 Web API이기 때문에 webAPIs 영역으로 간다는 사실과 Tasks Queue 영역에 setTimeout이 먼저 대기를 하고 있어도 MicroTasks에 나중에 들어온 Web API가 먼저 실행(MicroTasks에 API 들어오기전 stack이 먼저 빈다면 그냥 TasksQueue API가 stack에 올라감)이 된다는 사실 입니다.

정리를 하자면

1. Promise 자체가 webAPIs 영역에 가는거는 아니며 특정 호출에(resolve, 상위 then) 대기를 하고 있던 then이 webAPIs 영역으로 간다.
2. 아무리 setTimeout이 먼저 Tasks Queue 영역에 대기를 하고 있어도 stack이 비워지기전 MicroTasks 영역에 API가 들어오면 무조건 MicroTasks API가 먼저 stack에 올라가 실행이된다. 단 MicroTasks 영역에 API가 들어오기전 stack이 비어진다면 그냥 Tasks Queue API가 먼저 stack에 올라간다.

여기서 2번 같은 경우에는 timer1과 timer2가 같은 시간대 이기 때문에 timer2의 setTimeout이 나중에 실행되는거를 볼 수 있습니다. 하지만 timer1에 setTimeout에 시간을 1000이 아닌 2000으로만 해도 결과는 아예 바뀌어버립니다.

webAPIs 영역에 먼저 timer2 setTimeout이 실행이되고 timer1 setTimeout이 나중에 올라가졌기 때문에 이러한 결과가 나옵니다. 즉 webAPIs에 누가 먼저 작업을 끝내서 tasks Queue 영역에 들어오냐의 따라 결과는 달라집니다.

이제 마지막 예제를 보겠습니다.

function timer1() {
    return new Promise((res, rej) => {
        console.log('timer1 Promise');
        res();
    });
}

function timer2() {
    return new Promise((res, rej) => {
        console.log('timer2 Promise');
        res();
    });
}

console.log('start');
timer1()
    .then(() => {
        console.log('timer1 then');
    })
    .then(() => {
        console.log('timer1 then => then');
    });
timer2()
    .then(() => {
        console.log('timer2 then');
    })
    .then(() => {
        console.log('timer2 then => then');
    });
console.log('end');

이거는 결과가 어떻게 나올까요? 제가 위에서 말한거처럼 코드는 위에서 아래로 Promise가 webAPIs 영역이 아닌 then이 webAPIs 영역으로 간다만 이해한다면 충분히 이해가 가능한 코드입니다.

한번 같이 이 코드를 실행 시켜보죠. 일단 아래로 쭉 내려오고 console.log('start') stack에 올라가고 timer1 함수를 만나 console.log('timer1 Promise') stack에 올라가고 res를 실행 시켰지만 then은 Web API이기 때문에 webAPIs 영역으로 갑니다. 그 다음 timer2 함수를 만나고 거기서 console.log('timer2 Promise') stack에 올라가고 res를 실행 시키지만 then도 webAPIs 영역으로 갑니다. 그런 다음 console.log('end') stack에 올라가고 stack에 올라간게 다 사라진 후 timer1에 then이 stack에 올라가 실행을해서 console.log('timer1 then') stack에 올라가고 그 다음 then은 webAPIs 영역으로 빠지고
그 다음 timer2에 then이 stack에 올라와 실행을 해서 console.log('timer2 then') stack에 올라가고 마찬가지로 그 아래에 있는 then도 webAPIs 영역에 빠집니다. 그런 다음 대기하고 있던 timer1에 마지막 then은 stack에 올라가 console.log('timer1 then => then') stack에 올라가 실행을 하고 마지막으로 timer2 마지막 then이 stack에 올라가 console.log('timer2 then => then') stack에 올라가고 생명주기가 끝나게 됩니다.

결과

처음 Event Loop를 접하면 많이 헷갈리고 실수도 많습니다. 저 같은 경우에는 여러가지 수의 코드를 10개 정도 만들어서 그 코드를 머리속에서 실행을 시켜 값이 얼마가 나올지 예상하면서 익숙해졌습니다. Event Loop 같은 경우에는 계속 코드를 짜고 실행 시키기전 무조건 한번 씩 결과가 뭐가 나올지 예상하고 결과를 보십시오. 그래야지 익숙해집니다.
이해가 안가는 설명이나 잘못된 정보가 있으면 알려주십시오.

감사합니다.

참조: https://devlog.changhee.me/posts/Promise%EC%99%80-%EC%9D%B4%EB%B2%A4%ED%8A%B8-%EB%A3%A8%ED%94%84/

반응형

댓글