await 없이 비동기를 처리는 컴포저블을 만들어 봅시다.
해당 글은 컴포저블 잘 만들기 시리즈의 마지막 아티클입니다.
아래 글의 번역입니다.
https://www.vuemastery.com/blog/coding-better-composables-5-of-5/
이전 시리즈 보기
2022.12.08 - [Vue.js] - [번역] Vue3 Coding Better Composables : 옵션 객체 파라미터 활용하여 컴포저블 잘 만들기
2022.12.08 - [Vue.js] - [번역] Vue3 Coding Better Composables : ref 및 unref를 사용하여 유연한 컴포저블 만들기
2022.12.08 - [Vue.js] - [번역] Vue3 Coding Better Composables : 다형적인 리턴값을 활용하여 컴포저블 잘만들기
2022.12.09 - [Vue.js] - [번역] Vue3 Coding Better Composables : 인터페이스를 잘 설계하여 컴포저블 잘 만들기
요약 :
- 컴포저블은 ref만 리턴하고, ref 로직만 조합함
- promise 로직은 컴포저블 내부로 캡슐화
Await 없이 비동기 처리하기
Composition API로 비동기 로직을 작성하는 것은 때때로 까다로울 수 있습니다.
모든 비동기 코드는 모든 setup 함수 내 반응성 코드 이후에 나와야 합니다.
즉, await 후에 정의된 계산된 속성은 처음에는 템플릿에서 사용할 수 없습니다.
비동기 코드가 완료되고 setup 함수가 실행을 완료한 후에만 존재합니다.
const count = ref(0);
// This async data fetch won't interfere with our reactivity
const { state } = useAsyncState(fetchData());
const doubleCount = computed(() => count * 2);
이 패턴은 비동기 코드 작업을 훨씬 더 안전하고 간단하게 만듭니다.
머릿속에서 추적해야 하는 항목의 양을 줄이는 것은 항상 도움이 됩니다!
Await 없이 비동기 처리 패턴 구현
1. 먼저 상태를 준비하고 반환합니다.
아직 값이 무엇인지 모르기 때문에 null 값으로 초기화합니다.
export default useMyAsyncComposable(promise) {
const state = ref(null);
return state;
}
2. Promise을 기다린 다음 결과를 상태 참조로 설정하는 메서드를 만듭니다.
const execute = async () => {
state.value = await promise;
}
Promise가 resolve될 때마다 반응적으로 상태를 업데이트합니다.
3. 2번에서 만든 함수를 컴포저블에 추가하기만 하면 됩니다.
export default useMyAsyncComposable(promise) {
const state = ref(null);
// Add in the execute method...
const execute = async () => {
state.value = await promise;
}
// ...and execute it!
execute();
return state;
}
useMyAsyncComposable 메서드에서 반환되기 직전에 실행 함수를 호출합니다.
그러나 await 키워드는 사용하지 않습니다.
excute 함수 내에서 실행을 중지하고 promise의 resolve를 기다리면
실행 흐름이 즉시 useMyAsyncComposable 함수로 반환됩니다.
로직 실행이 execute() 문을 지나서 계속되고 컴포저블에서 리턴합니다.
export default useMyAsyncComposable(promise) {
const state = ref(null);
const execute = async () => {
// 2. 프로미스의 resolve 기다림
state.value = await promise
// 5. 잠시 후
// 프로미스가 resolve 되고 상태가 반응적으로 업데이트됨
}
// 1. `execute` 함수 실행
execute();
// 3. `await` 은 실행 흐름을 반환함
// 4. 샹태를 리턴하고 setup 함수 내 로직을 계속해서 실행함
return state;
}
promise는 "백그라운드에서" 실행되며 우리는 해당 promise를 await 하지 않기 때문에 setup 함수의 흐름을 방해하지 않습니다.
반응성을 방해하지 않고 이 컴포저블을 어디에나 배치할 수 있습니다.
useAsyncState
useAsyncState은 지금까지 설명한 예제의 좀 더 세련된 버전입니다.
const { state, isLoading } = useAsyncState(fetchData());
소스코드를 보면(looking at the source code) 위의 예제와 비슷하지만, 더 많은 엣지 케이스 처리를 구현한 것을 볼 수 있습니다.
간략한 버전입니다.
export function useAsyncState(promise, initialState) {
const state = ref(initialState);
const isReady = ref(false);
const isLoading = ref(false);
const error = ref(undefined);
async function execute() {
error.value = undefined;
isReady.value = false;
isLoading.value = true;
try {
const data = await promise;
state.value = data;
isReady.value = true;
}
catch (e) {
error.value = e;
}
isLoading.value = false;
}
execute();
return {
state,
isReady,
isLoading,
error,
};
}
이 컴포저블은 또한 데이터를 가져온 시점을 알려주는 isReady를 반환합니다.
또한 isLoading ref와 error ref를 반환하여
로딩 및 오류 상태를 추적할 수 있습니다.
useAsyncQueue
// This `result` will update as the tasks are executed
const { result } = useAsyncQueue([getFirstPromise, getSecondPromise]);
다음은 공식 문서를 기반으로 한 예입니다.
const getFirstPromise = () => {
// Create our first promise
return new Promise((resolve) => {
setTimeout(() => {
resolve(1000);
}, 10);
});
};
const getSecondPromise = (result) => {
return new Promise((resolve) => {
setTimeout(() => {
resolve(1000 + result);
}, 20);
});
};
const { activeIndex, result } = useAsyncQueue([
getFirstPromise,
getSecondPromise
]);
코드를 비동기식으로 실행하더라도 await를 사용할 필요는 없습니다.
내부적으로도 컴포저블은 await을 사용하지 않습니다.
대신 우리는 이러한 Promise를 "백그라운드에서" 실행하고 결과가 반응적으로 업데이트되도록 합니다.
이 컴포저블이 어떻게 동작하는지 살펴보겠습니다. Async Without Await 패턴을 구현하기 위해
이 컴포저블은 먼저 반환될 activeIndex 및 result 값을 설정합니다(first hooks up).
// Default state values that can be updated reactively
const initialResult = Array.from(new Array(tasks.length), () => ({
state: promiseState.pending,
data: null,
});
// Make the reactive version that we'll return
const result = reactive(initialResult);
// Also set up the active index as a ref
const activeIndex = ref(-1);
그러나 주요 기능은 각 함수를 하나씩 실행(works through each function)하는 reduce에 의해 구동됩니다.
tasks.reduce((prev, curr) => {
return prev.then((prevRes) => {
if (result[activeIndex.value]?.state === promiseState.rejected && interrupt) {
onFinished();
return;
}
return curr(prevRes).then((currentRes) => {
updateResult(promiseState.fulfilled, currentRes);
activeIndex.value === tasks.length - 1 && onFinished();
return currentRes;
})
}).catch((e) => {
updateResult(promiseState.rejected, e);
onError();
return e;
})
}, Promise.resolve());
리듀스 함수는 약간 복잡할 수 있으므로 분해하여 이해해 봅시다.
먼저 resolved된 promise로 전체 체인을 시작합니다.
tasks.reduce((prev, curr) => {
// ...
}, Promise.resolve());
그 다음 각 작업 처리를 시작합니다.
이전 Promise에서 .then을 연결하여 이를 수행합니다.
Promise가 거부된 경우 조기에 중단하고 반환할 수 있습니다.
// Check if our last promise was rejected
if (result[activeIndex.value]?.state === promiseState.rejected && interrupt) {
onFinished();
return;
}
일찍 중단하지 않는다면, 이전 약속의 결과를 전달하여 다음 작업을 실행합니다.
또한 updateResult 메서드를 호출하여 이 컴포저블이 반환하는 result 배열에 반응적으로 추가합니다.
// Execute the next task with the result from the previous task
return curr(prevRes).then((currentRes) => {
updateResult(promiseState.fulfilled, currentRes);
activeIndex.value === tasks.length - 1 && onFinished();
return currentRes;
});
보시다시피 이 컴포저블은 Async Without Await 패턴을 구현하지만 이 패턴은 전체 컴포저블의 몇 줄에 불과합니다.
따라서 많은 추가 작업이 필요하지 않으며 excute 역할을 하는 async 함수가 필요하다는 것만 기억하면 됩니다.
마무리
Async Without Await 패턴을 사용하면 훨씬 더 쉽게 비동기 컴포저블을 사용할 수 있습니다.
이 패턴을 사용하면 반응성 중단에 대한 걱정 없이 원하는 곳에 비동기 코드를 배치할 수 있습니다.
기억해야 할 핵심 원칙은 다음과 같습니다.
반응성 상태를 먼저 연결하면 원할 때마다 업데이트할 수 있으며 반응성으로 인해 값이 앱을 통해 흐릅니다.
그러면 await을 사용할 필요가 없습니다
이 기사는 시리즈의 끝입니다.
지금까지 다음과 같은 주제를 다루었습니다.
- 옵션 객체 파라미터를 사용하여 컴포저블을 더 합성 가능하게 만드는 방법
- ref 및 unref를 사용하여 인수를 보다 유연하게 만들기
- return 값을 더 유용하게 만드는 간단한 방법
- 인터페이스 먼저 생각하면 컴포저블이 더 강력해지는 이유
- await 없이 비동기 코드를 사용하는 방법 - 코드를 이해하기 쉽게 만들기
추가로 보면 좋은 글
vue3와 immer 사용하기
https://twitter.com/youyuxi/status/1493146218142208001?lang=en
shallowRef로 최적화
https://vuejs.org/api/reactivity-advanced.html#markraw
'FrontEnd' 카테고리의 다른 글
Vue3의 반응성 완벽 이해 1편 : 종속성 추적 (4) | 2022.12.10 |
---|---|
zustand와 react query를 같이 사용하는 방법 (1) | 2022.12.09 |
[번역] Vue3 Coding Better Composables : 인터페이스를 잘 설계하여 컴포저블 잘 만들기 (0) | 2022.12.09 |
[번역] Vue3 Coding Better Composables : 다형적인 리턴값을 활용하여 컴포저블 잘만들기 (0) | 2022.12.08 |
[번역] Vue3 Coding Better Composables : ref 및 unref를 사용하여 유연한 컴포저블 만들기 (0) | 2022.12.08 |