forEach와 Promise.all

박종대·2022년 9월 22일
0

Javascript / Typescript

목록 보기
1/4

ForEach의 동작

먼저 예시 프로그램을 하나 보겠습니다.

function test() {
    const testFunction = (num) =>
        new Promise((resolve) => setTimeout(() => resolve(`${num}`), num));
  
  	const list = [3000, 2000, 3000, 4000, 1000, 1000, 2000, 3000, 1000, 1000];
    
     list
        .forEach((val) => {
            const result = testFunction(val);
            result.then(console.log);
        }) 
}

test()

list에는 순서대로 timeout의 값들이 들어가 있고 forEach를 통해 순회하면서 testFunction을 실행하여 해당 결과를 출력해주는 프로그램입니다.

순서대로 반복한다는 점에서 개발자는 3000, 2000, 3000, 4000, 1000, 1000, 2000, 3000, 1000, 1000의 순서대로 결과가 출력된다는 것을 예측할 수 있습니다.

그런데 순서가 이상합니다. 숫자가 작은 것부터 큰 것 순으로 마치 정렬된 것 처럼 결과가 출력되었습니다.

Array.prototype.forEach = function (callback) {
  for (let index = 0; index < this.length; index++) {
    callback(this[index], index, this);
  }
};

forEach는 해당 코드와 같이 동작합니다. list의 요소를 순회하면서 callback 함수를 ‘호출’합니다. callback 함수에 비동기 로직이 존재한다면? 이전 callback 함수의 결과를 받지도 않고 다음 callback을 호출하기 때문에 순서가 지켜질 수 없는 구조입니다. 그럼 이전 프로그램의 문제를 보겠습니다.

forEach문은 다음과 같이 콜백함수 내부에서 testFunction을 호출합니다.

testFunction(3000); -> 콜백 함수를 '호출'만 할 뿐 결과를 받지 않고 다음 testFunction 실행
testFunction(2000); -> 동일
testFunction(3000); -> 동일
testFunction(4000); -> ...
testFunction(1000);
testFunction(1000);
testFunction(2000);
testFunction(3000);
testFunction(1000);
testFunction(1000);

다음과 같이 10번의 testFunction이 ‘호출’ 됩니다. 다만 호출만 하고 다음 호출로 넘어가기 때문에 이 열번의 testFunction은 ‘거의’ 동시에(완벽한 동시는 당연히 아님) 실행됩니다. 그래서 시간 순서대로 결과가 출력된 것을 확인할 수 있습니다.

하지만 우리는 ‘호출 순서’ - ‘결과 순서’ 가 동일하기를 원합니다. 프로그램을 수정하겠습니다.

async function test() {
    const testFunction = (num) =>
        new Promise((resolve) => setTimeout(() => resolve(`${num}`), num));
  
  	const list = [3000, 2000, 3000, 4000, 1000, 1000, 2000, 3000, 1000, 1000];
    
     
     for(let i = 0; i < list.length; i++){
     		const result = await testFunction(list[i]);
				console.log(result);
     }
}

test();

이제 forEach가 아닌 단순 for문으로 변경했습니다.

이제 원하는 순서대로 출력됩니다! 이제는 반복문의 각 단계에서 testFunction의 결과를 받을때까지 기다리기(await) 때문입니다.

그런데 뭔가 좀 아쉽습니다. 위의 forEach 에서 처럼 testFunction 호출은 ‘거의’ 동시에 해놓고 결과는 순서대로 받아올 수 있다면 더 효율적이지 않을까 하는 고민입니다.

for문으로 해결한 방식으로는 3+2+3+4+1+1+2+3+1+1= 21초만큼 기다려야 하지만 만약 위의 방식이 가능하다면 4초만 기다리고도 모든 결과를 받아올 수 있기 때문입니다.

프로그램을 조금 더 수정해보겠습니다.

function test() {
    const testFunction = (num) =>
        new Promise((resolve) => setTimeout(() => resolve(`${num}`), num));
  
  	const list = [3000, 2000, 3000, 4000, 1000, 1000, 2000, 3000, 1000, 1000];
    
     Promise.all(list.map((element) => 
     		testFunction(element)
     )).then(console.log)
     	 .catch(console.log);
}

test();

이 프로그램은 어떨까요? map 메소드를 활용하여 testFunction의 반환값인 Promise 객체를 가지고 있게 됩니다. 다만 결과값을 아직 가져오지 못했다면 pending 상태가 되겠습니다. 하지만 호출 순서는 보장되기 때문에 결과값을 가져온 순서에는 영향을 받지 않게 됩니다.

Promise.all은 인자로 받은 Promise 배열이 모두 resolve 상태가 되면 then의 콜백함수를 실행하게 됩니다. 그래서 결과를 보면

4초만에 결과를 얻어올 수 있었을 뿐더러 순서까지 보장이 되었습니다. Promise 객체를 순서대로 배열에 저장했기 때문에 가능한 일이었습니다.

Promise.all → map 메소드 활용 패턴과 forEach문의 동작은 기억하면 좋을 것 같습니다.

Multi query execute에서의 forEach와 Promise.all

해당 주제 고민에 대한 배경은 회사에서 Multi query execute 기능 개발을 담당했을 때입니다.
query를 비동기로 실행하고 response 순서를 보장해야 했기 때문에 공부가 필요하다고 생각했습니다. 하지만 DB단으로 넘어가면 사실상 의미는 크게 없는 부분이었습니다.

Promise.all → map 메소드 활용을 통해 쿼리를 실행하여 순서대로 결과를 받아온다고 가정하겠습니다. 이 때 10개의 쿼리를 작성하고 Run 버튼을 클릭하면 ‘거의’ 동시에 Execute(query) API가 10번 실행될 것입니다. 하지만 DB쪽에서는 Connection 객체 하나에 대해 여러 개의 execute 요청을 할 수 없습니다. 그렇다고 오류는 나지 않습니다. 알아서 이전 요청이 끝날때까지 기다려 주기 때문입니다.

다만 프론트에서는 거의 동시에 multi query 실행 요청을 보냈지만 DB에서는 결국 하나의 query 실행을 기다렸다가 다음 쿼리를 실행하는 for문의 동작이 되어 버린 것입니다.

결론적으로는 for문을 사용하나 Promise.all → map 패턴을 사용하나 동일한 시간이 걸리고 동일하게 동작을 하게 될 것입니다. 이는 DB의 동작이니 어쩔 수 없지만 for문, forEach, Promise.all에 대한 공부는 확실하게 할 수 있었습니다!

profile
Frontend Developer

1개의 댓글

comment-user-thumbnail
2023년 11월 22일

잘 읽고 갑니다. 감사합니다~

답글 달기