진짜 말도 안되는 시간 동안을 이것들로 머리를 싸맸다. 무수히 많은 블로그를 보고 셀 수 없이 많은 유튜브 강의를 봤고 마지막으로 공식 문서까지 읽었다.
나름의 정리가 됐고 (정확하게 꿰뚫어 정통으로 이해를 했는지는 아직도 확신을 할 수 없다) 내가 이해한 내용들을 나만의 언어? 나만의 방식대로 기록해두고 확실하게 이해가 될 때까지 계속 읽으려고 한다.
나와있는 문장들을 다 떼놓고 하나씩 읽으면 전부 맞는 말인데 이것들을 다 합쳐놓으니까 그럼 이거는? 그럼 이거는? ... 끝도 없이 의문이 생기더라.
제가 스스로 공부를 하고 임의로 이해한 내용을 기록하였으므로 내용이 잘못되었을 수도 있습니다. 혹시라도 틀린 내용이 있다면 댓글로 가감없는 피드백 주시길 바랍니다. 감사합니다!
JS는 동기 언어인가?
출처 - Change.log
출처 - jiwon.log
나랑 이름이 똑같아서 놀랬다(나도 모르는 사이에 내가 쓴 줄..)
둘 다 맞다.
하지만 나는 이렇게 정리했다.
자바스크립트는 기본적으로 동기 방식으로 동작한다. 하지만 특정 작업은 비동기적인 처리가 가능하다.
왜냐하면 한번에 한줄씩 순서대로 코드가 실행이 되기 때문에
(=> 단일 스택이기 때문에 => single thread)
하지만 싱글 쓰레드만을 사용하지 않는다. 싱글 쓰레드 언어라고 불리는 이유는 딱 하나, Call Stack이 하나이기 때문이다.
전역 Main 함수를 포함해, 함수의 실행을 하나의 쓰레드가 순회하면서 실행한다. 하지만 setTimeout, EventListener, ajax 통신 등은 비동기적인 처리가 가능하다. 꼭 그래야만 한다.
근데 아래에서 자세히 설명 하겠지만 JS는 하나가 모든 일을 담당하는 싱글 쓰레드의 단점을 회피하기 위해 비동기 프로그래밍을 사용한다. Source(코드)를 순회하는 쓰레드는 하나이지만 Network 통신이나 DB를 조회하는 등의 시간 비용이 큰 로직(이전 포스팅에서의 Heavy Work)은 다른 쓰레드로 위임을 하고 다른 로직으로 이동해 작업을 수행한다. (던져놓는다 라고 이해)
이 때의 다른 쓰레드가 WEB API가 되는 것이다. 이는 브라우저에선 WebAPI이고, NodeJS에선 Node API 라고 부르는 별개의 쓰레드 영역이다.
이렇게 시간이 오래 걸리는 일들을 다른 쓰레드로 던져서 위임하는 행위? 동작은 비동기의 대표적인 특징이다. 따라서 JS는 비동기라고 할 수 있다.
그니까... 동기도 맞지만 비동기도 맞고.. 둘 다 맞다고ㅠㅠ
아무나 저 좀 시원~하게 이해시켜주세요....ㅠㅠ
그렇게 시간이 오래 걸리는 큰 일을 저~기에다가 던져놓고 기존에 쓰레드는 쉬느냐? 쉬지 않는다. 던져준 일을 기다리지 않고 다른 일을 바로 진행하는 것을 논 블로킹이라고 한다.
다른 쓰레드에게 던져놓은 그 일(Heavy Work)이 끝나면, 그 큰 일이 마저 해야할 것을 처리할 수 있게 Source를 순회하는 쓰레드가 알 수 있게 이벤트로 알려주는 시스템을 이벤트 기반 아키텍처라고 부른다.
그 과정은 밑에서 자세하게 하겠지만 간단하게 먼저 알아보자.
알바하면서 다시 혼자 정리!!
여기서 조금만 꼬리를 물면, 왜 비동기적인 처리가 필요하냐? 라는 의문이 들 수 있는데
예를 들어 엄청 오래 걸리는 연산을 해야하는 경우(비동기 작업으로 해야 할 경우) 이걸 원래의 방식대로 Call Stack 에다가 넣어두면 그게 실행이 다 되서 리턴을 하기 전까지는 다음 연산을 수행하지 못한다. 그렇기 때문에 오래 걸리는건 재껴두고 (Queue에다가 넣어두고) 빠른거 먼저 수행 할 수 있다.
그럼 저런걸 비동기 작업이라고 하는겁니까? 비동기 작업이 뭔지 설명 한번 해줄래요? 라는 질문이 들어올 수 있는데, (갑분면모, 갑자기 분위기 면접관 모드)
사람마다 다를 수 있고 소개한 책마다 다를 수 있고 강의마다 다를 수 있지만, 나는 비동기 작업을 이렇게 이해하고 정의 내렸다.
한줄 한줄 순서대로 동작하는(동기식으로 동작하는) 코드가 아닌 작업들
그래서? setTimeout 함수를 비동기 작업으로 예를 들 수 있는 것이다. 이에 대해 단적인 예를 들면 다음과 같다.
setTimeout() 함수
기존 JS는 비동기 작업을 위해 지연 작업이 필요한 함수에 setTimeout() 함수를 이용했다.
setTimeout()은 Web API의 한 종류이다. 코드를 바로 실행하지 않고 지정한 시간만큼 기다렸다가 로직을 실행한다.
그리고 지연 작업 완료 이후 실행되어야 하는 함수는 지연 작업 함수의 인자로(콜백 함수로) 전달하여 처리했다.
// #1
console.log('Hello');
// #2
setTimeout(function() {
console.log('Bye');
}, 3000);
// #3
console.log('Hello Again');
비동기 처리에 대한 이해가 없다면 위 코드의 결과를
‘Hello’ 출력
3초 있다가 ‘Bye’ 출력
‘Hello Again’ 출력
으로 예상할 것이다. 하지만 실제로는
‘Hello’ 출력
‘Hello Again’ 출력
3초 있다가 ‘Bye’ 출력
이렇게 나온다. setTimeout() 역시 비동기 방식으로 실행되기 때문에
javascript 엔진이 먼저 코드를 실행후 비동기 이벤트인 Web API 를 실행 한다.
따라서 위와 같은 결과가 나온다.
=> 이전에 포스팅 했던 async, sync, call back 에서 했던 예제
그래서!!!!
혼자만의 결론을 내려보면
JS는 원래 동기로 동작하는데 비동기로 동작하는 작업들이 존재하고 원하는 결과를 얻기 위해서는 이 작업들에 비동기 처리를 해주어야 한다.
이 때 비동기로 동작하고 있다는 소리는
"뒤에서 즉, <백그라운드에서 돌아가고 있다>가 아니라 동기식으로(순서대로) 동작하는 게 아닌 작업들이다"을 말하는 것이고
이런 작업들은 비동기 처리를 해주어야 한다는 소리는
=> 비동기 처리 : 비동기식으로 동작하는 작업들을 동기로 동작하는 것처럼 만들어주는 것(순서 제어)
비동기 처리를 해주지 않는다면 다음과 같은 일이 벌어진다는 것을 의미한다.
function printString(callbackParam) {
console.log(callbackParam);
}
// 콜백 함수 호출
function callHello() {
let value;
console.log("Wait 3 sec.");
console.log('waiting...');
setTimeout(function() {
value = 'Hello';
}, 3000);
return value;
}
// 실행
const r = callHello();
printString(r);
결과
# 시작
Wait 3 sec.
waiting...
undefined
(3초 대기)
# 종료
callHello()의 결괏값이 r로 전달되기 전에 printString(r)을 실행하게 되면서 undefined가 출력된다.
그럼 위에서 말했던 이걸 원래의 방식대로 Call Stack 에다가 넣어두면 그게 실행이 다 되서 리턴을 하기 전까지는 다음 연산을 수행하지 못한다. 라는 내용을 알아보자.
웹 브라우저 (크롬)은 작업을 수행할 때 다음과 같은 순서로 이어진다.
이전에 관련 내용을 포스팅 했었는데 여기서 추가된 내용은 바로 매크로 큐와 마이크로 큐이다.
만약 setTimeout 과 promise 가 같이 있다면 뭐를 먼저 실행하는가? 에 대한 답이 될 수 있다.
=> Call Stack이 비어있으면 queue에서 하나 꺼내와서 Stack에다가 올릴텐데 뭐를 먼저 올릴거냐?
둘 다 비동기로 동작하기 때문에 call stack에 갔다가 Web API로 가서 시간 보내고 대기열(큐)로 올라간다. 이 때
setTimeout 은 macro queue에 올라가고
promise 는 micro queue에 올라간다.
setTimeout는 정해진 시간에 따라 Web API에서 타이머를 재고 일정 시간이 지나면 그 다음에서야 큐에 올라갈 것이며
promise는 resolve되서 then으로 오면? 해당 메소드의 콜백이 큐에 올라가는 것이다.
예를 들어서
setTimeout(function() { // (A)
console.log('A');
}, 0);
Promise.resolve().then(function() { // (B)
console.log('B');
}).then(function() { // (C)
console.log('C');
});
결과 : B -> C -> A
Micro Task는 쉽게 말해 일반 태스크보다 더 높은 우선순위를 갖는 태스크라고 할 수 있다. 즉, 태스크 큐(매크로 큐, macro queue)에 대기중인 태스크가 있더라도 마이크로 태스크가 먼저 실행된다.
위의 예제를 통해 좀더 자세히 알아보자. setTimeout() 함수는 콜백 A를 태스크 큐에 추가하고, 프라미스의 then() 메소드는 콜백 B를 태스크 큐가 아닌 별도의 마이크로 태스크 큐에 추가한다.
위의 코드의 실행이 끝나면 태스크 이벤트 루프는 macro task queue 대신 micro task queue가 비었는지 먼저 확인하고, 큐에 있는 콜백 B를 실행한다. 콜백 B가 실행되고 나면 두번째 then() 메소드가 콜백 C를 마이크로 태스크 큐에 추가한다.
이벤트 루프는 다시 마이크로 태스크를 확인하고, 큐에 있는 콜백 C를 실행한다. 이후에 마이크로 태스크 큐가 비었음을 확인한 다음 (일반) 태스크 큐에서 콜백 A를 꺼내와 실행한다.
이벤트 루프는?
Call Stack과 Queue 를 연결시켜주는 역할을 한다. 콜 스택에 아무것도 없을 때 태스크 큐에서 작업하나 건져와서 올려준다.
이벤트 루프의 동작 원리는 간단하게 다음과 같다.
현재 실행중인 태스크가 없을 때(콜 스택이 비었을 때) 다음 태스크가 큐에 추가될 때까지 대기하는 역할을 한다. 이런 식으로 이벤트 루프는 '현재 실행중인 태스크가 없는지'와 '태스크 큐에 태스크가 있는지'를 반복적으로 확인하는 것이다. 간단하게 정리하면 다음과 같을 것이다.
모든 비동기 API들은 작업이 완료되면 콜백 함수를 태스크 큐에 추가한다.이벤트 루프는 '현재 실행중인 태스크가 없을 때'(콜 스택이 비었을 때) 태스크 큐의 첫 번째 태스크를 꺼내와 실행한다.
이 때 뭘 꺼내올꺼냐? 에 대한 대답이 바로 바로 위의 내용이다.
그래서!!!
비동기 작업으로 인해 생긴 결과물? 을 처리해주는 방식으로 (비동기 처리 방법으로)
3가지가 있다고 했고, 그것에 대해 공부를 했던 것이다.
callback, promise, async & await ....
사실 이것도 내가 이해한 바로는... <비동기 처리 방법> 보다는 비동기 작업들을 순차적으로(내가 원하는 흐름으로) 제어하기 위한 방법에 가깝다고 생각한다.
다시 공부하자 ㅎ