[javascript] async & await 이 뭘까요?? (동기와 비동기)

김준경·2023년 6월 25일
94

Javascript

목록 보기
3/4
post-thumbnail




*이 글은 본인이 구글링 하며 찾은 내용을 토대로 이해한 부분을 작성한 것입니다.

( 훈수환영합니다제발. )







자바스크립트의 기본 동작원리

▶️ 싱글 스레드 언어?

  싱글 스레드 = 한번에 하나의 작업만 할 수 있음.

자바스크립트는 싱글 스레드 언어이기 때문에 함수 호출이 순차적으로 스택에 쌓이게 되고, 스택의 맨 위에서부터 순차적으로 하나의 함수 호출만 처리할 수 있다.

  그래서 한번에 하나의 작업밖에 못한다.

따라서 만약 시간을 많이 잡아먹는 작업이 스택에 쌓이게 되면, 이후 작업을 하지 못하는 상황이 발생한다. 이를 "블로킹(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. 절대아니다. 아니, 그래서는 안된다. 절대로. 기다리는 동안 소요되는 시간과 그로인해 박살나버린 사용자 경험은 누가 물어줄것인가 하는 말이다...


비동기 콜백(Asynchronous Callback)


▶️ 비동기 방식의 작동원리


비동기방식은 이런 동기 호출방식의 문제점을 해결해준다.

  테스크 큐

기본적으로 자바스크립트의 비동기 실행 방식은 브라우저와 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초가 끝난 시점에 스택이 비워져 있지 않으면, 바로 실행되지 않는다는 것이다. (위의 "중요" 부분 참고)

따라서, 비동기 방식의 가장 큰 특징은

"정해진 시간 뒤에 함수의 실행을 보장하지 못한다는 것이다."

아직 이해가 잘 안됐는데...

   "닥X. 나도지금 머리터질거같아..."

넵.

(머리가 달달달 떨립니다,,,)

ㅇㅋ. 일단 써보면 이해가 되겠죠 뭐

   "그래. 그런마인드로 한번 접근해보자"


▶️ 비동기 콜백의 사용 - Promise

우선 비동기 콜백을 사용하려면 Promise 에 대해서 먼저 알아야 한다.

이 글의 주제는 await & async 아닌가요???

   ".. 일단 들어봐"


  Promise

Promise 가 무엇이냐 하면, 비동기 처리에 사용되는 객체이다.

  Promise 객체의 선언

Promise 객체를 선언하기 위해서는, 아래의 패턴을 따라줘야 한다. 여러가지 방법이 있지만, 가장 정석적인 방법인 new Promise 방식으로 선언해주겠다.

const promise123 = new Promise((resolve, reject) => {
	//실행할 작업
})

여기서 살펴본 Promise 객체의 특징은 다음과 같다.

  • 끝까지 하나의 변수로 관리하는 것이 좋기 때문에 재할당을 하지 못하도록 상수로 선언하는 편이 좋다.

  • 생성자는 화살표 함수 하나를 인자로 받는다. 공식 문서에서는 이 화살표 함수를 executor 라고 부른다.

  • new Promise() 로 생성자를 사용하는 순간 여기에 할당된 비동기 작업은 바로 시작된다.

   executor?

위 코드에서 executor 함수는 인자로 각각 resolve reject 를 받았다. 이 친구들의 기능은 콜백 함수이다.

??? XX 이게대체 무슨 개판인가요, 함수에 함수에 함수라니...

   "아직이야. 좀더 들어봐"


resolve는 비동기 작업이 성공했을 때, reject는 비동기 작업이 실패했을 때 호출할 수 있는 함수이다.

그런데 이 비동기 작업이 실패인지 성공인지를 어떻게 알 수 있냐? 비동기 작업은 끝나는 시점이 명확하지 않기 때문에 우리는 thencatch 메서드로 이걸 알 수 있다.

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 에 담아 넘겨줄지 결정하기도 한다.

어떤 함수로 값을 넘겨주냐에 따라 thencatch 가 실행된다. reject 함수로 값을 넘겨줄 때에는 Error 객체에 담아서 주는게 보편적이라 한다.

   Promise 객체 최종 정리

  • 생성자는 화살표 함수 하나를 인자로 받는다. 공식 문서에서는 이 화살표 함수를 executor 라고 부른다.

  • new Promise() 로 생성자를 사용하는 순간 여기에 할당된 비동기 작업은 바로 시작된다.

  • executor 함수는 인자로 각각 resolve reject 를 받는다. 이 둘의 기능은 값을 담아 넘겨주는 콜백 함수이다.

  • then catch 메서드를 사용해 성공적으로 실행됐을 때와, 에러가 발생했을 때를 구분해줄 수 있다.

Promise 드디어 끝났네!

   "아직 끝이 아니란다. 한참 더 남음"

XX


▶️ 비동기 콜백의 사용 - async

   async ?

asyncPromise 와 마찬가지로 비동기 처리를 해주는 키워드이다. 함수를 선언할때 붙여주는 키워드라고 생각하면 된다.

   Promise 와 async

앞에서 Promise 에 대해서 질리듯이 다뤘던 이유도 Promiseasync은 밀접한 관련이 있기 때문이다.

Promise 에서 형태만 조금 바꿔주면 바로 async 에 적용시킬 수 있다.

   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 문으로 바꿔준다.


그리고 중요한 특징 하나!!!!

   "async 함수의 리턴값은 무조건 Promise 객체이다."

이게 대체 무슨 소리인가요.

아까 위의 Promise 코드와 지금의 Async 함수의 코드 공통점이 뭐라고 생각하는가. 바로 변수 Promise123 에 값을 넣어주고, 변수 Promise123 에서 then 메서드와 catch 메서드를 활용해 흐름을 제어한다는 것이다!!!!

어라? 메서드?????? 설마...

   "바로 그거다...! 메서드는 클래스 안에 선언된 걸 객체가 꺼내다 쓰는것이였다!!!


(소름이 쫘악)

위의 async 함수 코드에서 리턴해준 값은 Promise123 변수에 저장된다. 어라?? 근데 Promise123 에서 메서드를 사용했네??? 그것도 Promise 객체가 사용한 thencatch 를?????

그렇다면 [ async 함수의 리턴값 = Promise 객체 ] 라는 결론이 도출되게 된다.

따라서 async 함수는 무조건 thencatch를 이용하여 흐름을 제어해야만 한다.


▶️ 비동기를 동기처럼 - await

   await

await, 이름부터 뭔갈 기다린다.. 라는 의미가 내포되어 있는 것 같다. 실제로, awaitPromise 객체의 비동기 작업이 완료될 때 까지 대기하게끔 만드는 함수이다.

만약 부모님이 빵을 만들기 위해 재료를 사러 마트에 가셨다고 생각해보자, 만약 동기 방식으로 당신이 행동한다면, 부모님이 오는 것만 목이 빠져라 기다릴 것이다.

하지만 비동기 방식으로 생각해보자, 부모님이 재료를 사러 가신 사이, 당신은 다른 재료들을 미리 준비해 놓는다거나, 주방도구를 세팅해 놓는 등, 빵을 만들기 위한 준비를 할 것이다.

하지만, 정작 부모님이 사러 가신 밀가루가 오지 않으면 반죽을 할 수 없는 것처럼, 종종 비동기 작업으로 요청한 값이 오지 않으면 실행하기 어려운 상황이 있을 수 있다.

이럴 때 await 을 사용하게 되는 것이다.

   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시까지적고있었네김다빠진다내일학교수업어떻게듣지 하....

profile
프론트엔드개발자가될래요

4개의 댓글

comment-user-thumbnail
2023년 6월 26일

비동기에 대해 이해하셨다니 멋지군요 응원합니다~

답글 달기
comment-user-thumbnail
2023년 7월 3일

낑쭝꼉 갱방장님 항쌍응언하니따

답글 달기
comment-user-thumbnail
2023년 7월 4일

for문 내에서 비동기 코드가 돌아갈 때 신경쓰지 않으면 나중에 골치아픕니다

답글 달기
comment-user-thumbnail
2023년 7월 6일

자세한 정리글 잘 읽었습니다!!

"정해진 시간 뒤에 함수의 실행을 보장하지 못한다는 것이다."

부분에 대해서 이 영상
도움이 될 것 같아서 남깁니다.

답글 달기