자바스크립트 콜백함수와 비동기 프로그래밍

휴먼스케이프

콜백함수와 비동기 프로그래밍이 어떻게 동작하는지를 알아봅니다.

[코어자바스크립트 지난편]

데이터와 타입

실행 컨텍스트

this

이번에는 책 4장 [코어 자바스크립트]의 콜백함수에 대해 알아볼 차례입니다. 책의 콜백함수에 대한 간략한 설명과 더불어 비동기 프로그래밍에서의 콜백함수와 문제점, 이에 대한 해결방안으로 나온 ES6 문법들에 대하여 살펴보려고 합니다.

콜백함수

call(호출하다) + back(뒤돌아오다) 라는 의미가 포함된 콜백함수는 , 다른 코드의 인자로 넘겨주는 함수로, 특정조건일때 해당 함수를 실행해달라 라는 의미를 전달하는 것입니다.

다른 코드의 인자로 콜백함수를 전달하면서, 그 제어권도 전달된 코드에 위임을 하게 됩니다. 예를 들어 map 함수에 콜백함수를 넘겨주면, 콜백함수의 첫번째 인자에는 현재 순회값, 두번째 인자에는 인덱스가 들어갑니다. setInterval 안에 콜백함수를 넣어주게 되면 setInterval 실행에 따라 시간만큼 콜백함수가 반복되게 됩니다. 또한 콜백함수의 this도 넘겨진 코드에 따라 달라지는데, 예를 들면 다음과 같습니다.

document.body.querySelector('#a')
  .addEventListener('click', function(e) {
    console.log(this, e);
  });
document.body.querySelector('#a').click(); // HTML element

addEventListener는 콜백 함수를 호출할 때 call 메서드의 첫번째 인자에 addEventListener의 this를 그대로 넘기도록 정의되어 있기 때문에 this가 호출한 주체인 HTML Element를 가리키게 되는 것입니다.

이러한 콜백함수를 통한 코드의 흐름 제어는 비동기 상황이 복잡해지면 다음과 같이 작성이 될 수도 있습니다.

출처: Asynchrony: Under the Hood — Shelley Vohr — JSConf EU

보통 top-down으로 읽어내려가는 방식이 익숙하기 때문에, 어떤 부분이 어떤 순서로 진행되는지 한눈에 파악하기가 어렵습니다. 또한 상황에따라 콜백함수 내에서 상황에 따라 에러를 던지기도 하고, 에러를 출력하기도 하고 해야하는데 이런 형태이면 스코프를 파악하기도 까다로워 에러 핸들링도 어려워집니다.

프로미스의 등장

ES6에서는 이러한 콜백함수를 동기적인 형태로 코드를 작성하게끔 도와주는 Promise가 등장했습니다. Promise는 비동기 작업이 이행되었는지, 실패되었는지에 따라 .then, .catch로 콜백함수를 받아 각각의 경우를 실행시킵니다. 따라서 비동기 함수를 다룰 때 위에서처럼 콜백함수만을 사용하는 것보다 동기적인 흐름에 따라 코드가 읽힐 수 있도록 도와줍니다.

const p = Promise.resolve('hello');
p.then((value) => {
  console.log(value);
  return `%{value} world`;
}).then((newVal) => {
  console.log(newVal);
});

이 Promise가 자바스크립트 엔진 내에서 어떻게 동작하는지 잠깐 살펴보자면, 다음과 같이 Microtask Queue를 사용합니다.

출처: https://dev.to/lydiahallie/javascript-visualized-promises-async-await-5gke?fbclid=IwAR3cfIk3iVpt1EoFOflRVs4VFe6GC2m2nbkP99bWgSduAkxVCIFSXVgKYzE#tasks

setTimeout, setInterval과 같은 Web Api에 받아지는 콜백은 태스크 큐(macro task queue)에 들어가 해당 타이머실행이 끝나고, 콜스택이 비어지면 이벤트 루프에 의해 콜스택으로 불려져 실행됩니다. (이에 대해 더 알고싶으시다면 이 영상을 참고하시는 것을 추천드려요!)

Promise의 경우에는 콜백함수가 비동기 web api 콜백과 달리 마이크로 태스크 큐(micro task queue)에 들어가게 됩니다. .then, .catch. .finally에 들어가는 콜백 함수들이 이 큐에 들어가게 되는 것이죠. 마이크로 태스크 큐에 있는 함수들이 먼저 실행이 되며, 이 큐가 비워진 후에 매크로 태스크 큐의 함수들이 실행된다는 특징이 있습니다.

제너레이터

프로미스를 통해 함수의 이행이나 에러핸들링을하는 콜백함수를 조금 더 동기적인 형태로 작성할 수 있게 되었습니다. ES6에서는 프로미스 뿐 아니라 제너레이터라는 문법도 등장했는데, 이는 next/yield를 이용해 함수의 내부에서 함수의 실행과 중지가 가능하도록 도와줍니다.

function* foo() {
  try {
    const x = yield 3;
    console.log('fullfilled!');
  } catch (err) {
    console.error(err);
  }
}

foo 함수로 생성한 제너레이터 객체의 next나 return 메소드를 이용하여 함수의 실행을 제어할 수 있으며, 에러핸들링도 콜백함수를 이용하지 않고도 위와 같이 try/catch를 통해 해줄 수 있게 되었습니다.

async/await

async/await은 제너레이터와 마찬가지로 함수의 실행과 중지를 제어할 수 있고, Promise 이행이나 에러핸들링을 콜백함수 없이 동기적인 형태로 제어해 비동기 코드를 좀 더 직관적이고 편리한 형태로 작성할 수 있도록 도와줍니다.

async function bar() {
  try {
    const result = await fetch(url);
    console.log(result);
  } catch (err) {
    console.error(err);
  }
}

비동기를 다루기 위해 전통적인 콜백함수의 사용에서 Promise, Generator, async/await까지 알아보았습니다. 그 중에서 async/await이 가장 직관적인 코드 형태이고 사용하기도 쉬우니 앞으로 코드 내의 비동기 제어는 모두 async/await 을 사용하면 될까요?

저도 async/await이 가장 사용하기 쉽고 직관적인 형태로 코드를 작성할 수 있어서, 비동기를 다룰 상황이면 대부분 async/await을 사용하곤 하는데 2019 FEConf에서 그 생각을 바꿔주는 강연을 만나 그 부분에 대해서 잠깐 소개해 드리려고 합니다. (강연 그대로를 짤막하게 소개하려고 하니 더 궁금하신 분들은 강연을 참고해주세요!)

출처: FEConf 2019 유인동 — ES6+ 비동기 프로그래밍과 실전 에러 핸들링

정확한 에러핸들링을 위한 비동기 프로그래밍

배열로 된 이미지 url들을 로드하여 해당 이미지파일들의 총 높이값을 구하는 상황이 있다고 가정해봅시다. 그 상황을 async/await을 이용해 구현한 사항은 다음과 같습니다.

이미지를 각각 map을 이용해 async/await을 이용해 로드한 후 height 정보를 받아 reduce로 더하는 작업을 합니다.

이제 여기서 이미지를 로드할 때 try/catch를 이용해 에러핸들링을 추가해주는 로직을 작성합니다.

에러가 발생할 수 있는 loadImage 호출 로직을 try catch로 감싸고, 혹시 몰라 함수 전체를 try catch로 한번 더 감싸주었습니다. 에러핸들링이 되었으니 이제 끝난걸까요?

강연자님 표현에 따르면 이 코드는 사실상 에러보다 심각한 버그가 생긴 코드라고 합니다. 그 이유는 에러가 중간에 발생하더라도 멈추지 않고 map 함수를 계속 돌게 되는데, 이 중간에 post요청을 하고 잘못된 값이 db에 저장되는 형태의 로직이 들어있을 수 있기 때문입니다.

에러가 발생하면 중간에 로직을 멈출 수 있도록 하는, 이미지 높이 구하는 함수를 다시 작성해봅니다.

구현방법으로는 아까 비동기를 다루는 방법에서 살펴보았던 프로미스의 then/catch, 제너레이터, async/await을 모두 사용하였습니다. 이미지를 로드하고 프로미스를 이행하여 height를 구하는 로직은 map 메소드 대신 에러가나면 전체적인 실행을 중단하는 제너레이터를 이용했습니다. 또한 이터러블을 돌면서 프로미스 이행을 기다리도록 async/await을 사용하였으며, 함수형 프로그래밍의 형태에 맞게 에러핸들링 부분도 .catch를 사용하였습니다.

따라서 비동기를 다루는 문법들을 자유자재로 사용할 수 있게 되면 필요에 맞게 더 깔끔하고 버그가 적게 발생할 수 있는 코드를 만들 수 있습니다. 하나의 패턴만 계속 사용하기보다는, 여러가지 방법에 가능성을 열어두고 필요에 맞게 적용해보는 것이 도움이 될 거라고 생각합니다.

지금까지 코어자바스크립트 책 4장 콜백함수에서부터 비동기프로그래밍까지 알아보았습니다. 앞으로의 자바스크립트 책 포스팅도 이어질 예정입니다 :) 읽어주셔서 감사합니다.

[출처]

책 [코어 자바스크립트]

Asynchrony: Under the Hood — Shelley Vohr — JSConf EU

마이크로 태스크 큐 gif

유인동 — ES6+ 비동기 프로그래밍과 실전 에러 핸들링

Get to know us better! Join our official channels below.

Telegram(EN) : t.me/Humanscape KakaoTalk(KR) : open.kakao.com/o/gqbUQEM Website : humanscape.io Medium : medium.com/humanscape-ico Facebook : www.facebook.com/humanscape Twitter : twitter.com/Humanscape_io Reddit : https://www.reddit.com/r/Humanscape_official Bitcointalk announcement : https://bit.ly/2rVsP4T Email : support@humanscape.io

기업문화 엿볼 때, 더팀스

로그인

/