프로미스

Promise

Promise 클래스는 최신 자바스크립트 엔진에 존재하며 polyfilled일 수 있습니다. promise의 주요 목표는 동기식 스타일 에러처리를 비동기 / 콜백 스타일 코드로 가져오는 것 입니다.

Callback style code

promise를 제대로 이해하기위해 비동기 코드를 콜백만으로 신뢰할 수 있는 코드를 작성하는 것이 어렵다는 간단한 샘플이 있습니다. 파일에서 JSON을 로드하는 비동기 버전을 작성하는 간단한 경우를 생각해 보십시요. 이 동기식 버전은 간단할 수 있습니다.

import fs = require('fs')

function loadJSONSync(filename: string) {
    return JSON.parse(fs.readFileSync(filename))
}

// good json file
console.log(loadJSONSync('good.json'))

// non-existent file, so fs.readFileSync fails
try {
    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 fails
try {
    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 below
function loadJSON(filename: string, cb: (error: Error, data: any) => void) {
    fs.readFile(filename, function(err, data) {
        if (err) cb(err)
        else cb(null, JSON.parse(data))
    })
}

단순한 엔진, 그것은 콜백을 가지고 있습니다, 파일 시스템에서 에러를 콜백에 전달합니다. 만약 파일 시스템에 에러가 없다면 그것은 JSON.parse를 리턴합니다. 콜백에 기반한 비동기 함수를 작업할때 유의해야 할 점

  1. 콜백을 두번 호출하지 마십시요.

  2. 에러를 던지지 마십시요.

그러나 이 간단한 함수는 2가지의 포인트를 수용하지 못합니다. 사실 JSON.parse는 잘못된 JSON이 전달되고 콜백이 호출되지 않고 응용프로그램이 충돌하는 경우 에러를 던집니다. 아래에 예제를 통해서 입증하고 있습니다.

import fs = require('fs')

// A decent initial attempt .... but not correct
function loadJSON(filename: string, cb: (error: Error, data: any) => void) {
    fs.readFile(filename, function(err, data) {
        if (err) cb(err)
        else cb(null, JSON.parse(data))
    })
}

// load invalid json
loadJSON('invalid.json', function(err, data) {
    // This code never executes
    if (err) console.log('bad.json error', err.message)
    else console.log(data)
})

이 문제를 해결하기 위한 간단한 방법은 JSON.parse를 try catch로 감싸는 것 입니다.

import fs = require('fs')

// A better attempt ... but still not correct
function loadJSON(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 json
loadJSON('invalid.json', function(err, data) {
    if (err) console.log('bad.json error', err.message)
    else console.log(data)
})

그것은 미묘한 버그를 가지고 있는 코드입니다. JSON.parse가 아닌 콜백함수 cb가 에러를 던집니다. 우리가 try/catch를 이용해서 감싸기 때문에 catch가 실행되고 다시 콜백을 호출합니다. 즉 두번 불렀습니다. 이것은 아래 예제에서 설명됩니다.

import fs = require('fs')

function loadJSON(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 variable
        var foo
        // The following code throws `Error: Cannot read property 'bar' of undefined`
        console.log(foo.bar)
    }
})
$ node asyncbadcatchdemo.js
our callback called
our callback called
Error: Cannot read property 'bar' of undefined

이것은 우리의 loadJSON함수에서 try 블록에서 콜백을 부당하게 감싸기 때문입니다. 여기서 기억해야 할 교훈이 있습니다.

간단한 강의: 콜백을 호출할때를 제외하고 try catch에 모든 동기식 코드를 포함하십시요.

이것은 간단한 강의입니다, 우리는 비동기 버전의 완전한 함수 loadJSON를 아래에서 설명하고 있습니다.

import fs = require('fs')

function loadJSON(filename: string, cb: (error: Error) => void) {
    fs.readFile(filename, function(err, data) {
        if (err) return cb(err)
        // Contain all your sync code in a try catch
        try {
            var parsed = JSON.parse(data)
        } catch (err) {
            return cb(err)
        }
        // except when you call the callback
        return cb(null, parsed)
    })
}

그것은 많은 보일러플레이트 중에서 어렵지않고 단순하고 좋은 에러 핸들링 코드입니다. 지금 자바스크립트 프로미스를 이용해서 비동기 처리를 더 나은 방향으로 할 수 있습니다.

Creating a Promise

하나의 프로미스는 대기 혹은 이행 혹은 실패 중에 하나에 해당합니다.

프로미스를 생성에 대해서 살펴보겠습니다. 프로미스 생성자에 대해 new를 호출하는 것은 간단합니다. 프로미스 생성자는 상태를 결정하기 위해resolvereject 함수를 전달받습니다.

const promise = new Promise((resolve, reject) => {
    // the resolve / reject functions control the fate of the promise
})

Subscribing to the fate of the promise

프로미스는 .then을 이용해서 resolved를 처리하고 catch를 사용해서 rejected를 구독할 수 있습니다.

const promise = new Promise((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
})
const promise = new Promise((resolve, reject) => {
    reject(new Error('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) // 123
        return 456
    })
    .then(res => {
        console.log(res) // 456
        return Promise.resolve(123) // Notice that we are returning a Promise
    })
    .then(res => {
        console.log(res) // 123 : Notice that this `then` is called with the resolved value
        return 123
    })
  • 당신은 체인에 대한 에러처리를 단일 catch를 이용해서 처리할 수 있습니다.

// Create a rejected promise
Promise.reject(new Error('something bad happened'))
    .then(res => {
        console.log(res) // not called
        return 456
    })
    .then(res => {
        console.log(res) // not called
        return 123
    })
    .then(res => {
        console.log(res) // not called
        return 123
    })
    .catch(err => {
        console.log(err.message) // something bad happened
    })
  • catch는 새로운 프로미스를 반환합니다. (새로운 프로미스는 체인을 효과적으로 생성합니다.)

// Create a rejected promise
Promise.reject(new Error('something bad happened'))
    .then(res => {
        console.log(res) // not called
        return 456
    })
    .catch(err => {
        console.log(err.message) // something bad happened
        return 123
    })
    .then(res => {
        console.log(res) // 123
    })
  • then (혹은 catch)를 이용해서 던져진 에러코드는 프로미스에서 실패하게 됩니다.

Promise.resolve(123)
    .then(res => {
        throw new Error('something bad happened') // throw a synchronous error
        return 456
    })
    .then(res => {
        console.log(res) // never called
        return Promise.resolve(789)
    })
    .catch(err => {
        console.log(err.message) // something bad happened
    })
  • 주어진 오류에 관련된 가장 가까운 catch가 호출될때 에러를 전달받습니다. (catch가 새로운 프로미스 체인을 시작할때)

Promise.resolve(123)
    .then(res => {
        throw new Error('something bad happened') // throw a synchronous error
        return 456
    })
    .catch(err => {
        console.log('first catch: ' + err.message) // something bad happened
        return 123
    })
    .then(res => {
        console.log(res) // 123
        return Promise.resolve(789)
    })
    .catch(err => {
        console.log('second catch: ' + err.message) // never called
    })
  • catch는 체인에 오류가 있는 경우에만 호출됩니다.

Promise.resolve(123)
    .then(res => {
        return 456
    })
    .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`
        return true
    })
    .then(res => {
        // res is inferred to be of type `boolean`
    })

함수를 호출할때 리턴값이 프로미스일 경우 프로미스를 리턴합니다.

function iReturnPromiseAfter1Second(): Promise<string> {
    return new Promise(resolve => {
        setTimeout(() => resolve('Hello world!'), 1000)
    })
}

Promise.resolve(123)
    .then(res => {
        // res is inferred to be of type `number`
        return iReturnPromiseAfter1Second() // 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

함수를 호출을 프로미스로 감싸고

  • reject 만약 에러가 발생하면,

  • resolve 만약 모두 에러가 없다면.

예: fs.readFile을 감싸자

import fs = require('fs')
function readFileAsync(filename: string): Promise<any> {
    return new Promise((resolve, reject) => {
        fs.readFile(filename, (err, result) => {
            if (err) reject(err)
            else resolve(result)
        })
    })
}

장황하지않고 신뢰할 수 있는 코드를 작성하는 예제는 setTimeout를 프로미스로 변환하는 delay함수를 작성하는 것은 매우 쉽습니다.

const delay = (ms: number) => new Promise(res => setTimeout(res, ms))

다음은 NodeJS에서 유용하고 멋진 프로미스를 반환하는 함수를 작성하는 방법입니다.

/** Sample usage */
import fs from 'fs'
import util from 'util'
const readFile = util.promisify(fs.readFile)

웹팩이 util모듈을 지원하고 브라우저에서도 사용할 수 있습니다.

Node 콜백 스타일 함수를 멤버로 가지고 있다면 잊지말고 bind 해주어야 올바른 this 가 적용됩니다:

const dbGet = util.promisify(db.get).bind(db);

Revisiting the JSON example

지금 다시 loadJSON 가지고 비동기 버전의 프로미스를 예제를 설명하겠습니다. 우리는 프로미스를 읽은다음 JSON으로 파싱합니다. 이것은 아래의 예제에서 설명하고 있습니다.

function loadJSONAsync(filename: string): Promise<any> {
    return readFileAsync(filename) // Use the function we just wrote
        .then(function(res) {
            return JSON.parse(res)
        })
}

이 섹션의 시작부분에 도입된 원래의 sync 버전과 얼마나 유사한지 지켜봐 주십시요.

// good json file
loadJSONAsync('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() {
        return loadJSONAsync('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() {
        return loadJSONAsync('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 server
function loadItem(id: number): Promise<{ id: number }> {
    return new Promise(resolve => {
        console.log('loading item', id)
        setTimeout(() => {
            // simulate a server delay
            resolve({ id: id })
        }, 1000)
    })
}

// Chained / Sequential
let item1, item2;
loadItem(1)
    .then(res => {
        item1 = res
        return loadItem(2)
    })
    .then(res => {
        item2 = res
        console.log('done')
    }) // overall time will be around 2s

// Concurrent / Parallel
Promise.all([loadItem(1), loadItem(2)])
    .then((res) => {
        [item1, item2] = res;
        console.log('done');
    }); // overall time will be around 1s

때때로, 당신은 비동기를 연속적으로 실행해야 할 수도 있습니다. 그러나 이러한 작업중 하나가 해결되는 한 필요한 모든 것을 얻을 수 있습니다. 아래에 Promise.race함수를 이용해서 Promise를 주입하는 방법을 설명하고 있습니다.

var task1 = new Promise(function(resolve, reject) {
    setTimeout(resolve, 1000, 'one')
})
var task2 = new Promise(function(resolve, reject) {
    setTimeout(resolve, 2000, 'two')
})

Promise.race([task1, task2]).then(function(value) {
    console.log(value) // "one"
    // Both resolve, but task1 resolves faster
})

Last updated