비동기 작업은 JavaScript
를 사용한다면 반드시 알아야 할 개념입니다. 비동기 작업을 처리하기 위해 Promise
객체를 사용할 수 있지만, 가독성 때문에 async/await
를 더 자주 사용합니다. 그러나 async/await
도 Promise
객체를 기반으로 하므로, 직접 Promise
객체를 생성해야 하는 상황이 있을 수 있습니다. 따라서 Promise
객체에 대한 이해는 필수적이라고 생각했습니다.
기존의 JavaScript
는 비동기 작업을 처리하기 위해 콜백 함수를 사용했습니다. 그러나 중첩된 콜백 함수로 인해 가독성이 떨어지고, 복잡한 에러 처리 및 비동기 작업의 병렬 제어가 어려워졌습니다. 이러한 문제를 해결하기 위해 Promise
객체가 도입되었습니다.
Promise
객체는 작업이 진행중인지, 성공인지 아니면 실패했는지에 따라 3가지의 상태를 갖습니다.
Promise
객채가 생성되어 사용될 준비가 된 상태입니다. Promise
의 객체는 new Promise()
로 생성할 수 있으며, 콜백 함수로 resolve
, reject
를 선언할 수 있습니다.resolve
가 호출됩니다. reject
가 호출됩니다.new Promise((resolve, reject) => {
getData(
// 성공 상태
resolve(response.data),
// 실패 상태
reject(error.messase)
)
}
)
체이닝(chaining)이란 동일한 객체에 메서드를 연결하는 방법입니다.
resolve(성공)
시에 then
메서드에 실행할 콜백 함수를 인자로 넘겨줍니다. reject(실패)
시에 catch
메서드에 실행할 콜백 함수를 인자로 넘겨줍니다. 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()
현재 진행 중인 사이드 프로젝트에서는 키워드 배열을 받아 각 키워드별로 데이터를 조회하고 상태를 저장하는 로직이 있습니다. 키워드 배열의 길이는 알 수 없으며, 데이터 조회 시 데이터가 존재하지 않을 수도 있습니다. 따라서 상태 저장이 언제 완료될지 예측할 수 없습니다.
기존 코드는 setTimeout
을 사용하여 상태가 500밀리초 동안 변경되지 않으면 상태 저장이 완료된 것으로 판단하고 후속 작업을 진행했습니다.
그러나 이러한 방식은 상태 저장이 완료되었는지 확실하게 알 수 없고, 설정된 대기 시간이 너무 길거나 짧을 수 있어 사용자를 불필요하게 기다리게 만들 수 있어 비효율적이라고 생각했습니다.
이를 해결하기 위해 Promise
와 Promise.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();
}
});
};
먼저 데이터를 받는 콜백함수인 placesSearchCB
를 Promise
를 사용하여 수정했습니다.
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
을 사용하여 평탄화하여 결과값만을 가진 배열로 수정후 상태를 업데이트했습니다.