[JS] 비동기(async) 프로그래밍 이해하기[2]
JS 비동기 패러다임 소개
이 글은 Ethan Brown의 Learning Javascript 3판을 참조한 글이다.
이전 글: 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));
}
});
}
}