다시, 비동기 (TIL 68일차)

EenSung Kim·2021년 6월 12일
0

"미진한 부분 돌아보기"


Promise

Promise 는 비동기 작업을 위해 존재하는 객체입니다. 그리고 resolve 와 reject 를 인자로 갖는 콜백함수를 갖습니다.

동기적인 작업은 코드의 성공이나 실패 여부를 즉각적으로 확인할 수 있습니다. 순차적으로 코드를 실행하고 그 결과값을 가지고 그 다음 코드를 이어가기 때문이죠. 만약 코드 자체에 오류가 있었다면 실행을 멈추고, 다음 작업을 진행하지 않을 겁니다. 대신 오류 메시지를 띄워 우리가 디버깅을 할 수 있게끔 하겠죠.

비동기 작업은 성공이나 실패 여부를 즉각적으로 확인할 수 없습니다. 외부 서버에 무언가를 요청하고 응답을 받아야 하는 경우가 그렇죠. 아무리 올바른 요청을 보냈어도 서버의 사정으로 작업이 실패할 수도 있습니다. 그렇기 때문에 비동기적인 작업은 늘 성공인지 실패인지를 염두에 두고 두 상황에 대비해야만 합니다.

resolve 와 reject 가 바로 이 두 경우를 위해 존재합니다. resolve 는 비동기적 연산이 성공했을 때 값을 가지고 반환되는 promise 객체이고, reject 는 비동기적 연산이 실패했을 때 오류(error)의 원인을 반환하는 promise 객체입니다. 둘 모두 다 promise 객체이기 때문에 따로 promise 를 씌워주지 않아도 .then 메소드로 이어갈 수 있게 되죠.


Async / Await

Async / Await 은 비동기 함수를 사용하면서도 일반적인 동기 함수를 사용하는 것과 비슷한 구문 구조를 갖게끔 합니다.

mdn 공식 문서에서 Syntax 와 Descryption 그 다음으로 Examples 를 보여주고 있는데요. 아래의 코드가 공식문서에서 가져온 simple example 코드 예시입니다. 개인적으로 이 문서를 유심히 들여다보는 것이 async / await 을 이해하는 데 좀 도움이 되었던 것 같습니다.

var resolveAfter2Seconds = function() {
  console.log("starting slow promise");
  return new Promise(resolve => {
    setTimeout(function() {
      resolve(20);
      console.log("slow promise is done");
    }, 2000);
  });
};

var resolveAfter1Second = function() {
  console.log("starting fast promise");
  return new Promise(resolve => {
    setTimeout(function() {
      resolve(10);
      console.log("fast promise is done");
    }, 1000);
  });
};

var sequentialStart = async function() {
  console.log('==SEQUENTIAL START==');

  // If the value of the expression following the await operator is not a Promise, it's converted to a resolved Promise.
  const slow = await resolveAfter2Seconds();
  console.log(slow);

  const fast = await resolveAfter1Second();
  console.log(fast);
}

var concurrentStart = async function() {
  console.log('==CONCURRENT START with await==');
  const slow = resolveAfter2Seconds(); // starts timer immediately
  const fast = resolveAfter1Second();

  console.log(await slow);
  console.log(await fast); // waits for slow to finish, even though fast is already done!
}

var stillConcurrent = function() {
  console.log('==CONCURRENT START with Promise.all==');
  Promise.all([resolveAfter2Seconds(), resolveAfter1Second()]).then((messages) => {
    console.log(messages[0]); // slow
    console.log(messages[1]); // fast
  });
}

var parallel = function() {
  console.log('==PARALLEL with Promise.then==');
  resolveAfter2Seconds().then((message)=>console.log(message));
  resolveAfter1Second().then((message)=>console.log(message));
}

sequentialStart(); // after 2 seconds, logs "slow", then after 1 more second, "fast"
// wait above to finish
setTimeout(concurrentStart, 4000); // after 2 seconds, logs "slow" and then "fast"
// wait again
setTimeout(stillConcurrent, 7000); // same as concurrentStart
// wait again
setTimeout(parallel, 10000); // trully parallel: after 1 second, logs "fast", then after 1 more second, "slow"

가장 위의 두 함수는 promise 객체를 리턴하는 비동기 함수입니다. 이름에서 잘 설명하는 것처럼 첫 번째 함수(slow promise)는 setTimeout 을 활용해 2초 이후에 20 이라는 값을 resolve 로 리턴하고 있고, 두 번째 함수(fast promise)는 1초 후에 10 이라는 값을 resolve 로 리턴합니다.

그 아래 4개의 함수는 각각 async/await 의 활용, await 없는 async 의 활용, Promise.all 의 활용, 마지막으로 .then 메소드를 활용했을 경우에 어떻게 출력이 되는지를 보기 위한 함수입니다.

sequentialStart 는 async 와 await 을 둘다 활용하고 있죠. 이 경우에 작업의 순서는 먼저 slow promise 를 시작하고 그 결과로 20을 리턴한 이후 console.log 까지 마무리 되고 나서야 그 다음 비동기 함수를 실행합니다. 일반적인 동기 함수를 사용하는 것처럼 동작하게 되죠.

concurrent 함수는 제목처럼 비동기 함수 2개를 동시에(물론 순차적이긴 하지만 체감할 수 없는 속도로) 즉각적으로 실행합니다. fast promise 는 1초 만에 작업이 마무리되기 때문에 당연히 slow 보다 먼저 작업이 종료됩니다. 하지만 console.log 에서 await 을 사용하고 있기 때문에 fast 가 먼저 끝났음에도 불구하고 slow 가 끝나기를 기다려 slow 를 먼저 console.log 로 찍어준 이후에 await fast 를 이어서 console.log 로 찍어줍니다.

stillConcurrent 함수는 async / await 대신 Promise.all 을 사용하고 있습니다. 모양은 조금 다르지만 이 함수는 바로 위의 concurrent 함수와 같은 방법으로 동작합니다.

마지막으로 parallel 함수는 .then 으로 2 개의 비동기 함수를 각각 실행하고 있습니다. 이 경우에는 fast promise 가 먼저 끝나고 바로 그 값을 리턴하고, 그 이후에 slow promise 가 끝나면서 그 값을 이어서 리턴합니다.

비동기 함수 그 자체에 await 을 씌우게 되면 사실상 동기적으로 작동하는 것과 다를 바 없어지기 때문에, sequentialStart 에서처럼 불필요한 지연이 발생할 수 있을 겁니다. 그렇다고 parallel 함수처럼 활용하게 되면 때로는 원하는 순서대로 작동하지 않고 작업이 끝나는대로 뒤죽박죽 될 수도 있겠죠. 만약 모든 작업이 비동기적으로 시작하되, 그 결과를 우리가 원하는 순서대로 동기적으로 보여주기를 원한다면 concurrent 또는 stilConcurrent 의 방식이 더 유리할 수 있습니다.


async / await 를 사용하면 일반적인 동기적 함수처럼 코드의 가독성이 올라간다는 장점이 있지만, 잘못 사용하게 되면 지연이 발생할 때 모든 작업이 멈춰버리는 문제가 발생합니다. 비동기의 장점을 충분히 활용할 수 없게 되는 것이죠. 따라서 이러한 다양한 용례들을 잘 확인해서 적재적소에 필요한 만큼 사용할 수 있어야겠습니다.

예전에도 이 부분을 안 봤을리는 없는데, 아마도 구문이 길다보니 지레 겁을 먹고 찬찬히 살펴보지를 않았던 것 같습니다. 다시 살펴보니 크게 어려운 내용은 아니었던 것 같아요. 천천히 읽어보시고 꼭 크롬 개발자 도구를 활용해서 결과가 어떻게 나오는지를 하나씩 직접! 살펴보시는 것을 추천합니다.

profile
iOS 개발자로 전직하기 위해 공부 중입니다.

0개의 댓글