Promise

raccoonback·2020년 6월 28일
2

javascript

목록 보기
8/11
post-thumbnail

이 글은 You Don't Know JS 서적을 참고하여 작성하였습니다.

프라미스란

Promise지금 또는 나중 시점에 상관없이 동일한 결과를 낼 수 있도록 정규화해주는 도구이다. Promise를 사용하면 미랫값(결과값)을 추론할 수 있게 되는 이점이 있다.

function fetchX() {
    return new Promise(function (resolve) {
        setTimeout(() => resolve(11), 1000);
    })
}

function fetchY() {
    return Promise.resolve(22);
}

function add(xPromise, yPromise) {
    return Promise.all([xPromise, yPromise])
        .then(function (values) {
            console.log(...values);
            return values[0] + values[1];
        });
}

add(fetchX(), fetchY())
    .then(function (sum) {
        console.log(sum);
    });
// 11 22
// 33

위 예제와 같이, 프라미스가 시점에 상관없이 같은 결과를 도출해주기 때문에 미랫값인 values는 추론이 가능해진다.

PromiseFulfillment, Rejection 두 가지 중 하나로 Resolve될 수 있고, then() 함수는 Fulfillment, Rejection 함수를 전달받는다.

add(fetchX(), fetchY())
    .then(  
        // Fulfillment
        function (sum) {
            console.log(sum);
        },  
        // Rejection
        function (error) {
            console.log(error)
        });

fetchX() 또는 fetchY() 함수에서 예기치 못한 에러가 발생한다면 add() 함수가 반환하는 프라미스는 버려지고 Rejection 콜백 함수가 호출된다.

Promise시간에 의존적인 상태를 외부로 부터 캡슐화하기 때문에 프라미스 자체는 시간에 독립적이다. 따라서, 내부 결과값에 상관없이 예측 가능한 방향으로 구성할 수 있다.

또한, Promise는 한 번 **Resolve된 후에는 불변성을 유지하여 상태가 그대로 유지되기 때문에 재사용**이 가능하다.

결과적으로, Promise미랫값을 캡슐화하고 조합할 수 있게 해주는 손쉬운 반복 장치이다.

프라미스 믿음

콜백에 대해서 설명하면서, 믿음성에 대한 문제를 제기했었다. Promise콜백에 존재하던 믿음성에 대한 문제를 해결하였는데 하나씩 살펴보자.

너무 빨리 호출하는 경우

너무 빨리 호출되는 현상은 주로 동기적/비동기적으로 동작할 지 모르는 상황에서 발생하였다.

Promise는 아래와 같이 아무리 Resolve된 경우라도, then() 함수에 전달한 콜백은 무조건 비동기적으로 부르기 때문에 이 문제에 영향을 받지 않는다.

new Promise(function (resolve) {
    resolve(1);
}).then(function cb(arg) {
    console.log(arg)
});

너무 늦게 호출하는 경우

프라미스 then()에 등록한 콜백은 새 프라미스가 생성되면서 resolve(), reject() 함수중 하나로 부터 호출된다. 이렇게 등록된 콜백은 비동기 시점에 실행될 것이다.

따라서, 어떠한 동기적인 작업 연쇄가 예정된 다른 콜백의 실행을 지연시킬 수 없다. 즉, 프라미스가 Resolve되면 then() 함수에 등록된 콜백들이 다음 비동기 시점에 순서대로 실행되기 때문에, 어느 한 콜백 내부에서 등록한 콜백이 이미 등록된 콜백들보다 먼저 실행될 수는 없다.

const promise = new Promise(function (resolve) {
    resolve(1);
});

promise.then(function () {
    // 동기적인 작업 연쇄
    promise.then(
        // 새로운 콜백
        function () {
            console.log('C');
        });

    console.log('A');
});

promise.then(function() {
    console.log('B');
});
// A B C

절대로 C가 B보다 먼저 실행될 수 없다.

한번도 호출되지 않는 경우

프라미스에 resolve(), reject()에 대한 콜백이 등록된 상태라면 둘 중 하나의 콜백은 무조건 호출된다.

하지만, 비동기 처리 도중에 Hang이 걸리게 되면 처리할 방도가 없기 때문에, Promise.race을 이용해 일정 시간 후 타임아웃이 발생하도록 처리할 수 있다.

function timeoutPromise(delay) {
    return new Promise(function (resolve, reject) {
        setTimeout(reject.bind(null, '타임 아웃'), delay);
    });
}

function foo() {
    return new Promise(function (resolve) {
        setTimeout(resolve.bind(null, 'foo'), 2000);
    })
}

Promise.race([foo(), timeoutPromise(1000)])
    .then(function (arg) {
        console.log(arg);
    }, function (error) {
        console.error(error);
    });
// 타임 아웃 

종종 호출되는 경우

프라미스는 오직 단 한번만 Resolve되기 때문에 then() 함수에 등록한 콜백 또한 반드시 한 번씩만 호출된다.

프라미스 생성 코드에서 resolve(), reject() 함수들을 여러 번 호출하더라도, 오직 최초 호출만 처리되고 나머지는 무시한다.

인자 전달 실패하는 경우

프라미스는 무조건 등록한 콜백을 호출하고 인자를 전달한다.

하지만, 프라미스에서 반드시 알고 있어야 부분이 있는데, 프라미스의 미랫값은 오직 하나뿐이라는 것이다.

즉, resolve(), reject() 함수로 여러 인자를 전달하려 해도 오직 첫 번째 인자만 전달된다. 따라서, 여러개의 인자를 전달하기 위해서는 배열이나 객체로 감싸서 전달해야만 한다.

new Promise(function (resolve) {
    resolve('foo', 'bar', 'baz')
}).then(function (arg) {
    console.log(...arguments);
});
// foo

new Promise(function (resolve) {
    resolve(['foo', 'bar', 'baz'])
}).then(function (arg) {
    console.log(...arguments);
});
// [ 'foo', 'bar', 'baz' ]

Promise.resolve()

Promise.resolve()는 즉시값 또는 프라미스가 아닌, 데너블이 아닌 값을 프라미스로 래핑해준다.

즉, 아래의 코드는 동일한 것이다.

new Promise(function(resolve, reject) {
    resolve(1);
});

Promise.resolve(1);

또한, 프라미스를 인자로 넘겨도 동일한 프라미스를 반환한다.

const p1 = new Promise(function(resolve, reject) {
    resolve(1);
});

const p2 = Promise.resolve(p1);

console.log(p1 === p2);
// true

그럼 Promise.resolve()는 아래와 같이 프라미스가 아닌 thenable한 값을 전달받으면 어떻게 될까?

const p = {
    then: function (cb, errCb) {
        cb(1);
        errCb("에러");
    }
};

p.then(function (arg) {
    console.log(arg);
}, function (err) {
    console.error(err)
});
// 1
// 에러

결과적으로, Promise.resolve()는 전달받은 값을 데너블(thenable)하지 않은 구체적인 값이 발견될 때까지 탐색하여, 최종적인 값을 특정 상태(Fulfillment, Rejection)로 Resolv(귀결)하는 프라미스를 반환한다.

const p = {
    then: function (cb, errCb) {
        cb(1);
        errCb("에러");
    }
};

Promise.resolve(p)
    .then(function (arg) {
        console.log(arg);
    }, function (err) {
        console.error(err)
    });
// 1

따라서, 어떠한 값이 프라미스인지, 데너블한지를 판별하기 힘든 경우, Promise.resolve() 이용해 무조건 프로미스라는 믿음을 형성할 수 있다.

연쇄 흐름

Promisethen() 함수를 호출할 때마다 새로운 프라미스를 반환하므로 비동기 단계를 연쇄적으로 처리할 수 있다. 연쇄적인 처리가 가능한 이유는 then() 함수에서의 Fulfillment 콜백 함수가 반환하는 값은 무엇이든 간에 연쇄된 프라미스의 미랫값(Fulfillment)으로 설정되기 때문이다.

Promise.resolve(1)
    .then(function (arg) {
        console.log(arg);
        return arg;
    })
    .then(function (arg) {
        console.log(arg);
    });
// 1
// 1

여기서 연쇄의 각 단계마다 비동기적으로 작동하게 만드는 핵심은 Promise.resolve()프라미스, 데너블일 때 구체적인 값이 나올 때까지 계속 풀어보는 동작 덕분이다.(프라미스(or 데너블) 값이 전달되면 재귀적으로 풀어서 최종적인 값이 특정 상태(Fulfillment, Rejection)로 Resolv(귀결)하는 프라미스를 반환)

또한, Fulfillment, Rejection 처리하는 콜백 함수에서 프라미스, 데너블을 반환하는 경우에도 동일하게 동작한다 .

const promise = new Promise(function (resolve) {
    resolve(1);
});

promise
    .then(function (arg) {
        console.log(arg);
        return new Promise(function (resolve) {
            setTimeout(() => resolve(2), 1000);
        });
    })
    .then(function (arg) {
        console.log(arg);
    });
// 1
// 2

첫 번째 then() 함수에서 2를 Promise로 감싸서 반환했지만, 연쇄된 프라미스 Resolve에서 다시 풀어보기 때문에 두 번째 then() 함수가 받는 값은 2이다.

만약 프라미스 연쇄의 중간 단계에서 에러가 발생하면 어떻게 될까?

결론부터 말하면, 다음 then() 함수의 Rejection 콜백 함수가 호출되고 이후의 프라미스 연쇄가 정상 동작한다.

const promise = new Promise(function (resolve) {
    resolve(1);
});

promise
    .then(function (arg) {
        console.log('step 1', arg);
        throw '에러 발생!';
        return arg + 1;
    })
		// rejection 콜백 함수를 등록하지 않았기 때문에 
		// 연쇄적으로 에러 전파
    .then(function (arg) {
        console.log('step 2', arg);
        return arg + 1;
    })
		// rejection 콜백 함수를 등록됐기 때문에 여기서 처리 
    .then(function(arg) {
        console.log('step 3', arg);
        return arg + 1;
    }, function(error) {
				// 호출됨
        console.error('step 3', error);
				// resolve된다.
        return 1;
    })
		// fulfillment 콜백 함수를 등록하지 않았기 때문에 
		// 연쇄적으로 resolve 값 전파
    .then(null,
        function(error) {
        console.log('step 4', error);
        return 1;
    })
		// fulfillment 콜백 함수를 정의됐기 때문에 여기서 처리
    .then(function(arg) {
        console.log('step 5', arg);
        return arg + 1;
    });
// step 1 1
// step 3 에러 발생!
// step 5 1

위 예제에서 보다시피, 첫 번째 then() 함수에서 발생한 에러를 세 번째 then() 함수의 Rejection 콜백 함수가 처리하는 것을 볼 수 있다. 두 번째 then() 함수에서 Rejection 콜백 함수를 등록하지 않았기 때문에 Rejection 콜백 함수를 찾을 때까지 연쇄적으로 에러를 전파한다. 그래서 세 번째 then() 함수의 Rejection 콜백 함수가 호출됐던 것이다.

뿐만 아니라, 네 번째 then() 함수의 Fulfillment 콜백 함수가 null 이기 때문에 이 또한 Fulfillment 콜백 함수를 찾을 때까지 연쇄적으로 Resolve 값을 전파하게 되는데, 마지막 then() 함수의 Fulfillment 콜백 함수가 호출되는 것을 확인할 수 있다.

에러 처리

프라미스에서는 분산-콜백 스타일로 Fulfillment, Rejection 콜백을 지정해 에러를 처리한다.

Promise.reject('버림')
    .then(function (arg) { // Fulfillment
        console.log(arg);
    }, function (error) { // Rejection
        console.error(error);
    });
// 버림

그럼 만약 then() 함수의 Fulfillment, Rejection 콜백에서 에러가 발생한다면 어떻게 될까?

이전에 말했듯이, 에러는 조용히 무시될 것이다. 왜냐하면 then() 함수에 전달한 에러 처리기인 Rejection 콜백은 Promise.reject('버림') 소속이고 Promise.reject('버림') 결과 상태는 '버림'으로 불변이기 때문이다.

따라서, Fulfillment, Rejection 콜백에서 발생한 에러는 then() 함수가 반환한 Promise에서 처리가 가능하다.

Promise.resolve('버림')
    .then(function (arg) {
        console.log(arg);
        throw '에러 던진다.';
    })
    .then(null, function (error) {
        console.error(error);
    });
// 버림
// 에러 던진다.

위와 같이, 발생한 에러를 무시하지 않도록 하는 방법은 없을까?

이러한 문제에 대안으로 Promise 마지막에 catch() 를 추가해 볼 수 있다.

Promise.resolve('버림')
    .then(function (arg) {
        console.log(arg);
        throw '에러 던진다.';
    })
    .then(null, function (error) {
        console.error(error);
    })
    .catch(function(error) {
        console.error(error);
    });
// 버림
// 에러 던진다.

catch() 함수 이전 프라미스 연쇄에서 then() 함수들이 Rejection 콜백을 등록하지 않았다면, 마지막 에러 처리기로서의 catch()는 꽤 납득할만한 대안으로 보인다.

하지만, 여기에도 한 가지 허점이 있는데..

만약catch()에서 에러가 발생하면, catch()가 반환한 프라미스는 방치되게 된다.

이렇게 어떻게 해서든지 에러 처리기가 방치된 프라미스는 항상 존재하게 된다...;

아직까지 명확한 해결책이 강구되지 않은 듯하다..;;;

참고 자료

프라미스 패턴

Promise.all([])

Promise.all([])은 복수의 병렬 작업이 모두 끝날 때까지 대기하고 모든 작업이 끝마쳤을 때 Resolve 값을 배열 형태로 반환한다.

function delay(value, time) {
    return new Promise(function (resolve) {
        setTimeout(function () {
            resolve(value);
        }, time);
    })
}

console.time();
Promise.all([delay(1, 1000), delay(2, 2000), delay(3, 3000)])
    .then(function (response) {
        console.log(response);
        console.timeEnd();
    });
// [ 1, 2, 3 ]
// default: 3020.604ms

만약 배열 인자로 전달한 프라미스 중 단 한 개라도 정상적으로 처리되지 못한다면, Promise.all([])Rejection될 것이고 다른 프라미스 결과도 무효화된다.

function delay(value, time) {
    return new Promise(function (resolve, reject) {
        setTimeout(function () {
            if (value === 2) {
                reject("에러다!");
            } else {
                resolve(value);
            }
        }, time);
    })
}

console.time();
Promise.all([delay(1, 1000), delay(2, 2000), delay(3, 3000)])
    .then(function (response) {
        console.log(response);
    }, function (error) {
        console.error(error);
    })
    .finally(function () {
        console.timeEnd();
    });
// 에러다! [ 1, 2 ]
// default: 2016.295ms

Promise.race([])

Promise.race([])은 하나 이상의 프라미스, 데너블, 즉시값이 포함된 배열을 전달받아서 먼저 Resolve한 값을 반환한다.

function delay(value, time) {
    return new Promise(function (resolve, reject) {
        setTimeout(function () {
            resolve(value);
        }, time);
    })
}

console.time();
Promise.race([delay(1, 1000), delay(2, 2000), delay(3, 3000)])
    .then(function (response) {
        console.log(response);
    }, function (error) {
        console.error(error);
    })
    .finally(function () {
        console.timeEnd();
    });
// 1
// default: 1012.544ms

Promise.race([]) 또한 하나라도 정상적으로 처리되지 못한다면 Rejection된다.

아래와 같이, 첫 번째 비동기 처리가 정상적으로 Resolve된다면, 그외 비동기 처리 결과가 Rejection이더라도 무시되며 첫 번째 결과만 반환된다.

function delay(value, time) {
    return new Promise(function (resolve, reject) {
        setTimeout(function () {
            if (value === 2) {
                reject("에러다!");
            } else {
                resolve(value);
            }
        }, time);
    })
}

console.time();
Promise.race([delay(1, 1000), delay(2, 2000), delay(3, 3000)])
    .then(function (response) {
        console.log(response);
    }, function (error) {
        console.error(error);
    })
    .finally(function () {
        console.timeEnd();
    });
// 1
// default: 1009.797ms

동시 순회

기존의 forEach(), map()과 같이 동기적으로 동작하는 고차 함수를 보면서, 비동기적으로 처리하고 싶다는 생각을 해보았을 것이다.

각 원소를 비동기적으로 처리하는 함수를 직접 구현해볼 수 있다.

프라미스 리스트를 순회하면서 각 작업을 비동기로 처리하고 결과값을 배열로 Resolve하는 예제를 살펴보자.

Promise.map = function (values, cb) {
    const promiseArray = values.map(function (value) {
        return new Promise(function (resolve) {
            cb(value, resolve);
        });
    });

    return Promise.all(promiseArray);
};

const p1 = Promise.resolve(1);
const p2 = Promise.resolve(2);
const p3 = Promise.reject('에러');

Promise.map([p1, p2, p3], function (value, done) {
    Promise.resolve(value)
        .then(function (val) {
            done(val * 2);
        }, function (error) {
            done(error);
        })
}).then(function (response) {
    console.log(response);
});
// [ 2, 4, '에러' ]

각 작업을 비동기적으로 처리한 후에 Resolve 콜백 함수인 done()을 이용해 결과 상태를 반환한다.

뿐만 아니라, Promise.all([])Promise.race([]) 이용해서 다양한 추상화한 함수를 정의해서 사용할 수 있다.

추상화 함수는 해당 링크에서 참고 가능합니다.

프라미스 API

new Promise() 생성자

Promise() 생성자는 항상 new 키워드와 함께 사용하며, 인자에는 동기적으로 즉시 호출할 콜백 함수를 전달해야 한다. Promise() 함수는 프라미스를 Resolve 처리할 두 개의 콜백 함수인 resolve()reject() 함수를 전달한다.

new Promise(function(resolve, reject) {
   // resolve()는 프라미스를 귀결 처리한다.
   // reject()는 프라미스를 버린다. 
});

reject() 함수는 프라미스를 버리지만, resolve() 함수는 전달한 값에 따라 이루거나 버린다.

좀 더 구체적으로 살펴보자.

resolve() 함수는 프라미스(or 데너블) 값이 전달되면 재귀적으로 풀어서 최종적인 값이 특정 상태(Fulfillment, Rejection)로 Resolve하는 프라미스를 반환한다.

Promise.resolve(), Promise.reject()

Promise.reject()Rejection된 프라미스를 반환한다. 따라서, 아래의 코드는 동일하다.

new Promise(function (resolve, reject) {
    reject('error');
});

Promise.reject('error');

Promise.resolve()Promise.reject()와 유사하지만, 프라미스(or 데너블) 값이 전달되면 재귀적으로 풀어보고 최종적인 값이 특정 상태(Fulfillment, Rejection)로 Resolve(귀결)하는 프라미스를 반환한다.

const resolve = {
    then: function (cb, errCb) {
        console.log('Resolve');
        cb('Resolve!!!');
    }
};

const reject = {
    then: function (cb, errCb) {
        console.error('Reject');
        errCb('Reject!!!');
    }
};

Promise.resolve(resolve) // Fulfillment 프라미스가 반환됨
    .then(function (data) {
        console.log(data);
    });

Promise.resolve(reject) // Rejection 프라미스가 반환됨
    .then(null, function (error) {
        console.error(error);
    });
// Resolve
// Reject
// Resolve!!!
// Reject!!!

then(), catch()

프라미스 객체는 then(), catch() 함수를 포함하고 있고, 각 함수에 Fulfillment, Rejection 처리기(콜백 함수)를 등록할 수 있다. 해당 프라미스가 Resolve(귀결)되면 둘 중 하나의 처리기가 호출된다.

then() 함수는 두 인자로 Fulfillment, Rejection 처리기가 등록되는데, 만약 생략한다면 기본 처리기로 대체된다.

또한, catch() 함수는 하나의 인자로 Rejection 처리기를 등록하고, Fulfillment은 기본 처리기가 암묵적으로 등록된다. 즉, then(null, rejection)과 동일하다.

then(), catch() 또한 새 프라미스를 생성해 반환하므로, 연쇄적인 흐름 제어를 할 수 있다.

중요한 점은 Fulfillment 처리기가 프라미스(or 데너블)을 반환하면, 해당 프라미스의 풀어진 최종적인 값이 then() 함수에 의해 새로 생성된 프라미스의 Resolve 값이 된다.

const promise = Promise.resolve(23)
    .then(function (data) {
        // 프라미스 반환
        return new Promise(function (resolve) {
            // 풀어진 최종 값(46)이
            // then() 함수에 의해 생성된 Promise의 귀결 값이 된다.
            resolve(data * 2);
        });
    });

promise
    .then(function (response) {
        console.log(response);
    });
// 46

프라미스 한계

시퀀스 에러 처리

프라미스의 설계 상 한계로 프라미스 연쇄에서 발생하면 에러가 조용히 묻혀버리기 쉽다.

이를 위해, 권장되는 사항이 있지만 이 또한 완벽한 에러 처리를 보장하지는 않는다.

아래 예제를 살펴보자.

function fulfill(response) {
    console.log(response);
}

function reject(error) {
    console.error(error);
}

Promise.reject('Catch 에러')
    .then(fulfill)  // 기본 에러 처리기가 동작하여, 에러를 하위 프라미스로 내려준다.
    .then(fulfill)
    .catch(function (error) {    // catch 에러 처리기 호출된다.
        console.error("Call Catch Function", error);
    });
// Call Catch Function Catch 에러

Promise.reject('Reject 에러')
    .then(fulfill)  // 기본 에러 처리기가 동작하여, 에러를 하위 프라미스로 내려준다.
    .then(fulfill, reject)  // reject 에러 처리기 호출된다.
    .catch(function (error) {    // 호출되지 않음..;
        console.error("Call Catch Function", error);
    });
// Reject 에러

에러 처리기가 없는 프라미스 연쇄에서 에러가 발생하면 하위로 전파되는데, 마지막 프라미스에 catch() 함수를 이용해 에러를 처리할 수 있다.

이러한 방법은 연쇄 어디에서 에러가 나도 이를 받아 처리할 수 있을 것 같지만, 연쇄 어느 단계에서 나름대로의 에러 처리기를 등록해놓으면 catch() 함수는 호출되지 않는다.

단일값

프라미스는 Resolve 상태로는 Fulfillment, Rejection을 갖지만, Resolve 값은 오직 하나이다.

만약 여러 값을 전달하고자 한다면, 객체/배열로 감싸서 전달해야만 하는 불편함이 있다.

function getY(x) {
    return new Promise(function (resolve) {
        setTimeout(resolve, 100, (3 * x) - 1);
    })
}

function foo(bar, baz) {
    const x = bar * baz;
    return [
        Promise.resolve(x),
        getY(x)
    ];
}

Promise.all(foo(10, 20))
    .then(function (response) {
        console.log(response);
    });
// [ 200, 599 ]

Promise.all(foo(10, 20)) 함수의 Resolve 값은 배열 형태로 전달되는 것을 볼 수 있다.

이러한 불편함을 그나마 완충하기 위해서, 아래와 같이 spread() 함수를 구현할 수 도 있고, ES6부터 지원하는 Destructuring assignment 이용할 수 있다.

function getY(x) {
    return new Promise(function (resolve) {
        setTimeout(resolve, 100, (3 * x) - 1);
    })
}

function foo(bar, baz) {
    const x = bar * baz;
    return [
        Promise.resolve(x),
        getY(x)
    ];
}

function spread(fn) {
    return Function.apply.bind(fn, null); // args 인자로 호출하면, fn.apply(null, args)와 동일
}

// spread 구현
Promise.all(foo(10, 20))
    .then(spread(function (x, y) {
        console.log(x, y);
    }));
// 200 599

// Destructuring 이용
Promise.all(foo(10, 20))
    .then(function ([x, y]) {
        console.log(x, y);
    });
// 200 599

참고 자료

콜백에서 프라미스로

기존에 작성했던 콜백 베이스 코드를 프라미스를 이용한 코드로 변경하는 것은 쉬운 일이 아니다.

그래서 콜백 베이스 유틸리티를 감싸는 Promise Wrapper 유틸리티를 구현해 사용할 수도 있다. (콜백식 함수를 Promise Wrapper 헬퍼 유틸리티로 감싸는 것을 Promisify라고 부른다.)

그럼 에러 우선 스타일의 콜백 베이스 유틸리티를 프라미스로 처리해주는 Promise Wrapper 유틸리티를 구현해보자.

Promise.promisify = function (fn) {
    return function () {
        const args = [].slice.apply(arguments);
        return new Promise(function (resolve, reject) {
            fn.apply(null, args.concat(function cb(error, value) {
                if (error) {
                    reject(error);
                } else {
                    resolve(value);
                }
            }));
        });
    }
};

function delay(time, cb) {
    if (time > 1000) {
        cb('1초 이상은 지연할 수 없습니다.');
    } else {
        setTimeout(cb, time, null, '성공!!');
    }
}

const delayOnlyOneMinute = Promise.promisify(delay);

delayOnlyOneMinute(1000).then(console.log, console.error);
// 1초 이상은 지연할 수 없습니다.

delayOnlyOneMinute(1001).then(console.log, console.error);
// 성공!!
profile
한번도 실수하지 않은 사람은, 한번도 새로운 것을 시도하지 않은 사람이다.

0개의 댓글