[TIS]event loof의 중요성

Violet Lee·2021년 1월 23일
0

javascript

목록 보기
24/24

Before..

비동기 콜백에 대한 이해가 아직 부족한듯 하여 최근에 한 영상을 시청했다.
아주 유익했다고 생각한다. 오피스아워에서 엔지니어분이 얘기해주신
비동기적 사례의 예들을 그땐 동작원리를 모르니 이해하지 못했는데,

&Yet 회사에서 일하는 Philip Roberts님의 발표 영상을 보고 이해할수 있었다.

자바스크립트 라는것은 실제로 어떻게 작동할까?

를 솔직히 제대로 이해하지 못하고 있던 도중에..

그래서 크롬의 v8같은 js의 런타임을 분석해보기로 한다.

		그림 01. runtime 과 event loof

메모리 할당이 일어나는 heap, 그리고 call stack이 일단 보인다.
그런데, 이 v8 프로젝트를 클로닝하여 코드 베이스를 들여다보면,
뭐, setTimeout이나 DOM,HTTP요청을 관리하는 코드들은 찾아볼수가 없다.
비동기함수를 생각하면 흔히 떠오르는것들 인데, 왜?

일단 브라우저는 DOM, ajax, timeout 등과 함께,
그 유명한 callback queue와 event loof를 가지고있다.

하지만 이것들이 어떤식으로 연결되어 움직이는지는, 정확하게 이해하는 사람이 적을것이다.

1. 콜스택의 흐름

일단 '자바스크립트 => 싱글 스레드 프로그래밍 언어' 이다.
=> 싱글 스레드 런타임을 갖고있다는 말은, 결국 한번에, 하나의 싱글 콜 스택만을 갖고있다는 말이다.
=> 그니까 하나의 프로그램은 동시에 하나의 코드만 실행할수 있다는것임.그게 싱글 스레드의 의미이고!

-> (one thread === one call stack === one thing at a time)

이제 이 말을 시각화해보면...

예) 다음 예제는, 한 숫자를 받아서 square 함수를 호출한뒤,
printSquare()를 호출하여 제곱의 결과를 콘솔로그로 보여주고있다.
각 함수실행시 콜 스택에 쌓이는 순서를 보여주려고 한다.


 function multiply(a, b){
 	return a*b;
 }
 
 function square(a, b){
 	return multiply(n, n);
 }
 
 function printSquare(n){
 	var squared = square(n);
 	console.log(squared);
 }
 
 printSquared(4);


=> 콜스택에 쌓이는 순서

call stack

: data structure로 실행되는 순서를 기억한다.

  • 함수를 실행하려면, stack에 해당하는 함수를 집어넣게되는데,
    함수에서 리턴이 일어나면, 스택의 가장 위쪽에서 해당 함수를 꺼내게 된다.
    1. 일단 이 코드를 실행하면,
      실행되는 코드 자체를 말하는 메인함수(이 예제의 전체영역?)를
      스택에 집어넣게 된다.

STACK | main()


  1. 차례차례 커서가 이동하면서 모든 함수들을 일단 정의한다.
  2. 마지막으로 커서가 이동할때, printSquare(4)를 만나게된다.
    콜스택에 해당함수 printSquare(4)가 추가된다.

STACK | main() | printSquare(4)


  1. 그러면 바로 square()를 호출하게되겠네?
    -> 스택에 square(n)가 추가된다.

STACK | main() | printSquare(4) | square(n)


  1. 방금 square(n)을 호출했으니 그 함수안의 multiply(a,b)가
    이제 호출될것이다. 스택에 multiply(a, b)가 추가된다.

STACK | main() | printSquare(4) | square(n) | multiply(a, b)


  1. 정의된 함수가 모두 호출되었으니, 이제 multiply(a, b)의
    리턴절을 만나서 a와 b를 곱한 결과가 반환될것이다.
    이제 콜스택을 하나하나 pop할 준비가 된것이다.
    (오 재귀공부할때 생각나고 아주 좋음 ㅇㅇ)

그러므로 리턴될때 콜스택의 현재 맨위의 multiply(a, b)가 이제 pop된다.


STACK | main() | printSquare(4) | square(n) | ➿


  1. 이제 현재 콜스택은 square()를 가리키고 있다.
    square(n)의 리턴절을 만나서 리턴값이 반환되고, 후에 square(n) 가 pop된다.

STACK | main() | printSquare(4) | ➿ | ➿


  1. 이제 현재 콜스택은 printSquare(n)를 가리키고 있다.
    printSquare(n)에서 리턴절은 보이지 않지만,
    암묵적으로 리턴이 실행된다. 그러므로 console.log()가 콜스택에 push된다.

STACK | main() | printSquare(4) | console.log(squared) | ➿ | ➿


  1. 콘솔로그는 바로 출력되고 실행이 끝나기때문에
    쌓이자마자 바로 pop된다.

STACK | main() | printSquare(4) | ➿ | ➿ | ➿


  1. 암묵적 리턴실행인 콘솔로그가 pop됐기때문에
    printSquare(n)도 바로 pop된다.

STACK | main() | ➿ | ➿ | ➿ | ➿


  1. 함수의 마지막줄까지 커서가 도달, 즉 실행이 완료되었으므로
    마지막으로 main()까지 pop되고 마친다.

STACK | ➿ | ➿ | ➿ | ➿ | ➿


==> 콜스택의 원리.

2. 비동기를 사용해야 하는 이유

1). 순서를 제어하여 필요한부분 먼저 실행되게하기

ex) 이 예제는 baz()에서 호출하는 bar()에서 호출하는 foo()의
uncaught error를 호출하는예제이다.
에러가 발생한 스택의 상태를 보여주고자 한다.

 function foo(){
  throw new Error('Oops!');
 }
 
 function bar(){
  foo();
 }
 
 function baz(){
  bar();
 }

 ex) function foo(){
 	return foo(); //???
 }
 
 foo();

위 처럼 만약 foo()를 호출하는 foo()가 있다면 어떻게될까????

=> 콜스택에 쌓이는 순서

일단, main()이 foo()를 호출할거고,


STACK | main() | foo()


foo()는 foo()를 호출하는 foo()를 호출하게 될것이다.


STACK | main() | foo() | foo()


... foo()는 계속 foo()를 호출하고, 호출하고...
어쩌면 100번넘게 호출할지도 모른다...


STACK | main() | foo() | foo() | foo() | foo() | foo() | foo() | foo() ...


그러면 크롬이 어느순간 에러를 띄우며 "Blocking"하고, 묻는다.

 "RangeError: Maximum call stack size exceeded"
    해석: "스스로를 호출하는 foo()를 16000번이나 계속 하신건 아니죠? 
     일단 이녀석들을 '중지'시킬테니 버그좀 잡아주세요 ^^"

이때, 우리에겐 중요한 질문이 하나 생긴다.

*Blocking

: "What happends when things are slow?"
-> 블로킹, 즉 "느려진다는것은 어떤것인가?"
=> "느린 동작이 스택에 남아있는것."

아무것도 없는 빈 껍데기인 foo()나, 뭐 콘솔로그든간에
실행 자체는 느리지 않을것이다. 하지만 while루프 안에서
수십억번 실행된다면 당연히 느려질것이다 ㅋㅋㅋ

ex) 자, 여기서 예제를 하나 또 들어보자.
동기적으로 ajax요청을 보내는 jQuery 함수 getAsync가 있다고
가정하자. 어떤식으로 동작하게될까?

일단 비동기 콜백은 잠시 잊고, 동기적으로 작동한다고 생각해보자

  var foo = $.getAsync('//foo.com');
  var bar = $.getAsync('//bar.com'); 
  var baz = $.getAsync('//baz.com'); 
  
  console.log(foo);
  console.log(bar);
  console.log(baz);
  

=>
1. 일단 한줄 한줄 실행될꺼다. 먼저 콜스택에는
$.getAsync('//foo.com'); 가 쌓일거다.


STACK | main() | $.getAsync('//foo.com'); |


  1. 방금 쌓인 스택은 실행되어서 성공되면 pop될것이므로
    pop되면 또 다음 함수가 스택으로 쌓일꺼고 pop될거다.

STACK | main() | ➿ |



STACK | main() | $.getAsync('//bar.com'); |


.
.
.

이런식으로 한줄씩 '차례차례' 실행되고 쌓이고 pop되고...
콘솔로그까지 모두 쌓인후 pop되어 '언젠가는' 종료가 될것이다.

역시, 자바스크립트의 의미인, '싱글 스레드 언어' 답다.
네트워크 요청을 하고는, 마--냥 끝날때까지 하염없이 기다릴것이다.

==> 이것이 바로 우리가 '비동기 콜백'을 사용해야 하는 이유이다.

ex) 그러면 이제 비동기콜백을 사용하는 코드를 리뷰해보겠다.

console.log('Hi!');

setTimeout(function() {
	console.log('There');
}, 5000);

console.log('JSConfEU'); 

=>

  1. 먼저 console.log('Hi!'); 가 큐에 쌓였다가 실행되면 pop된다.

STACK | main() | console.log('Hi!');


  1. setTimeout이 큐에 쌓인후 바로 pop되지 않고, 일단 큐 상에서
    갑자기 사라진다.

STACK | main() | setTimeout() -> ➿


  1. console.log('JSConfEU'); 가 큐에 쌓였다가 실행되면 pop된다.

STACK | main() | console.log('JSConfEU');


  1. main()까지 pop된후 종료된것 같지만, 5초후 셋타임아웃()이
    다시 큐 상에 쌓이고 실행된후 pop된다...왜 일까??

STACK | console.log('There');


===> 여기서 드디어
" 이벤트 루프 "와 동시성(Concurrency & the Event Loop)이
역할을 하게된다. 위 예제에 대한 의문을 풀어줄 것이다.

일단 브라우저는 WEB API와 같은것들을 제공해준다고 사전에 말했었다...

↓↓ 그림 끌올 ↓↓



*WEB APIS :

  • JS에서 호출할수있는 스레드들을 효과적으로 지원한다.
    -> 즉 동시성을 지원해준다는 것이다.
  • 모든 web apis는 작동이 완료되면 콜백을 테스트큐에 밀어넣는다.

ex) 아래예제는 비동기 콜백이 실행되는 과정을 그려본것이다.

console.log('Hi!');

setTimeout(function() {
	console.log('There');
}, 5000);

console.log('JSConfEU'); 

(js 런타임의 v8엔진영역)
STACK
: main() | (1)log('Hi!') -> (2-2)➿
| (3) sto cb -> (4-2)➿
| (5)log('JSConfEU')-> (6-2)➿ | (8) cb(이제 (7-2)에서, event loop가 cb를 stack에 넣어주게된다.)
| (9) log('there'); (이제 js영역인 v8엔진으로 돌아가서, 콘솔로그를 실행한다.

(브라우저 상)
web apis :
(4) timer(5초실행중..) cb(브라우저가 타이머를 실행시키고, 카운트 다운을 시작함. === sto cb자체는 호출이 완료됨) -> (7)➿

task queue :
(7-2) cb ( 일단 모든 함수는 종료되고 main()도 pop됐지만, 아직 web apis에서 실행되고있는 timer()가 남아있다.
5초가 끝나면, timer()는 종료되고,
해당 콜백이 테스트 큐에 들어가게된다.)

console
: (2) Hi! | (6)JSConfEU | (10) there


=> 이때! 이거랑 이벤트루프가 이제 무슨상관이냐구?

이벤트루프는,
stack이 비어있다면, callback queue의 1st cb를 stack에 push해서
효과적으로 실행할수있게 도와준다.. 그럼 지금 이 상황이네?!

(8) 이제 (7-2)에서, event loop가 cb를 stack에 넣어주게된다.

3. 비동기를 사용해야 하는 이유

2). 렌더링의 속도를 줄이자! 비동기 vs 동기


(1).동기적 코드 실행시 렌더 큐의 상황

동기적코드가 모두 실행되기전에는,

화면상의 '동기코드가 끝난 후 누를수있는 버튼이나 어떤 동작들을 하지 못한다'를 직접 버튼을 누르며 보여주시면서,

실제로 브라우저가, 우리의 자바스크립트로 인해 제약을 받고있는모습,
즉, '코드가 스택에 남아있으면, 렌더링이 멈추게되어 콜백실행이 안되고있는'것을 보여주고 계신다.

=> 이유: 스택영역 < 콜백 << 우선순위가 더 높다고 한다.

기본적으로, 디폴트로!!
코드를 실행하기전에는,
매 16초마다 render queue라는 영역에 render가 들어가는데,
이는 스택이 현재 빈 상태일때 실행이 된다고 한다.

이 과정은,
16초마다, 브라우저가 '렌더해도 되는 상태인지 스택을 확인후,
없으면 render를 render queue에 넣는다고 한다.'

근데 이때, 우리가 동기적 js코드를 실행한다면?
우리가 이 느린..동기식 루프를 진행하는동안,
렌더링은 잠시 '멈춘다',

  1. 실행될 함수가 콜 스택에 쌓이고, 내부의 리턴값이 또 쌓이고,
    그렇게 실행될 함수의 내부 리턴값이 모두 쌓이고 pop되고가
    종료되어 실행함수까지 종료되고 pop되면
    그때 렌더링이 다시 시작된다.
    => 렌더링이 막히게되면, 화면의 텍스트선택이나 버튼클릭등의
    반응을 보는게 불가능하다.

(2)비동기적 코드 실행시 렌더 큐의 상황

ex) 콜백이 실행되면 내부 리턴값이 비동기적으로 리턴되는 예제.

function asyncForEach(array, cb){
   array.forEach(function (){
   	setTimeout(cb, 0);
   })
 }
 
 asyncForEach([1,2,3,4], function(i){
 	console.log('async', i);
 	delay();
 })

=> asyncForEach() 실행시 콜스택에 먼저 쌓이고,
내부의 forEach()함수 쌓이고,
내부의 셋타임아웃() 쌓이고,

web apis에서 cb가 실행완료되면
콜백큐에 anonymous()란 이름으로 쌓이고,

..그렇게 배열의 길이만큼 총 4번 이 과정이 반복되어
콜백큐에는 최종으로 4개의 anonymous() 스택이 쌓일것이다.

그렇게되면 call stack의 forEach()함수는 이제 pop되고,
후에 asyncForEach()까지 pop되어 스택은 비워지게된다.
이때, 잠시 render queue가 다시 실행되어 렌더링이 시작된다.
(근데 1초만 실행되고 바로 이벤트루프가 실행됨.. ㅜ)

---이제부터 중간에 렌더링이 사이사이 껴드는 과정----

잠시 렌더링 시작 후, 이벤트루프가 시작되며 렌더링은 다시멈춘다.
콜백큐에 제일 먼저 쌓인순으로 콜백이 스택으로 옮겨지게되고,
콜백의 내부 콘솔로그가 스택에 쌓여서 현재 스택에는 2개가
쌓여있다.

이제 콘솔로그 pop되고, 콜백 pop되고, 스택이 비워지게되면,

❗이때잠시 렌더가 끼어들수있게되어 렌더링이 다시시작된다❗

그리고 위 과정이 콜백이 전부 callback queue에서 비워질때까지
반복된다.

이때, 스택으로 콜백이 옮겨지고,
web api에서 콜백이 실행되잠시간의 시간이
생기게되고(?) 이때 render queue의 중단된 렌더링이
다시 시작된다고 한다.

... 즉, 이 과정이 반복된다. 뭐냐면
이벤트루프 실행시 스택으로 옮겨지고 렌더링이 시작됐다가,
스택에서 리턴될때는 다시 렌더링이 멈췄다가,
이벤트루프 실행시 스택으로 옮겨지고 렌더링이 시작됐다가
.
.

=> 결론: 어떤가? 우리는 여기서,
동기코드 실행시 아예 렌더링을 못하는 상황보단,

바로 실행될 필요 없는 느린코드를 비동기적으로 먼저 스택에 쌓아서
브라우저가 할일을 못하게, 오래 멈추게 하지말고,
유동적인 UI를 만들게 하는것이 더 좋은것이다! 를 알수있다.

예를 들면, 이미지 처리라던가, 애니메이션 실행이라던가 같은
작업은 브라우저가 열리자마자 바로 실행될필요가 없는 작업이다.
열리고나서 실행되어도 충분한 작업들이기 때문이다.

열리고있는 동시에 저 작업들이 실행된다면....
브라우저는 정말 엄청난 부하가 걸리게될것이다 ㅠㅠ

브라우저의 부담을 덜어주기 위해서라도 우리는!
이런 작업들을 비동기처리를 해줘야 할것이다.

❗그러면, 열리는동시에 작동되는 스크롤이벤트같은 경우는?❗

브라우저를 열고 스크롤을 내리다보면 분명 같이 작동하는경우를
봤을것이다. 그럼 이 경우는 어떻게해야 부하가 안 걸리는걸까?

=> 개발자 입장에서 능동적으로 생각해야하는 부분이다.
발표자의 경우는, 매 몇초마다,
or 유저가 스크롤을 멈출때까지, 작업량을 줄인다든지 하는
결정을 내릴것이라고 한다!

확실히 이벤트루프가 없었다면, 비동기처리를 할수없었을거다.
좋은거 발명했네....

참고 자료
: event loof

profile
예비개발자

0개의 댓글