Web programming/javascript

[JS] 비동기(async) 프로그래밍 이해하기[2]

annual 2019. 1. 7. 14:18
Medium에 게시한 글입니다. Medium에서 읽으시면 좀 더 좋은 환경에서 보실 수 있습니다.



[JS] 비동기(async) 프로그래밍 이해하기[2]

JS 비동기 패러다임 소개

이 글은 Ethan Brown의 Learning Javascript 3판을 참조한 글이다.
Photo by Paul Gardin on Flickr

이전 글: Part 1

지난 글에서 배웠던 것들을 recap 해보자.

  • 자바스크립트의 비동기적 실행은 콜백을 통해 이루어진다.
  • 프라미스는 콜백이 여러 번 호출되는 문제를 해결했다.
  • 프라미스를 콜백 대신 사용할 수 있는 건 아니다. 프라미스 역시 콜백을 사용한다.

마지막으로 글의 끝자락에서 프라미스는 비동기 작업의 현재 진행 상황을 알 수 없기 때문에 ‘이벤트’라는 녀석과 결합해야 한다고 했었다. 오늘은 이벤트와 이벤트를 사용하는 방법에 대해서 알아보자!

Event

이벤트의 개념은 간단하다. 이벤트가 일어나면 이벤트를 담당하는 이벤트개체(event emitter)에서 이벤트가 일어났음을 알린다. 그리고 정해둔 로직이 실행된다. 물론 이벤트가 일어났다는 것을 알아내기 위해서는 콜백을 이용해야 한다. node에서는 이미 이벤트를 지원하는 모듈 EventEmitter가 있기 때문에 이를 이용해 이벤트를 만들어보자.

지난 시간에 사용했던 countdown함수를 그대로 쓸 것이다. 하지만 EventEmitter를 상속해야 하니까 클래스로 다시 선언해보자.

const EventEmitter = require('events').EventEmitter;
class Countdown extends EventEmitter {
constructor(seconds, superstitious) {
super();
this.seconds = seconds;
this.superstitious = !!superstitious;
}
go() {
const countdown = this;
return new Promise((resolve, reject) => {
for (let i = countdown.seconds; i >= 0; i--) {
setTimeout(() => {
if(countdown.superstitious && i === 13)
return reject(new Error('OMG'));
countdown.emit('tick', i);
if(i === 0) resolve();
}, (countdown.seconds - i) * 1000);
}
});
}
}

EventEmitter를 상속하는 클래스는 이벤트를 발생시킬 수 있다. 실제 카운트다운을 시작하고 프라미스를 반환하는 메서드는 go이다. go는 콜백 안에서 this값을 불러와야 하는데 this는 콜백 안에서 값이 달라지기 때문에 첫 줄에서 미리 countdown라는 상수로 레퍼런스를 해 놓았다. 가장 중요한 부분은 countdown.emit('tick', i) 이다. tick이라는 이벤트를 생성하는 부분으로 이 코드가 실행되면 이 이벤트를 기다리고(listen)있던 객체들에게 알림이 간다. 이벤트 이름은 원하는 것을 써도 되며 뒤에 따라오는 파라미터들도 원하는 값을 사용할 수 있다.

다음과 같이 사용한다.

const c = new Countdown(5);
c.on('tick', i => {
if (i > 0) console.log(i + '...');
});
c.go()
.then(() => {
console.log('GO!');
}
.catch(err => {
console.error(err.message);
}

on은 특정 이벤트를 기다리게 만드는 메서드로 위 예제에서는 ‘tick’이라는 이름을 가진 이벤트를 기다리게 된다. 그리고 그 이벤트가 일어나게 되면 파라미터로 받은 i를 console에 출력한다

go는 프라미스가 resolve됐을 때 실행되는 메서드이다.

그런데 우리는 아직 문제를 해결하지 못했다. 지난 시간에 도전했던 것처럼 i가 13일 때 멈추도록 Countdown(15, true)를 실행해보자. 지난 시간과 똑같이 13에서 에러를 낸 다음 12, 11, 10 … , GO 까지 계속될 것이다.

const c = new Countdown(15, true)
.on('tick', i => { // 이렇게 체인으로 연결해도 된다
if (i > 0) console.log(i + '...');
});
c.go()
.then(() => {
console.log('GO!');
})
.catch(err => {
console.error(err.message);
});

이렇게 되는 이유는 timeout이 이미 모두 만들어져있기 때문이다. 그렇기 때문에 만들어진 timeout을 미리 저장해 두고 원하는 조건에서 timeout을 clear하는 단계가 필요하다.

const EventEmitter = require('events').EventEmitter;
class Countdown extends EventEmitter {
constructor(seconds, superstitious) {
super();
this.seconds = seconds;
this.superstitious = !!superstitious;
}
go() {
const countdown = this;
const timeoutIds = []; // timeout들을 저장할 리스트
    return new Promise((resolve, reject) => {
for (let i = countdown.seconds; i >= 0; i--) {
timeoutIds.push(setTimeout(() => {
if(countdown.superstitious && i === 13) {
timeoutIds.forEach(clearTimeout); // 대기중인 타임아웃 취소
return reject(new Error('OMG'));
}
countdown.emit('tick', i);
if(i === 0) resolve();
}, (countdown.seconds - i) * 1000));
}
});
}
}

프라미스체인

프라미스는 체인으로 연결할 수 있다. 즉, 프라미스가 완료되면 다른 프라미스를 반환하는 함수를 즉시호출 할 수 있는 것이다. launch함수를 만들어 카운트다운이 끝나면 실행되게 해 보자.

function launch() {
return new Promise((resolve, reject) => {
console.log("Lift off!");
setTimeout(() = {
resolve("In orbit!");
}, 2*1000);
});
}

다음과 같이 사용할 수 있다.

const c = new Countdown(5)
.on('tick', i => console.log(i + '...'));
c.go()
.then(launch)
.then(msg => {
console.log(msg);
})
.catch(err => {
console.error("We have a problem...");
});

프라미스 체인의 장점은 체인 어디에서든 에러가 생기면 체인 전체가 멈추고 catch 핸들러로 간다는 것이다.

그런데 프라미스의 문제점이 하나 더 있다. 프라미스는 반드시 결정된다는(성공 또는 실패한다는) 보장이 없다. 이를 해결하기 위해서는 프라미스에 타임아웃을 걸어야 한다. 프라미스에 타임아웃을 걸기 위해서는 해당 프라미스를 받아 타임아웃을 건 다음 프라미스가 성공,실패됐을 때 타임아웃을 해제하는 방법을 사용해야한다. 이 방법은 약간 복잡하기 때문에 advanced 단계에서 다시 살펴보도록 한다.


오늘은 여기까지가 좋겠다.

다음 글에서는 비동기 코드를 효율적으로 관리하게 해 줄 제너레이터(Generator)를 알아볼 것이다.

이 글은 Ethan Brown의 Learning javascript 3판을 참조한 글이다. 대부분의 예제와 글은 책에서 발췌했으며 약간의 축약, 수정을 거쳤다.

글이 좋았다면 👏🏻를 쳐주세요