[JavaScript] 비동기 처리 - Promise, await & async

Letmegooutside·2022년 1월 3일
0

JavaScript

목록 보기
4/25
post-thumbnail

개요

싱글스레드인 자바스크립트에서 비동기 처리를 위해서는 콜백을 사용해왔다.
어떤 작업을 요청하면서 콜백함수를 등록 하면, 작업이 수행된 뒤 결과를 콜백함수를 통해 알려주는 식이다.
이런 방식은 비동기 처리를 순차적으로 실행할 필요가 있는 경우에 비동기 처리를 중첩시켜 표현하므로 예외처리가 어렵고 중첩으로 인한 복잡도가 증가한다는 단점이 있다.
이를 해결하기 위해 Promise가 라이브러리로 지원되었고, 이것을 ES6에서는 언어적 차원에서 지원하게 되었다.

Promise

비동기 함수를 동기적으로 처리하기 위해 고안한 객체이다. 비동기 작업이 완료된 이후에 다음 작업을 연결시켜 진행할 수 있는 기능을 가지고 있고 작업 결과에 따라 성공 또는 실패를 리턴하며 결과 값을 전달 받을 수 있다.

프로미스의 3가지 상태

  • pending(대기) : 처리가 완료되지 않은 상태

  • fulfilled(이행) : 성공적으로 처리가 완료된 상태

  • rejected(거부) : 처리가 실패로 끝난 상태

간단히 얘기하면 프로미스 객체가 비동기 함수의 처리 상태를 확인하고 완료되었는지 판단하여 성공 여부에 따라 다음 처리를 다르게 수행할 수 있도록 해준다.

생성/호출

프로미스는 기본적으로 아래와 같은 형태의 객체이다

const promise = new Promise()

프로미스 생성자 함수는 함수를 인자로 받는다.
프로미스 생성자 함수에 인자로 들어갈 함수는 일반적으로 resolve, reject라고 부르는 2개의 매개 변수를 사용할 수 있다. resolvereject는 함수이다.
fulfilled 상태에서는 resolve함수가 실행되고, rejected 상태에서는 reject함수가 실행된다.

const promise = new Promise(function (resolve, reject) {
  // 비동기 작업
});

프로미스 생성자 함수에 인자로 들어간 함수 내부에서 우리는 비동기 작업을 하고, 비동기 작업이 성공할 경우 resolve를 실행해야하고, 실패한 경우 reject를 실행해야한다.

const promise = new Promise(function (resolve, reject) {
  // 비동기 작업
  // ...
  fs.readFile(path, "utf-8", (err, data) => {
    if (err) {
      // 실패했을 경우 reject 호출
      reject(err);
    } else {
      // 성공했을 경우 resolve 호출
      resolve(data);
    }
  });
});

이 함수를 다음과 같이 호출하여 사용할 수 있다.

// resolve가 호출되었다면 그 결과가 then의 data로 전달된다
promise.then(function(data) {
  console.log("success", data);
});
// reject가 호출되었다면 그 결과가 catch의 err로 전달된다.
promise.catch(function(err) {
  console.log("failed",err);
});

resolve(data)의 결과가 then의 data로 전달되고, reject(err)의 err이 catch로 전달된다.
then의 첫 번째 인자는 프로미스가 이행되었을 때 실행되는 함수이고, 두 번째 인자는 프로미스가 거부되었을 때 실행되는 함수이다.

아래와 같이 순차적으로 연결하여 처리할 수도 있다.
thencatch는 프로미스 객체의 메소드로 만약 then이 또 다른 값을 반환한다면 then을 다음에 한 번 더 연결해서 처리할 수 있다.

// 첫번째 .then() 에서 반환된 결과가 두번째 .then()으로 넘어간다
promise.then(function done (data) {
  console.log("Promise success!", data);
  return 1;
}).then(function handleOne (one) {
  console.log("I am one.");
  return one + 1;
}).then(function handleTwo (two) {
  console.log("I am two.");
}).catch(function handleError (err) {
  console.log("Promise Failed!", err);
});

에러 처리 방법

1. .then()의 두 번째 인자로 에러를 처리하는 방법

getData().then(
  handleSuccess,
  handleError
);

2..catch()를 이용하는 방법

getData().then().catch();

위의 두가지 방법 모두 프로미스의reject() 메서드가 호출되어 실패 상태가 된 경우에 실행된다.

가급적 catch()를 사용하는 것이 효율적이다.

// then()의 두 번째 인자로는 감지하지 못하는 오류
function getData() {
  return new Promise(function(resolve, reject) {
    resolve('hi');
  });
}

getData().then(function(result) {
  console.log(result);
  throw new Error("Error in then()"); // Uncaught (in promise) Error: Error in then()
}, function(err) {
  console.log('then error : ', err);
});

// catch를 사용하는 경우 
getData().then(function(result) {
  console.log(result); // hi
  throw new Error("Error in then()");
}).catch(function(err) {
  console.log('then error : ', err); // then error :  Error: Error in then()
});

getData() 함수의 프로미스에서 resolve()메서드를 호출하여 정상적으로 로직을 처리했지만, then()의 첫 번째 콜백함수 내부에서 에러가 발생하는 경우 제대로 잡아내지 못한다.
하지만 똑같은 오류를 catch()로 처리하면 발생한 에러를 성공적으로 처리할 수 있다.
따라서 더 많은 예외처리 상황을 위해 catch()를 사용하는 것이 바람직하다.

async & await

자바스크립트의 비동기 처리 패턴 중 가장 최근에 나온 문법으로 기존의 비동기 처리 방식인 콜백함수와 프로미스의 단점을 보완하고 개발자가 읽기 좋은 코드를 작성할 수 있도록 도와준다.

콜백함수 vs async & await

일반적으로 자바스크립트의 비동기 처리 코드는 아래와 같이 콜백을 사용해야 코드의 실행 순서를 보장받을 수 있다.
(fetchUser()는 서버에서 데이터를 받아오는 HTTP 통신 코드라고 가정)

function logName() {
  // fetchUser함수에 콜백함수를 넘겨주고 fetchUser함수의 동작이 끝나면 콜백함수가 실행된다
  var user = fetchUser('domain.com/users/1', function(user) {
    if (user.id === 1) {
      console.log(user.name);
    }
  });
}

만약 비동기 처리를 콜백으로 하지 않아도 된다면 아래와 같이 코드를 작성할 수 있다.

function logName() {
  // 사용자의 데이터를 불라와서 변수에 담는다
  var user = fetchUser('domain.com/users/1');
  // 사용자 아이디가 1이면 이름을 출력한다. (순차적으로 실행)
  if (user.id === 1) {
    console.log(user.name);
  }
}

콜백함수를 사용하는 것보다 코드를 읽고 이해하기가 훨씬 간편하다. 이렇게 코드를 작성하고 싶다면 async await을 사용하면 된다.

async function logName() {
  var user = await fetchUser('domain.com/users/1');
  if (user.id === 1) {
    console.log(user.name);
  }
}

기본 문법

async function 함수명() {
  await 비동기_처리_메서드_명();
}

먼저 함수의 앞에 async라는 예약어를 붙이고, 함수의 내부 로직 중 HTTP통신을 하는 비동기 처리 코드 앞에 await를 붙인다.
여기서 주의할 점은 비동기 처리 메서드가 꼭 프로미스 객체를 반환해야 await가 의도한 대로 동작한다.
일반적으로 await의 대상이 되는 비동기 처리 코드는 Axios 등 프로미스를 반환하는 API 호출함수이다.

예제

function fetchItems() {
  return new Promise(function(resolve, reject) {
    var items = [1,2,3];
    resolve(items)
  });
}

async function logItems() {
  var resultItems = await fetchItems();
  console.log(resultItems); // [1,2,3]
}

먼저 fetchItems() 함수는 프로미스 객체를 반환하는 함수이다. fetchItems() 함수를 실행하면 프로미스가 resolved되며 결과 값은 items 배열이 된다.

logItems()함수를 실행하면 fetchItems()함수의 결과 값인 items배열이 resultItems 변수에 담긴다. 따라서 콘솔에는 [1,2,3]이 출력된다.

async & await 문법은 여러 개의 비동기 처리코드를 다룰 때 효율적으로 사용할 수 있다.
아래와 같은 예제 코드가 있다. 이 두 함수는 각각 사용자와 할 일 목록을 받아오는 HTTP 통신 코드이다.

function fetchUser() {
  var url = 'usersUrl';
  return fetch(url).then(function(response) {
    return response.json();
  });
}

function fetchTodo() {
  var url = 'todosUrl';
  return fetch(url).then(function(response) {
    return response.json();
  });
}

위 함수들을 실행하면 각각 사용자 정보와 할 일 정보가 담긴 프로미스 객체가 반환된다.
이 두 함수를 이용하여 할 일 제목을 출력하기 위한 로직은 아래와 같다.
1. fetchUser()를 이용하여 사용자 정보 호출
2. 받아온 사용자 아이디가 1이면 할 일 정보 호출
3. 받아온 할 일 정보의 제목을 콘솔에 출력

async function logTodoTitle() {
  var user = await fetchUser();
  if (user.id === 1) {
    var todo = await fetchTodo();
    console.log(todo.title); 
  }
}

logTodoTitle() 함수를 실행하면 콘솔에 사용자 아이디가 1인 경우 할 일 목록이 출력될 것이다. 이처럼 async await 문법을 이용하면 간결하고 가독성 좋은 코드를 작성할 수 있다.

예외 처리

try catch를 사용하여 예외를 처리한다. 프로미스에서 에러 처리를 위해 .catch()를 사용했던 것처럼 async()에서는 catch{}를 사용하면 된다.

async function logTodoTitle() {
  try {
    var user = await fetchUser();
    if (user.id === 1) {
      var todo = await fetchTodo();
      console.log(todo.title); 
    }
  } catch (error) {
    console.log(error);
  }
}

✨ fetch 함수

fetch 함수는 HTTP response 객체를 래핑한 프로미스 객체를 반환한다. 따라서 프로미스의 후속 처리 메서드인 then을 사용하여 resolve한 객체를 전달받을 수 있다.

fetch('url')
  .then(res => res.json())
  .then(res => {
    // data를 응답 받은 후의 로직
  });
  • fetch 함수가 반환한 프로미스 객체
/*
Response {
  __proto__: Response {
    type: 'basic',
    url: 'https://jsonplaceholder.typicode.com/posts/1',
    redirected: false,
    status: 200,
    ok: true,
    statusText: '',
    headers: Headers {},
    body: ReadableStream {},
    bodyUsed: false,
    arrayBuffer: ƒ arrayBuffer(),
    blob: ƒ blob(),
    clone: ƒ clone(),
    formData: ƒ formData(),
    json: ƒ json(),
    text: ƒ text(),
    constructor: ƒ Response()
  }
}
*/

fetch 함수로 받은 Response 객체에는 HTTP 응답을 나타내는 프로퍼티들이 있다. 그 중 json() 내장 함수가 있는데 이 메서드 사용 시 HTTP응답 body 텍스트를 JSON 형식으로 바꾼 프로미스를 반환한다.







Reference

https://velog.io/@yunsungyang-omc
https://sangminem.tistory.com/284
https://joshua1988.github.io/web-development/javascript/promise-for-beginners/
https://joshua1988.github.io/web-development/javascript/js-async-await/
https://velog.io/@eunjin/JavaScript-fetch-함수-쓰는-법-fetch-함수로-HTTP-요청하는-법

0개의 댓글