조각조각 - 비동기

eocode·2024년 3월 12일
0
post-thumbnail

목차

  1. 비동기
    1.1. 자바스크립트 비동기 동작?
    1.2. 콜백 함수
    1.3. 프로미스
    1.4. async/await
  2. 참고자료

1. 비동기

1.1. 자바스크립트 비동기 동작?

자바스크립트의 비동기 동작이 무엇인지 알기 위해 우선 '동기적 동작'이 무엇인지와 '싱글 스레드 언어'가 무엇인지 알아야 합니다.

싱글 스레드 언어와 동기성

싱글 스레드 언어

한 번에 하나의 작업만 수행 가능한 프로그래밍 언어를 '싱글 스레드 언어'라고 합니다. 동시에 병렬적으로 여러 작업을 처리하지 못하고 한 작업이 끝난 후에야 다른 작업을 시작할 수 있습니다. 그렇기 때문에 한 작업의 긴 시간을 요구하는 경우 이 작업이 실행되는 동안 다른 작업을 할 수 없어 코드 처리 효율이 좋지 않습니다.

자바스크립트는 '싱글 스레드 언어'입니다. 따라서 자바스크립트 또한 한번의 하나의 작업만 가능합니다. 즉 호이스팅 된 이후 코드가 순서대로 하나씩 실행됩니다. 이처럼 코드 한줄이 완료 된 후에야 다음 줄 코드 작업이 실행되는 것을 '동기적 작업' 이라고하며 이러한 특성을 '동기적 특성' 이라고 합니다.

🔗 호이스팅 정리 게시글

코드가 동기적으로 동작하는 경우 앞선 코드의 처리 시간이 얼마나 걸리든 완료 될때까지 대기했다가 다음 줄 코드가 실행됩니다. 따라서 한 작업이 무한히 긴 처리 시간을 요구하는 경우 프로그램이 멈추기도 하며 작업의 처리 시간에 따라 작업의 효율이 매우 떨어지기도 합니다. 이러한 '싱글 스레드 언어 동기적 작업' 의 문제점을 보완하는 것이 바로 '비동기 작업' 입니다.

비동기 작업

async

비동기 작업은 동기적인 코드의 실행 흐름을 방해하지 않고 백그라운드에서 실행됩니다. 위 그림을 보면 이해가 쉽습니다. 자바스크립트 코드는 위 그림과 같이 '동기 코드'와 '비동기 코드'가 섞여 있습니다. 자바스크립트는 동기적으로 동작합니다. 즉 한번에 하나의 동작만 처리가 가능하기 때문에 하나의 라인 위를 이동하며 실행되어야 합니다. 하지만 비동기 코드는 기존에 작업들이 옮겨다니던 하나의 라인이 아닌 다른 라인 위를 움직이며 실행됩니다. 결과적으로 비동기 코드 뒤에 있던 동기 코드가 앞 비동기 코드가 완료되지 않았는데도 동기 코드가 움직이는 라인 위를 움직이며 실행됩니다. 동시에 비동기 코드 또한 비동기 코드가 움직이는 라인 위를 움직이며 실행됩니다.

위에서 언급한 싱글 스레드 언어의 단점이 해소됩니다. 긴 시간을 요구하는 작업을 비동기로 처리하면 해당 작업이 끝나기 전에 다음줄의 코드가 실행 될 수 있습니다. 이 덕분에 작업이 매우 효율적으로 진행되어 작업 처리 시간이 감소됩니다.

대표적인 비동기 작업

  • 네트워크 요청
  • 파일 I/O
  • 타이머

비동기 작업 처리 방식

비동기 작업은 대표적으로 아래 3가지 방식으로 처리됩니다.

  1. 콜백 함수(Callback Functions)
  2. 프로미스(Promises)
  3. async/await

아래에서 각각의 비동기 작업 처리 방식을 자세히 알아보겠습니다.

1.2. 콜백 함수

콜백 함수가 무엇인지는 이전 함수 정리 게시글에서 알아보았습니다. 간단히 말하자면 인자로 전달된 함수를 콜백 함수라 합니다. 이 콜백 함수는 인자를 받은 함수의 내부 구조에 따라 다르게 실행됩니다. 이때 이 함수 내부에서 비동기적으로 콜백 함수를 처리하는 방식이 가장 기본적인 비동기 처리 방식입니다.

//콜백 함수를 이용한 비동기 처리
function callbackFn(result){
	console.log(result); // '작업 완료' 출력
}

function asyncFn(fn) {
  // 비동기 작업 수행
  setTimeout(() => 13{
    const result = '작업 완료';
    fn(result); //1000ms 이후 콜백함수 실행
  }, 1000);
  console.log('나는 동기 코드야');
}

asyncFn(callbackFn);

위 코드에서 'asyncFn 함수'가 콜백 함수를 인자로 받는 함수이며 'callbackFn 함수'가 인자로 사용되는 콜백 함수입니다. asyncFn 함수를 살펴보면 setTimeout을 이용해 콜백 함수가 비동기 처리되었습니다. 따라서 1000ms 뒤 콜백함수가 실행됩니다. setTimeout 뒤에 등장하는 console.log('나는 동기 코드야');코드는 동기적으로 동작하여 비동기 비동기 코드가 완료되기 전에 바로 실행됩니다. 따라서 '나는 동기 코드야'가 먼저 출력되고 1000ms 뒤 '작업 완료'가 출력됩니다.

setTimeout

브라우저에서 제공하는 웹 api로 일정 시간 이후 콜백 함수를 호출해서 실행한다.

콜백 지옥?

비동기 처리를 위해 콜백 함수를 이용하는 경우 코드가 간단하면 괜찮지만 코드가 조금만 복잡해져도 시인성이 확연히 떨어집니다. 이 처럼 콜백 함수의 많은 중첩 사용으로 코드의 시인성이 안좋아진 상태를 '콜백 지옥' 이라고 합니다.

'물건 구입 과정' 예제 코드로 콜백 지옥을 확인해 보겠습니다.

//콜백 지옥 예제
function ItemCheck(itemId, callback) {
  // 아이템 정보 요청 비동기 함수
  setTimeout(() => {
    console.log('아이템 정보 요청 중...');
    const itemData = {
	    id : itemId,
	    name : '노트북',
	    purchaseAmount : '1000,000'
    }
    callback(itemData);
  }, 1000);
}

function AddToCart(item, callback) {
  // 장바구니 담기 비동기 함수
  setTimeout(() => {
    console.log(`${item.name}을 장바구니에 담는 중...`);
    callback(item);
  }, 1000);
}

function PayForItem(item, callback) {
  // 아이템 결제 비동기 함수
  setTimeout(() => {
    console.log(`${item.name}을 결제 중...`);
    callback(true);
  }, 1000);
}

// 콜백 지옥 실행
ItemCheck('item000', (item)=>{
	AddToCart(item, (item)=>{
		PayForItem(item, (isPurchase)=>{
			if(isPurchase){
				console.log("결제 완료");
			}
		});
	});
});

물품 확인, 장바구니 담기, 결제 과정을 간단하게 콜백 함수를 이용해 표현한 코드입니다. 확인한 물품을 장바구니에 담기위해 ItemCheck 함수 내부에서 콜백함수로 AddToCart 함수를 콜백 함수로 받아와 실행합니다. 이후 장바구니에 담긴 아이템을 결제하기 위해 AddToCart 함수 내부에서 PayForItem 함수를 콜백 함수로 받아와 실행합니다.

//콜백 지옥 구조 예시
function 함수A(콜백함수){
	콜백함수();
}
function 함수B(콜백함수){
	콜백함수();
}
function 함수C(콜백함수){
	콜백함수();
}
function 함수D(콜백함수){
	콜백함수();
}

함수A(()=>{
	함수B(()=>{
		함수C(()=>{
			함수D(()=>{
				...
			})
		})
	})
})

함수 내부에 콜백 함수가 존재하고 그 콜백 함수 내부에 또 콜백 함수가 존재하는 구조입니다. 비동기 과정이 진행되며 콜백 함수가 콜백 함수를 가지는 과정이 반복됩니다. 이러한 과정은 코드의 복잡성을 증가시키고 에러 처리와 유지 보수를 어렵게 만듭니다.

프로미스(Promise)와 async/await를 사용하면 이러한 콜백 지옥 문제를 해결 할 수 있습니다.

1.3. 프로미스(Promise)

프로미스?

프로미스는 비동기 처리에 사용되는 자바스크립트 내장 오브젝트입니다. 간단하게 표현하자면 프로미스 객체는 일종의 상자로 비동기 작업의 결과를 가집니다. 이는 '상태'와 '결과 데이터'로 이루어져 있습니다. '상태'는 비동기 작업의 결과를 나타내고 '결과 데이터'는 비동기 작업의 결과로 얻어낸 데이터 입니다.

프로미스 특징

  • 자바스크립트 내장 오브젝트
  • 비동기 동작을 위해 사용된다.
  • 비동기 작업의 수행 결과를 나타내는 '프로미스 상태' 를 가진다.
    - pending(비동기 동작 수행중) -> fulfilled(성공) or reject(실패)
  • 비동기 작업의 결과인 '프로미스 결과' 를 가진다.
  • 프로미스 객체를 반환하는 메소드들이 존재해 프로미스 체이닝이 가능하다.

프로미스 객체 생성

function executor(resolve, reject){
	//비동기 동작
	setTimeout(()=>{
		if(성공 조건){ //성공
			resolve('성공시 반환 값');
		}
		else{ //실패
			reject('실패 반환 값');
		}
	}, 1000);	
}

//프로미스 객체 생성
const promise = new Promise(executor);

new 키워드를 사용하여 프로미스 객체를 생성할 수 있습니다. 인자로는 executor 콜백 함수가 사용됩니다. executor 콜백 함수는 두가지 콜백 함수 resolve, reject를 전달 받습니다. resolve 콜백 함수는 기능 정상 수행 후의 결과를 전달하며 reject 콜백 함수는 기능 중간 실패의 결과를 전달합니다.

executor 콜백함수 자동 실행 방지

executor 콜백 함수는 프로미스 객체가 생성될 때 자동으로 실행됩니다. 이 때문에 문제가 발생할 수 있어 주의가 필요합니다. 함수를 사용하면 이 자동 실행을 방지할 수 있습니다. 바로 프로미스 객체를 함수 안에서 생성하는 것입니다. 이 방식을 사용하면 원하는 시점에 executor가 실행되는 프로미스 객체를 만들 수 있습니다.

//executor 자동 실행 방지
function makePromiseObj(){
	const promiseObj = new Promise((resolve, reject)=>{
		...
	});
	return promiseObj;
}
								
const asyncPromise = makePromiseObj();

프로미스 메소드(프로미스 컨슈머)

프로미스 객체는 단순히 비동기 동작의 결과만 담고있는 것이 아닙니다. 비동기 동작에 도움이 되는 메소드들을 가지고 있습니다. 메소드들은 프로미스 객체의 메소드 이면서 동시에 프로미스 객체를 반환합니다. 덕분에 메소드들을 연속적으로 사용하여 연결이 가능합니다. 즉 프로미스 체이닝이 가능합니다. 이 메소드들을 프로미스 컨슈머라고 부릅니다.

프로미스 컨슈머는 then, catch, finally 3가지가 존재합니다.

then

then 메소드를 호출한 프로미스 객체의 상태가 fulfiiled인 경우 then 메소드가 인자로 받은 콜백 함수를 실행합니다. 이 콜백 함수는 then 메소드 내부에서 처리될 때 '프로미스 객체의 성공 값' 을 인자로 받아 처리한 후 반환합니다.

프로미스 객체가 reject 상태인 경우 then 메소드가 인자로 받은 콜백 함수가 실행되지 않고 reject 상태를 가진 프로미스를 그대로 반환합니다. 이 덕분에 뒤이어 catch 메소드 활용이 가능해집니다.

const promise = new Promise((resolve, reject)=>{
	resolve('성공 반환 값');
	//reject('실패 반환 값');
})

promise.then((v)=>{
	console.log(v);
	return '단순 값';
});

위 코드에서 then 메소드를 호출한 promise 객체는 fulfilled 상태이며 '성공 반환 값' 텍스트 결과를 가지고 있습니다. 따라서 프로미스 객체의 성공 값을 인자로 받아 사용하는 then 메소드의 콜백 함수가 실행되어 '성공 반환 값'을 출력됩니다.

여기서 의문이 생깁니다. 분명 위에서 프로미스 컨슈머를 설명할때 프로미스 객체를 반환한다고 하였습니다. 콜백 함수 내부에서 새로운 프로미스 객체를 생성한 후 반환하면 반환된 프로미스 객체를 뒤에 이어질 프로미스 컨슈머가 받아 체이닝이 가능할 것 입니다. 하지만 위 처럼 콜백 함수 내부에서 단순 값을 반환하는 경우 프로미스 체이닝이 끊길 것입니다.

메소드 체이닝

메소드의 주체가 되는 객체와 반환 값이 동일한 객체인 경우 메소드를 이어 호출할 수 있습니다. 이를 체이닝이라 합니다.

예) 배열 객체.배열을 반환하는 메소드().배열을 반환하는 메소드().배열을 반환하는 메소드() ....

그렇기 때문에 then 메소드는 콜백 함수 내부에서 단순 값을 반환하는 경우 그 단순 값을 결과값으로 가지고 fulfilled 상태를 가진 새로운 프로미스 객체를 생성한 후 반환합니다.

정리하자면 아래와 같습니다.

  • 호출한 프로미스 객체가 fulfilled 상태인 경우 콜백함수 동작
  • 프로미스 객체의 성공 결과 값을 다룸
  • 콜백 함수 내부에서 새로운 프로미스 객체 생성 후 반환 가능
  • 단순 값 반환시 해당 값을가진 새로운 프로미스 객체 생성하여 반환

catch

catch 메소드를 호출한 프로미스 객체의 상태가 reject인 경우 catch 메소드가 인자로 받은 콜백 함수를 실행합니다. 이 콜백 함수는 catch 메소드 내부에서 처리될 때 '프로미스 객체의 실패 값'을 인자로 받아 처리한 후 반환합니다.

프로미스 객체가 fulfilled 상태인 경우 catch 메소드가 인자로 받은 콜백 함수가 실행되지 않고 fulfilled 상태를 가진 프로미스를 그대로 반환합니다. 이 덕분에 뒤이어 then 메소드 활용이 가능해집니다.

const promise = new Promise((resolve, reject)=>{
	//resolve('성공 반환 값');
	reject('실패 반환 값');
})

promise.catch((v)=>{
	console.log(v);
	return '단순 값';
});

위 코드에서 catch 메소드를 호출한 promise 객체는 reject 상태이며 '실패 반환 값' 텍스트 결과를 가지고 있습니다. 따라서 프로미스 객체의 실패 값을 인자로 받아 사용하는 catch 메소드의 콜백 함수가 실행되어 '실패 반환 값'을 출력합니다.

then 메소드와 동일하게 프로미스 객체를 반환합니다. 따라서 프로미스가 객체가 아닌 단순 값을 리턴하는 경우 단순 값을 가진 새로운 프로미스 객체를 생성해 반환합니다.

정리하자면 아래와 같습니다.

  • 호출한 프로미스 객체가 fulfilled 상태인 경우 콜백함수 동작
  • 프로미스 객체의 성공 결과 값을 다룸
  • 콜백 함수 내부에서 새로운 프로미스 객체 생성 후 반환 가능
  • 단순 값 반환시 해당 값을가진 새로운 프로미스 객체 생성하여 반환

finally

메소드를 호출한 프로미스 객체의 상태 여부와 상관없이 무조건 인자로 사용된 콜백 함수가 실행됩니다. 따라서 최종적인 작업을 수행할 때 사용됩니다.

const promise = new Promise((resolve, reject)=>{
	resolve('성공 반환 값');
	//reject('실패 반환 값');
})

promise.then((v)=>{
	console.log(v);
}).catch((e)=>{
	console.log(e);
}).finally(()=>{
	console.log('finally, 무조건 출력')
});

//출력 결과
//'성공 반환 값'
//'finally, 무조건 출력'

위 코드의 경우 프로미스 객체의 상태가 무엇이든 마지막에 'finally, 무조건 출력'가 출력됩니다.

프로미스 체이닝

프로미스 객체의 메소드를 활용한 프로미스 체이닝으로 비동기 동작을 다룰 수 있습니다. 위에서 언급한 then, catch, finally 메소드가 프로미스 체이닝에 활용됩니다.

//프로미스 체이닝 구조 (콜백 함수 생략)
promise.then(...)
	  .then(...)
	  .then(...)
	  .catch(...)
	  .finally(...);

위에서 말했듯이 프로미스 컨슈머들을 연이어 사용하는 것이 프로미스 체이닝입니다. 위는 간단히 표현한 프로미스 체이닝 형태입니다.

//프로미스 체이닝
const promise = new Promise((resolve, reject)=>{
	//resolve('성공 반환 값');
	reject('실패 반환 값');
})

promise.then((v)=>{
	console.log(v);
}).catch((v)=>{
	console.log(v);
	return '단순 값';
});

여기서 명심해야할 사항은 메소드가 실행되면 무조건 프로미스 객체를 반환한다는 것입니다. 위 코드에서 프로미스 객체는 reject 상태를 가집니다. 따라서 then 메소드의 인자로 사용된 콜백 함수가 실행되지 않습니다. 하지만 그렇다고 then 메소드가 아무 값도 반환하지 않는 것이 아닙니다. then 메소드를 호출한 프로미스 객체가 reject 상태를 가진 경우 호출 객체 그대로인 프로미스 객체를 반환합니다. 그 덕분에 그대로 반환된 이 객체가 catch 메소드를 호출하고 처리될 수 있습니다. 반대의 상황도 마찬가지로 동작해 프로미스 체이닝이 동작될 수 있습니다.

기타 프로미스 메소드

then, catch, finally 메소드 말고도 다양한 프로미스 메소드들이 존재합니다.

Promise.all

//비동기 작업 순차 처리
const promise = new Promise(...);

promise.then(()=>{비동기작업A();})
	.then(()=>{비동기작업B();})
	.then(()=>{비동기작업C();})

then() 메서드를 연결하여 사용하여 비동기 작업을 복수개 수행할 수 있습니다. 하지만 이 경우 비동기 작업이 순차적으로 진행됩니다. 순차 완료가 필수가 아닌 독립적인 비동기 작업은 병렬로 동시에 처리하는 것이 효율적입니다. Promise 객체의 all 메소드를 사용하면 효율적인 병렬 처리가 가능합니다.

Promise.all 메소드 사용법

//all 메소드 사용
const reseult = Promise.all([프로미스 객체, 프로미스 객체, ...]);

all 메소드는 인자로 프로미스 객체로 이루어진 배열을 받아 사용합니다. 배열로 전달 받은 프로미스 객체의 작업은 병렬로 수행됩니다.

Promise.all 메소드 반환 값

프로미스 객체를 반환합니다. 이때 프로미스 개체의 상태는 배열에 포함된 모든 프로미스 작업이 성공해야만 fulfilled가 됩니다. 하나의 프로미스 작업이 실패하더라도 reject 상태가 됩니다. 프로미스 결과는 상태가 fulfilled인 경우 모든 작업의 결과가 배열 형태로 나타납니다. 하지만 상태가 reject인 경우 가장 먼저 실패한 작업의 결과만 나타납니다. 모든 작업의 성공, 실패 여부를 알기위해 all 메소드 대신 allSetteld 메소드를 사용해야 합니다.

//모든 프로미스 작업이 이행된 경우
{
	PromiseState : "fulfilled",
	PromiseResult : [성공 결과 값, 성공 결과 값, 성공 결과 값 ...]
}
//작업이 하나라도 거부된 경우
{
	PromiseState : "rejected",
	PromiseResult : 실패 결과 값
}

Promise.all 메소드 정리

  • 프로미스 객체로 이루어진 배열을 인자로 받습니다.
  • 배열 내부 프로미스가 병렬로 작업됩니다.
  • 프로미스 작업이 하나라도 실패하면 중단되고 해당 작업 결과를 반환합니다.
  • 프로미스 객체를 반환합니다.
  • 배열의 모든 작업 성공한 경우
    - 상태 : fulfilled
    - 결과 : 배열 형태의 모든 결과
  • 작업 하나라도 실패한 경우
    - 상태 : reject
    - 결과 : 가장 먼저 실패한 프로미스 결과

Promise.allSetteld

all 메소드와 같이 프로미스로 이루어진 배열을 인자로 받습니다. all 메소드의 경우 수행되는 작업 중 하나라도 실패하면 reject 상태의 프로미스 객체를 바로 반환합니다. 반면에 allSetteld 메소드는 실패한 작업이 있더라도 모든 작업이 완료될 때 까지 기다립니다. 모든 작업의 결과를 기다리기 때문에 all 메소드와 다른 반환 값을 가집니다.

Promise.allSetteld 메소드 사용법

//all 메소드 사용
const reseult = Promise.allSettled([프로미스 객체, 프로미스 객체, ...]);

allSetteld 메소드는 인자로 프로미스 객체로 이루어진 배열을 받아 사용합니다. 배열로 전달 받은 프로미스 객체의 작업은 병렬로 수행되며 도중에 실패한 작업이 있더라도 도중에 중단하지 않습니다.

Promise.allSetteld 메소드 반환 값

병렬로 진행되는 작업의 결과에 상관없이 fulfilled 상태를 가진 프로미스 객체를 반환합니다. 모든 작업의 결과는 객체로 이루어진 배열로 나타납니다. 배열에 포함된 객체는 아래와 같습니다.

//반환된 프로미스 객체
{
	PromiseState : fulfilled,
	PromiseResult : [
		{status : "rejected", reason : '실패 결과 값'},
		{status : "fulfilled", value : '성공 결과 값'},
		...
	]
}

Promise.allSetteld 메소드 정리

  • 프로미스 객체로 이루어진 배열을 인자로 받습니다.
  • 배열 내부 프로미스가 병렬로 작업됩니다.
  • 프로미스 작업 중 실패하는 것이 있더라도 중단 없이 진행됩니다.
  • 프로미스 객체를 반환합니다.
  • 작업 결과에 상관없이 fulfilled 상태를 가진 프로미스 객체를 반환합니다.
  • 각각의 작업의 결과는 객체로 이루어진 배열로 나타납니다.

Promise.any

프로미스로 이루어진 배열을 인자로 받습니다. 프로미스 작업이 병렬로 진행되며 하나라도 이행되면 그 프로미스를 반환합니다. 즉 '가장 먼저 이행되는 프로미스' 를 반환합니다. 나머지 프로미스 결과는 무시되며 모든 프로미스 작업이 거부된 경우 에러를 반환합니다.

반환 결과가 무조건 이행 프로미스 객체 여야합니다. 따라서 만약 이행된 프로미스 객체가 없다면 에러를 반환합니다.

Promise.any 메소드 사용법

//any 메소드 사용
const reseult = Promise.any([프로미스 객체, 프로미스 객체, ...]);

Promise.any 메소드 반환 값

const promise1 = Promise.reject(0);
const promise2 = new Promise((resolve) => setTimeout(resolve('quick'), 100));
const promise3 = new Promise((resolve) => setTimeout(resolve('slow'), 500));

const promises = [promise1, promise2, promise3];

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

any 메소드는 가장 먼저 이행되는 프로미스를 반환합니다. 위 코드의 각 프로미스를 살펴보겠습니다.

  • promise1은 바로 실행되지만 이행되지 않고 거부됩니다.
  • promise2는 100ms 뒤 실행되며 이행되고 'quick' 결과값을 가진 프로미스를 반환합니다.
  • promise3는 500ms 뒤 실행되며 이행되고 'quick' 결과값을 가진 프로미스를 반환합니다.

이행되는 프로미스는 promise1와 promise2로 2개 존재합니다. 결과적으로 그 중 먼저 이행되는 promise1이 반환됩니다.

Promise.race

프로미스로 이루어진 배열을 인자로 받습니다. 프로미스 작업이 병렬로 진행되며 작업이 이행되든 거부되든 '가장 먼저 완료된 프로미스' 가 반환됩니다. any 메소드와 다르게 가장 먼저 이행된 프로미스의 결과를 신경쓰지 않습니다. 처음 프로미스가 완료되면 나머지 프로미스 결과는 무시됩니다.

Promise.race 메소드 사용법

//race 메소드 사용
const reseult = Promise.race([프로미스 객체, 프로미스 객체, ...]);

Promise.race 메소드 반환 값

const promise1 = Promise.reject(0);
const promise2 = new Promise((resolve) => setTimeout(resolve('quick'), 100));
const promise3 = new Promise((resolve) => setTimeout(resolve('slow'), 500));

const promises = [promise1, promise2, promise3];

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

위 any 메소드에서 사용한 코드에서 race 메소드를 사용하면 어떤 다른 결과가 나오는지 확인해보겠습니다.

  • promise1은 바로 실행되지만 이행되지 않고 거부됩니다.
  • promise2는 100ms 뒤 실행되며 이행되고 'quick' 결과값을 가진 프로미스를 반환합니다.
  • promise3는 500ms 뒤 실행되며 이행되고 'quick' 결과값을 가진 프로미스를 반환합니다.

프로미스는 위 결과를 가집니다. 이때 결과가 무엇이든 가장 먼저 이행된 프로미스가 반환되기 때문에 promise1이 반환됩니다.

1.4. async, await

위에서 프로미스 객체를 사용하면 상태를 가지고 있다는 특성과 프로미스 체이닝(then, catch, finally 메소드)이 가능하다는 특성 덕분에 콜백 함수를 이용한 비동기 처리보다 더욱 간결하고 가독성 좋은 코드를 작성할 수 있다고 하였습니다.

하지만 프로미스 객체를 사용해도 한계가 존재합니다. 바로 프로미스 체이닝의 특성 때문입니다. 물론 콜백 함수로 비동기 동작을 처리할 때 이 프로미스 체이닝 특성이 도움이 되지만 체이닝이 복잡하고 길게 구성된 경우 문제가 됩니다. .메소드(콜백함수).메소드(콜백함수).메소드(콜백함수)...' 구조의 코드는 이어진 한줄의 코드라는 점에서 연결이 많아지거나 인자로 사용되는 콜백 함수가 복잡해지는 경우 가독성이 매우 나빠집니다. 그렇기 때문에 추가되는 메소드 단계 단계마다 어떤식으로 진행 되는지 동작의 흐름을 이해해야 하는데 그것이 어려워집니다. 결과적으로 유지 보수 또한 어려워 집니다.

//복잡함 프로미스 체이닝
const result = promise.then(
	//콜백 함수
	)
	.then(
		//콜백 함수
	)
	.then(
		//콜백 함수
	)
	.then(
		//콜백 함수
	)
	.then(
		//콜백 함수
	)
	.catch(
		//콜백 함수
	)
	.finally(
		//콜백 함수
	);

이러한 프로미스 객체의 코드 복잡성을 해결해 줄 수 있는 것이 바로 'async/await' 입니다. 'async' 키워드와 함께 함수를 선언하면 해당 함수는 비동기로 동작하며 promise를 반환합니다. 함수 내부에서 'await' 키워드를 사용하면 프로미스 결과가 준비 될 때까지 기다리는 동작이 가능합니다. 따라서 await을 사용하면 동기적으로 작성된 코드처럼 처리되어 코드가 이해가 쉬워집니다. 아래에서 async 키워드를 사용하면 일반 함수와 얼마나 달라지는지 알아보겠습니다.

일반 함수 vs async 함수

'async' 키워드를 사용하여 함수를 선언하면 함수가 호출될 때 비동기적으로 동작합니다. 그렇기 때문에 프로미스 메소드와 마찬가지로 프로미스 상태와 프로미스 결과를 가진 프로미스 객체를 반환합니다. 반환 값의 형태도 프로미스와 동일합니다. 함수가 일반 값을 리턴하면 fulfilled 상태와 반환된 일반 값을 결과로 가진 프로미스 객체를 새로 만들어 반환합니다. 함수가 프로미스 객체를 반환하면 한번 더 프로미스 객체를 감싸지 않고 그대로 프로미스 객체를 반환합니다.

//async 함수 반환
function normalFn() {
	return 'normal';
}
async function asyncFn() {
	return 'async'; //일반 문자열 반환
}

console.log(normalFn()); //'normal'
console.log(asyncFn()); 
//프로미스 객체
//{
//	PromiseState : "fulfilled",
//	PromiseResult : "async"
//}

await

'async/await 문법' 에서 가장 큰 특징은 바로 'await' 입니다. await 키워드를 사용하면 비동기 작업의 결과를 동기적으로 다룰 수 있습니다. 즉 await을 사용한 코드 한줄 한줄이 앞선 코드가 완료되어야 다음 코드가 진행됩니다. 이 덕분에 동기적인 코드 작성이 익숙한 개발자에게 큰 편의성을 제공합니다. 이때 await 키워드는 async 함수 내부에서만 사용 가능하니 코드작성할 때 주의가 필요합니다.

async function fetchData(url) {
  try {
    const response = await fetch(url);
    const data = await response.json();
    return data;
  } catch (error) {
    console.error('데이터를 가져오는 중 에러가 발생했습니다:', error);
  }
}

await을 사용했기 때문에 fetch의 결과를 기다린 후 response 변수에 할당합니다. 이 이후에야 다음 코드가 실행되어 data 변수에 값이 할당됩니다. 비동기 동작 완료를 되면 다음줄로 넘어가는 형태이므로 동기 코드를 작성할 때와 유사하게 동작합니다. 이 덕분에 코드를 파악하고 이해하기 쉬워집니다.

try/catch

'async/await 문법' 에서 'try/catch'를 사용하여 에러 처리가 가능합니다. 이 경우 블록을 사용하여 에러를 처리한다는 것이 가장 큰 특징이자 장점입니다. 에러를 처리하고 싶은 부분을 모두 try 블록에 담아버리면되어 코드 작성이 쉽고 try 블록 내부에서 await 키워드를 이용해 비동기 작업의 결과를 동기적으로 다룰 수 있어 코드 흐름 파악이 쉬워집니다. 또 try 블록안에 비동기 코드만 존재하는게 아니라 동기 코드도 혼합되어 존재할 수 있는데 동기 코드에서 발생한 에러도 처리가 가능합니다.

function checkNumber(num) {
  if (typeof num !== 'number') {
    throw new Error('입력값이 숫자가 아닙니다.');
  }
  return num;
}

async function fetchData(url) {
  try {
    const response = await fetch(url);
    const data = await response.json();

	const result = checkNumber('이것은 숫자가 아닙니다');//동기 코드도 에러 발생 처리
	console.log(result);
    return data;
  } catch (error) {
    console.error('데이터를 가져오는 중 에러가 발생했습니다:', error);
  }
}

위 처럼 에러를 처리하고 싶은 코드를 모두 try 블록에 담아 한번에 처리가 가능합니다. 이때 try 블록 내부에서 사용된 await 덕분에 코드 흐름 파악이 쉬워집니다. 동기 코드 또한 에러 처리가 가능합니다.

정리하자면 async/await을 사용하면 매우 긴 한줄로 연결된 프로미스 체이닝과 다르게 각 코드를 비교적 짧은 한줄씩 이해하며 전체를 파악할 수 있습니다. 이 덕분에 코드의 가독성이 향상되고 유지 보수가 쉬워집니다.

정리

  • async 함수는 프로미스 객체 반환
    - 일반 값 반환하는 경우 프로미스 객체 감싸서 반환
    - 프로미스 객체 반환하는 경우 그대로 프로미스 객체 반환
  • 비동기 작업의 결과를 동기적으로 사용 가능한 await 사용 가능
  • 블록으로 에러 처리가 가능한 try/catch 사용 가능
  • 프로미스 체이닝은 매우 긴 한줄의 코드를 가지나 async/await을 사용하면 코드를 나눌 수 있어 가독성이 좋음

2. 참고자료

profile
프론트엔드 개발자

0개의 댓글