[자바스크립트 토픽 정리] 비동기 프로그래밍1
교생을 끝내고... 드디어 일상으로 복귀했다. 교생+과외 생활은 정말 눙물이었는데... 오자마자 기말이라는 게 더 눙물 🥲 그치만 정리와 공유는 꾸준히!
비동기에 대하여
🍊 계기
제로초 자스 강의에서 async / await가 나왔었는데 모르는 부분을 찾아보며 꼬리에 꼬리를 물다가 결국 비동기 프로그래밍의 개념까지 와버렸다. 특히, 사촌동생을 위한 두더지 게임 제작 중에도 비동기적으로 요청을 보내는 로직을 짜다가 의문인 부분이 많았다.
🍊 노트
비동기의 개념, 콜백, ... 프로미스 ... async, await 등 파고 파다가 계속 무한 꼬리를 물게 되었는데. 일단 공유하는 내용은 쪼개서 정리하고자 한다!
자바스크립트 엔진의 작동 방식
자바스크립트 엔진은 기본적으로 ‘싱글 스레드’ 방식으로 작동한다. ‘싱글 스레드’란 한 번에 하나의 일밖에 하지 못하는 것이다. 식당에 종업원이 1명 있는 것처럼 주문을 받는 것과 계산을 하는 것은 차례로 진행해야 한다. 따라서 자바스크립트 코드는 실행에 소요되는 시간이 어떻든 순서가 보장되어 작동한다.
그러나 우리가 아는 대부분의 상황들은 그렇지 않다. 또한, 코드상의 위치보다 특정 조건이나 상황에 맞게 실행되어야 하는 것들도 있다. 이렇게 실행 순서를 보장하지 않고 처리되는 것을 ‘비동기 asynchronous’ 처리라고 한다.
그러면 비동기적 처리는 어떻게 가능한 것일까?
원래의 자바스크립트 엔진의 싱글 스레드 방식에 따르면, 일련의 작업들을 처리할 때 ‘블로킹(작업 중단)’이 발생한다. 1번함수 - 2번함수가 있을 때 1번함수를 실행하는 데 3초가 걸린다면, 2번 함수는 실행되지 못하고 계속 대기하고 있어야 한다.
비동기적 처리는 여기서 나아가 브라우저 환경의 ‘콜 스택’과 ‘이벤트 루프’ 덕분에 가능해지는데, 아래와 같은 구성이다.
- 힙 메모리에서는 객체가 저장되는 메모리 공간이며, 크기가 정해져 있지 않기 때문에 구조화 되지 않은 형태다.
- 콜 스택은, 이름 그대로 ‘스택’ 자료 구조의 형태이며, 함수가 호출될 때 콜스택에 해당 함수의 실행 컨텍스트가 푸쉬(추가)되고, 최상위의 실행 컨텍스트에 있는 것을 처리한다. 최상위의 것이 종료되고 팝(제거) 되어야, 다른 태스크들도 실행될 수 있다.
- 태스크 큐에는 비동기 함수의 콜백 함수, 이벤트 핸들러 등이 일시적으로 보관된다.
- 이벤트 루프는 콜 스택에서 실행되고 있는 실행 컨텍스트가 있는지, 태스크 큐에 대기 상태인 함수가 있는지 계속해서 확인한다. 콜 스택이 비어있고, 태스크 큐에 대기 함수가 있다면 이를 콜 스택으로 이동시킨다.
/* 대표적인 예제 */
function foo() {
console.log('foo');
}
function bar() {
console.log('bar');
}
setTimeout(foo, 0);
bar();
위와 같은 코드가 있을 때, setTimeout함수 자체에 대한 실행 컨텍스트가 콜 스택에서 호출 스케줄링을 종료한 후 팝된다.
→ 브라우저가 타이머 설정을 해두고, 4ms가 지나 만료되면 setTimeout의 콜백함수 foo를 태스크 큐에 푸시한다. (* 지연 시간은 0이지만, 4ms 이하면 최소 지연시간 4ms으로 지정된다)
→ bar함수가 호출되어 콜 스택에 최상위 실행 컨텍스트로 푸시되고 종료하면 팝된다. 4ms가 경과되어도 bar함수 실행 컨텍스트가 팝되지 않았다면, foo는 여전히 태스크 큐에서 대기중인 상태다.
위의 두 가지지 작업은 병행하여 처리된다. 타이머 설정과 태스크 큐 푸시는 브라우저가 하고, 콜 스택의 최상위 컨텍스트를 종료하는 것은 자바스크립트 엔진이 각자 하기 때문이다. 즉, 비동기 처리는 브라우저가 ‘멀티 스레드’로 동작하기 때문에 가능한 것이다. 또한, 4ms의 지연시간이 완벽히 보장되지 않는 이유도 위의 작동방식 때문이다.
콜백이 왜 필요한가?
앞선 내용에서 우리는 자바스크립트가 어떻게 비동기적으로 실행될 수 있는지 살펴보았다. 그런데 비동기적으로 동작하는 것을 떠나서, 비동기적으로 동작한 것을 잘 ‘처리’하기 위해서는 어떻게 해야할까?
비동기 함수에서 비동기적으로 발생한 값을 활용하고 싶어요(1) - 외부 변수에 할당하기
다음의 예제를 살펴보자. 넘겨받은 url로 요청을 보내 응답을 받은 것을 콘솔에 출력하고 있다. 아래의 get함수가 해당 응답을 콘솔에 출력하는 것이 아니라 ‘반환’하거나 해당값을 외부에서 활용하려면 어떻게 해야할까?
const get = url => {
const xhr = new XMLHttpRequest();
xhr.open('GET', url);
xhr.send();
xhr.onload = () => {
if (xhr.status === 200) {
console.log(JSON.parse(xhr.response));
} else {
console.error(`${xhr.status} ${xhr.statusText}`);
}
};
};
get('https://cats.com/posts/1');
(실제 동작하지 않아요. 임의의 상상 속 url입니다.)
일단 아래의 코드처럼 외부 변수에 할당해버리자.
let data;
const get = url => {
const xhr = new XMLHttpRequest();
xhr.open('GET', url);
xhr.send();
xhr.onload = () => {
if (xhr.status === 200) {
data = JSON.parse(xhr.response));
}
console.error(`${xhr.status} ${xhr.statusText}`);
};
};
get('https://cats.com/posts/1');
console.log(data);
그러면 undefined가 출력된다! 왜일까?
일단 get함수가 호출되면서 아래와 같은 상태가 된다. 콜 스택에 get함수의 실행 컨텍스트가 push된다. get함수의 코드를 실행할 때 xhr.onload 이벤트 핸들러가 바인딩된다.
get함수 코드 실행을 마친 후, 콜 스택에서 pop되면 곧바로 console.log(response)부분의 console.log함수 실행 컨텍스트가 콜 스택에 push된다.
문제는 load이벤트가 console.log 호출 직전에 발생했다고 해도, 이벤트 핸들러는 태스크 큐에 저장되어 대기해야 한다.
콜 스택이 비고 나서야 이벤트 루프에 의해 태스크 큐에서 콜 스택으로 옮겨지고 실행될 수 있기때문에! 서버의 응답 결과를 할당하기 전에 출력이 먼저 이루어질 수밖에 없다.
비동기 함수에서 비동기적으로 발생한 값을 활용하고 싶어요(2) - 반환하기
get함수의 예제를 그대로 가져와서 이번엔 아래와 같이 반환해보자.
const get = url => {
const xhr = new XMLHttpRequest();
xhr.open('GET', url);
xhr.send();
xhr.onload = () => {
if (xhr.status == 200) {
return JSON.parse(xhr.response)
}
console.error(`${xhr.status} ${xhr.statusText}`);
};
};
const response = get('https://cats.com/posts/1');
console.log(response);
마찬가지로 undefined값이 출력된다. 왜?!
return JSON.parse(xhr.response) 반환문은 get함수의 것이 아니다. onload 이벤트 핸들러의 반환값이다.
'프로그래밍 언어 > JavaScript' 카테고리의 다른 글
[프로그래머스 Lv.2] k진수에서 소수 개수 구하기 (자바스크립트) (0) | 2024.06.04 |
---|---|
[프로그래머스 Lv.1] 숫자 문자열과 영단어 (자바스크립트) (1) | 2024.06.04 |
[프로그래머스 Lv.2] 영어 끝말잇기 (자바스크립트) (0) | 2024.05.30 |
[프로그래머스 Lv.1] 1차 비밀지도 (자바스크립트) (0) | 2024.05.27 |
[프로그래머스 Lv.1] 개인정보 수집 유효기간 (자바스크립트) (1) | 2024.05.25 |