*비동기 처리 - 특정 코드의 실행이 완료될 때까지 기다리지 않고 다음 코드를 먼저 수행함. |
Promise 객체는 ES6에 도입된 기능으로 비동기 작업에 대한 결과(실패, 성공)를 나타내는 객체이다.
기본적으로 Promise는 함수에 콜백을 전달하는 대신 콜백을 첨부하는 방식의 객체이다.
const myPromise = new Promise((resolve, reject) => {
// code here
})
[예시1]
비동기로 음성파일을 생성해주는 createAudioAsync(음성 설정 정보, 음성파일 생성 시 콜백함수, 음성파일 생성 실패 시 콜백함수)라는 함수가 있다고 가정해보자.
// createAudioAsync() - 비동기로 음성 파일을 생성해주는 함수
// audioSetting(음성 설정 정보), successCallback(성공 시에 실행되는 콜백),
// failureCallback(error 발생 시 실행되는 콜백)
function successCallback(result) {
console.log("Audio file ready at URL: " + result);
}
function failureCallback(result) {
console.log("Error generating audio file: " + error);
}
// 콜백함수로 비동기 처리
createAudioAsync(audioSettings, successCallback, failureCallback);
// promise로 비동기 처리
createAudioAsync(audioSettings).then(successCallback, failureCallback);
// or
const myPromise = createAudioAsync(audioSettings);
myPromise.then(successCallback, failureCallback);
[예시 2]
promise는 주로 서버에서 받아온 데이터를 화면에 표시할 때 사용한다. 일반적으로 웹 애플리케이션을 구현할 때 서버에서 데이터를 요청하고 받아오기 위해 ajax 통신 코드를 예시로 살펴보자.
// jQuery의 ajax 통신 API를 이용하여 지정된 url에서 1번 상품 데이터를 받아오는 코드
// 콜백함수로 비동기 처리
function getData(callbackFunc) {
$.get('url/products/1', function(response) {
callbackFunc(response); // 서버에서 받은 데이터 response를 callbackFunc() 함수에 넘겨줌
});
}
getData(function(tableData) {
console.log(tableData); // $.get()의 response 값이 tableData에 전달됨
});
// Promise로 비동기 처리
function getData(callback) {
// new Promise() 추가
return new Promise(function(resolve, reject) {
$.get('url/product/1', function(response) {
// 데이터를 받으면 resolve() 호출
resolve(response);
});
});
}
// getData()의 실행이 끝나면 호출되는 then()
getData().then(function(tableData) {
// resolve()의 결과 값이 여기로 전달됨
console.log(tableData); // $.get()의 response 값이 tableData에 전달됨
});
콜백 함수로 처리하던 구조에서 newPromise(), resolve(), then()과 같은 Promise API를 사용한 구조로 바뀌었다.
비동기 함수의 특징
✨ Guarantees
콜백 함수를 전달해주는 고전방식과는 다르게 Promise는 다음과 같은 특징을 보장한다.
- 콜백은 자바스크립트 Event Loop가 현재 실행중인 콜 스택을 완료하기 이전에 절대 호출되지 않는다.
- 비동기 작업이 성공하거나 실패한 뒤 then()을 이용하여 추가한 콜백의 경우에도 위와 같다.
- then()을 여러번 사용하여 여러개의 콜백을 추가할 수 있다. 그리고 각각의 콜백은 주어진 순서대로 하나 하나 실행하게 된다.
✨ Chaining
보통 두 개 이상의 비동기 작업을 순차적으로 실행해야 하는 상황 즉, 각각의 작업이 이전 단계 비동기 작업이 성공하고 나서 그 결과값을 이용하여 다음 비동기 작업을 실행해야 하는 경우 Promise chain을 사용한다.
const myPromise = doSomething();
const myPromise2 = myPromise.then(successCallback, failureCallback);
or
const myPromise2 = doSomething().then(successCallback, failureCallback);
then() 함수는 새로운 promise를 반환한다. 처음 만든 Promise와는 다른 새로운 Promise이다.
Promise2는 doSomething() 뿐만 아니라 successCallback 이나 failureCallback의 완료를 의미한다. 여기서 successCallback이나 failureCallback은 promise를 반환하는 다른 비동기 함수일 수 있다.
이 경우, promise2에 추가된 콜백은 successCallback 또는 failureCallback에 의해 반환된 Promise 뒤에 대기한다.
기본적으로 각각의 Promise는 체인 안에서 서로 다른 비동기 단계의 완료를 나타낸다.
promise 이전에는 여러 비동기 작업을 연속적으로 수행하기 위해 콜백지옥이 만들어져야 했다.
[콜백 지옥 예시]
doSomething(function (result) {
doSomethingElse (result, function(newResult) {
doThirdThing(newResult, function(finalResult) {
console.log('Got the final Result: ' + finalResult);
}, failureCallback);
}, failureCallback);
}, failureCallback);
[Promise chain]
doSomething().then(function(result) {
return doSomethingElse(result);
})
.then(function(newResult) {
return doThirdThing(newResult);
})
.then(function(finalResult) {
console.log('Got the final result: ' + finalResult);
})
.catch(failureCallback);
then에 넘겨지는 인자는 선택적이며, catch(failureCallback)는 then(null, failureCallback)의 축약이다.
[Promise chain - 화살표 함수]
doSomething()
.then(result => doSomethingElse(result))
.then(newResult => doThirdThing(newResult))
.then(finalResult => {
console.log(`Got the final result: ${finalResult}`);
})
.catch(failureCallback);
🔥 주의할 점
반환값이 반드시 있어야 한다. 반환값이 없을 경우, 콜백 함수가 이전의 promise의 결과를 받지 못함(화살표 함수() => x는 () => {return x;}와 같다. 첫번째 핸들러가 Promise를 시작했지만 결과 값을 반환하지 않으면 더 이상의 해결방법은 없으며, Promise는 Floating이 된다.
doSomething()
.then((url) => {
// I forgot to return this...
fetch(url);
})
.then((result) => {
// result is undefined, because nothing is returned from the previous handler
// There's no way to know the return value of the fetch()
// call anymore, or whether it succeeded at all.
});
Promise의 반환값이 다음 콜백을 실행하는 데에 필요한 값이라면 상황이 더 나빠질 수 있다. 마지막 핸들러의 Promise가 반환되지 않으면 다음 then이 일찍 호출되게 되고 읽은 값이 불완전할 수 있다.
const listOfIngredients = [];
doSomething()
.then((url) => {
// I forgot to return this...
fetch(url)
.then((res) => res.json())
.then((data) => {
listOfIngredients.push(data);
});
})
.then(() => {
console.log(listOfIngredients);
// Always [], because the fetch request hasn't completed yet.
});
그러므로 경험삼 코드가 Promise를 만날 때마다 Promise(결과값)를 반환하고 처리를 다음 then에게 맡기도록 하자.
const listOfIngredients = [];
doSomething()
.then((url) => {
return fetch(url)
.then((res) => res.json())
.then((data) => {
listOfIngredients.push(data);
});
})
.then(() => {
console.log(listOfIngredients);
});
or
doSomething()
.then((url) => fetch(url))
.then((res) => res.json())
.then((data) => {
listOfIngredients.push(data);
});
.then(() => {
console.log(listOfIngredients);
});
- Chaining after a catch
Chain에서 작업이 실패한 후, 새로운 작업을 수행하는 것이 가능하다.
new Promise((resolve, reject) => {
console.log('Initial');
resolve();
})
.then(() => {
throw new Error('Something failed');
console.log('Do this');
})
.catch(() => {
console.log('Do that');
})
.then(() => {
console.log('Do this, whatever happened before');
});
실행 결과.
Initial
Do that
Do this, whatever happened before
"Something failed" 에러가 rejection을 발생했기 때문에 "Do this"는 출력되지 않음.
✨ Error propagation
콜백 지옥에서는 failureCallback이 3번 발생했지만 Promise chain에서는 단 한번 발생한다.
doSomething()
.then(result => doSomethingElse(result))
.then(newResult => doThirdThing(newResult))
.then(finalResult => console.log(`Got the final result: ${finalResult}`))
.catch(failureCallback);
기본적으로 Promise chain은 예외가 발생하면 멈추고 chain 아래에서 catch를 찾는다. 이것은 동기 코드가 어떻게 동작하는 지 모델링 한 것이다.
try {
const result = syncDoSomething();
const newResult = syncDoSomethingElse(result);
const finalResult = syncDoThirdThing(newResult);
console.log(`Got the final result: ${finalResult}`);
} catch(error) {
failureCallback(error);
}
비동기 코드를 사용한 이러한 배치는 ECMAScript 2017의 async/await로 더 편하게 사용할 수 있게 되었다.
async function foo() {
try {
const result = await doSomething();
const newResult = await doSomethingElse(result);
const finalResult = await doThirdThing(newResult);
console.log(`Got the final result: ${finalResult}`);
} catch(error) {
failureCallback(error);
}
}
이것은 Promise를 기반으로 하면 doSomething()은 이전 함수와 같다.
Promise는 모든 오류를 잡아내며, 예외 및 프로그래밍 오류가 발생해도 콜백 지옥의 근본적인 결함을 해결한다. 이는 비동기 작업의 기능 구성에 필수적인 기능이다.
✨ Promise rejection events
Promise가 reject될때마다 이벤트가 전역 범위에 발생한다.(일반적으로 전역범위는 window이거나, 웹워커에서 사용되는 경우, worker 혹은 워커 기반 인터페이스이다)
이벤트는 다음과 같이 두가지가 있다.
rejectionhandled
executor의 reject 함수에 의해 reject가 처리된 후 promise가 reject 될때 발생한다.
unhandledrejection
promise가 reject되었지만 사용할 수 있는 reject 핸들러가 없을 때 발생한다.
두 이벤트에는 변수로 reject된 promise를 가리키는 속성인 promise와 promise가 reject된 이유를 알려주는 속성인 reason이 있다.
이것을 이용해 promise에 대한 에러 처리를 대체(fallback)하는 것이 가능해지며, 또한 Promise 관리 시 발생하는 이슈들을 디버깅하는 데 도움을 얻을 수 있다. 이 핸들러들은 모든 맥락에서 전역적이기 때문에 모든 에러는 발생한 지점(source)에 상관없이 동일한 핸들러로 전달된다.
** 유용한 사례
Node.js로 코드를 작성할 때, 흔히 프로젝트에서 사용하는 모듈이 reject된 promise를 처리하지 않을 수 있다. 이런 경우 실행 히 콘솔에 로그가 남으므로 이를 수집해서 분석하고 직접 처리할 수 있다. 다음과 같이 unhandledrejection 이벤트를 처리하는 핸들러를 추가해주면 된다.
window.addEventListener("unhandledrejection", event => {
/* You might start here by adding code to examine the promise specified by event.promise
and the reason in event.reson */
event.preventDefault();
}, false);
이벤트의 preventDefault() 메서드를 호출하면 reject된 promise가 처리되리 않았을 때 JavaScript 런타임이 기본 동작을 수행하지 않는다. 이 기본 동작은 대개 콘솔에 오류를 기록하는 것이기 때문에 이것은 확실히 Node.js를 위한 것이라고 할 수 있다.
하지만 제대로하려면 이 이벤트를 무시해버리기 전에 reject된 promise 코드가 실제로 버그가 없는지 확실히 검사하는 것이 좋다.
✨ 오래된 콜백 API를 사용해서 Promise 만들기
오래된 API를 감싸려면 생성자를 사용하여 Promise를 처음부터 생성할 수 있다.
이상적인 프로그래밍 세계에서는 모든 비동기 함수는 promise를 반환해야 하지만, 불행히도 일부 API는 여전히 success 및 failure콜백을 전달하는 방식이다. 예를 들면, setTimeout()함수가 있다.
setTimeout(() => saySomething("10 seconds paseed"), 10000);
예전 스타일의 콜백과 Promise를 합치는 것은 문제가 있다. 함수 saySomething()이 실패하거나 프로그래밍 오류가 있으면 아무것도 잡아내지 않는다. 이것은 setTimeout의 문제점이다.
다행히도 setTimeout을 promise로 감쌀 수 있다. 가장 좋은 방법은 가장 낮은 수준에서 문제가 되는 함수를 감싼 다음 다시 직접 호출하지 않는 것이다.
const wait = ms => new Promise(resolve => setTimeout(resolve, ms));
wait(10000).then(() => saySomething("10 seconds")).catch(failureCallback);
기본적으로 promise constuctor는 promise를 직접 해결하거나 reject할 수 있는 실행자 함수를 사용한다.
setTimeout()은 함수에서 fail이 일어나거나 error가 발생하지 않기 때문에 이 경우 reject를 사용하지 않는다.
✨ Composition
Promise.resolve()와 Promise.reject()는 각각 이미 resolve되거나 reject된 Promise를 직접 생성하기 위한 바로 가기이다.
Promise.all()과 Promise.race()는 비동기 작업을 병렬로 실행하기 위한 두가지 구성 도구이다. 이것은 병렬로 작업을 시작하고 다음과 같이 모두 완료될때까지 기다릴 수 있다.
Promise.all([func1(), func2(), func3()])
.then(([result1, result2, result3]) => {
/* use result1, result2, result3 */
});
Javascript를 이용해 순차적 구성이 가능하다.
[func1, func2, func3].reduce((p,f) => p.then(f), Promise.resolve())
.then(result3 -> { /* use result3 */});
기본적으로 비동기 함수 배열은 다음과 같은 promise 체인으로 줄인다.
Promise.resolve().then(func1).then(func2).then(func3);
이것을 재사용 가능한 합성 함수로 만들 수 있는데 이는 함수형 프로그래밍에서 일반적인 방식이다.
const applyAsync = (acc, val) => acc.then(val);
const composeAsync = (...funcs) => x => funcs.reduce(applyAsync, Promise.resolve(x));
composeAsync() 함수는 여러 함수를 인수로 받아들이고 composition 파이프 라인을 통해 전달되는 초기값을 허용하는 새 함수를 반환한다.
const transformData = composeAsync(func1, func2, func3);
const result3 = transformData(data);
ECMA2017에서는 async/await를 사용하여 순차적 구성을 보다 간단하게 수행할 수 있다.
let result;
for(const f of [func1, func2, func3]) {
result = await f(result);
}
/* use last result(i.e. result3) */
✨ Timing
예기치 못한 상황(에러 또는 코드의 문제)을 피하기 위해 then()에 전달된 함수는 already-resolved promise에 있는 경우에도 동기적으로 호출되지 않는다.
Promise.resolve().then(() => console.log(2));
console.log(1); // 1, 2
즉시 실행되는 대신 전달된 함수는 마이크로 태스크 대기열에 저장된다. 즉, 자바스크립트 이벤트 루프의 현재 실행이 끝나고, 대기열도 비어 있을 때 제어권이 이벤트 루프로 반환되기 직전에 실행된다.
const wait = ms => new Promise(resolve => setTimeout(resolve, ms));
wait().then(() => console.log(4));
Promise.resolve().then(() => console.log(2)).then(() => console.log(3));
console.log(1); // 1, 2, 3, 4
✨ Nesting
간단한 Promise 체인은 평평하게 유지하는 것이 좋다. 중첩된 체인은 부주의한 구성의 결과일 수 있다. 잠시 뒤 설명할 Common mistakes를 참조하길 바란다.
중첩은 catch 문 범위를 제한하는 제어 구조이다. 특히, 중첩된 catch는 중첩된 범위 외부의 체인에 있는 오류가 아닌 범위 및 그 이하의 오류만 잡는다. 올바르게 사용하면 오류 복구 시 더 정확한 결과를 얻을 수 있다.
doSomethingCritical()
.then(result =>
doSomethingOptional(result)
.then(optionalResult => doSomethingExtraNice(optionalResult))
.catch(e => {})
) // Ignore if optional stuff fails; preceed.
.then(() => moreCriticalSturff())
.catch( e => console.log("Critical failure: " + e.message));
여기 있는 선택적 단계는 들여쓰기가 아닌 중첩되어 있지만 주위의 바깥 쪽 "(" 및 ")"의 규칙적이지 않은 배치를 하지 않도록 조심해야 한다.
중첩 내부의 catch 문은 doSomething Optional() 및 doSomethingExtraNice() 에서 발생한 오류를 catch한 후 코드가 moreCriticalStuff()로 다시 시작된다. 중요한 것은 doSomethingCritical()이 실패하면 해당 오류는 최종(외부) catch에서만 포착된다는 것이다.
✨ Common mistakes
Promise chain을 작성할 때 주의해야할 몇가지 일반적인 실수 예제
// Bad example! Spot 3 mistakes!
doSomething().then(function(result) {
doSomethingElse(result) // Forgot to return promise from inner chain + unnecessary nesting
.then(newResult => doThirdThing(newResult));
}
).then(() => doFourthThing());
// Forgot to terminate chain with a catch!
1. 제대로 체인을 연결하지 않았다. 새로운 Promise를 만들었지만 그것을 반환하는 것을 잊었을 때 일어나는 실수이다.
결과적으로 체인이 끊어지거나 오히려 두 개의 독립적인 체인이 경쟁하게 된다. 즉, doFourthThing()은 doSomethingElse() 또는 doThirdThing()이 완료될때까지 기다리지 않고 우리가 의도하지 않았지만 병렬로 실행된다. 또한, 별도의 체인은 별도의 오류 처리 기능을 가지고 있어서 잡기 어려운 오류가 발생한다.
2. 불필요한 중첩으로 실수가 발생했다.
3. 내부 오류 처리의 범위가 중첩으로 인해 제한되어 의도와 상관없이 캐치되지 않는 오류가 발생할 수 있다.
4. catch로 체인을 종료하지 않았다. 종료되지 않은 Promise 체인은 대부분의 브라우저에서 예상치 못한 Promise rejection을 초래한다.
이 같은 실수를 예방하기 위해 항상 Promise 체인을 반환하거나 종결하고, 새로운 Promise를 얻자마자 즉시 반환하여 복잡도를 낮추어야 한다.
doSomething()
.then(function(result) {
return doSomethingElse(result);
})
.then(newResult => doThirdThing(newResult))
// Even if the previous chained promise returns a result, the next one doesn't necessarily have to use it. You can pass a handler that doesn't consume any result
.then((/* result ignored */) => doFourthThing())
.catch(error => console.log(error));
async/await을 사용하면 대부분의 문제를 해결할 수 있다. 이러한 문법의 가장 흔한 실수는 await 키워드를 빼먹는 것이므로 주의해야한다.
✨ Promise와 작업이 충돌할 때
예측할 수 없는 순서로 실행되는 promise 및 작업(이벤트 또는 콜백)이 있는 상황에 직면하면 마이크로 태스크를 사용하여 상태를 확인하거나 promise가 조건부터 생성될 때 promise의 균형을 맞추는 것이 좋다.
마이크로 태스크가 이 문제를 해결하는 데 도움이 될 수 있다고 생각되면 queueMicrotask()를 참조해보기 바란다.
- queueMicrotask() - window 또는 worker 인터페이스에서의 메서드로 브라우저 이벤트 루프로 통제권이 넘어가기 전, 안전한 시점에 실행할 마이크로태스크를 큐에 추가하는 메서드
- 마이크로태스크 가이드
'javascript' 카테고리의 다른 글
표준 빌트인 객체(standard built-in objects/native objects/global objects) (0) | 2022.07.28 |
---|---|
part 2. Promise 사용법 (0) | 2022.07.14 |
[ES6] Spread Operator(...) (0) | 2022.07.12 |
배열의 중복 제거 (0) | 2022.06.23 |
정규식 모음 (0) | 2022.06.03 |