Promise 클래스는 최신 자바스크립트 엔진에 존재하며 polyfilled일 수 있습니다. promise의 주요 목표는 동기식 스타일 에러처리를 비동기 / 콜백 스타일 코드로 가져오는 것 입니다.
Callback style code
promise를 제대로 이해하기위해 비동기 코드를 콜백만으로 신뢰할 수 있는 코드를 작성하는 것이 어렵다는 간단한 샘플이 있습니다. 파일에서 JSON을 로드하는 비동기 버전을 작성하는 간단한 경우를 생각해 보십시요. 이 동기식 버전은 간단할 수 있습니다.
import fs =require('fs')functionloadJSONSync(filename:string) {returnJSON.parse(fs.readFileSync(filename))}// good json fileconsole.log(loadJSONSync('good.json'))// non-existent file, so fs.readFileSync failstry {console.log(loadJSONSync('absent.json'))} catch (err) {console.log('absent.json error',err.message)}// invalid json file i.e. the file exists but contains invalid JSON so JSON.parse failstry {console.log(loadJSONSync('invalid.json'))} catch (err) {console.log('invalid.json error',err.message)}
loadJSONSync는 3가지 동작 유효한 리턴값 파일, 시스템 에러, JSON.parse 에러가 있습니다. 우리는 에러를 try/catch를 사용해서 쉽게 조작할 수 있습니다. 이것은 동기식 프로그래밍 언어에 존재합니다. 지금 좋은 비동기식 버전의 함수를 만들수 있습니다. 에러를 체크하는데 괜찮은 방식입니다.
import fs =require('fs')// A decent initial attempt .... but not correct. We explain the reasons belowfunctionloadJSON(filename:string,cb: (error:Error, data:any) =>void) {fs.readFile(filename,function(err, data) {if (err) cb(err)elsecb(null,JSON.parse(data)) })}
단순한 엔진, 그것은 콜백을 가지고 있습니다, 파일 시스템에서 에러를 콜백에 전달합니다. 만약 파일 시스템에 에러가 없다면 그것은 JSON.parse를 리턴합니다. 콜백에 기반한 비동기 함수를 작업할때 유의해야 할 점
콜백을 두번 호출하지 마십시요.
에러를 던지지 마십시요.
그러나 이 간단한 함수는 2가지의 포인트를 수용하지 못합니다. 사실 JSON.parse는 잘못된 JSON이 전달되고 콜백이 호출되지 않고 응용프로그램이 충돌하는 경우 에러를 던집니다. 아래에 예제를 통해서 입증하고 있습니다.
import fs =require('fs')// A decent initial attempt .... but not correctfunctionloadJSON(filename:string,cb: (error:Error, data:any) =>void) {fs.readFile(filename,function(err, data) {if (err) cb(err)elsecb(null,JSON.parse(data)) })}// load invalid jsonloadJSON('invalid.json',function(err, data) {// This code never executesif (err) console.log('bad.json error',err.message)elseconsole.log(data)})
이 문제를 해결하기 위한 간단한 방법은 JSON.parse를 try catch로 감싸는 것 입니다.
import fs =require('fs')// A better attempt ... but still not correctfunctionloadJSON(filename:string,cb: (error:Error) =>void) {fs.readFile(filename,function(err, data) {if (err) {cb(err) } else {try {cb(null,JSON.parse(data)) } catch (err) {cb(err) } } })}// load invalid jsonloadJSON('invalid.json',function(err, data) {if (err) console.log('bad.json error',err.message)elseconsole.log(data)})
그것은 미묘한 버그를 가지고 있는 코드입니다. JSON.parse가 아닌 콜백함수 cb가 에러를 던집니다. 우리가 try/catch를 이용해서 감싸기 때문에 catch가 실행되고 다시 콜백을 호출합니다. 즉 두번 불렀습니다. 이것은 아래 예제에서 설명됩니다.
import fs =require('fs')functionloadJSON(filename:string,cb: (error:Error) =>void) {fs.readFile(filename,function(err, data) {if (err) {cb(err) } else {try {cb(null,JSON.parse(data)) } catch (err) {cb(err) } } })}// a good file but a bad callback ... gets called again!loadJSON('good.json',function(err, data) {console.log('our callback called')if (err) console.log('Error:',err.message)else {// let's simulate an error by trying to access a property on an undefined variablevar foo// The following code throws `Error: Cannot read property 'bar' of undefined`console.log(foo.bar) }})
이것은 우리의 loadJSON함수에서 try 블록에서 콜백을 부당하게 감싸기 때문입니다. 여기서 기억해야 할 교훈이 있습니다.
간단한 강의: 콜백을 호출할때를 제외하고 try catch에 모든 동기식 코드를 포함하십시요.
이것은 간단한 강의입니다, 우리는 비동기 버전의 완전한 함수 loadJSON를 아래에서 설명하고 있습니다.
import fs =require('fs')functionloadJSON(filename:string,cb: (error:Error) =>void) {fs.readFile(filename,function(err, data) {if (err) returncb(err)// Contain all your sync code in a try catchtry {var parsed =JSON.parse(data) } catch (err) {returncb(err) }// except when you call the callbackreturncb(null, parsed) })}
그것은 많은 보일러플레이트 중에서 어렵지않고 단순하고 좋은 에러 핸들링 코드입니다. 지금 자바스크립트 프로미스를 이용해서 비동기 처리를 더 나은 방향으로 할 수 있습니다.
Creating a Promise
하나의 프로미스는 대기 혹은 이행 혹은 실패 중에 하나에 해당합니다.
프로미스를 생성에 대해서 살펴보겠습니다. 프로미스 생성자에 대해 new를 호출하는 것은 간단합니다. 프로미스 생성자는 상태를 결정하기 위해resolve와 reject 함수를 전달받습니다.
constpromise=newPromise((resolve, reject) => {// the resolve / reject functions control the fate of the promise})
Subscribing to the fate of the promise
프로미스는 .then을 이용해서 resolved를 처리하고 catch를 사용해서 rejected를 구독할 수 있습니다.
constpromise=newPromise((resolve, reject) => {resolve(123)})promise.then(res => {console.log('I get called:', res ===123) // I get called: true})promise.catch(err => {// This is never called})
constpromise=newPromise((resolve, reject) => {reject(newError('Something awful happened'))})promise.then(res => {// This is never called})promise.catch(err => {console.log('I get called:',err.message) // I get called: 'Something awful happened'})
프로미스 꿀팁
프로미스 resolved를 빠르게 생성하는 방법: Promise.resolve(result)
프로미스 rejected를 빠르게 생성하는 방법: Promise.reject(error)
Chain-ability of Promises
체이닝 방식으로 프로미스를 사용할때 얻는 이점. 당신은 하나의 프로미스를 가지고 있고, then함수를 생성해서 프로미스를 체이닝해서 사용할 수 있습니다.
당신은 프로미스를 체이닝해서 어떤 함수라도 리턴할수 있고, resolved되면 then만 호출됩니다.
Promise.resolve(123).then(res => {console.log(res) // 123return456 }).then(res => {console.log(res) // 456returnPromise.resolve(123) // Notice that we are returning a Promise }).then(res => {console.log(res) // 123 : Notice that this `then` is called with the resolved valuereturn123 })
당신은 체인에 대한 에러처리를 단일 catch를 이용해서 처리할 수 있습니다.
// Create a rejected promisePromise.reject(newError('something bad happened')).then(res => {console.log(res) // not calledreturn456 }).then(res => {console.log(res) // not calledreturn123 }).then(res => {console.log(res) // not calledreturn123 }).catch(err => {console.log(err.message) // something bad happened })
catch는 새로운 프로미스를 반환합니다. (새로운 프로미스는 체인을 효과적으로 생성합니다.)
// Create a rejected promisePromise.reject(newError('something bad happened')).then(res => {console.log(res) // not calledreturn456 }).catch(err => {console.log(err.message) // something bad happenedreturn123 }).then(res => {console.log(res) // 123 })
then (혹은 catch)를 이용해서 던져진 에러코드는 프로미스에서 실패하게 됩니다.
Promise.resolve(123).then(res => {thrownewError('something bad happened') // throw a synchronous errorreturn456 }).then(res => {console.log(res) // never calledreturnPromise.resolve(789) }).catch(err => {console.log(err.message) // something bad happened })
주어진 오류에 관련된 가장 가까운 catch가 호출될때 에러를 전달받습니다. (catch가 새로운 프로미스 체인을 시작할때)
Promise.resolve(123).then(res => {thrownewError('something bad happened') // throw a synchronous errorreturn456 }).catch(err => {console.log('first catch: '+err.message) // something bad happenedreturn123 }).then(res => {console.log(res) // 123returnPromise.resolve(789) }).catch(err => {console.log('second catch: '+err.message) // never called })
catch는 체인에 오류가 있는 경우에만 호출됩니다.
Promise.resolve(123).then(res => {return456 }).catch(err => {console.log('HERE') // never called })
사실은
catch를 사용할때 중간에 then을 호출하면 에러를 건너뛸수 있습니다.
catch를 사용하면 동기식으로 에러를 처리할 수 있습니다.
실제로 원시콜백보다 나은 에러처리를 허용하는 비동기 프로그래밍 패러다임을 제공합니다. 이에대한 자세한 내용은 아래를 참고하십시요.
TypeScript and promises
타입스크립트의 장점은 프로미스 체인의 값의 흐름을 이해하는데 훌륭합니다.
Promise.resolve(123).then(res => {// res is inferred to be of type `number`returntrue }).then(res => {// res is inferred to be of type `boolean` })
함수를 호출할때 리턴값이 프로미스일 경우 프로미스를 리턴합니다.
functioniReturnPromiseAfter1Second():Promise<string> {returnnewPromise(resolve => {setTimeout(() =>resolve('Hello world!'),1000) })}Promise.resolve(123).then(res => {// res is inferred to be of type `number`returniReturnPromiseAfter1Second() // We are returning `Promise<string>` }).then(res => {// res is inferred to be of type `string`console.log(res) // Hello world! })
Converting a callback style function to return a promise
Node 콜백 스타일 함수를 멤버로 가지고 있다면 잊지말고 bind 해주어야 올바른 this 가 적용됩니다:
constdbGet=util.promisify(db.get).bind(db);
Revisiting the JSON example
지금 다시 loadJSON 가지고 비동기 버전의 프로미스를 예제를 설명하겠습니다. 우리는 프로미스를 읽은다음 JSON으로 파싱합니다. 이것은 아래의 예제에서 설명하고 있습니다.
functionloadJSONAsync(filename:string):Promise<any> {returnreadFileAsync(filename) // Use the function we just wrote.then(function(res) {returnJSON.parse(res) })}
이 섹션의 시작부분에 도입된 원래의 sync 버전과 얼마나 유사한지 지켜봐 주십시요.
// good json fileloadJSONAsync('good.json').then(function(val) {console.log(val) }).catch(function(err) {console.log('good.json error',err.message) // never called })// non-existent json file.then(function() {returnloadJSONAsync('absent.json') }).then(function(val) {console.log(val) }) // never called.catch(function(err) {console.log('absent.json error',err.message) })// invalid json file.then(function() {returnloadJSONAsync('invalid.json') }).then(function(val) {console.log(val) }) // never called.catch(function(err) {console.log('bad.json error',err.message) })
loadFile(async) + JSON.parse (sync) => catch 이 구조를 사용하면 프로미스함수를 체인을 이용해서 손쉽게 통합할수 있습니다. 또한 콜백은 프로미스 체인에 의해 호출되엇으므로 try/catch를 이용해서 감싸는 실수를 하지 않습니다.
Parallel control flow
우리는 일련의 비동기 작업과 시퀀스가 수행될때 프로미스가 얼마나 사소한지 보았습니다. 그 문제는 then을 이용한 체이닝 호출로 손쉽게 해결할 수 있습니다.
그러나 일련의 비동기 작업들을 실행한 후 이러한 작업의 결과로 무언가를 수행하려 할 수 있습니다. Promise.all함수를 이용해서 Promise에 숫자를 인자로 전달해서 실행할 수 있습니다. 프로미스를 배열형태로 실행하고 전달받는 값도 배열형태로 전달받습니다. 아래에 체이닝을 이용해서 잘 설명되어있습니다.
// an async function to simulate loading an item from some serverfunctionloadItem(id:number):Promise<{ id:number }> {returnnewPromise(resolve => {console.log('loading item', id)setTimeout(() => {// simulate a server delayresolve({ id: id }) },1000) })}// Chained / Sequentiallet item1, item2;loadItem(1).then(res => { item1 = resreturnloadItem(2) }).then(res => { item2 = resconsole.log('done') }) // overall time will be around 2s// Concurrent / ParallelPromise.all([loadItem(1),loadItem(2)]).then((res) => { [item1, item2] = res;console.log('done'); }); // overall time will be around 1s
때때로, 당신은 비동기를 연속적으로 실행해야 할 수도 있습니다. 그러나 이러한 작업중 하나가 해결되는 한 필요한 모든 것을 얻을 수 있습니다. 아래에 Promise.race함수를 이용해서 Promise를 주입하는 방법을 설명하고 있습니다.
var task1 =newPromise(function(resolve, reject) {setTimeout(resolve,1000,'one')})var task2 =newPromise(function(resolve, reject) {setTimeout(resolve,2000,'two')})Promise.race([task1, task2]).then(function(value) {console.log(value) // "one"// Both resolve, but task1 resolves faster})