본문 바로가기
Programming/JavaScript

Promise 추가 내용

 

Promise.all([ ])

배열 안의 Promise를 동시에 브라우저에 요청(JS 자체 병렬처리 아님!)하고, 전부 완료되면 결과를 배열로 반환

 

// 배열 안에 요청할 것 자유롭게 넣을 수 있음
Promise.all([
    fetch("/api/users"),                    // fetch
    new Promise(resolve => ...),            // 직접 만든 Promise
    someAsync(),                            // async 함수 호출
    Promise.resolve({ value: [] }),         // 즉시 이행된 Promise
    axios.get("/api/data"),                 // axios 등 라이브러리
    42,                  // 그냥 숫자
    "hello"             // 그냥 문자열
])

// 그냥 숫자 또는 문자열은 Promise.resolve(42), Promise.resolve("hello") 로 자동 처리됨

 

 

유의점

하나라도 reject되면 나머지 결과와 상관없이 즉시 전체 실패. catch로 처리하거나 개별 Promise에 폴백을 달아야 함.
순수 JS 연산(CPU 작업)은 효과 없음. 브라우저/OS에 위임되는 I/O 작업에서만 시간 단축됨.

 * 순수 작업: web Worker로 병렬처리

 

 

 

실전예제1

1) 안의 함수 동시 요청/ 실행하기

        async loadReviews(placeId) {
            this._placeId = placeId;
            this._page = 1;
            this._clearPanel();
            await Promise.all([
                this._fetchReviews(),
                this._fetchStats()
            ]);
        }

 

 

2) .map()으로 계산된 결과(배열)을 입력해서 요청하기

* .map() 배열 메서드: 배열내 요소를 연산해서 새 배열 반환

const numbers = [1, 2, 3];
const doubled = numbers.map(n => n * 2);
// doubled = [2, 4, 6]

 

        _resolveImageUrls: async function (aImages, jwt) {
            return Promise.all(aImages.map(async (img, i) => {  // .map() 안 콜백 async여야 await 사용 가능
                if (img.isExisting) {
                    return { ID: img.ID, imgurl: img.imgurl, sort_order: i };
                }
                const formData = new FormData();
                formData.append("file", img.file);
                const res = await fetch(`${BASE}/uploadImage`, {
                    method: "POST",
                    headers: { "Authorization": `Bearer ${jwt}` },
                    body: formData
                });
                if (!res.ok) throw new Error(this._i18n.getText("reviewImageFailMsg"));
                const { url } = await res.json();
                return { imgurl: url, sort_order: i };
            }));
        },

 

 

 

 

 

 

 


 

 

 

 

Promise.resolve(값)

처음부터 성공 상태인 Promise를 즉시 반환. 로직 없이 값을 Promise형태로 포장해서 출력할 때 사용.

Promise.resolve(42)
Promise.resolve({ value: [] })
Promise.resolve("완료")

 

유의점

pending 과정 없이 즉시 fulfilled. await해도 기다리는 시간 없이 바로 값이 나옴.

 

 

 

Promise.reject(에러)

처음부터 실패 상태인 Promise를 즉시 반환. 로직 없이 의도적으로 에러를 던질 때 사용.

await Promise.reject(new Error("실패")); // await가 throw처럼 동작
Promise.reject(new Error("권한 없음"))
Promise.reject("잘못된 요청")

 

 

유의점

반드시 뒤에 .catch() 또는 try/catch로 처리구간 필수. 처리 안 하면 "UnhandledPromiseRejection" 경고 또는 오류 발생.

 

Promise.reject(new Error("실패"))
    .catch(err => console.log(err));

 

async function fn() {
    try {
        await Promise.reject(new Error("실패")); // 여기서 던짐
    } catch (err) {
        console.log(err); // 여기서 잡음
    }
}

 

 

 

// ❌ 잘못된 사용
async function fn() {
    await Promise.reject(new Error("실패"));
    console.log("여기는 실행 안 됨");
}

fn(); // UnhandledPromiseRejection 경고 발생

 

 

즉, Promise 뒤에 .resolve() 나 .reject() 가 나오면 () 안의 내용물을 Promise형식의 결과로 반환하는 것


 

 

 

 

실전 예제2

 

        // ── 내부: fetch ────────────────────────────────────────

        async _fetchAllBookmarks() {
            this._controller.getView().setBusy(true);
            const jwt    = sessionStorage.getItem("jwt");
            const userId = sessionStorage.getItem("userId");

            try {
                // 북마크 + 내 리뷰 목록 동시 조회 요청
                const [bmRes, rvRes] = await Promise.all([
                    fetch(
                        `/jmt/PersonalMarkers`
                        + `?$filter=user_ID eq '${userId}'`
                        + `&$expand=restaurant($select=ID,place_name,category_name,`
                        + `address_name,road_address_name,latitude,longitude,phone,place_url)`
                        + `&$orderby=createdAt desc`,
                        { headers: { "Authorization": `Bearer ${jwt}` } }
                    ),
                    fetch(
                        `/jmt/Reviews?$filter=createdBy eq '${userId}'&$select=restaurant_ID`,
                        { headers: { "Authorization": `Bearer ${jwt}` } }
                    )
                ]);
                if (!bmRes.ok) throw new Error(await bmRes.text());
                const [bmData, rvData] = await Promise.all([
                    bmRes.json(),
                    rvRes.ok ? rvRes.json() : Promise.resolve({ value: [] })
                ]);
                const reviewedIds = new Set(rvData.value.map(r => r.restaurant_ID));

                this._allBookmarks = bmData.value.map(b => {
                    const { id, x, y, ...r } = restaurant.buildPlace(b.restaurant || {});
                    const categoryClean = r.category_name.replace(/^음식점\s*>\s*/, "");
                    const categoryFirst = categoryClean.split(">")[0].trim();

                    return {
                        _markerId:          b.ID,
                        restaurant_ID:      r.ID,
                        ...r,
                        categoryDisplay:    categoryClean,         // 리스트 표시용 (음식점> 제거 후 전체 경로)
                        categoryFirst,                             // 필터 판단용
                        displayAddress:     r.road_address_name  || r.address_name || "",
                        memo:               b.memo               || null,
                        createdAt:          b.createdAt,
                        createdAtFormatted: date.formatDate(b.createdAt),
                        hasMyReview:        reviewedIds.has(r.ID)
                    };
                });

                this._applyFiltersAndRender();

            } catch (e) {
                console.error("북마크 조회 실패", e);
            } finally {
                this._controller.getView().setBusy(false);
            }
        }

 

 

 

원본은 참 긴데 Promise를 중점으로 하나하나 해석해보자

const [bmRes, rvRes] = await Promise.all([
                    fetch(
                        `/jmt/PersonalMarkers`
                        + `?$filter=user_ID eq '${userId}'`
                        + `&$expand=restaurant($select=ID,place_name,category_name,`
                        + `address_name,road_address_name,latitude,longitude,phone,place_url)`
                        + `&$orderby=createdAt desc`,
                        { headers: { "Authorization": `Bearer ${jwt}` } }
                    ),
                    fetch(
                        `/jmt/Reviews?$filter=createdBy eq '${userId}'&$select=restaurant_ID`,
                        { headers: { "Authorization": `Bearer ${jwt}` } }
                    )
                ]);

 

fetch() : ()안의 주소로 브라우저 요청

 

-> 두 fetch를 동시에 요청한다. 

const [첫번째, 두번째] = await Promise.all([fetch1, fetch2]);
// 첫번째 === fetch1 결과
// 두번째 === fetch2 결과

-> 각 fetch를 bmRes, rvRes에 저장

 

이 바로 뒤에 .text()는 서버가 에러 응답을 body로 보내므로 이 body를 텍스트로 읽는 함수

 * fetch는 에러를 내도 reject하지않으므로 수동으로 잡아야함 -> fetch담은변수.ok 확인 필요

if (!bmRes.ok) throw new Error(await bmRes.text());

 

 

그런데, 이 구조를 보니

                const [bmData, rvData] = await Promise.all([
                    bmRes.json(),
                    rvRes.ok ? rvRes.json() : Promise.resolve({ value: [] })
                ]);

 

이걸보니 bmRes은 throw, rvRes는 Promise.all안에서 처리해서 방식이 일관되지 않음

 

 

if (!bmRes.ok) throw new Error(await bmRes.text());
const bmData = await bmRes.json();
const rvData = rvRes.ok ? await rvRes.json() : { value: [] };

 

 

그런데 또 자세히보니 .ok로 검증하는 방식은 여기저기서 여러번 반복해서 사용하는 로직인데 util/validation.js로 빼야겠는데?

        /**
         * fetch 응답을 검증하고 필요시 JSON으로 파싱해서 반환합니다.
         *
         * @param {Response} res - fetch()가 반환한 Response 객체
         * @param {string} [errorMessage] - 커스텀 에러 메시지. 없으면 서버 메시지 사용
         * @param {boolean} [parseJson=true] - false면 검증만 하고 파싱 안 함
         * @returns {Promise<any|void>} parseJson이 true면 파싱된 JSON, false면 undefined
         * @throws {Error} HTTP 에러 응답일 경우 에러
         *
         * @example
         * // 기본 사용 (서버 메시지 + JSON 파싱)
         * const data = await validation.parseResponse(res);
         *
         * // 커스텀 에러 메시지 + JSON 파싱
         * const url = await validation.parseResponse(res, this._i18n.getText("reviewImageFailMsg"));
         *
         * // 검증만 (JSON 파싱 없이)
         * await validation.parseResponse(res, null, false);
         */
        parseResponse: async function (res, errorMessage, parseJson = true) {
            if (!res.ok) {
                throw new Error(errorMessage ?? await res.text());
            }
            if (parseJson) return res.json();
        },

 

 

이렇게 util 에 넣어놓고

 

const bmData = await validation.parseResponse(bmRes);
const rvData = rvRes.ok ? await rvRes.json() : { value: [] };

이렇게 바꾸면 됨  (2번째 Promise가 굳이 필요없는듯?)

 

 

 

 

 

const reviewedIds = new Set(rvData.value.map(r => r.restaurant_ID));
// ...
return {
// ...
hasMyReview:        reviewedIds.has(r.ID)
};

Set로 쓰면 .has()가 배열의 .includes()보다 빠른 연산처리: 리뷰데이터 목록에서 빠른 조회

 

 

this._allBookmarks = bmData.value.map(b => { ... });

북마크 목록 데이터(b)를 중첩구조를 없애고 납작하게 펼치기

 

 

728x90
반응형