비동기 콜백에 대한 이해가 아직 부족한듯 하여 최근에 한 영상을 시청했다.
아주 유익했다고 생각한다. 오피스아워에서 엔지니어분이 얘기해주신
비동기적 사례의 예들을 그땐 동작원리를 모르니 이해하지 못했는데,&Yet 회사에서 일하는 Philip Roberts님의 발표 영상을 보고 이해할수 있었다.
를 솔직히 제대로 이해하지 못하고 있던 도중에..
그래서 크롬의 v8같은 js의 런타임을 분석해보기로 한다.
그림 01. runtime 과 event loof
메모리 할당이 일어나는 heap, 그리고 call stack이 일단 보인다.
그런데, 이 v8 프로젝트를 클로닝하여 코드 베이스를 들여다보면,
뭐, setTimeout이나 DOM,HTTP요청을 관리하는 코드들은 찾아볼수가 없다.
비동기함수를 생각하면 흔히 떠오르는것들 인데, 왜?
일단 브라우저는 DOM, ajax, timeout 등과 함께,
그 유명한 callback queue와 event loof를 가지고있다.
하지만 이것들이 어떤식으로 연결되어 움직이는지는, 정확하게 이해하는 사람이 적을것이다.
일단 '자바스크립트 => 싱글 스레드 프로그래밍 언어' 이다.
=> 싱글 스레드 런타임을 갖고있다는 말은, 결국 한번에, 하나의 싱글 콜 스택만을 갖고있다는 말이다.
=> 그니까 하나의 프로그램은 동시에 하나의 코드만 실행할수 있다는것임.그게 싱글 스레드의 의미이고!
이제 이 말을 시각화해보면...
예) 다음 예제는, 한 숫자를 받아서 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);
: data structure로 실행되는 순서를 기억한다.
STACK | main()
STACK | main() | printSquare(4)
STACK | main() | printSquare(4) | square(n)
STACK | main() | printSquare(4) | square(n) | multiply(a, b)
그러므로 리턴될때 콜스택의 현재 맨위의 multiply(a, b)가 이제 pop된다.
STACK | main() | printSquare(4) | square(n) | ➿
STACK | main() | printSquare(4) | ➿ | ➿
STACK | main() | printSquare(4) | console.log(squared) | ➿ | ➿
STACK | main() | printSquare(4) | ➿ | ➿ | ➿
STACK | main() | ➿ | ➿ | ➿ | ➿
STACK | ➿ | ➿ | ➿ | ➿ | ➿
==> 콜스택의 원리.
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();
일단, 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번이나 계속 하신건 아니죠?
일단 이녀석들을 '중지'시킬테니 버그좀 잡아주세요 ^^"
이때, 우리에겐 중요한 질문이 하나 생긴다.
: "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'); |
STACK | main() | ➿ |
↓
STACK | main() | $.getAsync('//bar.com'); |
.
.
.
이런식으로 한줄씩 '차례차례' 실행되고 쌓이고 pop되고...
콘솔로그까지 모두 쌓인후 pop되어 '언젠가는' 종료가 될것이다.
역시, 자바스크립트의 의미인, '싱글 스레드 언어' 답다.
네트워크 요청을 하고는, 마--냥 끝날때까지 하염없이 기다릴것이다.
ex) 그러면 이제 비동기콜백을 사용하는 코드를 리뷰해보겠다.
console.log('Hi!');
setTimeout(function() {
console.log('There');
}, 5000);
console.log('JSConfEU');
=>
STACK | main() | console.log('Hi!');
STACK | main() | setTimeout() -> ➿
STACK | main() | console.log('JSConfEU');
STACK | console.log('There');
===> 여기서 드디어
" 이벤트 루프 "와 동시성(Concurrency & the Event Loop)이
역할을 하게된다. 위 예제에 대한 의문을 풀어줄 것이다.
일단 브라우저는 WEB API와 같은것들을 제공해준다고 사전에 말했었다...
↓↓ 그림 끌올 ↓↓
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에 넣어주게된다.
2). 렌더링의 속도를 줄이자! 비동기 vs 동기
동기적코드가 모두 실행되기전에는,
화면상의 '동기코드가 끝난 후 누를수있는 버튼이나 어떤 동작들을 하지 못한다'를 직접 버튼을 누르며 보여주시면서,
실제로 브라우저가, 우리의 자바스크립트로 인해 제약을 받고있는모습,
즉, '코드가 스택에 남아있으면, 렌더링이 멈추게되어 콜백실행이 안되고있는'것을 보여주고 계신다.
=> 이유: 스택영역 < 콜백 << 우선순위가 더 높다고 한다.
기본적으로, 디폴트로!!
코드를 실행하기전에는,
매 16초마다 render queue라는 영역에 render가 들어가는데,
이는 스택이 현재 빈 상태일때 실행이 된다고 한다.
이 과정은,
16초마다, 브라우저가 '렌더해도 되는 상태인지 스택을 확인후,
없으면 render를 render queue에 넣는다고 한다.'
근데 이때, 우리가 동기적 js코드를 실행한다면?
우리가 이 느린..동기식 루프를 진행하는동안,
렌더링은 잠시 '멈춘다',
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