[JS] Promise 객체, 직접 사용해보기

강경서·2024년 5월 22일
0
post-thumbnail

Intro

비동기 작업은 JavaScript 를 사용한다면 반드시 알아야 할 개념입니다. 비동기 작업을 처리하기 위해 Promise 객체를 사용할 수 있지만, 가독성 때문에 async/await 를 더 자주 사용합니다. 그러나 async/awaitPromise 객체를 기반으로 하므로, 직접 Promise 객체를 생성해야 하는 상황이 있을 수 있습니다. 따라서 Promise 객체에 대한 이해는 필수적이라고 생각했습니다.


Promise

기존의 JavaScript 는 비동기 작업을 처리하기 위해 콜백 함수를 사용했습니다. 그러나 중첩된 콜백 함수로 인해 가독성이 떨어지고, 복잡한 에러 처리 및 비동기 작업의 병렬 제어가 어려워졌습니다. 이러한 문제를 해결하기 위해 Promise 객체가 도입되었습니다.

Promise 상태

Promise 객체는 작업이 진행중인지, 성공인지 아니면 실패했는지에 따라 3가지의 상태를 갖습니다.

  • Pending(대기) : 진행 상태, Promise 객채가 생성되어 사용될 준비가 된 상태입니다. Promise의 객체는 new Promise()로 생성할 수 있으며, 콜백 함수로 resolve, reject를 선언할 수 있습니다.
  • Fulfilled(이행) : 성공 상태, 비동기 처리 후 결과를 정상적으로 처리할 경우입니다. resolve가 호출됩니다.
  • Rejected(거부) : 실패 상태, 에러가 발생될 경우입니다. reject가 호출됩니다.
new Promise((resolve, reject) => {
		getData(
			// 성공 상태
			resolve(response.data),
			// 실패 상태
			reject(error.messase)                
		)
	}
)

Promise 메서드 체인

체이닝(chaining)이란 동일한 객체에 메서드를 연결하는 방법입니다.

  • then : resolve(성공) 시에 then 메서드에 실행할 콜백 함수를 인자로 넘겨줍니다.
  • catch : reject(실패) 시에 catch 메서드에 실행할 콜백 함수를 인자로 넘겨줍니다.
  • finally : 성공 또는 실패 여부와 상관없이 모두 실행할 경우에 finally 메서드에 실행할 콜백 함수를 인자로 넘겨줍니다.
function getData() {
  return new Promise(function(resolve, reject) {
    // 데이터 요청
	if (response) {
		resolve(response);
	}
	reject(new Error("Request is failed"));
  });
}

function main() {
	getData().then((data) => {
		console.log(data) // response 출력
	}).catch((err) => {
		console.error(err); // error 출력
	}).finally(() => {
    	// 성공 및 실패 여부와 상관없이 실행
    });
}

main()

Refactor

현재 진행 중인 사이드 프로젝트에서는 키워드 배열을 받아 각 키워드별로 데이터를 조회하고 상태를 저장하는 로직이 있습니다. 키워드 배열의 길이는 알 수 없으며, 데이터 조회 시 데이터가 존재하지 않을 수도 있습니다. 따라서 상태 저장이 언제 완료될지 예측할 수 없습니다.

기존 코드는 setTimeout 을 사용하여 상태가 500밀리초 동안 변경되지 않으면 상태 저장이 완료된 것으로 판단하고 후속 작업을 진행했습니다.

그러나 이러한 방식은 상태 저장이 완료되었는지 확실하게 알 수 없고, 설정된 대기 시간이 너무 길거나 짧을 수 있어 사용자를 불필요하게 기다리게 만들 수 있어 비효율적이라고 생각했습니다.

이를 해결하기 위해 PromisePromise.allSettled를 사용하여 상태 저장이 완료될 때까지 대기하고 후속 작업을 진행하는 방식으로 코드를 개선했습니다.

기존의 코드

 useEffect(() => {
    keywordsSearch(keywords);
  }, [keywords]);

키워드 배열이 변경될 때 keywordsSearch 함수가 실행됩니다.

 const keywordsSearch = (keywords: string[]) => {
    for (let i = 0; i < keywords.length; i++) {
      ps.keywordSearch(keywords[i], placesSearchCB, {
        // options
      });
    }
  };

keywordsSearch 함수는 반복문을 통해 각각의 키워드별로 카카오의 장소 검색 서비스 객체인 ps를 사용하여 콜백함수인 placesSearchCB 를 통해 데이터를 받을 수 있습니다.

 const placesSearchCB = (data: Dataype[], status: string) => {
    if (status === window.kakao.maps.services.Status.OK) {
      setState(pre => [...pre, ...data]);
    } else {
      // failed
    }
  };

placesSearchCB는 데이터를 받아 상태를 업데이트합니다

 useEffect(() => {
    const timeoutId = setTimeout(() => {
    	// 후속 작업
    }, 500);
    return () => clearTimeout(timeoutId);
  }, [state]);

상태가 변경되면 useEffect가 실행되고 setTimeout 에서 지정된 대기 시간이 지나기 전에 상태가 다시 변경되면 useEffect가 새로 시작되는 방식으로 상태 저장의 완료를 예측했습니다.

개선된 코드

 const placesSearchCB = (data: DataType[], status: string) => {
    return new Promise<CafeType[]>((resolve, reject) => {
      if (status === window.kakao.maps.services.Status.OK) {
        resolve(data);
      } else {
        reject();
      }
    });
  };

먼저 데이터를 받는 콜백함수인 placesSearchCBPromise 를 사용하여 수정했습니다.

 const keywordsSearch = (keywords: string[]) => {
    const promises = keywords.map(
      keyword =>
        new Promise<CafeType[]>((resolve, reject) => {
          ps.keywordSearch(
            keyword,
            (data: CafeType[], status: string) =>
              placesSearchCB(data, status)
            	.then((data) => resolve(data))
            	.catch(() => reject()),
            {
              // options
            },
          );
        }),
    );

    return Promise.allSettled(promises);
  };

keywordsSearch 함수는 키워드 배열을 받아 각 키워드에 대한 데이터 검색 작업을 Promise 객체로 변경하고, 이러한 Promise 객체들이 모두 작업을 완료하는 시점을 Promise.allSettled를 통해 알 수 있도록 수정했습니다. 이때, 위에서 수정한 placesSearchCB 콜백 함수가 각 Promise 객체의 완료를 담당합니다.


 useEffect(() => {
  const fetchData = async () => {
      const results = await keywordsSearch(keywords);
      const fulfilledResults = results
        .filter(result => result.status === 'fulfilled')
        .flatMap(
          result => (result as PromiseFulfilledResult<CafeType[]>).value,
        );
      setState(fulfilledResults);
    };

    fetchData();
  }, [keywords]);

키워드 배열이 변경되면 위의 useEffect가 실행되고, keywordsSearch 함수는 모든 키워드들의 검색 작업이 완료되면 결과값을 반환합니다. 반환된 값은 Promise.allSettled로 받아 검색에 성공한 데이터만을 필터링하였습니다.

keywordsSearch 함수가 반환하는 결과값은 각 키워드의 결과값을 가진 배열들을 요소로 가진 배열입니다. 이 배열을 flatMap을 사용하여 평탄화하여 결과값만을 가진 배열로 수정후 상태를 업데이트했습니다.


📝 후기


🧾 Reference

profile
기록하고 배우고 시도하고

0개의 댓글