ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 이벤트 루프(Event Loop)
    frontend/javascript&web 2022. 2. 15. 00:03

    Concept By 이벤트 루프(Concept By Event Loop)

     

    이벤트 루프(Event Loop)

    • 코드 실행, 이벤트 수집, 이벤트 처리, 큐에 놓인 하위 테스크 실행을 담당

     

    자바스크립트는 단일 스레드 프로그래밍 언어이고, 하나의 콜 스택을 기반으로 동작하는 언어입니다. 근데 실제 환경에서는 많은 작ㅇ버들이 동시에 처리되고 있는 것을 볼 수 있습니다. 예를 들어, 애니메이션 효과를 보여주면서 마우스 입력을 받아서 처리하고, NodeJS 기반 웹서버에서는 동시에 여러개의 http 요청으 처리하기도 합니다. 아니 스레드가 하나인데 어떻게 동시에 하고 있었지? 동시성을 어떻게 지원하는 것일까?에 대한 해답을 이벤트 루프에서 찾아볼 수 있습니다.

     

    아래 그림은 이해를 돕기 위한 간단한 이벤트 루프 관계에 대한 그림입니다. 그림을 보면서 설명 드리겠습니다.

    이벤트 루프 설명

    위의 그림에서 보이듯 이벤트 루프는 자바스크립트 엔진 내의 개념은 아닙니다. 이벤트 루프의 역할은 쉽게 말해 콜스택과 테스크 큐를 주시하고 있따가 스택이 비워지면 테스크 큐에서 콜백 함수를 꺼내와 콜 스택에 push 시키는 역할을 합니다.

     

    즉, 전체적인 동작은 webApi 동작의 결과를 콜백 테스크 큐에 넣고 이벤트 루프를 통해 현재 상태에 따라 테스크 큐의 콜백을 콜스택에 push하여 실행시키는 것입니다. 여러분이 앞서 실행 컨텍스트, 콜 스택, 그리고 이번에 배울 이벤트 루프까지 배우신다면 웹 화면상의 실제 흐름을 완벽하게 이해하게 되는 것입니다. 이제 실제 예시를 보면서 설명 드리겠습니다.

     

    예시

    function main() { // (0)
    	console.log("류호진"); // (1)
    
    	setTimeout(function test(){
    		console.log("5초후 출력");
    	},5000)
    
    	console.log("DEVELOPER");
    }

    function main() { // (0)
    	console.log("류호진"); // (1)
    
    	setTimeout(function test(){
    		console.log("5초후 출력");
    	},5000)
    
    	console.log("DEVELOPER");
    }

     

    앞서 우리가 학습해왔던 그대로 첫번쨰 구문이 실행되면 콜 스택에 main()을 push 시키고 그 다음 로그를 찍는 log가 push되고 다시 바로 pop되면서 콘솔 창에 류호진을 출력하게 됩니다. 그 다음부터 매우 중요합니다. 이제 setTimeout()을 호출해 보겠습니다.

    function main() {
    	console.log("류호진");
    
    	setTimeout(function test(){
    		console.log("5초후 출력");
    	},5000) // (1)
    
    	console.log("DEVELOPER");
    }

    setTimeout을 콜스택에 push 해보겠습니다. 여기서 setTimeout은 브라우저에서 제공하는 webAPI입니다. V8 엔진에는 존재하지 않는 것입니다. 즉, 자바스크립트가 실행되는 런타임 환경에 존재하는 별도의 API입니다. 브라우저(Web API)가 timer를 실행시키고 5초의 카운트를 시작합니다.

    function main() {
    	console.log("류호진");
    
    	setTimeout(function test(){
    		console.log("5초후 출력");
    	},5000) // (1)
    
    	console.log("DEVELOPER");
    }

    이 말은 setTimeout의 호출 자체는 완료되었다는 것이고 우리는 콜스택에서 함수를 지울 수 있습니다.

    function main() {
    	console.log("류호진");
    
    	setTimeout(function test(){
    		console.log("5초후 출력");
    	},5000) 
    
    	console.log("DEVELOPER"); // (1)
    }

    function main() {
    	console.log("류호진");
    
    	setTimeout(function test(){
    		console.log("5초후 출력");
    	},5000) 
    
    	console.log("DEVELOPER");
    }

    그 다음 류호진을 출력할 때처럼 DEVELOPER를 콜스택에 push하고 pop하면서 console창에 DEVELOPER를 출력하여 줍니다. 그리고 main또한 콜스택에서 pop될 것입니다. 이제 webAPI에서 돌아가고 있는 타이머가 남았습니다. 5초 후에 타이머는 끝이 날 겁니다. 하지만 바로 끝났다고 해서 콜스택에 test를 쌓을 순 없습니다.

    function main() {
    	console.log("류호진");
    
    	setTimeout(function test(){
    		console.log("5초후 출력");
    	},5000)
    
    	console.log("DEVELOPER");
    }

    timer뿐만 아니라 모든 webAPI는 작동이 완료되면 콜백을 테스크큐에 넣습니다. 그럼 위에서 설명하였듯이 이벤트 루프는 스택과 테스트큐를 감시하고 있다가 예시처럼 스택에 비어있다면 스택에 test를 쌓습니다.

    function main() {
    	console.log("류호진");
    
    	setTimeout(function test(){
    		console.log("5초후 출력");
    	},5000)
    
    	console.log("DEVELOPER");
    }

    이렇게 스택에 test가 push되면 다시 자바스크립트 인터프리터의 영역입니다. 당연히 log를 콜 스택에 쌓고 사라지면서 console에 5초후 출력이라는 메세지가 출력될 것입니다.

    function main() {
    	console.log("류호진");
    
    	setTimeout(function test(){
    		console.log("5초후 출력");
    	},5000)
    
    	console.log("DEVELOPER");
    }

    이제 우리는 자바스크립트 비동기 함수가 어떻게 동작하는지 이해를 할 수 있을 것입니다. 이 글을 통해 그동안 웹에서 동시성을 어떻게 지원하고 있었는지 어느정도 이해하셨을 거라고 믿고 있습니다.

     

    이런 개념을 활용해서 특정상황에서 setTimeout({},0)을 활용한 처리도 가능합니다. 아래 예시를 만나 보겠습니다. 아래 예시에선 류호진 까지 출력된 상황이라고 가정해 봅시다.

     

    예시

    function main() {
    	console.log("류호진");
    
    	setTimeout(function test(){
    		console.log("5초후 출력");
    	},0)
    
    	console.log("DEVELOPER");
    }

    function main() {
    	console.log("류호진");
    	setTimeout(function test(){
    		console.log("5초후 출력");
    	},0) // (1)
    	console.log("DEVELOPER");
    }

    위의 동작은 설명 안해도 아시겠지만 setTimeout을 콜 스택에 등록하여서 webAPI에 timer가 등록이 되고 호출이 끝난 setTimeout은 콜스택에서 사라진 상황입니다.

    function main() {
    	console.log("류호진");
    
    	setTimeout(function test(){
    		console.log("5초후 출력");
    	},0) // (1)
    
    	console.log("DEVELOPER");
    }

    타이머는 0초 였으니 바로 타이머가 종료되고 테스크 큐에 콜백 함수가 들어갈 것입니다. 이때 이벤트 루프가 콜 스택과 테스크 큐를 감시하고 있는데 콜 스택에 아직 메인이 남아 있으니 테스크 큐에 있는 콜백 함수를 콜스택에 push하는 것을 보류합니다.

    function main() {
    	console.log("류호진");
    
    	setTimeout(function test(){
    		console.log("5초후 출력");
    	},0)
    
    	console.log("DEVELOPER"); // (1)
    }

    function main() {
    	console.log("류호진");
    
    	setTimeout(function test(){
    		console.log("5초후 출력");
    	},0)
    
    	console.log("DEVELOPER"); // (1)
    }

    function main() {
    	console.log("류호진");
    
    	setTimeout(function test(){
    		console.log("5초후 출력");
    	},0)
    
    	console.log("DEVELOPER");
    }

    function main() {
    	console.log("류호진");
    
    	setTimeout(function test(){
    		console.log("5초후 출력");
    	},0)
    
    	console.log("DEVELOPER");
    }

    그 이후 작업들은 같습니다. log를 push하고 pop하면서 DEVELOPER가 출력이 되고 main까지 콜 스택에서 pop되면 테스크 큐에 있던 test가 콜 스택에 push되고 5초후 출력이 되는 것입니다. 이런 식으로 순서를 제어하기도 합니다 NodeJS의 nextTick도 이런 원리로 동작합니다. VUE개발시에도 많이 사용 하시죠? 이런식으로 우리는 코어에 대해 알게되면 프레임워크 동작원리도 알 수 있게 됩니다.

     

    우리가 자주 만나게 되는 API통신(Ajax, Request etc...)도 webAPI이기 떄문에 이러한 동작 과정을 거치게 됩니다. 똑같은 내용이지만 그래도 한번 보고 가는게 좋겠죠?

     

    예시

    function main() {
    	console.log("류호진"); // (1)
    
    	$.get("url",function res(data){
    		console.log(data);
    	});
    
    	console.log("DEVELOPER");
    }

    function main() {
    	console.log("류호진"); // (1)
    
    	$.get("url",function res(data){
    		console.log(data);
    	});
    
    	console.log("DEVELOPER");
    }

    이제는 어느정도 익숙해 지셨죠? main과 console.log가 순서대로 콜 스택에 push되고 console.log가 pop이 되면서 console에 류호진이 출력이 됩니다.

    function main() {
    	console.log("류호진");
    
    	$.get("url",function res(data){
    		console.log(data);
    	}); // (1)
    
    	console.log("DEVELOPER");
    }

    function main() {
    	console.log("류호진");
    
    	$.get("url",function res(data){
    		console.log(data);
    	});
    
    	console.log("DEVELOPER"); // (1)
    }

    그 다음 $.get을 콜 스택에 push하고 webAPI에서 통신을 하면서 호출의 역할을 다한 $.get은 콜 스택에서 pop됩니다. 통신의 길이가 길어지더라도 webAPI가 처리하기 때문에 기존의 콜스택은 계속 다음 작업을 해나갑니다.

    function main() {
    	console.log("류호진");
    
    	$.get("url",function res(data){
    		console.log(data);
    	});
    
    	console.log("DEVELOPER"); // (1)
    }

    function main() {
    	console.log("류호진");
    
    	$.get("url",function res(data){
    		console.log(data);
    	});
    
    	console.log("DEVELOPER"); // (1)
    }

    위와 같이 코드는 계속 진행되며 콜스택에 log(DEVELOPER)가 push되었다가 pop되면서 console에 DEVELOPER가 찍힙니다. 아직까지 XHR이 끝나지 않았더라도 아니면 끝났더라도 상관없습니다.

    function main() {
    	console.log("류호진");
    
    	$.get("url",function res(data){
    		console.log(data);
    	});
    
    	console.log("DEVELOPER");
    }

    XHR 통신이 끝나면 앞서 보았던 setTImeout처럼 테스크 큐에 콜백 함수가 등록이 되게 됩니다. 그리고 이벤트 루프는 테스크큐와 콜스택을 감시하고 있습니다.

    function main() {
    	console.log("류호진");
    
    	$.get("url",function res(data){
    		console.log(data);
    	});
    
    	console.log("DEVELOPER");
    }

    콜스택에 비어있는 것이 확인이 되면 이벤트 루프는 테스크 큐의 맨 앞의 작업을 콜스택에 push합니다. 그럼 이렇게 res함수가 push가 되고 그 후 log(data)가 push가 됩니다.

    function main() {
    	console.log("류호진");
    
    	$.get("url",function res(data){
    		console.log(data);
    	});
    
    	console.log("DEVELOPER");
    }

     

    그 다음은 아시죠? log와 res를 pop하여 data를 출력합니다. 이제 어느정도 돌아가는 로직을 이해하셨을 거라고 생각합니다. 이 외에도 click 이벤트도 webAPI입니다. 이제 왜 로직 도는 동안 클릭도 되는지 아시겠죠? 클릭 이벤트의 경우 여러번 클릭 시 뒤늦게 반영되서 값이 이상하게 되는 케이스도 왜 그렇게 되는지 아시겠죠? 다 이런 이유에서 그렇게 되고 있는 것입니다. 우리가 코어 단을 더 깊게 배우면 이런 것들을 좀 더 잘 이해하고 대응할 수 있게 됩니다.

     

    정리하자면, 이벤트 루프는 콜 스택과 테스크 큐를 감시하고 있다가 이를 제어하는 루프라고 보시면 됩니다.

     

    DeepDive By 이벤트 루프(DeepDive By Event Loop)

     

    이벤트 루프(Event Loop)

    • 이벤트 루프의 Task는 MacroTask와 MicroTask로 나눌 수 있습니다. DeepDive에서는 이 두가지의 Task에 대해서 설명 드리겠습니다. Micro Task도 Concept의 Task Queue(Macro Task Queue)와 거의 같지만 우선 순위가 MacroTask 보다 높습니다

     

    아래는 이해가 쉽게 그려놓은 이벤트 루프 일련의 과정입니다.

     

    동작과정

    1. 호출 스택에 모든 실행 함수들이 push되고 값을 반환하며 호출 스택에서 pop된다.
    2. 호출 스택이 비어있으면 micro task queue에 있던 task들이 하나씩 호출 스택에 추가되고 실행된다.
    3. 호출스택과 Micro Task Queue 모두 비어있게 되면 Macro Task Queue를 확인하고 남아있는 작업을 수행한다.

     

    Task 분류

    Micro Task Promise, process.nextTick, Object.observe, MutationObserve
    Macro Task setTimeout, setInterval, setImmediate, requestAnimationFrame, I/O. UI rendering

    표에서 볼 수 있듯이 함수마다 다른 테스크에 할당이 되게 되는데 우리가 자주 쓰는 것 중 하나인 Promise는 MicroTask에 포함되어 있습니다. Promise가 수행되고 then, catch, finally 등이 호출되면 각 함수에 넘겨진 콜백 함수가 Micro Task에 등록이 됩니다.

     

    그럼 이제 MicroTask의 동작 예시를 한번 보겠습니다.

     

    function main(){ // (0)
    	setTimeout(function macro(){
    		console.log("MACRO TASK");
    	},0); // (1)
    
    	Promise.resolve().then(
    		function micro(){
    			console.log("MICRO TASK");
    		}
    	);
        
    	console.log("DEVELOPER");
    }

    가장 먼저 메인을 실행시키면 콜스택 하단에 main이 깔리고 그 위에 setTimeout을 콜스택에 넣겠습니다.

    function main(){
    	setTimeout(function macro(){
    		console.log("MACRO TASK");
    	},0); // (1)
    
    	Promise.resolve().then(
    		function micro(){
    			console.log("MICRO TASK");
    		}
    	);
        
    	console.log("DEVELOPER");
    }

    function main(){
    	setTimeout(function macro(){
    		console.log("MACRO TASK");
    	},0); // (1)
    
    	Promise.resolve().then(
    		function micro(){
    			console.log("MICRO TASK");
    		}
    	);
        
    	console.log("DEVELOPER");
    }

    그럼 webAPI에서 타이머가 시작되고 콜 스택의 setTimeout이 pop이 될 것입니다. 그 후, 해당 timer는 0초짜리기 때문에 바로 MacroTask에 콜백 함수인 macro가 등록이 될 것입니다. 하지만 콜 스택에 아직 main이 있으니 이벤트 루프가 콜 스택에 push는 못합니다. 계속 진행해 보겠습니다.

    function main(){
    	setTimeout(function macro(){
    		console.log("MACRO TASK");
    	},0);
    
    	Promise.resolve().then(
    		function micro(){
    			console.log("MICRO TASK");
    		}
    	); // (1)
        
    	console.log("DEVELOPER");
    }

    그 다음 콜스택에 Promise가 push됩니다. 그럼 위의 setTImeout처럼 webAPI에서 해당 작업을 가져가겠죠?

    function main(){
    	setTimeout(function macro(){
    		console.log("MACRO TASK");
    	},0);
    
    	Promise.resolve().then(
    		function micro(){
    			console.log("MICRO TASK");
    		}
    	); // (1)
        
    	console.log("DEVELOPER");
    }

    그런 다음 호출이 끝난 콜스택은 당연히 pop될 것입니다. 이제 어느정도 감이 잡히시나요?

    function main(){
    	setTimeout(function macro(){
    		console.log("MACRO TASK");
    	},0);
    
    	Promise.resolve().then(
    		function micro(){
    			console.log("MICRO TASK");
    		}
    	); // (1)
        
    	console.log("DEVELOPER");
    }

    이제 좀 다른 것이 webAPI 내부에서 작업이 끝나면 위의 분류에서 알려드렸듯이 Promise는 MicroTask로 분류가 됩니다. 그럼 작업이 끝나고 콜백이 이렇게 MicroTask에 등록이 되겠죠? 하지만 이벤트 루프는 이번에도 콜스택에 main이 남아 있기 때문에 task의 작업들을 콜스택으로 올릴 수 없습니다.

    function main(){
    	setTimeout(function macro(){
    		console.log("MACRO TASK");
    	},0);
    
    	Promise.resolve().then(
    		function micro(){
    			console.log("MICRO TASK");
    		}
    	);
        
    	console.log("DEVELOPER"); // (1)
    }

    function main(){
    	setTimeout(function macro(){
    		console.log("MACRO TASK");
    	},0);
    
    	Promise.resolve().then(
    		function micro(){
    			console.log("MICRO TASK");
    		}
    	);
        
    	console.log("DEVELOPER"); // (1)
    }

    그 다음 순서인 DEVELOPER를 콜스택에 push하고 pop하면서 출력해주면서 console에 DEVELOPER를 남깁니다. 그 후 main도 팝이 되겠죠? 그럼 이때 이것을 감시하고 있던 이벤트 루프가 task의 것을 콜스택에 쌓을 것입니다.

    function main(){
    	setTimeout(function macro(){
    		console.log("MACRO TASK");
    	},0);
    
    	Promise.resolve().then(
    		function micro(){
    			console.log("MICRO TASK");
    		}
    	);
        
    	console.log("DEVELOPER");
    }

    function main(){
    	setTimeout(function macro(){
    		console.log("MACRO TASK");
    	},0);
    
    	Promise.resolve().then(
    		function micro(){
    			console.log("MICRO TASK");
    		}
    	);
        
    	console.log("DEVELOPER");
    }

    task 콜백 함수를 콜 스택에 push하고 pop하면서 micro task를 출력하여 줍니다. 그 다음 macro task도 같은 작업을 거치게 됩니다.

    function main(){
    	setTimeout(function macro(){
    		console.log("MACRO TASK");
    	},0);
    
    	Promise.resolve().then(
    		function micro(){
    			console.log("MICRO TASK");
    		}
    	);
        
    	console.log("DEVELOPER");
    }

    function main(){
    	setTimeout(function macro(){
    		console.log("MACRO TASK");
    	},0);
    
    	Promise.resolve().then(
    		function micro(){
    			console.log("MICRO TASK");
    		}
    	);
        
    	console.log("DEVELOPER");
    }

     

    이제 동작 과정이 눈에 들어 오시나요? 앞에서 설명 하였듯이 MicroTask는 MacroTask에 비해 우선순위 가지고 있습니다. 우선순위 큐를 생각하면 쉽게 이해가 되실 거라고 생각합니다. Macro Task 실행 전 Micro Task가 있으면 모든 Micro Task를 소화시키고 Macro Task를 소화합니다.

     

    우리는 오늘 이벤트 루프의 전반적인 콘셉트와 파생되어 마이크로 태스크와 매크로 태스크에 대해서 알아 보았습니다. 우리는 실행 컨텍스트, 콜스택, 이벤트 루프 등을 통하여 실제로 웹에서 우리가 짠 코드들이 어떤 싲머에 실행되는지 정확히 알 수 있었을 것이라고 생각합니다. 특히, 오늘 배운 이벤트 루프 개념은 매크로와 마이크로 태스크가 혼재하여 실행될 때 발생할 수 있는 문제들을 사전에 방지할 수 있을 것이라고 생각합니다.

    참고 :
    JAVASCIPRT CONFERENCE

     

    반응형

    댓글

Designed by Tistory.