브라우저의 이해 #2 히스토리 그리고 history API
큐(Queue)구조의 버퍼(Buffer)로 비동기 순차 실행시키기 for Javascript
2019-08-25
Explanation
오늘은 큐(Queue)와 프로미스(Promise)를 활용한 비동기 통신에 대한 버퍼를 만들어서 동시에 여러곳에서 실행되는 비동기 통신을 호출한 순서대로 진행되도록 하는 코드를 만들어보려 합니다.
음…
정확하게 이게 필요한 상황을 만들기엔 좀 어려움이 있는데.. 그냥 Promise와 Queue를 이해하는데 도움이 될 거 같아서 한번 정리해보려 합니다.
우선 예제는 깃허브를 통해 코드를 배포하였습니다. clone 받으시고 간단하게 ‘npm install’, ‘npm start’를 하고 개발자 도구 콘솔에서 확인하시면 되움이 될 것 같습니다.
https://github.com/falsy/blog-post-example/tree/master/buffer-queue-promise
간단하게 큐는.. 먼저 들어온게 먼저 실행되는 자료 구조를 말하는데요. 일상 생활에서도 많이 쓰이는 그 큐에요. 게임에서 레이드나 랭크전에 참여할 때 큐를 돌린다고 하잖아요? 그때 그 큐를 이야기 한답니다.
간단히 ‘선입선출’이라고 말해요. 우선 간단하게 만든 코드를 보면 아래와 같습니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 |
class Queue { constructor() { this.inProgress = false; this.queue = []; } enqueue(method, callback) { this.queue.push({ method: method, callback: callback }); this.autoAction(); } front() { return this.queue[0]; } dequeue() { this.queue.shift(); } clear() { this.inProgress = false; this.queue = []; } isEmpty() { return this.queue.length === 0; } autoAction() { if(this.inProgress === false) { this.inProgress = true; this.action(); } } async action() { const result = await this.front().method(); this.front().callback(result); this.dequeue(); if(this.isEmpty()) { this.clear(); } else { this.action(); } } } export default new Queue() |
간단하게 큐라는 객체를 만들고 프로퍼티로 inProgress 와, queue를 만들고 요청이 들어오면 queue에 들어온 순서대로 쌓고 가장 처음 들어온 요청을 먼저 실행하고 실행되면 빼내는 구조로 되어있답니다.
그리고 inProgress는 현재 큐안에 실행중인게 있는지 확인하기 위한 프로퍼티입니다. 최종적으로는 action이라는 메서드가 호출되고 큐에 다음 호출할게 있다면 재귀하도록 되어 있습니다.
여기서 버퍼는 프로미스 객체로 큐에 담아서 실행하는데 그 실행을 다시 한번 프로미스 객체로 감싸서 여러곳에서 실행되는 비동기 프로미스의 순서를 보장하게 하는…
말로 적으려니 더 어렵네요.
간단하게 프로미스 객체를 이중으로 만들어서 들어온 순서까지 기억해서 그 순서에 해당하는 비동기 통신이 끝났을때 콜백을 실행해주도록 합니다.
그리고 async-await를 사용하면 비동기 통신의 끝난 완료된 프로미스 객체의 응답의 순서를 보장해주지만 프로미스 함수의 실행은 (거의) 함께 진행이 되는데요, 버퍼를 사용하면 프로미스의 함수의 실행까지의 완전한 순서를 보장할 수 있답니다.
1 2 3 4 5 6 7 8 9 10 11 |
import Queue from './Queue.js'; export default (request) => { return new Promise(resolve => { Queue.enqueue(() => { return new Promise(innerResolve => { request().then(res => innerResolve(res)); }); }, res => resolve(res)); }); } |
여기서 거의 함께라는 말이 좀 이상하게 들릴 수 있는데요.
맞아요. 거의 함께라는 건 말이 안되고..
await가 프로미스의 함수의 실행이 끝나는 것까지 기다리지는 않는다는 말을 하고 싶은건 데, 아래 예제와 이야기를 보시면 조금 이해가 될 거 같아요.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
import Buffer from './Buffer'; const basicRequest1 = () => { return new Promise(resolve => { setTimeout(() => { console.log('basic request1 start'); resolve('ok'); }, 2000); }); }; const basicRequest2 = () => { return new Promise(resolve => { setTimeout(() => { console.log('basic requset2 start'); resolve('ok'); }, 1000); }); }; const request1 = () => { return new Promise(resolve => { setTimeout(() => { console.log('request1 start'); resolve('ok'); }, 2000); }); }; const request2 = () => { return new Promise(resolve => { setTimeout(() => { console.log('request2 start'); resolve('ok'); }, 1000); }); }; const pro1 = basicRequest1(); const pro2 = basicRequest2(); const req1 = Buffer(request1); const req2 = Buffer(request2); |
간단한게 pro1, pro2는 프로미스 객체를 반환하는데요. 1번을 먼저 호출하고 2번을 나중에 호출했지만 1번 호출이 응답시간이 더 오래 걸리기 때문에 브라우저에서 콘솔을 확인해보면 ‘basic request2 start’가 먼저 출력되고 ‘basic request1 start’가 그 다음에 호출이 된답니다.
단순히 출력되는 순서보다, 그냥 프로미스 함수를 호출하면 두개의 함수는 (거의)동시에 시작해서 1초 후, ‘basic request2 start’가 출력되고 시작점에서 2초 후 ‘basic request1 start’가 출력되는데요.
Buffer로 감싸서 실행된 프로미스는 request1이 호출되고 2초 후 ‘request1 start’가 출력되며 그로부터 다시 1초 후 ‘request2 start’가 출력되는 차이가 있답니다.
글이 너무… 밑도 끝도 없이 쓰여졌는데…
그냥 프로미스를 이렇게 사용할 수도 있구나.. 정도로 봐주시면 좋을 거 같아요.