최근 게시글에서 Nuxt3에서의 데이터 호출 방식인 $fetch, useAsyncData, useFetch에 대해 알아보았습니다.
https://cychann.tistory.com/entry/Data-fetching-in-Nuxt3-fetch-useFetch-useAsyncData
Data fetching in Nuxt3: Nuxt3 개발자가 알아야 할 데이터 페칭 방법($fetch, useAsyncData, useFetch)
Nuxt 3에서는 클라이언트와 서버 환경 모두에서 데이터를 가져오는 다양한 방식($fetch, useFetch, useAsyncData)을 제공합니다. 그런데 왜 같은 목적을 위한 것처럼 보이는 이 세 가지 도구가 필요할까
cychann.tistory.com
이 개념들을 공부하다 보면 저와 같은 궁금증을 가지게 될 수 있습니다. 바로, useAsyncData와 useFetch의 차이점은 무엇일까? 라는 질문이죠.
Nuxt 공식 문서에 따르면, useFetch(url)은 useAsyncData('', () => $fetch(url))와 동일하다고 설명하고 있으며, 반환 값과 파라미터도 거의 비슷합니다.
실제로 Nuxt 커뮤니티에서도 이 주제에 대한 활발한 논의가 이루어지고 있습니다.
따라서 이번 게시글에서는 useAsyncData와 useFetch의 차이점을 분석한 제 의견을 정리하여 공유하려고 합니다.
useFetch(url)
useAsyncData(‘’, () => $fetch(url))
위 코드를 봤을 때, 가장 눈에 띄는 차이는 무엇인가요?
맞습니다. 바로 매개변수의 차이입니다. useFetch는 URL을 문자열 형태로 직접 전달하는 반면, useAsyncData는 콜백 함수를 통해 API를 호출합니다. 이처럼 다른 방식으로 매개변수를 넘겨주기 때문에, 두 훅의 동작 방식과 반응성에서 차이가 발생할 수 있습니다.
아래의 예시 코드를 살펴보면, 페이지 번호를 관리하고 API로부터 해당 페이지의 데이터를 요청하려는 의도를 가지고 있습니다.
<script setup lang="ts">
const page = ref(1);
const { data: useFetchResult } = useFetch("/api/test", {
query: { page: page.value },
});
const { data: useAsyncDataResult } = useAsyncData("page", () =>
$fetch("/api/test", {
query: { page: page.value },
})
);
const addPage = () => {
page.value++;
};
</script>
<template>
<pre>{{ useFetchResult }}</pre>
<pre>{{ useAsyncDataResult }}</pre>
<button @click="addPage">Inc Page</button>
</template>
// api/test.ts
export default defineEventHandler(async (event) => {
const { page } = getQuery(event);
return {
text: "this page is",
page,
};
});
위의 예시에서, /api/test API는 page 값을 받아서 응답을 반환하도록 구현되었습니다
export default defineEventHandler(async (event) => {
const { page } = getQuery(event);
return {
text: "this page is",
page,
};
});
이 코드를 실행하면, Inc Page 버튼을 눌렀을 때 +1이 된 page 값을 받아오기를 기대했지만, 실제로는 페이지가 변경되지 않고 데이터 요청이 이루어지지 않습니다.
그 이유는 Vue 3의 ref와 computed에서 .value를 사용할 때 발생하는 반응성 차이 때문입니다.
.value를 사용하여 page.value를 넘기면, Nuxt는 해당 값의 변경을 추적할 수 없습니다. 이는 ref 객체 자체가 아닌 그 값만 전달되기 때문입니다. 따라서 page.value가 변경되더라도 Nuxt는 그 변화를 감지하지 않고 데이터를 다시 요청하지 않습니다.
이 문제를 해결하려면 ref와 computed 자체를 넘겨줘야 합니다. 아래와 같이 수정할 수 있습니다:
const { data: useFetchResult } = useFetch("/api/test", {
query: { page },
});
const { data: useAsyncDataResult } = useAsyncData("page", () =>
$fetch("/api/test", {
query: { page },
})
);
하지만 이때 useFetch는 잘 처리되지만, useAsyncData를 사용했을 때는 객체 자체가 렌더링되는 문제가 발생합니다. 그 이유는 다음과 같습니다.
useFetch는 내부적으로 query 옵션을 활용하여 전달된 객체를 자동으로 문자열화하고, URL에 적절히 추가하는 기능이 있습니다. 이는 unjs/ofetch와 unjs/ufo의 기능을 통해 이루어지며, 객체가 URL 쿼리 문자열로 변환되어 API 호출 시 올바른 형태로 전달됩니다.
반면, useAsyncData는 $fetch를 사용할 때, query 객체를 직접 전달하더라도 문자열로 변환되지 않고 객체 참조로 전달됩니다. 이로 인해 API 응답에서 페이지 값이 기대하는 형식으로 반환되지 않고, 객체 자체가 렌더링되는 문제가 발생합니다.
이러한 기능은 오직 useFetch에서만 정의되어 있으며, 공식 문서에도 해당 내용이 명시되어 있습니다:
Using the query option, you can add search parameters to your query. This option is extended from unjs/ofetch and is using unjs/ufo to create the URL. Objects are automatically stringified.
그렇다면 useAsyncData에서는 어떻게 처리할 수 있을까요? .value 값에 반응성을 부여하는 watch 옵션을 이용하는 것입니다.
const { data: useFetchResult } = useFetch("/api/test", {
query: { page: page.value },
watch: [page],
});
const { data: useAsyncDataResult } = useAsyncData(
"page",
() =>
$fetch("/api/test", {
query: { page: page.value },
}),
{ watch: [page] }
);
이렇게 하면 useAsyncData는 잘 작동하지만, 이번엔 useFetch가 작동하지 않네요😅
하지만 바로 위에서 반응성 객체인 ref를 바로 넘겨줬을 때와의 차이가 보이시나요?
반응성 객체를 바로 넘겨줬을 때 useAsyncData는 api 호출조차 발생하지 않았으나, .value 값에 watch 옵션으로 반응성을 부여했을 땐, useFetch와 useAsyncData 모두 api 호출은 발생하고 있다는 점입니다.
이제 여기서 매개변수가 다른 것의 차이가 발생하는 것입니다.
먼저 useFetch부터 살펴봅시다. useFetch는 매개변수로 URL을 문자열 형태로 직접 전달하고 있습니다. 따라서 useFetch를 호출하는 시점에 넘겨준 URL 객체만을 참조하여 호출하게 됩니다. 따라서 watch를 통해 반응성을 부여하려고 해도, 처음 호출된 시점의 page.value만을 참조하게 되는 것이죠.
해당 이슈에 대한 이 현상은 Nuxt3에서 Freezing 이라는 단어로 표현됩니다.
아까 내부적으로 query 옵션을 활용하여 전달된 객체를 자동으로 문자열화하고, URL에 적절히 추가하는 기능이 useFetch 에만 있는 이유가 이 때문인 것 같다는 것이 저의 판단입니다.
반면 useAsyncData는 매개변수로 콜백 함수를 전달하므로, 콜백 함수가 실행되는 시점의 외부 값, 즉 변경된 page.value 값을 참조할 수 있습니다.
이 차이를 설명하기 위한 예시와 설명이 길었는데요! 다시 한 번 간략하게 정리해보겠습니다.
useFetch
매개변수로 URL 객체를 전달하기 때문에 호출 시점의 URL 객체만 참조하여 작동합니다. 따라서 반응성 객체를 처리하기 위해 Nuxt3에서 내부적으로 객체를 자동으로 문자열화하는 기능을 추가해줬습니다.
useAsyncData
콜백 함수를 사용하여 외부 값을 참조할 수 있게 됩니다. 그러나 반응성을 부여하기 위해서는 ref와 computed의 .value 값을 꺼내서 값으로 넣어주고, watch 옵션으로 반응성을 부여해야 합니다.
추가로, useAsyncData가 콜백 함수로 데이터를 처리한다는 점에서 useFetch로는 불가능한 상황들이 몇 가지 있습니다.
1. 외부 Fetching 라이브러리를 사용한다면 useFetch는 사용할 수 없습니다.
예를 들어 다음과 같은 경우가 있습니다:
const { data } = await useAsyncData('users', () => myFetchingFunc('users'))
useFetch는 콜백 함수를 허용하지 않기 때문에 이 경우에는 useAsyncData로만 처리가 가능합니다.
2. 여러 개의 $fetch 요청이 완료될 때까지 기다린 다음 결과를 처리해야 하는 상황에서도 useAsyncData만 사용할 수 있습니다
예를 들어:
<script setup lang="ts">
const { data: discounts, status } = await useAsyncData('cart-discount', async () => {
const [coupons, offers] = await Promise.all([
$fetch('/cart/coupons'),
$fetch('/cart/offers')
])
return { coupons, offers }
})
</script>
이처럼 useFetch와 useAsyncData + $fetch는 유사한 동작을 수행하지만, 세부적으로 차이가 존재합니다.
그래서 useFetch vs useAsyncData 어떤 것을 선택해야할까?
정해진 정답은 없으며, 이는 사용하는 개발자의 차이에 달려있습니다. 하지만 저는 개인적으로 useAsyncData를 선호합니다. 그 이유는 useFetch보다 훨씬 넓은 범위로 활용할 수 있기 때문입니다.
하지만, 제가 useAsyncData를 주로 사용하기로 결심한 가장 큰 이유는 Nuxt 3에서 $fetch를 커스텀하고, API 호출에 디자인 패턴을 적용하기 위해서는 useAsyncData를 활용해야 했기 때문입니다.
해당 경험에 대한 더 많은 이야기는 앞으로의 포스트에서 다시 찾아오겠습니다.
'Nuxt.js' 카테고리의 다른 글
[Nuxt3] 스마트한 API 호출 관리: Factory Pattern, 모듈화 (0) | 2024.10.24 |
---|---|
JWT in Nuxt3 (0) | 2024.09.03 |
Caching in Nuxt3 (0) | 2024.07.30 |
Data fetching in Nuxt3: Nuxt3 개발자가 알아야 할 데이터 페칭 방법($fetch, useAsyncData, useFetch) (0) | 2024.07.06 |
Nuxt.js 렌더링 이해하기: 성공적인 웹 개발의 시작 (1) | 2024.07.06 |