프로미스 & async await

1Hoit·2023년 1월 18일
0

자바스크립트

목록 보기
22/25

들어가기전..

  • 동기 : 특정 코드의 실행이 완료될 때까지 기다리고 난 후 다음 코드를 수행하는 것
  • 비동기 : 특정 코드의 실행이 완료될 때까지 기다리지 않고 다음 코드들을 수행하는 것

비동기 함수 문제점

비동기 함수(setTimeout 등..)를 호출 시 함수 내부의 비동기로 동작하는 코드가 완료되지 않아도 즉시 종료된다. 즉, 비동기 함수 내부의 비동기로 동작하는 코드는 비동기 함수가 종료된 이후에 완료된다.
그래서 비동기 함수 내부의 비동기로 동작하는 코드의 처리 결과를 외부로 반환, 상위 스코프의 변수에 할당하면 기대한 대로 동작하지 않는다.

위와 같은 이유로 비동기 함수의 처리 결과에 대한 후속 처리는 비동기 함수 내부에서 해야한다.
비동기 함수 처리 결과에 대한 후속 처리를 위해 콜백함수를 전달하는 것이 일반적이다.
비동기 처리 성공 실패 여부에 따라 콜백함수를 전달할 수 있다.

하지만 여기에도 문제가 있다. 비동기 함수가 비동기 처리결과를 가지고 또 다시 비동기 함수를 호출한다면 콜백 함수의 호출이 중첩되어 콜백 헬이 발생한다.


프로미스의 등장

자바스크립트는 비동기 처리를 위한 패턴으로 콜백 함수를 사용했지만 콜백 헬로 인해 가독성이 나쁘고 에러 처리의 문제, 여러 개의 비동기 처리를 한번에 처리하는 데도 한계가 있다.

ES6에서는 비동기 처리를 위한 패턴으로 프로미스(Promise)를 도입했고 콜백 패턴의 단점을 보완하며 비동기 처리 시점을 명확하게 표현할 수 있다는 장점이 있다.

프로미스

비동기 처리 상태와 처리 결과를 관리하는 객체

프로미스(=Promise 객체) 생성

  • Promise 생성자 함수를 new 연산자와 함께 호출
  • Promise 생성자 함수는 비동기 처리를 수행할 콜백 함수 (ECMAScript 에선 executor 함수)를 인수로 전달받는데 이 콜백 함수는 resolve와 reject 함수를 인수로 전달 받는다.
  • resolve : 비동기 처리 성공 시 호출 되며 비동기 처리 결과를 resolve 함수의 인수로 전달하여 호출한다.
  • reject : 비동기 처리 실패시 호출 되며 비동기 처리 결과를 reject 함수의 인수로 전달하여 호출한다.
//프로미스 생성
const promise = new Promise((resolve,reject)=>{
  //Promise 함수의 콜백함수 내부에서 비동기 처리
  if(/*비동기 처리 성공조건*/){
     resolve('result');
} else{/*비동기 처리 실패시*/
	reject('failure reason');                            
}
});

프로미스 상태

비동기 처리의 진행 상태 정보를 갖는다.

프로미스 상태정보의미상태변견조건
pending비동기처리 수행 전프로미스 생성후 기본상태
fulfilled비동기처리 수행 후 (성공)resolve 함수 호출
rejected비동기처리 수행 후 (실패)reject 함수 호출
  • 프로미스 상태 정보
  • settled 상태 : fulfilled 또는 rejected 상태로 비동기 처리가 수행된 상태를 말한다.
    settled 상태가 되면 다른 상태로 변화할 수 없다.

    참고로 Promise 객체의 비동기 처리 상태와 처리 결과는
    [[PromiseStatus]], [[PromiseValue]] 라는 내부슬롯에서 확인 할 수 있다.


프로미스 후속 처리 메서드

  • resolve와 reject 상태가 되고 프로미스의 처리결과를 위한 후속 메서드가 있다.
  • 즉, 프로미스의 비동기 처리 상태가 변화하면 후석 처리 메서드에 인수로 전달한 콜백 함수가 호출 된다.
  • 모든 후속 처리 메서드는 프로미스를 반환하며 비동기로 동작한다.

1. Promise.prototype.then 메서드

2개의 콜백 함수를 인수로 전달 받는다.

  • 첫번째 콜백 함수는 비동기 처리 성공 시 호출
  • 첫번째 콜백 함수는 비동기 처리 실패 시 호출
// 성공 fulfilled
new Promise(resolve => resolve("성공"))
    .then(v=> console.log(v),
          e=> console.log(e)); // 성공
// 실패 rejected
new Promise((_, reject) => reject(new Error("실패")))
    .then(v=> console.log(v),
          e=> console.log(e)); // Error: 실패

2. Promise.prototype.catch 메서드

catch메서드는 then(undefined,OnRejected)와 동일하게 동작.

  • 하지만 catch메서드가 가독성이좋고 명확하다.
  • catch 메서드를 모든 then 메서드를 호출한 이후에 호출하면 비동기 처리에서 발생한 에러와 then 메서드 내부에서 발생한 에러까지 모두 캐치할 수 있다.
  • 에러가 발생하지 않으면 호출되지 않는다!
  • 그러므로 에러처리는 catch에서 하자!!
new Promise((_, reject) => reject(new Error("실패")))
	.catch(e=>console.log(e)); //Error: 실패
//동일한 코드
new Promise((_, reject) => reject(new Error("실패")))
	.then(undefined, e=>console.log(e)); //Error: 실패
//then 후 catch
promiseGet("https://jsonplaceholder.typicode.com/todos.1")
	.then(res=>console.log(xxx(res))
    .catch(err=>console.error(err)) // TypeError : console.xxx is not a function

3. Promise.prototype.finally 메서드

한개의 콜백함수를 인수로 전달 받으며 프로미스의 성공 실패와 상관없이 무조건 한번 호출된다.

new Promise( ()=>{} )
	.finally(()=> console.log("finally")); // finally

프로미스 체이닝

then , catch, finally 후속처리 메서드는 언제나 프로미스를 반환하므로 연속적으로 호출할 수 있다.

프로미스 정적 메서드

Promise는 주로 생성자 함수로 사용되지만 함수도 객체이므로 메서드를 가진다.

1. Promise.resolve / Promise.reject

이미 존재하는 값을 래핑하여 프로미스를 생성하기 위해 사용 된다.

//배열을 resolve하는 프로미스 생성
const resolvedPromise = Promise.resolve([1,2,3]);
resolvedPromise.then(console.log); //[1,2,3]
//위와 동일하게 동작
const resolvedPromise = new Promise(resolve => resolve([1,2,3]))
resolvedPromise.then(console.log); //[1,2,3]

const rejectedPromise = Promise.reject(new Error("에러"));
rejectedPromise.catch(console.log); // Error: 에러
//위와 동일하게 동작
const rejectedPromise = new Promise((_,reject)=> reject(new Error("에러")));
rejectedPromise.catch(console.log); // Error: 에러

2. Promise.all

여러개의 비동기 처리를 모두 병렬 처리할 때 사용
예제)
배열에 있는 모든 Promise에서 executor 내 작성했던 코드들이 정상적으로 처리가 되었다면 결과를 배열에 저장해 새로운 Promise를 반환.

const promiseOne = () => new Promise((resolve, reject) => setTimeout(() => resolve('1초'), 1000));
const promiseTwo = () => new Promise((resolve, reject) => setTimeout(() => resolve('2초'), 2000));
const promiseThree = () => new Promise((resolve, reject) => setTimeout(() => resolve('3초'), 3000));

// 기존 - 코드들이 순차적으로 동작되기에 총 6초의 시간이 걸리게 된다.
  // 중복된 코드들도 많다.
const result = [];
promiseOne()
  .then(value => {
    result.push(value);
    return promiseTwo();
  })
  .then(value => {
    result.push(value);
    return promiseThree();
  })
  .then(value => {
    result.push(value);
   console.log(result);  
	 // ['1초', '2초', '3초']
  })

// Promise.all() 비동기 작업들을 동시에 처리해서 3초 안에 모든 작업 종료
  // 훨씬 간결함
Promise.all([promiseOne(), promiseTwo(), promiseThree()])
  .then((value) => console.log(value))
  // ['1초', '2초', '3초']
  .catch((err) => console.log(err));

추가적으로 Promise.all은 인자로 받는 배열에 있는 Promise 중 하나라도 에러가 발생하게 되면 나머지 Promise의 state와 상관없이 즉시 종료된다.

Promise.all([
	new Promise((resolve, reject) => setTimeout(() => reject(new Error('에러1'))), 1000),
	new Promise((resolve, reject) => setTimeout(() => reject(new Error('에러2'))), 2000),
	new Promise((resolve, reject) => setTimeout(() => reject(new Error('에러3'))), 3000),
])
	.then((value) => console.log(value))
  .catch((err) => console.log(err));
	// Error: 에러1

마이크로 태스크 큐

setTimeout(()=>console.log(1),0);

Promise.resolve()
	.then(()=>console.log(2))
	.then(()=>console.log(3));
	// 2->3->1 순서로 출력 된다.

프로미스의 후속처리 메서드의 콜백 함수는 태스크큐가 아니라 마이크로 태스크 큐에 저장되며
우선순위가 마이크로 태스크 큐가 더 높기 때문에 위와 같이 동작한다.


fetch와 axious

내용이 길어 질거 같으므로 따로 포스팅을 했다. 아래를 클릭.
fetch & axious


Promise Hell

Promise를 통해 비동기 코드의 순서를 제어할 수 있지만 Callback 함수와 같이 코드가 길어질수록 복잡해지고 가독성이 낮아지는 Promise Hell이 발생하는 단점이 있다.


async/await

JavaScript는 ES8에서 async/await키워드를 제공했다.

  • 가독성 좋게 비동기 처리를 동기 처리처럼 동작하도록 구현할 수 있다.
  • async/await 는 프로미스를 기반으로 동작하며 프로미스의 후석처리 메서드에 대한 후속 처리할 필요 없이 동기 처리처럼 프로미스를 사용할 수 있다.
  • 즉, 동기 처리처럼 프로미스가 처리 결과를 반환하도록 구현할 수 있다.

async 함수

  • await 키워드는 반드시 async 함수 내부에서 사용해야하며
    async 함수는 async 키워드를 사용해 정의하며 언제나 프로미스를 반환한다.
    (async 함수는 암묵적으로 반환값을 resolve하는 프로미스를 반환함)

  • 단, 클래스의 constructor 메서드는 async 메서드가 될 수 없다. constructor 메서드는 인스턴스를 반환해야 하지만 async 함수는 프로미스를 반환하기 때문이다.

await 키워드

  • await 키워드는 프로미스가 settled 상태(비동기 처리가 수행된 상태)가 될 때까지 대기하다가 settled 상태가 되면 프로미스가 resolve한 처리 결과 [[PromiseResult]]를 반환한다.
  • await 키워드는 반드시 프로미스 앞에서 사용해야 한다.
  • 예제 1
const fetch = require( 'node-fetch');
const getGithubUserName = async id => {
const res = await fetch("https://api.github.com/users/${id}'); // 1
const { name } = await res.json(); // 2
console.log(name); // Ungmo Lee
};
getGithubUserName ( 'ungmo2');

①의 fetch 함수가 수행한 HTTP 요청에 대한 서버의 응답이 도착해서 fetch 함수가 반환한 프로미스가 settled 상태가 될 때까지 ① 은 대기하게 된다. 이후 프로미스가 settled 상태가 되면 프로미스가 resolve한 처리 결과가 res 변수에 할 당된다.
이처럼 await 키워드는 다음 실행을 일시 중지시켰다가 프로미스가 settled 상태가 되면 다시 재개한다.

  • 예제 2
const printString = (string) => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve();
      console.log(string);
    }, Math.floor(Math.random() * 100) + 1);
  });
};

const printAll = async () => {
  await printString('A');
  await printString('B');
  await printString('C');
};

printAll(); // A->B->C 순으로 출력
  • 예제 3 : 비동기 처리 결과를 가지고 다음 비동기 처리 예제
async function bar(n){
  const a = await new Promise(resolve => setTimeout( ()=> resolve(n), 3000));
  const b = await new Promise(resolve => setTimeout( ()=> resolve(a+1), 2000));
  const c = await new Promise(resolve => setTimeout( ()=> resolve(b+1), 1000));
  
  console.log([a,b,c]); // [1,2,3]
}
bar(1); // 6초 소요

Promise처리 메서드 vs await 사용 비교

결론은 이 둘은 같은 처리를 한다.
다만 ,

  • Promise 처리메서드 : 프로미스 객체를 반환하고
  • await 키워드 : 프로미스 객체에 있는 [[PromiseResult]] 를 반환한다.
const a = fetch("https://koreanjson.com/posts/1").then(res=>res);

const a = await fetch("https://koreanjson.com/posts/1");
const a = fetch("https://koreanjson.com/posts/1").then(res=>res.json());
console.log(a);

const b = await fetch("https://koreanjson.com/posts/1");
console.log(b.json());

async / await 에러처리

async/await에서 에러 처리는 try ... catch 문을 사용할 수 있다.
콜백 함수를 인수로 전달받는 비동기 함수 와는 달리 프로미스를 반환하는 비동기 함수는 명시적으로 호출할 수 있기 때문에 호출자가 명확하다.

const fetch = require( 'node-fetch');
const foo = async () ={
	try {
		const wrongUrl = 'https://wrong.url';
		const response = await fetch(wrongUrl);
		const data = await response.json();
		console.log (data);
	} catch (err) {
		console.error(err); // TypeError: Failed to fetch
	}
};
foo();
  • foo함수의 catch 문은 HTTP 통신에서 발생한 네트워크 에러뿐 아니라 try 코드 블록 내의 모든 문에서 발생한 일반적인 에러까지 모두 캐치할 수 있다.

  • async 함수 내에서 catch 문을 사용해서 에러 처리를 하지 않으면 async 함수는 발생한 에러를 reject하는 프로미스를 반환한다.
    따라서 async 함수를 호출하고 Pronise. prototype. catch 후속 처리 메서드를 사용해 에러를 캐치할 수도 있다.
const fetch = require( 'node-fetch');
const foo = async () ={
	const wrongUrl = 'https://wrong.url';
	const response = await fetch(wrongUrl);
	const data = await response. json();
	return data;
};
foo()
.then(console.log)
catch(console.error); // TypeError: Failed to fetch

마무리..

에러처리에 대한 내용은 다음 포스트에서 다뤄보려고 한다.
프로미스를 이해함으로써 node.js환경의 비동기적인 처리 과정을 동기적으로 사용하기 위해 어떻게 해야하는지 알게 되었다.

profile
프론트엔드 개발자를 꿈꾸는 원호잇!

0개의 댓글