[JavaScript] 비동기 처리에 대하여

change·2020년 10월 4일
0
post-thumbnail

이 포스트에서는 자바스크립트의 비동기 처리방식에 대하여 정리해 보려 한다.

비동기 방식에 관한 내용 설명에 앞서 먼저 동기식, 비동기식에 대한 개념을 잡고 가도록 하자.

동기식 (Synchronous) / 비동기식 (Asynchronous)

동기식 (Synchronous)

먼저 시작된 하나의 작업이 끝날 때까지 다른 작업을 시작하지 않고 기다렸다가 다 끝나면 새로운 작업을 시작하는 방식이다.
위 그림 Synchronous와 같이 작업이 직렬로 배치되어 실행되며 작업 실행의 순서가 확실히 정해져 있는 것을 동기식 처리라 부른다.

비동기식 (Asynchronous)

동기식 방식과는 다르게 먼저 시작된 작업의 완료 여부와는 상관없이 새로운 작업을 시작하는 방식이다.
위 그림 Asynchronous와 같이 작업이 병렬로 배치되어 실행되며 작업의 순서가 확실하지 않아 나중에 시작된 작업이 먼저 끝나는 경우도 발생한다.
이와 같은 방식을 비동기식 처리라 부른다.


자바스크립트 비동기 처리

자바스크립트에는 아래와 같이 3가지 비동기 처리방식이 있다.

  1. 콜백 함수 사용
  2. Promise
  3. Promise를 활용한 async/await

자바스크립트는 기본적으로 비동기적으로 동작을 한다.
개발하다 보면 비동기식으로 동작하는 부분이 동기적으로 동작해야 하는 경우가 생긴다.
이럴 때 비동기식 동작이 동기식으로 동작하도록 해주는 걸 비동기 처리라고 한다.

각 방식들을 하나씩 알아보도록 하자.

1. 콜백 함수 사용

  • 콜백 함수를 사용하지 않을 경우
// 콜백 함수
function printString(callbackParam) {
  console.log(callbackParam);
}

// 콜백 함수 호출
function callHello() {
  let value;
  
  console.log("Wait 3 sec.");
  console.log('waiting...');
  
  setTimeout(function() {
    value = 'Hello';
  }, 3000);
  
  return value;
}

// 실행
const r = callHello();
printString(r);
  • 결과
# 시작
Wait 3 sec.
waiting...
undefined
(3초 대기)
# 종료

위에서 말했듯 자바스크립트는 비동기적으로 동작하기 때문에 callHello()의 결괏값이 r로 전달되기 전에 printString(r)을 실행하게 되면서 undefined가 출력된다.
그리고 그 후 setTimeout()에 의해서 3초 대기 후 종료하게 된다.


  • 콜백 함수를 사용할 경우
// 콜백 함수
function printString(callbackParam) {
  console.log(callbackParam);
}

// 콜백 함수 호출
function printString(callbackParam) {
  console.log(callbackParam);
}

function callPrint(callback) {
  let value;
  
  console.log("Wait 3 sec.");
  console.log("waiting...");
  
  setTimeout(function() {
    value = "Hello";
    callback(value);
  }, 3000);
}

// 실행
callPrint(printString);
  • 결과
# 시작
Wait 3 sec.
waiting...
(3초 대기)
Hello
# 종료

콜백 함수를 사용하면 위 결과에서 볼 수 있듯이 setTimeout()에 의해 3초간 대기 후 콜백 함수로 전달된 printString()을 실행하여 Hello를 출력하게 된다.


그러나 위의 예제와 다르게 실제로 개발을 하다 보면 콜백 함수가 복잡해지는 경우가 많아진다.
소위 말하는 콜백 지옥(Callback hell)에 빠지는 경우가 생길 수 있다.

a(function (resultsFromA) {
  b(resultsFromA, function (resultsFromB) {
    c(resultsFromB, function (resultsFromC) {
      d(resultsFromC, function (resultsFromD) {
        e(resultsFromD, function (resultsFromE) {
          f(resultsFromE, function (resultsFromF) {
            console.log(resultFromF);
          })    
        })
      })
    })
  })
});

위 코드는 콜백 지옥의 극단적 예시
놀랍게도 업무 중에 이 정도의 콜백 지옥을 실제로 본 적이 있다...=

이렇듯 콜백 함수를 남용하게 되면 가독성에러 처리 등에서 불편함이 발생한다.
이를 해소하기 위해 ES6에서 비동기 처리의 새로운 방법으로 Promise 객체가 등장한다.


2. Promise

The Promise object represents the eventual completion (or failure) of an asynchronous operation and its resulting value.
(번역: Promise 개체는 비동기 작업의 최종 완료 (또는 실패)와 그 결과 값을 나타낸다.)
출처 MDN Web Docs

Promise는 자바스크립트 비동기 처리에 사용되는 객체이다.
이를 사용하면 비동기 작업이 종료된 이후의 성공 결괏값이나 실패에 대하여 처리할 수 있다.

  • Promise 처리 흐름

출처 MDN Web Docs

Promise는 다음 중 하나의 상태를 가진다.

  • 대기(pending): 이행하거나 거부되지 않은 초기 상태
  • 이행(fulfilled): 연산이 성공적으로 완료됨
  • 거부(rejected): 연산이 실패함

Promise 사용 방법

new Promise(function(resolve, reject) {
  // ...
});

new Promise() 객체를 생성하여 콜백 함수를 선언할 수 있는데 이때의 인자는 resolve, reject를 사용한다.
resolve는 결과가 성공인 Promise 객체를 반환하고 reject는 결과가 실패인 Promise 객체를 반환한다.

반환된 Promise 객체를 처리할 때 성공 시 then을 사용하고 실패 시 catch를 사용하여 처리한다.

백문이 불여일견!
복잡한 내용은 일단 생각하지 말고 성공 시에는 resolve, then 실패 시에는 reject, catch라는 것만 머리에 남겨둔 채로 다음 예시를 확인하여 개념을 잡도록 하자.

Promise 사용 예시

  • 성공 예시
let myFirstPromise = new Promise((resolve, reject) => {
  setTimeout(function(){
    // 성공 시 resolve 사용
    resolve(`Success!`);
  }, 3000);
});

// 성공 시 then 사용하여 결과 처리
myFirstPromise.then((successMessage) => {
  console.log(`Yay! ` + successMessage);
});
  • 결과
# 시작
(3초 대기)
Yay! Success!
# 종료

  • 실패 예시
let myFirstPromise = new Promise((resolve, reject) => {
  setTimeout(function(){
    // 실패 시 resolve 사용
    reject(new Error(`Fail!`));
  }, 3000);
});

// reject를 사용하여 Promise 객체를 반환하기 때문에 then은 실행되지 않는다.
myFirstPromise.then((successMessage) => {
  console.log(`Yay! ` + successMessage);
})
// 실패 시 catch 사용하여 결과 처리
.catch((reason) => {
  console.log('여기서 거부된 프로미스( ' + reason + ' )를 처리하세요.');
});
  • 결과
# 시작
(3초 대기)
여기서 거부된 프로미스( Error: Fail )를 처리하세요.
# 종료

예시 코드를 직접 실행해보면 위의 복잡한 설명을 이해하는 데 도움이 될 것이다.


그렇다면 Promise 방식이 콜백 함수보다 좋은 점이 무엇일까?

바로 위에서 말했던 콜백 지옥을 해결할 수 있는 방식이기 때문이다.

function a() {
  return new Promise({
    // ...
  });
}

function b() {
  return new Promise({
    // ...
  });
}

function c() {
  return new Promise({
    // ...
  });
}

myFirstPromise()
.then(a)
.then(b)
.then(c);

콜백 함수를 사용했으면 훨씬 길어지고 복잡해졌을 코드도 위 예시와 같이 Promise 객체를 반환하면 여러 개의 then을 연결하여 간단히 처리할 수 있다.

비동기 처리할 때 콜백 함수를 사용하는 것보다는 이번 기회에 Promise 객체를 사용하는 법을 익히도록 하자.
이 내용은 아래에서 나올 async/await를 이해하는 데에 꼭 필요한 내용이기 때문에 꼭 익혔으면 한다.


3. Promise를 활용한 async/await

ES2017에 새로 추가된 async/await는 위에서 알아봤던 Promise 객체를 기반으로 사용한다.
Promise 설명의 마지막에서 말했듯이 Promise 대신 쓰는 새로운 방식이 아니라 Promise를 다른 방식으로 사용하는 내용이기 때문에 Promise에 대한 개념을 익히고 와야 이해할 수 있다.

Promise에 대하여 충분히 이해됐다면 함께 async/await를 알아보도록 하자.


기존 Promise와의 차이점

가장 큰 차이점은 위에서 배웠던 resolve, reject, then, catch를 쓰지 않는다는 것이다.

아니! 위 내용 익히고 오라더니 열심히 공부하고 오니까 안 쓴다고요?????????

사실 async/await만 사용하려면 저 4가지는 어떻게 사용했는지 잊어버려도 된다. ~~죄송합니다...~~

하지만 위에서 알아본 Promise 객체에 대한 내용을 전혀 모른 채로 왔다면 async/await를 이해하는 데에 더 머리가 아팠을 것이다.

이미 충분히 개념을 익혀온 여러분은 아주 짧은 설명만으로도 async/await를 이해하고 사용하는 데에 어려움이 없을 것이다!

바로 알아보자!


async

  • async 사용 예시
// async 키워드만 붙이면 된다.
async function hello() {
  return 'Hello';
}

function callHello() {
  const r = hello();
  console.log(r); 
}

callHello();
  • 결과
# 시작
Promise { 'Hello' }
# 끝

놀랍게도 위에서 사용했던 new Promise()고 뭐고 아무것도 없다.
그냥 기본 메소드 앞에 async만 붙이면 반환되는 값이 Promise 객체가 된다.
Promise 객체가 무엇인지는 이미 익히고 온 여러분은 이 간단한 예시만으로 async를 어떻게 쓰는지 이해가 다 됐을 거라 생각된다.

그런데 우리는 위 예시처럼 객체 형태의 Promise { 'Hello' }가 아니라 그 안에 있는 String 형태의 Hello를 출력하고 싶다.

어떻게 해야 할까??
다음 설명을 보도록 하자.


await

이제 우리는 async를 어떻게 쓰는지 또 어떤 결과가 나오는지는 알고 있다.
이번엔 그 결과를 어떻게 내가 원하는 방식으로 사용할지를 알아보도록 하겠다.

  • await 사용 예시
async function hello() {
  return 'Hello';
}

// (2) 새로 추가된 async 키워드
async function callHello() {
  // (1) 새로 추가된 await 키워드
  const r = await hello();
  console.log(r);
}

callHello();
  • 결과
# 시작
Hello
# 끝

자 달라진 내용을 살펴보자.
먼저 (1) 부분을 보면 hello(); 앞에 await 키워드가 추가됐다.
await가 붙으면 반환된 Promise 객체에서 실제 hello() 메소드의 반환 값인 String 값을 뽑아낸다고 보면 된다.
그렇다면 위 예시의 r 값에 String 형태의 Hello가 입력될 것이다.
출력 결과를 보면 await가 붙기 전이랑 다르게 r을 출력하면 문자열만 출력되는 걸 확인 할 수 있다.

다음은 (2) 부분을 보면 callHello() 메소드에도 async가 추가됐다.
await 키워드는 async 키워드가 붙은 메소드에서만 사용할 수 있다.
만약 async가 없는 메소드에서 await를 사용한다면 아래와 같은 SyntaxError가 발생한다.


결과 처리방법

위 내용은 굉장히 쉬워서 이해하는 데에 어려움이 없었을 것 같다.

하지만 아직 끝난 게 아니다.

이제 우리는 성공과 실패에 대해 처리를 해서 마무리를 지어줘야 하는데 기존 Promise 방식으로 생각하면 then, catch 부분을 처리해줘야 한다.
당연히 이 부분도 훨씬 쉽다.

위에서 사용한 예시를 활용하여 바로 알아보자!

  • 성공 처리 예시
async function hello() {
  return 'Hello';
}

async function callHello() {
  // 새로 추가된 try 키워드
  try {
    const r = await hello();
    console.log('성공: ' + r);
  } catch (e) {
    console.log('실패: ' + e.message);
   }
}

callHello();
  • 결과
# 시작
성공: Hello
# 끝
  • 실패 처리 예시
async function hello() {
  throw new Error(`Fail!`);
}

async function callHello() {
  try {
    const r = await hello();
    console.log(r);
    // 새로 추가된 catch 키워드
  } catch (e) {
    console.log(e.message);
   }
}

callHello();
  • 결과
# 시작
실패: Fail!
# 끝

기존 then과 catch는 각각 try-catch로 바뀌었다.
위 예시와 같이 await를 붙인 메소드의 결과가 성공일 경우는 try 부분이 실행되고 실패일 경우는 catch 부분이 실행된다.
이 내용은 기존 C언어나 Java언어 등에서 사용하는 try-catch 개념과 비슷하기 때문에 별다른 설명 없이도 충분히 이해가 될 거라 생각한다.

여기까지 자바스크립트의 3가지 비동기 처리 방법에 대해서 알아봤다.


마치며...

이번 포스팅은 회사 업무와는 별개로 개인적으로 공부했던 내용에 대해 다시 정리해봤다.
내가 처음 접했을 때 어려웠던 부분을 좀 더 자세히 설명하고 공부하며 들었던 생각의 흐름을 따라서 글을 쓰다 보니 다시 한번 복습하는 느낌이 들었다.

확실히 남에게 가르치면서 더 배운다는 말이 맞는 거 같다.

최대한 예제를 사용해서 이해하기 쉽도록 작성했는데 나에게만 쉬운 건 아닌지 모르겠다.
더 자세하게 쓰려고 하면 너무 길어질 것 같아 예제와 간단한 설명으로 마무리를 지었다.

0개의 댓글