Javascript 엔진은 저 메모리 힙과 콜 스택을 가지고 있다. 그리고 그것을 이용해서 자바스크립트로 쓰여진 코드를 해석하고 실행한다.
저 바깥의 네모는 바로 브라우저를 뜻하는 것이다. 이제 브라우저 안에 Javscript 엔진(크롬의 경우 V8)이 있고, Web에서 제공하는 API들(DOM api/document, AJAX/XMLHttpRequest, Timeout/setTimeout)과 바로 오늘 알아볼 Event Loop와 Callback queue라는 것이 있다.
이것들 또한 브라우저 혹은 런타임(node JS)에 내장되어 있는 기능 중 하나라고 보면 된다.
Callback queue : 콜백 펑션(다른 함수에 인자로 전달되어진 함수)를 쌓아놓는 자리
Event Loop: 콜 스택과 콜백큐를 모니터하면서 콜 스택이 비어있으면 콜백 큐에서 이벤트를 하나 던져주어 실행되게 하는 역할을 한다.
예제로 한번 이해를 해보겠다.
console.log('Hi');
setTimeout(function cb1() {
console.log('call back1');
}, 5000);
console.log('Bye');
아주 간단한 코드이다.
결과는 우리 모두 다 알고 있을 것이다.
Hi
Bye
(5초 뒤)
call back1
그렇다면 이런 비동기 처리가 되었는지 그림으로 살펴보자.
state clear
처음 상태로 브라우저 콘솔, 콜 스택, 콜백 큐 등 모두 비어있는 상태이다.
console.log('Hi') 스택에 추가
console.log('Hi')가 call stack에 들어가게 된다.
console.log('Hi') 실행
console.log('Hi')가 실행되어 브라우저 콘솔에 Hi라고 뜬다.
console.log('Hi') 제거
console.log('Hi')가 실행되었으므로 콜 스택에서 제거된다.
setTimeout(function cb1() {...}, 5000) 스택에 추가
setTimeout(function cb1() {...}, 5000) 콜 스택에 추가된다.
setTimeout(function cb1() {...}, 5000) 실행
setTimeout(function cb1() {...}, 5000)가 실행된다. setTimeout은 브라우저에서 제공하는 api이므로 web api가 알아서 시간을 세준다. (자바스크립트 실행 엔진은 브라우저에 수 세달라고 안에 다시 받을 함수만 담아서 요청한다.)
setTimeout(function cb1() {...}, 5000) 제거
자바스크립트 엔진 입장에서는 setTimeout이라는 api 요청은 끝났기 때문에 할 일 끝. 따라서 콜 스택에서 제거된다.
console.log('Bye') 스택에 추가
console.log('Bye') 가 콜 스택에 추가된다.
console.log('Bye') 실행
console.log('Bye') 실행되어 브라우저 콘솔에 Bye라고 뜬다.
console.log('Bye') 제거
console.log('Bye')가 실행되었으므로 콜 스택에서 제거된다.
이 와중에도 web api는 timer를 통해 요청받은 시간을 세고 있다...
콜백 큐에 cb1 추가
Web api가 5000ms를 다 세고 나서 cb1이라는 요청 받을 때 온 함수를 콜백 큐에다가 집어넣는다.
이벤트 루프가 cb1을 콜 스택에 추가
이벤트 루프가 콜백 큐에 추가된 함수를 콜 스택에 추가한다.(이벤트 루프는 콜 스택도 확인하여 비어있을 때 콜백 큐에서 함수를 꺼내서 스택에 추가한다.)
cb1 함수 실행
콜 스택에 있는 cb1이라는 함수를 실행한다. 그 안에는 console.log('call back1')이 있으므로 해당 실행 컨텍스트를 또 콜 스택에다 추가한다.
console.log('call back1') 실행
콜 스택에 console.log('call back1')가 가장 위에 있으므로 실행되어 브라우저 콘솔에 call back1가 뜨게 된다.
console.log('call back1') 제거
console.log('call back1')가 실행되었으므로 콜 스택에서 제거된다.
cb1 함수 제거
cb1는 이미 실행되었으므로 콜 스택에서 제거된다.
는 아님.
그저 web api에서 전달 받은 시간을 다 세면 콜백 큐에 넣어주는 것이지, 실행이 되는 것이 아니다.
setTimeout(myCallback, 1000);
이런 코드가 있다면, 1000ms 뒤에 (1초 뒤에) myCallback이라는 함수를 실행시켜줘 라기보다.
1초 뒤에 내가 준 함수(콜백 펑션)를 콜백 큐에 추가해줘 라는 것이다.
그러니 만약 콜 스택에서 굉장히 오래 걸리는 작업을 하고 있다고 한다면, 바로 1초 뒤에 "실행"되지 않는 것이다.(콜백 큐에 저장되어 실행되기를 기다리고 있는 것)
그럼 이건?
setTimeout(myCallback, 0);
어림없다. 0초 뒤에 바로 실행가능한 것이 아니라 0초 뒤에 콜백 큐에 추가되는 것이다. 이미 콜 스택에서는 이 setTimeout이 나오면 web api에 요청을 하고 콜 스택을 비워버리고 다음 실행을 넘어간다. 따라서 0초라고 해도 다음 라인이 먼저 실행되게 된다.
만약 그래서 콜백안에 콜백안에 콜백이 있다면..?
listen('click', function (e){
setTimeout(function(){
ajax('https://api.example.com/endpoint', function (text){
if (text == "hello") {
doSomething();
}
else if (text == "world") {
doSomethingElse();
}
});
}, 500);
});
이것을 콜백 지옥이라고 부르는데(nested callbacks / callback hell),
document addEventListner인 DOM api니까 안에 담겨진 내용과 같이 web apis로 보내고,
그럼 스택은 비워졌으니 web apis가 저 할일(버튼 클릭)이 끝나면 큐로 넘겨주어서 스택에는 setTimeout이 추가되고, 그럼 얘를 또 web apis로 보내고, 500ms 기다리는 도중에 연산이 조금이라도 늦은 게 실행되면 더더 늦어지고,
스택 비워져서 setTimeout 안에 있던 콜백 펑션 ajax가 스택에 추가되자마자 또 web apis로 요청하고....
원하는 요청이 제 시간에 오지 않아 에러를 일으킬 수 있고, 딱봐도 성능에 좋지 않다.
따라서 이를 해결하기 위한 방법으로 Promise, 최근에 나온 async, await 가 있다. 다음 시간에...
[참고 및 출처]
https://blog.sessionstack.com/how-javascript-works-event-loop-and-the-rise-of-async-programming-5-ways-to-better-coding-with-2f077c4438b5
https://www.youtube.com/watch?v=zi-IG6VHBh8
useState는 비동기적으로 동작한다.
이벤트 핸들러 내에 setState가 여러번 호출이 된다면, 일괄적으로 업데이트하고 렌더링한다.
state1 set하고 state2 set 하고 동기적으로 처리된다면 느려질 것이기 때문이다.
import React, { useState } from "react"
function App() {
const [num, setNum] = useState(1)
async function plus() {
setNum(num + 1)
setNum(num + 1)
setNum(num + 1)
}
async function minus() {
setNum(num - 1)
}
return (
<div className="App">
<h1>{num}</h1>
<button onClick={plus}>PLUS</button>
<button onClick={minus}>MINUS</button>
</div>
);
}
export default App;
setNum이 세번 사용이 되었다고 해서 num에다가 3이 더해지는 것은 아니다.
한 이벤트 핸들러 내에서는 비동기적으로 동작하기 때문에, 1만 증가하게 된다.
이 동일한 state를 업데이트 하는 경우 react는 이 동작들을 batch 처리한다.
batcting이란, setState를 하나로 병합한 후 최종적으로 한번만 setState 해주는 것이다.
state가 변경이 되면 re-render를 하게 되는데, 계속 렌더링이 일어나면 성능에 좋지 않으므로 state가 변경된 후 반영하는 것은 대기열에 넣은 후 한꺼번에 반영하도록 되어 있다.
따라서 개발자가 의도하지 않은 값을 참조하게 될 수도 있다.
해결 방법:
1. 최신 state 값을 참조하고 싶으면 업데이트 후에 하면 된다.
따라서 useEffect, componentDidUpdate를 사용해서 최신 state 값을 볼 수 있다.
이렇게 바꾸란 얘기
async function plus() {
setNum(num => num + 1)
setNum(num => num + 1)
setNum(num => num + 1)
}
[참고 및 출처]
https://baegofda.tistory.com/220
https://garve32.tistory.com/39
git online repo가 연결된 것이 있는데 다른 repo를 또 만들어서 연결하고 싶으면??
그때는 git remote set -url origin [옮길 주소] 를 하고 push 하면 된다!