공부 및 일상기록

동기와 비동기 그리고 Promise와 async await 본문

개발/Javascript

동기와 비동기 그리고 Promise와 async await

낚시하고싶어요 2023. 5. 3. 17:24

한번 정리를 했던 내용이지만 다시한번 상기시키고 스토리식으로 재정리하여 보다 강력하게 뇌리에 박아두고 싶어서 다시 작성하게 되었다. 

 

동기와 비동기

 

동기적인 코드 실행은 순차적으로 실행되며, 이전 작업이 끝나야만 다음 작업을 수행하게 된다. 

const arr = [1, 2, 3, 4, 5];

console.log("Start");

arr.forEach(function(num) {
  console.log(num);
});

console.log("End");

결과는 아래와 같이 순서대로 나온다!
Start
1
2
3
4
5
End

 

반면 비동기적인 코드 실행은 순차적으로 실행되지 않으며 비동기 함수를 호출하면 해당 함수가 완료되지 않았어도 다음 작업을 수행할 수 있게 된다. 

console.log("Start");

setTimeout(function() {
  console.log("Hello, world!");
}, 1000);

console.log("End");

결과는 Start와 End가 찍히고 코드실행 1초뒤에 Hello, world!가 찍힌다!
Start
End
Hello, world!

 

비동기 함수는 보통 콜백함수나 프로미스를 사용하여 작업이 완료될 때 결과를 반환하게 된다. 이를 통해 동기적인 코드 실행보다 더욱 유연하고 빠른 코드 실행이 가능해진다.

 

동기적인 코드 실행은 코드가 직관적이고 이해하기 쉽지만, 시간이 오래 걸리는 작업을 수행할 때, 전체 어플리케이션 속도가 느려지는 문제가 발생할 수 있다. 반면 비동기적인 코드 실행은 복잡하기 이해하기 어렵지만, 전체적인 어플리케이션 성능을 향상시킬 수 있다.


비동기를 사용하는 이유

위에서 설명했듯이 비동기적인 코드 실행은 어플리케이션 성능 향상에 도움이 된다.

만약 서버로부터 데이터를 받아와서 화면에 표시하는 작업을 수행한다고 가정해보자.

이 작업은 서버에서 데이터를 가져오는 동안 브라우저가 다른 작업을 수행할 수 있어야 한다. 그렇지 않으면 화면이 멈추거나, 사용자 입력에 대한 반응이 늦어지는 등의 문제가 발생할 수 있다.

 

따라서 서버에서 데이터를 가져오는 함수는 비동기 함수이고, 해당 비동기 함수의 작업이 완료되면 그 시점에 결과를 반환하게 된다.

 

비동기 코드가 동작하는 순서

1. 비동기 함수를 호출한다. 이 때, 비동기 함수는 즉시 실행되지만, 작업이 완료될 때까지 다른 작업을 수행할 수 있게 한다.

2. 비동기 함수는 Promise객체나 콜백 함수를 반환한다.

3. 비동기 작업이 완료될 때까지 기다리거나, 다른작업이 있다면 다른작업을 먼저 수행한다.

4. 비동기 작업이 완료되면 Promise객체나 콜백함수가 호출된다. 이때, 완료된 결과를 인자로 받아 처리한다.

5. 결과를 처리하고 다음 작업이 있다면 다음 작업을 수행한다.

 

그럼 내가 서버로부터 받아온 데이터를 반환 받으면 어떤 방법으로 이 데이터를 사용해야 할까? 데이터를 이용하여 작업을 수행하려면 함수가 있어야하는데, 데이터를 받은 시점에 맞게 실행할 수 있는 방법은 무엇일까?

 

이처럼 비동기함수의 결과를 처리하는 방법에는 크게 두가지가 있다.

 

첫 번째, 콜백 함수를 사용한다. 비동기 함수를 호출할 때 콜백함수를 인자로 전달하면, 비동기함수가 작업을 완료한 후 콜백함수를 호출한다. 이를 통해 비동기 함수가 작업을 처리하는 동안 다른 작업을 수행할 수 있다.

 

두 번째, Promise를 사용하는 방법이다. Promise는 비동기 작업의 상태를 나타내는 객체이다. Promise를 사용하면 비동기 작업이 완료될 때 성공결과 혹은 실패결과를 반환한다. 이를 통해 비동기 작업의 상태를 관리하고, 코드의 가독성과 유지보수성을 향상시킬 수 있다. 

 

ES6부터는 async/await 구문을 사용하여 비동기 함수를 처리할 수 있다. 이는 새로운 방법이 아닌 Promise를 기반으로 동작하는 것이다. async키워드를 사용하여 함수를 정의하면 해당 함수는 비동기 함수가 되고, 이후에 await 키워드를 사용하여 Promise가 완료될 때까지 대기한다. 이를 통해 코드 가독성을 더욱 향상시킬 수 있다.

 

콜백함수의 사용

간단하게 비동기 함수를 이용해 ATM에 가서 카드를 넣고 돈을 뽑는 과정을 1초 간격으로 콘솔에 찍는 비동기 함수를 만들어 보자

const ATM가기 = (callback) => {
  setTimeout(() => {
    console.log("ATM도착");
    callback();
  }, 1000);
};
const 카드넣기 = (callback) => {
  setTimeout(() => {
    console.log("카드넣기");
    callback();
  }, 1000);
};

const 돈뽑기 = () => {
  setTimeout(() => console.log("돈뽑기 성공!"), 1000);
};

ATM가기(() => 카드넣기(() => 돈뽑기()));

//코드실행하고 1초 경과
ATM도착

//ATM도착 후 1초경과
카드넣기

//카드넣기 후 1초경과
돈뽑기 성공!

ATM가기() 함수는 1초를 대기했다가 setTimeout함수 안에 콜백함수를 실행하게 된다. 콜백 함수 내부에는 ATM도착을 찍어주는 콘솔로그와 인자로 받은 콜백함수가 있다. 

해당 함수를 실행하기 위해

ATM가기(() => 카드넣기(() => 돈뽑기()));

이러한 알아보기 힘든 구조로 콜백함수를 전달했다. 겨우 세개의 비동기 함수를 실행했는데 알아보기 힘들다. 이렇게 콜백함수를 사용해서 알아보기 힘들고 복잡해져버린 구조를 콜백지옥 이라고 부른다.

 

Promise 사용

위에서 보았듯 코드의 가독성과 유지보수성이 매우 떨어지는 콜백 함수를 이용하는 것보다 조금 더 직관적으로 볼 수 있는 방법이 Promise객체를 이용하는 것이다. 일단 먼저 코드를 보고 Promise를 이해해보자

const ATM가기 = () => {
  return new Promise((resolve) => {
    setTimeout(() => {
      console.log("ATM도착");
      resolve();
    }, 1000);
  });
};

const 카드넣기 = () => {
  return new Promise((resolve) => {
    setTimeout(() => {
      console.log("카드넣기");
      resolve();
    }, 1000);
  });
};

const 돈뽑기 = () => {
  return new Promise((resolve) => {
    setTimeout(() => {
      console.log("돈뽑기 성공!");
      resolve();
    }, 1000);
  });
};

ATM가기()
  .then(() => 카드넣기())
  .then(() => 돈뽑기());

결과는 콜백때와 같으므로 쓰지 않았다.

각 함수를 먼저 보면 setTimeout함수를 new Promise로 감싸서 Promise객체로 만들었다. 

그리고 함수호출 부분을 보면 ATM가기()를 실행하고 해당 비동기 작업이 완료되면 .then()을 통해 다음 수행할 함수를 콜백함수를 이용하여 넣은 것을 볼 수 있다.

 

이처럼 콜백함수를 사용하는 것보다 프로미스 객체를 사용하면 가독성과 유지보수성이 좋게된다.

 

Promise의 이해

Promise는 자바스크립트에서 비동기 작업을 처리하는데 사용되는 객체이다. 

Promise는 다음과 같은 세 가지 상태를 가질 수 있다.

1. 대기중(pending) : 아직 비동기 작업이 수행되지 않은 상태

2. 이행(fulfilled) : 비동기 작업이 성공적으로 완료된 상태

3. 거부(rejected) : 비동기 작업이 실패한 상태

 

Promise 객체는 다음과 같은 구조로 생성된다.

new Promise((resolve, reject) => {
  // 비동기 작업 수행
});

위 코드에서 new Promise함수에는 비동기 작업을 처리하는 콜백 함수가 전달된다. 이 콜백함수는 두개의 매개변수 resolve와 reject를 가지며 비동기 작업이 성공했을 때는 resolve함수를 호출하게 되고, 실패하면 reject함수를 호출하게 된다.

 

resolve함수는 Promise객체를 이행상태로 변경하고, 결과값을 반환한다. reject함수는 Promise객체를 거부상태로 변경하고 에러를 반환한다.

 

Promise객체는 다음과 같은 메서드를 가진다.

  • .then() : Promise 객체가 이행 상태가 되었을 때 수행할 작업을 등록한다.
  • .catch() : Promise 객체가 거부 상태가 되었을 때 수행할 작업을 등록한다.
  • .finally() :  Promise 객체가 이행 상태나 거부 상태가 되었을 때 항상 수행할 작업을 등록한다.

 

async await 를 이용한 방법

그렇다면 Promise에도 과연 단점이 있을까?

콜백함수를 사용할 때 보다는 덜하지만 이 역시도 .then 체이닝이 반복되면 then지옥에 빠진다고 표현한다. 사실 나는 아직 그정도의 문제점을 겪어보진 못해서 더 많은 설명을 적는게 오류를 범할 수 있어서 바로 본론으로 들어가본다..

 

아래는 async await 구문을 사용한 예제이다.

const ATM가기 = () => {
  return new Promise((resolve) => {
    setTimeout(() => {
      console.log("ATM도착");
      resolve();
    }, 1000);
  });
};

const 카드넣기 = () => {
  return new Promise((resolve) => {
    setTimeout(() => {
      console.log("카드넣기");
      resolve();
    }, 1000);
  });
};

const 돈뽑기 = () => {
  return new Promise((resolve) => {
    setTimeout(() => {
      console.log("돈뽑기 성공!");
      resolve();
    }, 1000);
  });
};

const 돈뽑기_전체과정 = async () => {
  await ATM가기();
  await 카드넣기();
  await 돈뽑기();
};

돈뽑기_전체과정();

가독성 측면에서만 봐도 정말 확연히 좋아졌다고 생각한다.

 

async await 구문의 사용 방법은 다음과 같다.

1. Promise객체를 반환하는 함수를 선언할 때 async 키워드를 사용한다.

2. Promise객체를 사용하는 코드 앞에 await키워드를 붙인다.

 

await키워드를 사용하면 Promise객체를 반환하는 함수의 실행을 기다릴 수 있다.

 

만약 위 구문에서 에러를 캐치하고싶다면 반드시 try/catch문을 사용해야한다.

const 돈뽑기_전체과정 = async () => {
  try {
    await ATM가기();
    await 카드넣기();
    await 돈뽑기();
  } catch (error) {
    console.error(error);
  }
};

 

그렇다면 이렇게 좋아보이는 async await에도 단점이 있을까? 단점은 다음과 같다.

 

1. async await 구문은 ES8부터 지원되므로 이전 버전의 브라우저에서 사용이 불가능 하다.

2. async await 구문은 Promise객체를 기반으로 하기 때문에 Promise객체를 반환하지 않는 함수에서는 사용할 수 없다.

 

 

'개발 > Javascript' 카테고리의 다른 글

매개변수와 인자의 차이  (0) 2023.07.20
호이스팅이란?  (0) 2023.07.20
[Typescript] 컴파일 세부설정 (tsconfig.json)  (0) 2023.01.25
[Javascript] 콜스택과 힙  (0) 2023.01.17
[Javascript] 자바스크립트 클래스  (0) 2023.01.17