네이버에서 일할 당시 알 수 없는 버그 때문에 곯머리를 앓은 적이 있다.
forEach 내에서 루프를 돌며 비동기 구문을 처리하려 했다.
대강 이런 식의 코드였다.
console.log("start Analysis")
...
urls.forEach(url => {
const screenEmulation = ...
await exec( ... )
.then((message) => {
logger.debug(message)
})
.catch((error) => {
logger.debug(error)
})
})
...
console.log("finish Analysis", Date())
그러나 순서대로 작동할 것이라는 예상과는 달리 콘솔에는 다음과 같이 출력이 됐다.
start Analysis
finish Analysis
...
코드 실행 간의 순서가 보장되지 않았다.
다시 말해 첫 번째 console.log 실행 뒤 forEach의 루프를 모두 완료한 뒤 두 번째 console.log가 실행되었어야 했다.
그러나 예상과는 달리 forEach 루프가 완료되기 전에 다음 console.log가 실행되고만 것인데,
구글링으로 열심히 찾아본 결과, 다음과 같은 결론을 얻을 수 있었다.
forEach는 내부에서 비동기 구문을 실행하는 것까지만 보장하고, resolve 반환을 보장하지는 않는다.
물론 비동기 구문에만 한정된 동작은 아니고 코드의 실행까지만 보장할 뿐, 그게 비동기 구문이라면 resolve 반환까지는 기다려주지 않는다는 뜻이다.
대신 for of, map 등으로 문제를 해결할 수 있다.
가장 먼저 머릿 속에 떠오른 의문은 "왜?"였다.
javascript의 시스템 레벨에서 막은 것인지 알기 위해 ECMAscript 스펙을 조사해보기도 하고
forEach 소스코드를 다른 루프 메소드의 소스코드와 비교해보기도 했지만 아직 결론에 다다르진 못했다.
다음을 기약하고자, 일단 소스코드를 저장해두겠다.
// Production steps of ECMA-262, Edition 5, 15.4.4.19
// Reference: http://es5.github.io/#x15.4.4.19
if (!Array.prototype.map) {
Array.prototype.map = function(callback, thisArg) {
var T, A, k;
if (this == null) {
throw new TypeError(' this is null or not defined');
}
// 1. Let O be the result of calling ToObject passing the |this|
// value as the argument.
var O = Object(this);
// 2. Let lenValue be the result of calling the Get internal
// method of O with the argument "length".
// 3. Let len be ToUint32(lenValue).
var len = O.length >>> 0;
// 4. If IsCallable(callback) is false, throw a TypeError exception.
// See: http://es5.github.com/#x9.11
if (typeof callback !== 'function') {
throw new TypeError(callback + ' is not a function');
}
// 5. If thisArg was supplied, let T be thisArg; else let T be undefined.
if (arguments.length > 1) {
T = thisArg;
}
// 6. Let A be a new array created as if by the expression new Array(len)
// where Array is the standard built-in constructor with that name and
// len is the value of len.
A = new Array(len);
// 7. Let k be 0
k = 0;
// 8. Repeat, while k < len
while (k < len) {
var kValue, mappedValue;
// a. Let Pk be ToString(k).
// This is implicit for LHS operands of the in operator
// b. Let kPresent be the result of calling the HasProperty internal
// method of O with argument Pk.
// This step can be combined with c
// c. If kPresent is true, then
if (k in O) {
// i. Let kValue be the result of calling the Get internal
// method of O with argument Pk.
kValue = O[k];
// ii. Let mappedValue be the result of calling the Call internal
// method of callback with T as the this value and argument
// list containing kValue, k, and O.
mappedValue = callback.call(T, kValue, k, O);
// iii. Call the DefineOwnProperty internal method of A with arguments
// Pk, Property Descriptor
// { Value: mappedValue,
// Writable: true,
// Enumerable: true,
// Configurable: true },
// and false.
// In browsers that support Object.defineProperty, use the following:
// Object.defineProperty(A, k, {
// value: mappedValue,
// writable: true,
// enumerable: true,
// configurable: true
// });
// For best browser support, use the following:
A[k] = mappedValue;
}
// d. Increase k by 1.
k++;
}
// 9. return A
return A;
};
}
(function () {
if (!Array.prototype.forEach) {
Array.prototype.forEach = function forEach (callback, thisArg) {
if (typeof callback !== 'function') {
throw new TypeError(callback + ' is not a function');
}
var array = this;
thisArg = thisArg || this;
for (var i = 0, l = array.length; i !== l; ++i) {
callback.call(thisArg, array[i], i, array);
}
};
}
})();
-출처-