개인 용도의 프로그램 개발을 위한 기상 정보 크롤링 과정에서 배열에 저장되어 있는 지역 ID에 대한 HTTP Request가 필요했습니다. HTTP Request에는 JavaScript에서 인기가 좋은 Axios를 사용하였는데, Axios는 HTTP Response를 비동기로 처리한 후 Promise로 반환하도록 설계되어 있습니다. JavaScript의 기본 Array prototype에 속한 forEach 메서드는 동기 함수만을 처리하므로 Axios의 HTTP Request가 순차적으로 처리되지 못하는 문제가 있었습니다. 저는 누구나 JavaScript 스크립팅 중 이러한 문제를 빈번하게 겪을 것으로 보고 정보 공유를 위하여 이 글을 작성하였습니다.

핵심

정보를 공유하기 앞서 이 문제를 해결하는 핵심을 요약하였습니다.

기본 흐름

이 문제를 해결하기 위한 보조 함수의 기본 흐름을 먼저 짚어보겠습니다.

  1. forEach와 같이 처리할 배열과 콜백 함수를 입력받습니다.
  2. 배열의 원소를 queue 형태로 처리할 것이므로 원본 배열의 손실을 방지하기 위해서는 입력된 배열을 복제합니다.
  3. 하나의 원소에 대한 콜백 함수의 종료를 확인받는데 사용하게 될 next() 함수를 생성합니다.
  4. next() 함수는 항상 복제된 배열의 첫 번째 원소를, 그리고 다음 원소에 대한 콜백 함수의 호출을 위하여 자기 자신인 next 함수를 인자로 제공합니다.
  5. next() 함수는 콜백 함수를 실행한 직후 복제된 배열에서 첫 번째 원소를 삭제합니다. - arr.shift()
  6. 콜백 함수에서 매개 변수로 받은 next() 함수를 실행하면 첫 번째 원소는 존재하지 않으므로 그 다음 원소가 입력됩니다.
  7. next() 함수가 호출되었을 때 배열에 원소가 존재하지 않으면(= 모든 원소에 대한 반복문 처리의 완료) Promise.resolve()를 반환합니다.

코드 및 주석을 통한 이해

// array: 반복 처리할 배열
// callback(원소, next): 반복 처리를 위한 함수
function asyncForEach(array, callback) {
    // 비동기 함수의 종료를 기다린 후 Promise.resolve()를 반환하기 위한 Promise 선언
    return new Promise(function(resolve) {
        // 입력받은 배열 복제
        let queue = array;
    
        // 콜백 함수를 반복적으로 호출하기 위한 재귀 함수 생성
        function next() {
            // 배열의 원소의 개수가 0개인 경우
            // 더 이상 처리할 작업이 없으므로 resolve를 통한 종료 처리
            if(queue.length == 0)
                resolve();
            
            // 배열의 첫 번째 원소와 다음 원소를 위한 콜백 함수 호출을 위한 next 함수 제공
            // 이때 next()를 두 번째 인자로 제공하면 next 함수 자체 대신
            // next 함수의 처리 결과가 제공되므로 주의 필요
            callback(queue[0], next);

            // 콜백 함수를 호출한 직후 배열에서 첫 번째 원소 제거
            // 기존 원소들은 앞으로 당겨짐
            queue.shift();
        }

        // 첫 원소 처리를 위한 호출
        next();
    });
}

마치며…

이러한 방법은 다량의 웹 페이지를 크롤링하는 경우에 많은 편의를 제공할 수 있을 것으로 기대하고 있습니다. 더 빠른 사용을 위해 NPM에 asyncForEach라는 Array Prototype 함수를 제공하는 모듈을 배포하였습니다. 필요하신 독자분은 참고 바랍니다.

더 좋은 비동기 함수의 순차 실행 방법이 있다면 새 글을 통하여 공유해 보겠습니다.