Promise, async/await

정비호·2023년 11월 14일

테크톡 발표

목록 보기
2/2

이번 글에선 제 첫 테크톡 발표였던 Promise, async/await에 관한 발표 내용에 대해서 정리 해보겠습니다.
본격적인 내용을 설명하기에 앞서 몇가지 내용을 짚고 가겠습니다.

1. 동기/비동기, 콜백함수

JavaScript의 특성

  • 자바스크립트는 동적인 언어이다.(hoisting이 된 이후 부터 코드를 한 줄씩 읽어 내려감.)

hoisting이란?

  • var, 함수 선언 등등이 자동적으로 제일 먼저 실행되는 것.
    위를 보면 함수를 선언하기 이전에 함수 abc를 호출해도 정상적으로 잘 작동하는 모습을 볼 수 있습니다.

동기(synchronous)

  • 우리가 코드를 작성한 순서대로 실행이 됨.

비동기(asynchronous)

  • 코드가 작성한 순서대로 실행이 되지만, 동기와 다르게 각 코드의 연산 결과를 기다리지 않고 다음 코드를 바로 실행.

콜백 함수가 필요한 이유

  • 콜백은 특정 코드가 끝나기 전까지는 다른 코드가 실행되지 않게 하기 위해 쓰인다. (비동기 처리 방식의 문제점을 해결하기 위함.)
  • 필요한 예시 중 하나로는 Ajax 통신

    만약 우리가 data를 받아오고 그것을 return 해주려고 하는데, 단순하게 data를 받고 그것을 바로 return 해주는 식으로 작성하게 되면 data를 다 받아오지 못한 상태에서 return을 하기 때문에 undefined가 출력되게 됩니다. 이런 문제들을 콜백을 통해 해결할 수 있습니다.

  • 콜백 함수의 동작 방식은 식당 예약과 같다.

    웨이팅이 있는 맛집을 찾아갔다고 가정을 해보겠습니다. 그 식당에서 식사를 하기 위해선 웨이팅을 걸게 될 것이고 우리는 그 시간동안 식당 주변을 걸으면서 다른 행동을 할 수도 있고, 혹은 또 다른 식당을 찾아서 들어갈 수도 있습니다. 그러다가 가게에서 웨이팅이 전화가 옵니다.(이 전화를 받는 시점이 콜백함수가 호출되는 시점)

하지만 이러한 콜백함수의 문제점이 있습니다.보기만 해도 어질어질 해지는 가독성과 과도한 들여쓰기, 그리고 단순히 읽는 순서로 보았을 때 호비정 이라고 출력될 것이라는 예측과 정반대로 정비호 라고 출력되는 기괴한 구조까지 갖추게 됩니다.
이러한 콜백 지옥🔥 을 이제부터 Promise를 알아 가면서 개선 해봅시다.

2. Promise

Promise?

  • JavaScript에 내장된 Object다.

Promise의 State?

  • Pending(대기) : 처리가 이행하지도, 거부 되지도 않은 상태
  • Fulfilled(이행) : 연산이 성공적으로 완료됨
  • Rejected(거부) : 연산이 실패함

이번엔 executor(실행자)에 대해 알아봅시다.

Executor(실행자)

  • executornew Promise에 전달되는 함수이다

    executor는 새로운 Promise 인스턴스 객체가 생성될 때, 자동으로 실행이 됩니다.
    인수로 resolve, reject를 받는데, resolve, reject는 자바스크립트 내부에서 자체제공하는 콜백함수입니다. 늦든 빠르든 executor 내에서 인수로 넘겨준 콜백중 하나는 반드시 호출해야 합니다.

  • resolve : 기능을 정상적으로 수행해서 마지막에 최종 데이터를 전달하는 콜백함수
  • reject : 기능을 수행하다 문제가 생기면 호출하게 될 콜백함수.

.then, .catch, .finally

아까의 Executor가 생산자의 관점이었다면, 이 부분 부터는 소비자의 관점으로 보면 되겠습니다.

  • .then : resolve를 통해 성공적으로 연산 처리 시, 결과값을 받을 수 있음.
    • .then은 우리가 요청한 것이 성공적으로 이행이 되어서, resolve호출을 통한 결과값을 .then을 통해 받을 수 있습니다.
  • .catch : reject를 통해 연산 실패 시, 실패 처리 결과값을 받을 수 있음.
    • 반대로 연산 실패시 reject를 호출해 반환한 error값을 .catch를 통해 전달 받을 수 있습니다.
  • .finally : 연산의 성공 실패 여부 상관 없이 무조건적으로 실행됨.
    • 이와 다르게 finally는 성공 실패 여부와 상관 없이 무조건 적으로 실행이 됩니다.

먼저 첫번째 코드는 resolve를 통해 "성공"이라는 값을 반환했고, Promise의 State는 Fulfilled 상태가 됩니다.
.then을 통해 받은 value값은 console.log로 찍어보면 "성공"이 정상적으로 출력되고 .catchresolve를 호출했기 때문에 실행되지 않습니다.
finally는 말씀드렸다 싶이 성공실패 여부와 상관없이 무조건 실행되기 때문에 "아무튼 실행" 또한 같이 출력되는 것을 볼 수 있습니다.


반대로 두번째 코드는 reject를 통해 "실패"라는 값을 반환했고, Promise의 State는 Rejected상태가 됩니다. .then은 무시당했고 .catch를 통해 console.log(err)가 실행되어 "실패"라는 값을 출력했습니다. 마찬가지로 .finally는 "아무튼 실행"이라는 문자열을 출력합니다.

Promise에서 resolve, reject / return

  • resolve와 reject는 동시에 호출이 불가능 합니다. (더 먼저 작성된 콜백함수가 실행됨.)
  • Executor 내에서의 return 은 무시됨 (pending 상태)

왜 Promise(비동기 처리)를 사용할까?

  • 비동기적인 것을 수행할 때 콜백함수 대신 유용하게 사용할 수 있음. (콜백지옥 해결, 간결한 코드 작성)
  • 네트워크 통신에서 동기적으로 코드 처리 시, 시간이 너무 오래 걸림
function fetchUser() {
  // 10초동안 네트워크에서 데이터를 받아오는 코드
  return "UserDate"
  
const user = fetchUser();
console.log(user);
// 웹페이지 UI 표시 코드들
}

예시 코드에서 fetchUser은 네트워크에서 데이터를 약 10초동안 받아오고 그 data 값을 return 해주는 data라고 가정해봅시다. 그리고 아래에는 웹 페이지의 UI를 표시해주는 코드들이 나열되어 있습니다.
만약 우리가 이 코드를 동기적으로 처리하게 되면, 데이터를 받아오는 10초동안은 아래에 블럭에 있는 코드들은 실행이 되지 않습니다.
그렇다면 웹페이지를 이용하는 유저의 입장에서는 저 데이터를 받아오는 10초 동안은 텅텅빈 웹페이지만 보고 있어야 하는 상황이 발생하게 됩니다.
그래서 이렇게 처리가 오래 걸리는 코드들은 Promise를 이용한 비동기 처리를 해줘야 합니다.

코드 개선

먼저 myName("정")을 실행하면 파라미터 name에 값이 담깁니다.
그리고 Promise 내부의 코드를 살펴보면, 초기값으로 "정"이 들어왔고 현재 prevName은 존재하지 않는 상태이기 때문에 name값을 그대로 myName에 담고 resolve를 통해 myName을 반환합니다.
그리고 return (prevName) 을 만나서 myName값은 prevName에 담겨서 반환하게 됩니다.
같은 형식으로 .then으로 값을 받고 myName에 "비"를 집어넣습니다. 현재 prevName에는 "정"이라는 값이 담겨있기 때문에 ${prevName}${name}이라는 값이 myName에 담기게 됩니다.
여기서 prevName은 당연히 "정"이고 name은 "비"가 됩니다.
마지막으로 "호"를 집어넣으면 같은 형식으로 "정비"에 "호"가 붙어서 "정비호"가 출력되게 됩니다.

위 아래 두 코드는 1장에서 다뤘던 콜백지옥의 예시 코드를 Promise로 변환한 코드들입니다.

위쪽의 경우 코드의 길이는 좀 더 길어지긴 했지만 값이 전달되는 순서 또한 제대로 보여지고 있고, 아까처럼 들여쓰기가 깊어지는 현상 또한 없습니다.

밑의 코드는 Closure를 도입해서 더 간결하게 표현한 코드인데, 클로저에 대해서는 이 게시글에서 다루지 않겠습니다.

3. async/await

async

이제는 async/await에 관한 주제를 다뤄 보겠습니다. 2번째의 코드는 Promise를 async 함수로 변환한 코드입니다. 코드가 좀 더 간결해졌고 결과값 또한 같은 것을 알 수 있습니다.
async라는 Promise가 아닌 값을 반환하더라도 이행(resolve) 상태의 Promise로 값을 감싸 이행된 Promise가 반환되도록 합니다.

await

한 예시 코드입니다. 여기서 await에 대한 설명을 해보겠습니다.
awaitasync 함수 안에서만 동작합니다. 자바스크립트는 await 키워드를 만나면 Promise가 처리될 때까지 기다립니다 결과는 그 이후 반환됩니다. await의 좀 더 자세한 동작에 대한 설명은 EventLoop를 다룬 제 다른 글인 이벤트 루프 에서 설명을 볼 수 있습니다.
이제 코드를 살펴보면 가장 먼저 delay라는 함수가 선언 되어 있습니다. 이 함수는 시간을 받아서 setTimeOut를 통해 해당 시간만큼 기다렸다가 resolve상태를 반환하는 함수입니다.
그리고 각각 getApple, getbananadelay함수에 인수로 1000을 넘겨주고 apple, banana라는 string값을 반환해주는 함수입니다.
그리고 마지막으로 pickFruits()getApple, getBananaawait 키워드를 통해 호출하고 해당 함수들의 반환값을 담은 변수들을 문자열로 합쳐서 반환해줍니다.

사실 단순히 생각해보면 문제가 없는 코드로 보이고 await 키워드는 정말 좋구나! 라고 생각할 수도 있겠지만, 이 코드에선 하나의 오류가 있습니다.
일단 getApplegetBanana의 경우엔 서로 참조하는 값이 전혀 없습니다.
그 말인 즉슨, 서로가 없어도 서로의 함수가 동작하는데 아무런 문제가 없다는 뜻입니다. 쓸데없이 각각 1초씩 기다리면서 결과값을 반환받을 이유가 없다는 뜻이죠.
이러한 문제를 해결하는 방법이 하나 있습니다.

Promise.all

바로 Promise.all을 이용해 각 로직을 병렬적으로 처리해주는 것입니다.Promise.all이라는 메서드를 이용하면 배열 내에 있는 메서드들을 병렬적으로 처리하고 배열에 담아서 반환 해줍니다. 실로 꽤나 유용한 기능이라고 할 수 있죠.
그리고 반환받은 각 string 값을 join메서드로 + 라는 구분자와 함께 합쳐줍니다. 이렇게 하면 1초만에 기존 코드와 같은 값인 apple + banana를 반환 받을 수 있습니다. Promise.all에 대한 내용은 공식 문서를 통해 더 자세히 알아보시는걸 추천드립니다.


그렇다면 여기서 들 수 있는 다른 한가지 의문은 await을 그냥 안쓰면 되는거 아닌가? 라는 생각이 들 수도 있는데, 이에 대한 의문은 다음 사진을 보면 바로 사라지게 됩니다.
await의 또 다른 기능은 .then처럼 뒤에 오는 값이 Promise 객체의 값이면 Promiseresolve값으로 바꿔줍니다.

await이 필요한 예제

개발을 시작한지 얼마 안됐을 때에 작성한 코드라 camelCase에 대한 숙지가 부족했어서 불편한 점 이해 부탁 드립니다..ㅠㅠ🥲

위 코드에서 만약 await이 없는
const result = PostCRUD.postCreate(post);
로 코드를 작성한다면, 2번째 사진과 같은 에러가 발생합니다. 이유는 당연히 함수의 실행을 기다리지 않고 바로 해당 함수의 return값을 담은 변수를 참조하는 코드가 실행 되었기 때문입니다.
그로 인해 result의 0번 인덱스에 insertId라는 프로퍼티를 받아오지 못해서 에러가 발생합니다.
이제 Promise에서 콜백 지옥을 개선한 코드를 async/await를 적용해서 보여 드리겠습니다.
이전 보다 더 깔끔해진 것을 확인할 수 있습니다.
마지막으로 제가 해당 내용으로 발표한 영상의 링크를 첨부하고 글을 마치겠습니다. 감사합니다!
(따끔한 지적 환영합니다!🙇)

https://www.youtube.com/watch?v=VuJEuEL8WuA&t=4s

profile
잘하고 싶은 개발자

0개의 댓글