본문 바로가기

JavaScript

JavaScript: 비동기 처리, Promise, async/await, Promise.all, Promise.race

반응형

비동기 처리의 이해

동기적 처리란 위 그림과 같이 1번 작업이 끝나기 전까지는 2번이 시작하지 못하고, 2번이 끝나기 전까지는 3번이 

시작하지 못하고 전 작업이 끝나야 다음 작업 시작이 가능한 것을 동기적 처리라고 한다.

비동기적 처리는 동시에 여러가지 작업을 실행할 수 있다. 어떤 코드가 실행 중일 때도 다른 함수 호출이 가능해

병렬적으로 작업을 수행한다. 비동기적 처리가 훨신 효율적인 것을 확인할 수 있다.

 

function work(callback) {
  setTimeout(() => {
    const start = Date.now();
    for (let i = 0; i < 1000000000; i++) {}
    const end = Date.now();
    console.log(end - start + "ms");
    callback(end - start);
  }, 0);
}

console.log("작업 시작!");
work((ms) => {
  console.log("작업이 끝났어요!");
  console.log(ms + "ms 걸렸다고 해요. ");
});

console.log("다음 작업");
작업 시작!
다음 작업
923ms
작업이 끝났어요!
923ms 걸렸다고 해요.

다음과 같이 setTimeout() 함수를 이용해서 비동기 처리를 할 수 있다. 함수 끝의 '0'은 0ms 후에 실행하라는 뜻이다. 

하지만 실제로는 0ms가 아닌 4ms 후에 실행된다고 한다.

결과를 보면 먼저 작성한 work 함수가 나중에 실행되는 것을 알 수 있다. work 함수를 실행하는 동안 다음 작업이 

기다리는 것이 아니라, 비동기 처리를 통해 다음 작업이 실행되는 것이다. 

work 함수를 호출할 때 "작업이 끝났어요!"를 출력해주는 함수를 넘겨줬는데, 이렇게 하면 work 함수 종료 후 해당

함수를 실행시킬 수 있다.

 

자바스크립트에서는 주로 다음과 같은 작업들을 비동기적으로 처리한다.

 

Promise 

Promise는 비동기 작업을 더 편리하게 처리할 수 있도록 ES6에 도입된 기능이다.

이전에는 비동기 작업을 처리할 때 callback 함수를 이용해 처리를 해야해서 코드가 쉽게 난잡해질 수 있었다.

그래서 Promise라는 것이 만들어졌고, 원래는 라이브러리로 존재했으나 이후에 공식적으로 추가되었다.

 

Promise를 만드는 방법에 대해 알아보자.

const myPromise = new Promise((resolve, reject) => {
  // 구현.....
  setTimeout(() => {
    resolve("result");
  }, 1000);
});

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

// result

다음과 같이 만들 수 있고, 이는 1초 뒤에 성공하는 Promise이다.

Promise는 resolvereject를 파라미터로 받아와줘야 한다. Promise는 성공할 수도 있고, 실패할 수도 있다. 성공할 때는 resolve를 호출해주면 되고, 실패할 때는 reject를 호출해주면 된다. 여기서 resolve와 reject는 둘 다 함수이다.

만약 Promise가 끝나고 어떤 작업을 하고싶을 때는 then()이라는 것을 사용해서 설정 해줄 수 있다.

 

const myPromise = new Promise((resolve, reject) => {
  // 구현.....
  setTimeout(() => {
    reject(new Error());
  }, 1000);
});

myPromise
  .then((result) => {
    console.log(result);
  })
  .catch((e) => {
    console.error(e);
  });

// Error

 다음은 1초뒤에 실패하는 예제이다. resolve대신 reject를 사용했다.

catch 함수는 에러를 잡아내주고 에러가 발생했을 때 catch 함수 안의 코드를 실행하게 된다.

 

function increaseAndPrint(n) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      const value = n + 1;
      if (value === 5) {
        const error = new Error();
        error.name = "ValueFiveError";
        reject(error);
        return;
      }
      console.log(value);
      resolve(value);
    }, 1000);
  });
}

increaseAndPrint(0)
  .then(increaseAndPrint)
  .then(increaseAndPrint)
  .then(increaseAndPrint)
  .then(increaseAndPrint)
  .catch((e) => {
    console.log(e);
  });

다음의 코드를 살펴보자. increaseAndPrint 함수는 1초뒤에 받아온 n에 1을 더해주고 출력해준 다음, resolve()를 통해서

1을 더한 값을 반환해준다. 1을 더한 value가 5일경우, "ValueFiveError"를 출력하고 리턴을 통해 종료한다.

Promise를 사용하면 이렇게 비동기 처리의 개수가 많아져도 코드의 깊이가 깊어지지 않는다.

하지만 이 코드도 불편한 점이 있다. 에러를 잡을 때 어떤 부분에서 에러가 발생했는지 파악이 어렵고 특정 조건에 따라

분기를 나누는 작업도 힘들고 번거롭다. 특정 값을 공유해가면서 하는 작업도 하기 어렵다.

그래서 사용하는 것이 바로 async, await이다. 

 

async / await

자바스크립트에서 비동기 처리를 하게 될 때, Promise를 더욱 쉽게 사용할 수 있게 해주는 async/await 문법에 대해 알아보자. 이 문법은 ES8에 소개된 문법이다. 

async/await 문법을 사용할 때는 함수의 앞에다가 async 키워드를 붙여주면 된다. 그리고 Promise를 호출하는 함수

앞에 await 키워드를 붙여준다.  

 

function sleep(ms) {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

async function process() {
  console.log("안녕하세요!");
  await sleep(1000);
  console.log("반갑습니다!");
}

process();

 다음 코드는  '안녕하세요!'가 먼저 출력되고 1초뒤에 '반갑습니다!'가 출력된다. 

async/await을 사용하면 .then()을 하지않고도 await을 통해 기다려줄 수 있다.

그리고 async를 사용하게 된다면 해당 함수는 Promise를 리턴하게 된다. 

 

function sleep(ms) {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

async function process() {
  console.log("안녕하세요!");
  await sleep(1000);
  console.log("반갑습니다!");
  return true;
}

process().then((value) => {
  console.log(value);
});
안녕하세요!
반갑습니다!
true

다음과 같이 Promise를 리턴하는 것을 확인할 수 있다.

 

그리고 만약 Promise에서 에러를 발생시키고 싶을 땐 throw문을 , 에러를 잡고 싶을 때는 try, catch문을 사용하면 된다.

function sleep(ms) {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

async function makeError() {
  await sleep(1000);
  const error = new Error();
  throw error;
} 

async function process() {
  try {
    await makeError();
  } catch (e) {
    console.error(e);
  }
}

process();
Error
    at makeError (c:\Users\ayxlt\Desktop\Coding\JavaScript\tutorial\fastcampus\js\text.js:7:17)
    at async process

다음과 같이 async/await에서 에러를 잡아낼 때는 try/catch를 사용하면 된다.

14번째 줄에 'e'는 8번째 줄의 error를 가리킨다. 

 

Promise.all

function sleep(ms) {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

const getDog = async () => {
  await sleep(1000);
  return "멍멍이";
};

const getRabbit = async () => {
  await sleep(500);
  return "토끼";
};

const getTurtle = async () => {
  await sleep(3000);
  return "거북이";
};

async function process() {
  const dog = await getDog();
  console.log(dog);
  const rabbit = await getRabbit();
  console.log(rabbit);
  const turtle = await getTurtle();
  console.log(turtle);
}

process();

다음의 코드를 실행 시키면 "멍멍이" 출력 후 0.5초 뒤에 "토끼"가 출력되고 3초 뒤에 "거북이"가 출력된다. 

만약 여기서 여러개의 Promise를 동시에 처리하고 싶다면 어떻게 해야 할까? 그럴때는 Promise.all이라는 함수를 

사용하면 된다.

 

async function process() {
  // 배열에다가 Promise를 등록해줘야 한다.
  const result = await Promise.all([getDog(), getRabbit(), getTurtle()]);
  console.log(result)
}

process();

// [ '멍멍이', '토끼', '거북이' ]

이렇게 process 함수를 바꿔주면 된다. 여기서 Promise가 끝나는 시간은 동시에 시작해서 가장 늦게 끝나는 함수의 

시간과 같다. 함수가 다 끝나면, result에는 각각 끝난 결과값이 들어있는 배열이 반환된다. 

3초 뒤에 [멍멍이, 토끼, 거북이] 배열이 반환되는 것이다. 

 

async function process() {
  // 배열에다가 Promise를 등록해줘야 한다.
  const [dog, rabbit, turtle] = await Promise.all([
    getDog(),
    getRabbit(),
    getTurtle(),
  ]);
  console.log(dog);
  console.log(rabbit);
  console.log(turtle);
}

process();

// 멍멍이
// 토끼
// 거북이

다음처럼 배열 비구조화 할당 문법으로 각각의 결과값을 꺼내올 수도 있다.

 

async function process() {
  // 배열에다가 Promise를 등록해줘야 한다.
  const first = await Promise.race([getDog(), getRabbit(), getTurtle()]);
  console.log(first);
}

process();

// 토끼

Promise.racePromise.all과 사용방법은 비슷하지만 결과는 다르다. 배열을 등록한다는 점은 같다. 

Promise.race는 등록된 Promise들 중 가장 빨리 끝난 것 하나만 리턴된다.

 

추가로 Promise.all은 배열 중 하나만 에러가 발생해도 전체에 에러가 발생한 것으로 간주한다. 

Promise.race는 가장 빨리 끝난 것이 에러일 때만 에러로 간주한다. 나중에 끝난 것에 에러가 발생하더라도

에러로 간주하지 않는다. 이때도 에러가 발생해 이를 잡을 때는 try/catch문을 사용한다.

Promise.all은 앞으로 개발할 때 많이 사용하게 될 것이라고 한다.

반응형