오늘도 개발

자바스크립트와 비동기 프로그래밍(콜백 함수, Promise, async/await) 본문

웹 프로그래밍/Javascript

자바스크립트와 비동기 프로그래밍(콜백 함수, Promise, async/await)

Sueeeeeee 2022. 9. 27. 18:36

1. 비동기 프로그래밍이란?

동기적 프로그래밍

한 작업이 끝나야 다음 작업을 하는 방식.

실행 순서가 보장됨.

function main(){
  console.log('1');
  console.log('2');
  console.log('3');
}

main();
// 1
// 2
// 3

 

비동기적 프로그래밍

한 작업이 오래 걸리면 기다리는 동안 다음 작업을 하는 방식.

 

내가 할 일으로 설거지하기, 청소하기, 공부하기, 잠자기가 있다고 생각해보자.

이 때 나는 수세미가 없어서 쇼핑몰에서 수세미를 주문했다.

수세미가 올 때까지 몇 일이 걸릴지 모르는데 수세미가 올 때까지 청소, 공부, 잠을 미루면 비효율적이다.

나는 수세미가 언제 오든, 오기 전까지 다른 일을 하고 있으면 된다. (=비동기적 처리)

 

js는 기본적으로 한 번에 하나의 Task만 처리하는 single thread 방식을 사용한다.

기본적으로 동기 방식이라는 뜻이다.

하지만 비동기 함수를 만나면 기다리지 않고 알아서 다른 일을 먼저 처리한다. (= Non-blocking)

 

그러다보니 기다려야 하는 작업 실행 시(ex. I/O bound 작업, 네트워크 통신) 의도치 않은 결과를 얻을 수 있다.

이런 작업을 제대로 처리하려면 콜백 함수, Promise 또는 async/await을 사용해야 한다.

 

2. 콜백 함수로 처리하기

다음 코드에서 findUser에 들어간 setTimeout은 실행에 1초가 걸리는 비동기 함수이다.

아래 코드에서 자바스크립트는 findUser의 반환값을 기다리지 않고 바로 다음줄로 넘어간다.

따라서 user undefined라는 뜻밖의 결과를 얻게 된다.

function findUser(id){
  let user;
  setTimeout(function(){
    console.log('1초 기다리기')
    user = {
      userId: id,
    }
  }, 1000);
  return user;
}

const user = findUser(1);
console.log('user : ', user);

// user undefined
// 1초 기다리기

이 때 findUser함수에 콜백함수를 넣어 처리하면 우리가 의도한 대로 user 1이라는 값을 얻을 수 있다.

findUser 실행이 끝나야 콜백함수가 호출되기 때문이다.

function findUser(id, callback){
  setTimeout(function(){
    console.log('1초 기다리기')
    const user = {
      userId: id,
    };
    callback(user)
  }, 1000);
}

findUser(1, function(user){
  console.log('user : ', user);
})

// 1초 기다리기
// user :  { userId: 1 }

하지만 콜백 함수를 사용하면 콜백 지옥에 빠질수도 있다.

nesting이 많이 일어나서 무엇이 무엇의 콜백인지 알아보기 어려운 코드를 콜백 지옥에 빠졌다고 한다.

콜백 함수를 사용하면 이렇게 가독성이 떨어지고 디버깅이 어려워질 수 있으므로,

요즘은 Promise나 async/await을 사용하는 방법을 주로 쓴다.

 

3. Promise로 처리하기

Promise란?

Promise는 현재는 얻을 수 없지만 기다리면 값을 얻을 수 있는 오브젝트이다.

모든 Promise는 Promise 클래스의 객체이다.

주로 fetch() 함수와 같이 사용한다.(ex. 네트워크 통신)

Promise를 사용하면 콜백함수를 사용할 때보다 더 직관적으로 코드를 작성할 수 있다.

 

Promise의 state(상태)

pending(대기) : 실행 대기

fulfilled(이행) : 비동기 처리 완료 후 값 반환함

rejected(실패) : 처리 실패

 

Promise 생성 방법

Promise는 new 키워드와 생성자로 만들 수 있다.

Promise의 생성자는 resolve와 reject라는 함수를 매개변수로 받는다.

resolve는 성공적으로 Promise를 실행한 경우 실행되고,

reject는 실패한 경우 실행된다.

const p = new Promise((resolve, reject) => {...})

resolve와 reject는 함수 바디에서 알맞게 호출해주어야 한다.

function divide(x, y){
  return new Promise((resolve, reject) => {
    if (y === 0) reject(new Error('DivisionByZero'))
    else resolve(x / y)
  })
}

divide(10, 2)
.then((result) => console.log(result))
.catch((error) => console.log(error))
// 5

divide(10, 0)
.then((result) => console.log(result))
.catch((error) => console.log(error))
// Error: DivisionByZero

findUser 함수에 콜백 함수 대신 Promise를 적용하면 이렇게 된다.

function findUser(id){
// reject는 생략 가능
  return new Promise(function(resolve){
    setTimeout(function(){
      console.log('1초 기다리기')
      const user = {
        userId : id
      };
      resolve(user)
    }, 1000)
  })
}

findUser(1)
.then(function(user){
  console.log('user : ', user);
})

// 1초 기다리기
// user :  { userId: 1 }

 

fetch()

fetch()는 REST api를 호출할 때 사용하는 브라우저 내장 함수이다.

URL을 인자로 받아 api를 호출한 뒤, 결과를 Promise로 반환한다.

fetch('https://example.com')
.then((response) => console.log(response))
.catch((error) => console.log(error))

then()과 catch()는 Promise를 반환하며, 이로 얻은 Promise는 또 then()과 catch()로 처리할 수 있다.

then()과 catch()를 계속 이을 수 있다는 뜻이다. 이를 method chaining이라고 한다.

fetch('https://example.com')
.then((response) => response.json())
.then((result) => console.log(result))
.catch((error) => console.log(error))

fetch()에는 다음과 같은 단점이 있다.

1) 디버깅이 어려움 : 오류 발생 시 몇 번째 then에서 발생한 것인지 알 수 없다.

2) 예외 처리가 어려움 : try/catch가 아니라 catch() 메서드를 사용해야 하는데, 누락하기도 쉽고 사용하기도 어렵다.

3) 가독성이 떨어짐 : method chaining 시 가독성이 떨어지기 쉽다.

function fetchExample(id) {
  return fetch(`https://example.com`)
    .then((response) => response.json())
    .then((post) => post.userId)
    .then((userId) => {
      return fetch(`https://example.com/profile/${userId}`)
        .then((response) => response.json())
        .then((user) => user.name);
    });
}

fetchExample(1).then((name) => console.log("name:", name));

 

4. async/await로 처리하기

async 함수는 Promise를 반환하는 비동기 함수이다.

async/await는 Promise의 단점을 해결하기 위해 생긴 문법으로,

비동기 코드를 동기 코드처럼 보이게 작성할수 있어서 가독성이 높아진다.

 

await은 Promise를 리턴하는 모든 비동기 함수 호출부 앞에 붙여준다.

await은 async 키워드를 사용하여 정의한 함수 내부에서만 쓸 수 있다.

await을 붙이지 않으면 비동기 함수는 건너뛰어지지만, await을 붙이면 결과를 얻을 때까지 기다린다음 다음줄을 실행한다.

async function fetchExample(id){
  const postResponse = await fetch('https://example.com')
  const post = await postResponse.json();
  const userId = post.userId;
  const userResponse = await fetch('https://example.com/profile/${userId}');
  const user = await userResponse.json()
  return user.name;
}

// async 키워드로 정의한 함수를 호출하면 무조건 Promise 객체를 반환한다.
// fetchExample 함수도 Promise 반환을 명시하진 않지만 어쨌든 Promise 객체가 반환된다.
// 그렇기 때문에 then을 사용해서 처리할 수 있다.
fetchExample(1)
.then((name) => console.log('name : ', name))

 

예외 처리

동기 함수와 마찬가지로 try/catch를 사용하면 된다.

async function fetchExample(id){
  const postResponse = await fetch('https://example.com')
  const post = await postResponse.json();
  
  try{
    const userId = post.userId;
    const userResponse = await fetch('https://example.com/profile/${userId}');
    const user = await userResponse.json()
    return user.name;
  } catch(error) {
    console.log(error);
    return 'Unknown';
  }
  
}

fetchExample(1)
.then((name) => console.log('name : ', name))

 

 

참고

DaleSeo - [자바스크립트] 비동기 처리 1부 - Callback

DaleSeo - [자바스크립트] 비동기 처리 2부 - Promise

DaleSeo - [자바스크립트] 비동기 처리 3부 - async/await