async, await, Promise 그리고 동기와 비동기

KIM SOO MIN·2023년 12월 11일

learning

목록 보기
12/16
post-thumbnail
💡 그동안 async/await, Promise를 수없이 써왔지만 왜 쓰는지, 어떤 방식으로 작동하는지 잘 몰랐던 것 같다. ᕙ( ︡’︡ 益 ’︠)ง▬▬█ 그래서 이번 기회에 다시 공부하고 공부한 것을 기록해보려고한다.

동기 vs 비동기

나는 위의 그림을 보면서 어느정도 이해가 됐다.

  • 동기 : 앞의 작업의 응답을 기다린 뒤 다음 작업을 수행한다.
  • 비동기 : 앞의 작업의 응답을 기다리지 않고 빨리 수행하는 작업부터 완료된다.

보통 동기는 은행, 비동기는 카페로 예를 많이 들던데, 나도 다른 예가 생각이 나지 않아 해당 예를 기록해두겠다.

동기적 방식

은행에 가면 순번표를 뽑고, 이전 대기자의 작업이 모두 끝나야 내 차례가 돌아온다.
예약시스템이란건 없기 때문에 내 앞에 갑자기 다른 이벤트가 생길 수도 없고, 나와 동시에 다른 사람의 업무를 봐주지도 않는다.

다음 이벤트를 동작하는 실행 순서가 확실한 것을 동기적 방식이라 부른다.

비동기적 방식

아메리카노 단체손님 30명과 아메리카노 한 잔씩 두 대기자가 있는다고 했을 때, 카페 직원은 ‘앞에 주문이 밀려서 죄송합니다' 라고 말하겠지만 어쨌든 단체손님 30명보다 내가 시킨 아메리카노 한 잔이 더 빨리 나올 것이다.

연속적으로 발생하는 이벤트를 담은 후 완료되는 순서대로 일을 처리하는 실행 순서가 확실하지 않은 것을 비동기적 방식이라 한다.

비동기적 방식을 처리하는 방법들

비동기적 방식을 처리하는 방법은 콜백 함수 사용, Promise, async/await가 있다고 하는데, 콜백 함수 사용은 이번 포스팅의 주제가 아니기 때문에 넘어가도록 하겠다.

Promise

💡 나는 생각보다 Promise를 많이 써보지는 않았다. 그래서 then이라던지 체이닝이라던지 약간은 생소했으나 이번 스터디로 조금은 알게 된 것 같다.
  • 아래는 Promise 예제이다
let num = 1;
let promise1 = new Promise((resolve, reject) => {
	if(num === 1){
		resolve('success');
	}else{
		reject('fail');
	}
});

promise1.then(data => {
	console.log(data);
}).catch(error => {
	console.log(error);
}).then(data => {
	console.log('something new!');
});

위처럼 catch 뒤에 또 체이닝이 되기도 한다고 한다.

  • 위의 예처럼 resolve 시 값은 then 으로 넘어가고, rejectcatch 로 넘어간다.
    • 나는 thencatch 뒤에 다시 then 을 체이닝 했으므로 rejectcatch 가 실행되고, 그 다음 마지막 then 도 실행된다.
  • Promise는 다음 중 하나의 상태를 가진다.
    • 대기(pending): 이행하지도, 거부하지도 않은 초기 상태
    • 이행(fulfilled): 연산이 성공적으로 완료됨
    • 거부(rejected): 연산이 실패함

다음은 Promise의 정적메서드를 간단한 MDN의 설명과 간단한 예제와 함께 살펴보겠다.

Promise.all(iterable)

💡 주어진 모든 프로미스가 이행하거나, 한 프로미스가 거부될 때까지 대기하는 새로운 프로미스를 반환합니다. 반환하는 프로미스가 이행한다면, 매개변수로 제공한 프로미스 각각의 이행 값을 모두 모아놓은 배열로 이행합니다. 배열 요소의 순서는 매개변수에 지정한 프로미스의 순서를 유지합니다. 반환하는 프로미스가 거부된다면, 매개변수의 프로미스 중 거부된 첫 프로미스의 사유를 그대로 사용합니다.
let num = 5;
let promise1 = Promise.resolve(3);
let promise2 = 42;
let promise3 = new Promise((resolve, reject) => {
	setTimeout(resolve, 100, 'foo');
});
let promise4 = async (num)=>{
	return num;
}

Promise.all([promise1, promise2, promise3, promise4(num)]).then((values) => {
	console.log(values)
})
// [3, 42, 'foo', 5]
  • Promise.all 에 함수도 넣을 수 있었다!
    배열 형태라면 다 들어갈 수 있구나.

Promise.allSettled

💡 주어진 모든 프로미스가 처리(이행 또는 거부)될 때까지 대기하는 새로운 프로미스를 반환합니다. `Promise.allSettled()` 가 반환하는 프로미스는 매개변수로 제공한 모든 프로미스 각각의 상태와 값(또는 거부 사유)을 모아놓은 배열로 이행합니다.
let num = 5;
let promise1 = Promise.resolve(3);
let promise2 = new Promise((resolve, reject) => 
	setTimeout(reject, 100, 'foo'));
let promise3 = async (num)=>{
	return num;
}
let promises = [promise1, promise2, promise3(num)]

Promise.allSettled(promises)
	.then((results) => results.forEach((result) => console.log(result.status)));

// fulfilled
// rejected
// fulfilled

// result도 궁금해서 찍어보았습니다.
// { status: 'fulfilled', value: 3 }
// { status: 'rejected', reason: 'foo' }
// { status: 'fulfilled', value: 5 }

Promise.any(iterable)

💡 주어진 모든 프로미스 중 하나라도 이행하는 순간, 즉시 그 프로미스의 값으로 이행하는 새로운 프로미스를 반환합니다.
let promise1 = Promise.reject(0)
let promise2 = new Promise((resolve) => setTimeout(resolve, 100, 'quick'));
let promise3 = new Promise((resolve) => setTimeout(resolve, 500, 'slow'));

const promises = [promise1, promise2, promise3]

Promise.any(promises).then((value) => console.log(value));

// "quick"
  • 이 기능은 node.js 15.0.0에서 지원했으므로 12를 쓰는 나는 내 playground에서 실행해볼 수 없었다..◑.◑
    MDN에서는 정상적으로 테스트 된다.

Promise.race(iterable)

💡 주어진 모든 프로미스 중 하나라도 처리될 때까지 대기하는 프로미스를 반환합니다. 반환하는 프로미스가 이행한다면, 매개변수의 프로미스 중 첫 번째로 이행한 프로미스의 값으로 이행합니다. 반환하는 프로미스가 거부된다면, 매개변수의 프로미스 중 거부된 첫 프로미스의 사유를 그대로 사용합니다.
let promise1 = new Promise((resolve, reject) => {
  setTimeout(resolve, 500, 'one');
});

let promise2 = new Promise((resolve, reject) => {
  setTimeout(resolve, 100, 'two');
});

let promise3 = new Promise((resolve, reject) => {
	setTimeout(reject, 50, "three");
})

Promise.race([promise1, promise2, promise3]).then((value) => {
  console.log(value);
  // promise1과 promise2가 resolve 됐지만 promise3이 더 빠르다.
});
// "three"
  • 위의 Promise.any 와 다른점이라면 거부한 프로미스의 사유도 그대로 사용한다는 점이 있겠다.

async/await

async/awaitPromise 객체를 반환한다.
그렇기 때문에 Promise 객체에서 사용하는 then 을 사용할 수 있다.

let delay = (sec) => {
	let promiseData = new Promise((resolve, reject) => {
		setTimeout(() => {
			resolve(console.log(sec + "초에 실행"));
		}, sec * 1000);
	});
	return promiseData;
};

let funcAsync1 = async () => {
	delay(5);
	return "async1";
}

funcAsync1().then(result => {
	console.log(result);
});

// await 썼을 때
let funcAsync2 = async () => {
	await delay(2);
	return "async2";
}

funcAsync2().then(result => {
	console.log(result);
});

// async1
// 2초에 실행
// async2
// 5초에 실행

예외처리

async/await의 경우 예외처리는 try/catch문을 사용한다.

let func = async () => {
	try{
		await delay(3);
	}catch(e){
		console.log(e);
	}
};

마치며…

그동안 너무 있는 코드로만 꾸역꾸역 코딩했던 것 같다.
내가 짜고 있는 코드가 어떤 코드인지 궁금해할 만도 한데 그렇지 않았다는 것이 실망스럽다.
Promise.all 이랑 async/await 만 주구장창 썼지 다른 건 관심도 없었다.

이제는 조금 더 관심을 갖고 공부해서 더 나은 코드를 짤 수 있게 노력해야겠다.

참고한 자료들

https://studyingych.tistory.com/63

https://velog.io/@yejinh/비동기-파헤치기

https://www.youtube.com/watch?v=pLiz2LdhYJM&list=PLjQHn5jzATkvggyE5oT7qU6FnmOv4ceY-&index=3

https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Promise/race

profile
3년차 풀스택 엔지니어입니다.

0개의 댓글