자바스크립트는 싱글 스레드 언어이기 때문에 함수 호출이 순차적으로 스택에 쌓이게 되고, 스택의 맨 위에서부터 순차적으로 하나의 함수 호출만 처리할 수 있다.
따라서 만약 시간을 많이 잡아먹는 작업이 스택에 쌓이게 되면, 이후 작업을 하지 못하는 상황이 발생한다. 이를 "블로킹(blocking)" 이라 한다.
근데? 어째서? 웹페이지에서는 그 수많은 요청들을 한꺼번에 처리할 수 있는거지???
보통 우리가 함수를 호출할 때에는 아래의 코드와 같이 호출하게 된다.
function f() {
console.log("1");
console.log("2");
console.log("3");
}
이를 "동기 호출" 이라 하며, 이 경우에는 함수 내부의 내용이 스택에 차곡차곡 쌓여 순차적으로 실행되게 된다.
따라서 스택에 console.log("1");
console.log("2");
console.log("3");
이 순서대로 pop되므로 출력값은 1
2
3
이다.
ㅇㅋ. 여기까진 괜찮다. 왜냐하면 console.log
를 실행하는 데에는 아주아주아주 짧은 시간만이 필요하기 때문이다.
하지만? 만약 우리가 API 요청을 보낼때에도 이런 동기방식을 쓴다면 어떨까? 서버로부터 응답이 올 때까지 넋놓고 기다리고 있을 것인가???
No. 절대아니다. 아니, 그래서는 안된다. 절대로. 기다리는 동안 소요되는 시간과 그로인해 박살나버린 사용자 경험은 누가 물어줄것인가 하는 말이다...
비동기방식은 이런 동기 호출방식의 문제점을 해결해준다.
기본적으로 자바스크립트의 비동기 실행 방식은 브라우저와 nodejs영역이므로 자바스크립트가 자체적으로 비동기 방식을 실행한다는 점은 아니라는걸 기억하자.
아무튼, 비동기 실행방식을 위해서는 테스크 큐가 필요하다.
그래서 테스크 큐가 뭔데???
이게 뭔말이냐, 비동기 방식으로 실행되는 대표격인 setTimeout
과 함께 알아보자.
function f() {
console.log("1");
setTimeout(function two(){
console.log("2");
}, 5000);
console.log("3");
}
위 코드에서 아까 알아봤던 대로 일단 console.log("1");
이 먼저 스택에 들어가게 된다. 그리고 아래를 딱 봤더니??? 이게 왠걸. setTimeout
님이 떡하니 자리잡고 계신다.
setTimeout
함수는 Web Api
의 일종으로, 비동기 방식으로 작동한다.
아니 XX 뜸들이지 말고 말하라고
근데 여기서, setTimeout
함수 내부의 함수 two
는 갑자기 테스크 큐로 빠져버린다????
????? ????? ???
왜 그러냐 하면, 비동기방식으로 실행되는 함수 내부에 있는 함수호출은 자바스크립트가 스택에 넣은다음 호출 권한을 브라우저에게 줘버리고 비동기 함수를 스택 내부에서 종료시킨다.
따라서 위 코드의 실행과정은, setTimeout
함수를 일단 스택 내부에 넣은 다음, two
의 호출을 테스크 큐 내부로 보내버리고, 스택에서 setTimeout
함수를 종료시키는 것이다.
(저는 적어도 이렇게 이해하기로 하였습니다...)
그리고 setTimeout
함수의 대기시간이 5초이므로, 5초가 지난 뒤 스택이 비워져 있다면 (->중요), 스택에 다시 push하여서 실행시킨다.
여기서 비동기 방식의 중요한 특징이 나오는데, 만약 위 코드에서 setTimeout
함수 내부의 대기시간이 5초라고 하더라도, 5초가 끝난 시점에 스택이 비워져 있지 않으면, 바로 실행되지 않는다는 것이다. (위의 "중요" 부분 참고)
따라서, 비동기 방식의 가장 큰 특징은
아직 이해가 잘 안됐는데...
넵.
(머리가 달달달 떨립니다,,,)
ㅇㅋ. 일단 써보면 이해가 되겠죠 뭐
Promise
우선 비동기 콜백을 사용하려면 Promise
에 대해서 먼저 알아야 한다.
이 글의 주제는 await & async 아닌가요???
Promise 가 무엇이냐 하면, 비동기 처리에 사용되는 객체이다.
Promise 객체를 선언하기 위해서는, 아래의 패턴을 따라줘야 한다. 여러가지 방법이 있지만, 가장 정석적인 방법인 new Promise
방식으로 선언해주겠다.
const promise123 = new Promise((resolve, reject) => {
//실행할 작업
})
여기서 살펴본 Promise 객체의 특징은 다음과 같다.
끝까지 하나의 변수로 관리하는 것이 좋기 때문에 재할당을 하지 못하도록 상수로 선언하는 편이 좋다.
생성자는 화살표 함수 하나를 인자로 받는다. 공식 문서에서는 이 화살표 함수를 executor
라고 부른다.
new Promise()
로 생성자를 사용하는 순간 여기에 할당된 비동기 작업은 바로 시작된다.
위 코드에서 executor
함수는 인자로 각각 resolve
reject
를 받았다. 이 친구들의 기능은 콜백 함수이다.
??? XX 이게대체 무슨 개판인가요, 함수에 함수에 함수라니...
resolve
는 비동기 작업이 성공했을 때, reject
는 비동기 작업이 실패했을 때 호출할 수 있는 함수이다.
그런데 이 비동기 작업이 실패인지 성공인지를 어떻게 알 수 있냐? 비동기 작업은 끝나는 시점이 명확하지 않기 때문에 우리는 then
과 catch
메서드로 이걸 알 수 있다.
then
해당 비동기 작업이 성공했을 때의 동작을 지정한다. 그렇다면 당연히 catch
는 해당 비동기 작업이 실패했을 때의 동작을 지정하게 되는것이다.
그럼 아래 코드를 보며 Promise 객체의 형태를 정리해보자.
const promise123 = new Promise((resolve, reject) => {
resolve('success');
//reject(new Error('failed'));
})
promise123
.then((value)=> {
console.log(value);
})
.catch((Error)=> {
console.log(Error);
})
//success 출력
이제 Promise
객체의 형태에 대해서 대강 감이 잡힐거라 생각한다. 위 코드를 실행시켜보면 콘솔창에 success
가 출력된다.
주석 처리를 해놓은 reject
부분과 resolve
부분의 주석을 서로 바꿔주면 콘솔창에 failed
가 출력되게 된다.
실제 활용 코드에서는 if문 등을 활용하여 resolve
에 값을 담아 넘겨줄지 reject
에 담아 넘겨줄지 결정하기도 한다.
어떤 함수로 값을 넘겨주냐에 따라 then
과 catch
가 실행된다. reject
함수로 값을 넘겨줄 때에는 Error
객체에 담아서 주는게 보편적이라 한다.
생성자는 화살표 함수 하나를 인자로 받는다. 공식 문서에서는 이 화살표 함수를 executor
라고 부른다.
new Promise()
로 생성자를 사용하는 순간 여기에 할당된 비동기 작업은 바로 시작된다.
executor
함수는 인자로 각각 resolve
reject
를 받는다. 이 둘의 기능은 값을 담아 넘겨주는 콜백 함수이다.
then
catch
메서드를 사용해 성공적으로 실행됐을 때와, 에러가 발생했을 때를 구분해줄 수 있다.
Promise 드디어 끝났네!
XX
async
async
은 Promise
와 마찬가지로 비동기 처리를 해주는 키워드이다. 함수를 선언할때 붙여주는 키워드라고 생각하면 된다.
앞에서 Promise
에 대해서 질리듯이 다뤘던 이유도 Promise
와 async
은 밀접한 관련이 있기 때문이다.
Promise
에서 형태만 조금 바꿔주면 바로 async
에 적용시킬 수 있다.
async function async123 (isTrue) {
if(isTrue)
return 'success';
else
throw new Error('failed');
}
const promise123 = async123(true);
promise123
.then((value)=> {
console.log(value);
})
.catch((Error)=> {
console.log(Error);
})
//success 출력
위 코드를 보면 알겠지만, Promise
와 유사한 점이 상당히 많다. async
함수의 특징을 대략 정리해보자면 요정도가 있겠다.
함수에 async
키워드를 붙여 선언 가능하다.
객체를 선언하는 것이 아니라 함수 선언문이기 때문에 생성자(new Promise
)를 없애고 executor
의 본문 내용만 남긴다.
resolve
부분을 return
문으로, reject
부분을 throw
문으로 바꿔준다.
이게 대체 무슨 소리인가요.
아까 위의 Promise
코드와 지금의 Async
함수의 코드 공통점이 뭐라고 생각하는가. 바로 변수 Promise123
에 값을 넣어주고, 변수 Promise123
에서 then
메서드와 catch
메서드를 활용해 흐름을 제어한다는 것이다!!!!
어라? 메서드?????? 설마...
(소름이 쫘악)
위의 async
함수 코드에서 리턴해준 값은 Promise123
변수에 저장된다. 어라?? 근데 Promise123
에서 메서드를 사용했네??? 그것도 Promise
객체가 사용한 then
과 catch
를?????
그렇다면 [ async
함수의 리턴값 = Promise
객체 ] 라는 결론이 도출되게 된다.
따라서 async
함수는 무조건 then
과 catch
를 이용하여 흐름을 제어해야만 한다.
await
await
, 이름부터 뭔갈 기다린다.. 라는 의미가 내포되어 있는 것 같다. 실제로, await
은 Promise
객체의 비동기 작업이 완료될 때 까지 대기하게끔 만드는 함수이다.
만약 부모님이 빵을 만들기 위해 재료를 사러 마트에 가셨다고 생각해보자, 만약 동기 방식으로 당신이 행동한다면, 부모님이 오는 것만 목이 빠져라 기다릴 것이다.
하지만 비동기 방식으로 생각해보자, 부모님이 재료를 사러 가신 사이, 당신은 다른 재료들을 미리 준비해 놓는다거나, 주방도구를 세팅해 놓는 등, 빵을 만들기 위한 준비를 할 것이다.
하지만, 정작 부모님이 사러 가신 밀가루가 오지 않으면 반죽을 할 수 없는 것처럼, 종종 비동기 작업으로 요청한 값이 오지 않으면 실행하기 어려운 상황이 있을 수 있다.
이럴 때 await
을 사용하게 되는 것이다.
await
의 사용 형태는 다음과 같다.
async function async123 (isTrue) {
if(isTrue)
return 'success';
else
throw new Error('failed');
}
async function await123() {
const promise123 = async123(true);
try {
const value = await promise123;
console.log(value);
} catch(e) {
console.error(e);
}
const promise1234 = async123(false);
try {
const value = await promise1234;
console.log(value);
} catch(e) {
console.error(e);
}
}
await123(); //success와 error문 차례대로 출력
사실 위 코드는 굳이 await 이 필요 없지만, await의 형태를 보기 위해 바꾸어 보았다.
(머리가 안따라준게 절대 아닙니다.)
async123()
함수의 처리 시간이 아주 적게 걸려서 거의 동시에 실행된 것 처럼 보일 수 있으나, 실제 실행과정은 이렇다.
await123
함수를 실행시킨다
promise123
변수에 async123(true)
의 값이 들어올 때까지 기다린다. 이 동안 await123
은 비동기 함수이지만, await
문 때문에 실행이 멈춘 상태로 있게 된다.
promise123
변수에 값이 들어오면 try
문으로 가서 async123()
함수의 반환 값에 따라 콘솔에 출력하며 처리한다.
이후 Promise1234
변수의 처리 과정도 딱히 다를 것은 없다.
드디어 다 끝난건가요???
와진짜개힘들었다이걸12시부터새벽3시까지적고있었네김다빠진다내일학교수업어떻게듣지 하....
비동기에 대해 이해하셨다니 멋지군요 응원합니다~