Nuxt의 Data fetching composable: useFetch, useAsyncData, $fetch
Nuxt는 브라우저와 서버 환경 모두에서 데이터 패칭을 수행할 수 있는 두 가지 컴포저블(useFetch, useAsyncData)과 내장 라이브러리($fetch)를 제공한다. 이 세 가지 도구의 차이점과 사용 용도를 간단히 요약하면 다음과 같다:
- $fetch: 사용자 상호작용을 기반으로 네트워크 요청을 보낼 때 유용함
- useAsyncData: $fetch와 결합하여 더 세밀한 제어를 제공.
- useFetch: 컴포넌트의 setup fucntion에서 data fetching을 처리하는 가장 간단한 방법
useFetch와 useAsyncData는 공통의 옵션과 패턴을 공유하지만, 이 composable들이 존재하는 이유를 이해하는 것이 중요하다.
왜 특정 Data fetching composable을 사용해야 하는가?
Nuxt는 서버와 클라이언트 환경 모두에서 실행될 수 있는 Universal 모드를 지원하는 프레임워크이다.
만약 Vue 컴포넌트의 setup 함수에서 $fetch 함수를 사용해 데이터를 패칭하면, 서버에서 한 번(HTML을 렌더링하기 위해) 그리고 클라이언트에서 다시 한 번(HTML이 하이드레이션될 때). 이렇게 총 데이터가 두 번 패칭될 수 있다.
이를 방지하기 위해 Nuxt의 Data fetching composable(useAsyncData, useFetch)을 제공하여 데이터가 한 번만 패칭되도록 해야 한다.
네트워크 호출 중복 방지
useFetch와 useAsyncData composable은 API 호출이 서버에서 한 번 이루어지면, 그 데이터를 페이로드(payload)를 통해 클라이언트로 전달되도록 보장한다.
이 페이로드는 useNuxtApp().payload를 통해 접근할 수 있는 JavaScript 객체로, 브라우저에서 하이드레이션 중 동일한 데이터를 다시 패칭하지 않도록 한다.
그럼 이제 본격적으로 $fetch, useAsyncData, useFetch에 대해 살펴보자.
$fetch
Nuxt는 ofetch 라이브러리를 포함하고, 이를 $fetch 별칭으로 전역에서 자동으로 가져온다.
useAsyncData, useFetch와 대비되는 $fetch의 가장 큰 특징은, 상태를 서버에서 클라이언트로 전이하지 않는다는 점이다. 이는 위에서 언급했던 중복 호출을 일으키는 원인이 된다. Top level의 script setup 코드에서 $fetch를 바로 호출하면, 서버와 클라이언트 양쪽에서 각각 두 번 API를 호출하게 된다.
<script setup lang="ts">
const apiURL = "https://jsonplaceholder.typicode.com/todos/1"
const $fetchData = await $fetch(`${apiURL}`)
console.log("$fetchData", $fetchData);
</script>
위 코드에서 서버 로그와 브라우저 콘솔 로그를 통해 두 번 요청이 발생하는 것을 볼 수 있다.
이런 중복 호출을 방지하기 위해, Nuxt3 공식문서에서는 useFetch 또는 useAsyncData + $fetch를 사용할 것을 권장한다.
We recommend to use useFetch or useAsyncData + $fetch to prevent double data fetching when fetching the component data.
그럼 $fetch는 언제 사용할까?
$fetch는 클라이언트 사이드 상호작용(이벤트 기반)에 사용하는 것이 적합하다.
const postURL = "https://jsonplaceholder.typicode.com/posts"
function contactForm() {
$fetch(`${postURL}`, {
method: 'POST',
body: { hello: 'world' }
}).then(response => {
console.log('Response:', response)
}).catch(error => {
console.error('Error:', error)
})
}
위와 같은 코드는, 사용자 반응에 따라 클라이언트 사이드에서만 API 호출을 하게 되어 $fetch의 사용이 적합하다. 즉 POST/PUT/PATCH/DELETE와 같은 메소드에 주로 활용할 수 있다.
useAsyncData
useAsyncData는 비동기 데이터를 SSR에 맞춰 손쉽게 다룰 수 있게 해주는 composable이다.
페이지, 컴포넌트, 플러그인 등에서 사용하여 비동기 데이터를 가져오고, 해당 데이터를 서버에서 클라이언트로 전달한다. 이를 통해 클라이언트 사이드에서 데이터를 다시 가져오는 불필요한 작업을 줄여준다.
위의 예시에서 봤던대로, 기본적인 사용법은 다음과 같다.
<script setup lang="ts">
const getURL = "https://jsonplaceholder.typicode.com/todos/1"
const { data, pending, error, refresh, clear } = await useAsyncData('item', () => $fetch(`${getURL}`))
console.log("data", data.value);
console.log("pending", pending.value);
console.log("error", error.value);
console.log("refresh", refresh);
console.log("clear", clear);
</script>
<template>
<div class="button-wrap">
<button @click="refresh">refresh button</button>
<button @click="clear">clear button</button>
</div>
<div v-if="pending">Loading...</div>
<div v-else-if="error">Error: {{ error.message }}</div>
<div v-else>{{ data }}</div>
</template>
data, pending, error는 Vue의 ref로 반환되므로, script setup 내에서 사용 시 .value로 접근해야 한다.
refresh와 clear 함수는 데이터를 다시 가져오거나 초기화하는 데 사용된다. 이 함수들이 어떻게 동작하는지 자세하게 이해하기 위해서는 파라미터로 넘겨주는 key의 역할을 정확히 아는 것이 중요하다.
key의 역할
key는 데이터 가져오기를 요청할 때, 해당 요청을 고유하게 식별하는 데 사용된다. 만약 key를 제공하지 않으면, Nuxt는 파일 이름과 useAsyncData 인스턴스의 라인 번호를 기반으로 고유한 키를 생성한다.
이 key는 여러 요청이 있을 때, 각 요청을 구분하고 중복되지 않도록 보장한다. Nuxt 내부 함수에서는 이 key와 더불어, dedupe 옵션으로 중복 호출을 방지한다.
dedupe 옵션은 기본적으로 cancel로 설정되어 있으며, 두 가지 가능한 값이 있다:
- cancel: 새 요청이 있을 때 기존 요청을 취소
- defer: 기존 요청이 진행 중일 때 새로운 요청을 만들지 않음
이 두 가지 옵션을 통해 동일한 키를 가진 요청이 중복으로 발생하지 않게 함으로써, 서버와 클라이언트 간의 중복 호출을 막는데 중요한 역할을 하는 것이다. 이를 통해 성능을 최적화하고, 불필요한 데이터 요청을 줄일 수 있다.
실제로 refresh와 clear 함수 내부를 뜯어보면, 일부 로직에서 key를 통해 서버와 클라이언트에서 중복 호출을 방지하는 방법과 작동 원리를 이해하는 데에 도움이 된다.
· refresh
asyncData.refresh = asyncData.execute = (opts = {}) => {
/*
TODO 1. 중복 요청 확인
- nuxtApp._asyncDataPromises[key]에 현재 요청이 이미 존재하는지 확인
- 존재하면, dedupe 옵션에 따라 기존 요청을 취소할지(cancel) 또는 새 요청을 수행하지 않을지(defer) 결정함
- 이때 별도로 지정한 dedupe의 옵션이 있으면 해당 값(opts.dedupe)을, 아니면 기본값(options.dedupe = cancel)을 사용
- dedupe 옵션이 defer인 경우, 기존 요청을 반환. 이는 기존 요청이 완료될 때까지 새로운 요청을 만들지 않도록 함
- dedupe 옵션이 cancel인 경우, 기존 요청을 취소하고 새로운 요청을 진행. 기존 요청을 취소하기 위해 cancelled 속성을 true로 설정
*/
if (nuxtApp._asyncDataPromises[key]) {
if (isDefer(opts.dedupe ?? options.dedupe)) {
return nuxtApp._asyncDataPromises[key];
}
nuxtApp._asyncDataPromises[key].cancelled = true;
}
// TODO 2. 캐시된 데이터가 존재하면 초기화 시 캐시된 데이터를 반환
if ((opts._initial || nuxtApp.isHydrating && opts._initial !== false) && hasCachedData()) {
return Promise.resolve(options.getCachedData(key, nuxtApp));
}
asyncData.pending.value = true; // 로딩 상태 설정
asyncData.status.value = "pending"; // 상태 설정
· clear
// nuxtApp 객체에서 특정 key에 해당하는 데이터를 제거
asyncData.clear = () => clearNuxtDataByKey(nuxtApp, key);
useAsyncData 함수 내에서 refresh 함수는 초기 데이터 로딩부터 사용자 상호작용에 따른 데이터 갱신까지 모든 데이터 요청에 사용되는 주요 매커니즘으로 동작한다. 실제로 이 refresh 함수를 분석하며 useAsyncData를 통해 제공되는 이점을 파악하는 데에 도움이 됐다.
refresh 함수 요약
- 데이터 요청 및 캐시 관리: refresh 함수는 데이터를 요청하고, 서버로부터 받은 데이터를 클라이언트에서 관리할 수 있도록 한다. 이때 key를 통해 각 데이터 요청을 식별하고, 캐시된 데이터를 활용하여 중복 요청을 방지하며 성능을 최적화한다.
- 중복 호출 방지: refresh 함수 내부에서는 nuxtApp._asyncDataPromises[key]를 통해 현재 진행 중인 데이터 요청이 있는지 확인한다. 이미 진행 중인 경우, 중복 호출을 방지하기 위해 해당 Promise를 반환하거나 취소 처리한다. 이를 통해 동일한 데이터에 대한 복수의 요청을 방지하고, 한 번 요청한 데이터는 캐시를 통해 재사용할 수 있다.
- 데이터 변환 및 처리: 받은 데이터는 옵션에 따라 변환(transform), 필드 추출(pick) 등의 처리를 거칠 수 있다. 이는 받은 데이터를 필요한 형태로 가공하여 컴포넌트에 제공하는 역할을 한다.
- 상태 관리: refresh 함수는 데이터 요청의 상태를 관리한다. 데이터가 요청 중인지, 성공적으로 완료되었는지, 오류가 발생했는지 등의 상태를 asyncData 객체에 반영하여 컴포넌트에서 상태 변화를 실시간으로 반영할 수 있도록 한다.
이처럼 refresh 함수는 useAsyncData 함수의 핵심 메서드로서, 데이터 요청과 관리를 통합적으로 처리하여 컴포넌트의 데이터 의존성과 라이프사이클에 맞춰 최적화된 데이터 로딩을 구현하는 중요 로직이다.
useAsyncData Parameters
key: 데이터 가져오기 요청을 고유하게 식별하는 데 사용되는 고유키. 제공하지 않으면, 파일 이름과 useAsyncData 인스턴스의 라인 번호를 기반으로 고유 키가 생성.
handler: 비동기 함수로, 반드시 정의된 값을 반환해야 함. 그렇지 않으면 요청이 클라이언트 측에서 중복될 수 있음.
options:
· server: 데이터를 서버에서 가져올지 여부를 설정. (기본값: true)
const { data, pending, error, refresh, clear } = await useAsyncData(
"item",
() => $fetch(`${getURL}`),
{
server: false,
}
);
false로 설정하면 서버에서 데이터를 패칭하지 않으므로, 클라이언트 단에서 API 호출이 발생한다.
· lazy: 클라이언트 측 탐색을 차단하지 않고 라우트 로딩 후 비동기 함수를 해결할지 여부를 설정. (기본값: false)
다른 페이지에서 라우팅을 통해 메인페이지로 넘어오는 코드를 실행했을 때의 화면을 각각 살펴보자.
lazy: false
lazy: false를 사용하면 Suspense를 통해 데이터를 가져오기 전까지 라우트 로딩을 차단한다. 이는 데이터를 모두 가져온 후에 화면이 렌더링되므로 사용자 경험이 매끄럽지만, 데이터 로드 시간이 길어질 경우 사용자가 빈 화면을 볼 수 있다.
lazy: true
잠깐의 loading... 메시지가 나오는 것이 보인다.
lazy: true를 사용하면 초기 로드 속도가 빨라지지만, 데이터가 아직 로드되지 않았기 때문에 로딩 상태를 구현하여 사용자에게 피드백을 제공해야 한다. 로딩 상태를 처리하면 사용자 경험이 더 쾌적해질 수 있다.
· immediate: false로 설정하면 요청이 즉시 실행되지 않도록 함. (기본값: true)
const { data, pending, error, refresh, clear } = await useAsyncData(
"item",
() => $fetch(`${getURL}`),
{
immediate: false,
}
);
사용자가 명시적으로 데이터를 가져오기 위해 `refresh` 함수를 호출할 때까지 요청이 보류된다.
따라서 특정 상호작용 후 데이터를 가져오거나, 특정 조건이 충족될 때만 데이터를 가져오고 싶을 때 refresh를 통해 데이터를 가져오고 싶을 때 유용하게 사용할 수 있다.
· default: 비동기 함수가 해결되기 전에 데이터의 기본값을 설정하는 팩토리 함수. lazy: true 또는 immediate: false 옵션과 유용하게 사용됨.
const { data, pending, error, refresh, clear } = await useAsyncData(
"item",
() => $fetch(`${getURL}/1`),
{
immediate: false, // or lazy: false
default: () => ({
userId: null,
id: null,
title: "loading...",
completed: false,
}),
}
);
default 옵션을 활용하면, 데이터를 가져오는 동안 로딩 상태를 표시하면서도 기본값을 제공하여 로딩 중에도 기본 구조나 데이터 형식을 유지할 수 있다. 이는 로딩 중에도 UI의 레이아웃이 불안정해지는 것을 방지할 수 있다.
· transform: handler 함수의 결과를 변환하는 데 사용할 수 있는 함수.
const { data, pending, error, refresh, clear } = await useAsyncData(
"item",
() => $fetch(`${getURL}/1`),
{
transform: (data) => ({
userId: data.userId,
id: data.id,
title: data.title.toUpperCase(),
state: data.completed ? "completed" : "pending",
}),
}
);
데이터를 가져온 후, 이 데이터는 transform 함수에 전달되어 변환된 결과를 반환한다. 이를 통해 데이터의 구조를 변경하거나 추가적인 처리를 수행할 수 있다.
transform 옵션은 데이터 구조를 변경하거나, 특정 조건에 맞는 데이터를 필터링하거나, 추가적인 계산 및 가공을 수행하는 등 정말 유용하게 사용할 수 있는 옵션이다.
· getCachedData: 캐시된 데이터를 반환하는 함수를 제공. null 또는 undefined를 반환하면 페치가 트리거됨. 기본값: key => nuxt.isHydrating ? nuxt.payload.data[key] : nuxt.static.data[key], 이는 payloadExtraction이 활성화된 경우에만 데이터를 캐시함.
·pick: handler 함수의 결과에서 이 배열에 지정된 키만 선택.
const { data, pending, error, refresh, clear } = await useAsyncData(
"item",
() => $fetch(`${getURL}/1`),
{
pick: ["id", "title"],
}
);
pick 옵션을 활용해 불필요한 데이터를 필터링하고, 데이터 구조를 간결하게 유지할 수 있다.
·watch: 자동 갱신을 위해 반응형 소스를 감시.
const { data, pending, error, refresh, clear } = await useAsyncData(
"item",
() => $fetch(`${getURL}/${id.value}`),
{
watch: [id],
}
);
watch 옵션을 설정하면 지정된 반응형 소스가 변경될 때마다 자동으로 데이터를 업데이트할 수 있다.
이를 통해 동적 데이터 요청, 실시간 데이터 갱신 등 다양한 상황에서 데이터를 자동으로 업데이트할 수 있다.
·deep: 데이터를 깊은 ref 객체로 반환한다. (기본값: true) 데이터가 깊게 반응할 필요가 없는 경우 성능을 개선하기 위해 false로 설정할 수 있음.
·dedupe: 동일한 키를 한 번에 여러 번 가져오지 않도록 함. (기본값: cancel)
cancel: 새 요청이 있을 때 기존 요청을 취소.
defer: 진행 중인 요청이 있으면 새 요청을 만들지 않음.
useFetch
useFetch는 useAsyncData와 $fetch를 편리하게 감싸주는 메소드이다. URL과 fetch 옵션을 기반으로 자동으로 키를 생성하고, 서버 라우트를 기반으로 요청 URL에 대한 타입 힌트를 제공하며, API 응답 타입을 추론한다.
useFetch는 useAsyncData와 마찬가지로 setup 함수, 플러그인, 또는 라우트 미들웨어에서 직접 호출될 수 있다.
또한 컴포저블은 반응형 데이터를 반환하며, 서버에서 클라이언트로 응답을 Nuxt 페이로드에 추가하여 페이지가 하이드레이션될 때 클라이언트에서 데이터를 다시 패칭하지 않도록 처리한다.
useFetch Parameter
useFetch는 useAsyncData 의 handler 를 제외한 모든 파라미터에서 와 $fetch 의 options 파라미터 를 포함한다.
useFetch 의 Return Value
useAsyncData 와 동일하다.
useFetch interceptors
useFetch 메소드를 사용하면, 인터셉터도 사용할 수 있다. 인터셉터를 통해 요청과 응답을 가로채고, 추가적인 처리를 할 수 있다.
const { data, pending, error, refresh, clear } = await useFetch('/api/auth/login', {
onRequest({ request, options }) {
// Set the request headers
options.headers = options.headers || {}
options.headers.authorization = '...'
},
onRequestError({ request, options, error }) {
// Handle the request errors
},
onResponse({ request, response, options }) {
// Process the response data
localStorage.setItem('token', response._data.token)
},
onResponseError({ request, response, options }) {
// Handle the response errors
}
})
- onRequest: 요청을 보내기 전에 호출된다. 여기서 요청 헤더를 설정할 수 있다.
- onRequestError: 요청 중 에러가 발생했을 때 호출된다
- onResponse: 응답을 받은 후 호출된다. 여기서 응답 데이터를 처리할 수 있다
- onResponseError: 응답 중 에러가 발생했을 때 호출된다
그럼 useFetch useAsyncData의 차이점이 뭐야?
이 부분이 꽤나 혼란스러웠다. 공식문서와 다른 글들을 참조 해봤을 땐, useFetch는 그저 useAsyncData + $fetch를 편하게 호출하기 위해 만들어 놓은 걸까?
실제로 api 호출을 여러 번 테스트해보고, 삽질해본 나의 결론은 다음과 같다.
두 composable를 구분짓는 가장 큰 차이점은, 실행 context의 차이이다.
useAsyncData의 두번째 매개변수는 콜백함수이다. 이는 데이터 패칭 시점에 외부 값을 참조할 수 있도록 한다.
반면 useFetch 의 두번째 매개변수는 객체이다. 이는 초기 호출 시 객체를 기반으로 데이터를 패칭하며, 이후의 refresh 시점에서도 동일한 객체를 사용한다. 이를 통해 동적으로 변경되는 URL이나 매개변수를 처리하는 방식에 차이가 생긴다.
아래 예시 코드의 실행 결과를 살펴보자.
// useAsyncData
const id = ref(1);
const { data, pending, error, refresh, clear } = await useAsyncData(
"item",
() => $fetch(`${getURL}/${id.value}`),
{
watch: [id],
}
);
const { data, pending, error, refresh, clear } = await useAsyncData(
"item",
() =>
$fetch(`${getURL}`, {
params: params.value,
}),
{
watch: [id],
}
);
useAsyncData는 콜백 함수를 사용하여 id 값이 변경될 때마다 이를 반영하여 데이터 요청을 보낸다.
// useFetch
const id = ref(1);
const { data, pending, error, refresh, clear } = await useFetch(
`${getURL}/${id.value}`,
{
key: "item",
watch: [id],
}
);
const { data, pending, error, refresh, clear } = await useFetch(`${getURL}`, {
params: params,
key: "item",
watch: [id],
});
반면 useFetch는 객체를 사용하기 때문에 초기 설정한 URL이나 params를 기반으로 데이터 요청을 하며, 이후 id 값이 변경되어도 이를 반영하지 않는다.
위의 특성 때문에, 동적으로 url이 바뀔 때 요청하는 부분에서 각 다른 결과를 만들어내며, 그래서 useFetch 는 값이 변경이 안되는것이다. Nuxt3 진영에서는 해당 현상을 freezing 이라고 부른다.
하지만 useFetch에서도 동적으로 URL에 대한 처리가 가능하다.
공식문서를 살펴보니, useFetch의 인자로 콜백함수도 넘겨줄 수 있다는 것을 알았다.
function useFetch<DataT, ErrorT>(
url: string | Request | Ref<string | Request> | () => string | Request,
options?: UseFetchOptions<DataT>
): Promise<AsyncData<DataT, ErrorT>>
그래서 URL을 콜백 함수 형태로 전달했더니, useAsyncData처럼 처리가 가능했다.
const id = ref(1);
const { data, pending, error, refresh, clear } = await useFetch(
() => `${getURL}/${id.value}`,
{
key: "item",
watch: [id],
}
);
또한 params가 동적으로 변경되는 것을 감지하기 위해 computed 속성을 사용하는 방법으로도 처리해봤다. 해당 방식도 잘 작동했다.
const id = ref(1);
const params = computed(() => ({ id: id.value }));
const { data, pending, error, refresh, clear } = await useFetch(`${getURL}`, {
params: params,
key: "item",
});
useFetch에서도 useAsyncData처럼 콜백함수로 처리할 수 있다는 것을 알게되고 2차적으로 다시 혼동이 왔다.
처음 삽질 이후엔, 콜백함수의 유무에 따라 두 개의 특징이 크게 구분된다고 생각했기 때문이다.
그럼 대체 왜 두 개가 구분되어져 있으며, 어떤 상황에서 각 메소드를 구분해서 사용해야하는 걸지 더 미궁속으로 빠지게 되었다...
추가적인 연구 이후, 공식 문서에서는 useAsyncData가 다음과 같은 상황에서 유용하다고 설명하고 있는 것을 확인할 수 있었다.
1. CMS나 서드 파티 서비스가 자체적인 쿼리 레이어를 제공하는 경우
There are some cases when using the useFetch composable is not appropriate, for example when a CMS or a third-party provide their own query layer. In this case, you can use useAsyncData to wrap your calls and still keep the benefits provided by the composable. (Nuxt 공식문서)
CMS나 서드 파티 서비스가 자체적인 쿼리 레이어를 제공하는 경우에 useFetch가 적합하지 않을 수 있다. 이럴 때는 useAsyncData를 사용하여 해당 쿼리 레이어를 감싸는 것이 좋다.
const { data, error } = await useAsyncData('users', () => myGetFunction('users'))
여기서 myGetFunction은 자체적인 쿼리 레이어를 제공하는 API 호출 함수이다. 따라서 $fetch와 useFetch로는 이를 처리할 수 없기 때문에 useAsyncData를 사용한다.
2. 여러 $fetch 요청을 병렬로 처리하고, 그 결과를 하나로 결합하여 반환하고 싶을 때
The useAsyncData composable is a great way to wrap and wait for multiple $fetch requests to be completed, and then process the results. (Nuxt 공식문서)
여러 개의 API 요청을 보내고 그 결과를 하나로 묶어 처리해야 할 때 useAsyncData가 유용하다. 아래 예시는 두 개의 fetch 요청을 병렬로 보내고, 그 결과를 하나로 결합하는 코드이다.
<script setup lang="ts">
const { data: discounts, pending } = await useAsyncData('cart-discount', async () => {
const [coupons, offers] = await Promise.all([
$fetch('/cart/coupons'),
$fetch('/cart/offers')
])
return { coupons, offers }
})
// discounts.value.coupons
// discounts.value.offers
</script>
위 코드를 useFetch로 활용하려면, 다음과 같이 useFetch를 2번 사용해야한다.
const { data: coupons, pending: couponsPending } = await useFetch('/cart/coupons')
const { data: offers, pending: offersPending } = await useFetch('/cart/offers')
const discounts = computed(() => ({
coupons: coupons.value,
offers: offers.value
}))
하지만 useAsyncData로 wrapping 하는 경우, 두 개의 API 호출이 모두 완료될 때까지 대기 상태가 되므로, 상황에 맞게 적절한 방법을 선택해야 한다.
최종 정리: useFetch와 useAsyncData
useFetch와 useAsyncData를 이해하고 분석하는 데 많은 시간을 투자했다. Nuxt GitHub 커뮤니티에서도 다양한 의견이 나뉘고 있으며, 특별한 경우가 아니라면 두 메소드를 명확하게 구분하여 사용하는 것이 드물다는 점에서 많은 사람들이 동의하는 것 같다.
정답은 없으며, 각 상황에 맞게 메소드를 선택하여 사용하는 것이 중요하다고 판단된다. 내가 결론은 다음과 같다:
- $fetch: 클라이언트 사이드 상호작용(이벤트 기반)과 같은 POST/PUT/PATCH/DELETE 요청에 사용
- useFetch: 페이지, 컴포넌트, 플러그인 등에서 비동기 데이터를 가져오는 경우에 사용
- useAsyncData: CMS나 서드 파티 서비스가 자체적인 쿼리 레이어를 제공하거나, 여러 API 요청을 하나로 묶어 처리하고 싶은 특수한 경우에 사용
useFetch에서 콜백 함수를 사용할 수 없는 경우, useAsyncData + $fetch를 사용하는 것이 적절할 수 있다. 그러나 useFetch에서도 콜백 함수를 활용할 수 있다.
특히 URL만 입력하는 API 호출이라면 코드도 조금이나마 보기 편해진다.
무엇보다 useFetch를 활용하면 인터셉터를 활용할 수 있다. 따라서, 전역적으로 API 코드를 관리하는 데 매우 유용한 도구가 될 것이라고 생각했다.
원래 위와 같이 결론 내렸으나, 추후에 프로젝트에서 API 호출과 관리를 효율적으로 하기 위해 Repository 패턴을 도입하면서 결론을 다음과 수정하게 되었다.
- $fetch: 클라이언트 사이드 상호작용(이벤트 기반)과 같은 POST/PUT/PATCH/DELETE 요청에 사용
- useAsyncData: 페이지 로드 시 필요한 데이터를 미리 가져와 렌더링을 해야하는 경우. 혹은 CMS나 서드 파티 서비스가 자체적인 쿼리 레이어를 제공하거나, 여러 API 요청을 하나로 묶어 처리하고 싶은 특수한 경우에 사용
useFetch에서 $fetch + useAsyncData를 사용하기로 결심한 이유는 다음과 같다.
1. Repository 패턴 적용의 유연함
Repository 패턴을 적용할 때, $fetch를 커스터마이즈하고 전역적으로 API 클라이언트를 관리할 수 있다. 이를 통해 전체 프로젝트에서 일관된 방식으로 API 호출을 처리할 수 있으며, 코드의 가독성과 유지보수성을 향상시킬 수 있었다.
또한 필요한 부분에서는 useAsyncData로 감싸기만 하면 되기 때문에 보다 간편하게 패턴을 적용하고 활용할 수 있었다.
API 호출 및 관리 부분에서 useAsyncData를 사용하기로 결심한 이유가 가장 컸다.
2. 특수한 경우 처리의 용이함
기본적으로 useAsyncData로 처리하게 되면, MS나 서드 파티 서비스가 자체적인 쿼리 레이어를 제공하거나, 여러 API 요청을 하나로 묶어 처리하고 싶은 특수한 경우도 포괄하여 처리가 가능하다
3. useFetch 콜백함수에 대한 의문
useFetch(url)과 useFetch(() => $fetch(url))은 기본적으로 동일한 기능을 수행하지만, useFetch의 인자로 콜백 함수를 사용하는 경우 코드의 의도를 명확히 전달하기 어려울 수 있다는 점을 우려했다.
협업을 하는 입장에서 이러한 혼동을 방지하기 위해, $fetch + useAsyncData를 사용하는 것이 좋겠다고 판단했다.
마치며...
이러한 컴포저블을 사용함으로써, Nuxt는 서버 사이드 렌더링(SSR)과 클라이언트 사이드 네비게이션(CSR) 모두에서 효율적으로 데이터 패칭을 관리할 수 있다. 이는 성능 최적화와 사용자 경험 향상에 큰 도움이 된다.
결론적으로, Nuxt의 useFetch, useAsyncData, $fetch 컴포저블을 적절히 활용함으로써 데이터 패칭을 최적화하고, 서버와 클라이언트 사이에서 일관된 데이터 상태를 유지할 수 있다.
이를 위해서는 SSR과 CSR이 작동하는 원리와 함께 Nuxt에서 제공하는 data fetching composables의 옵션과 작동 방식을 잘 숙지하는 것이 중요하다.
'Nuxt.js' 카테고리의 다른 글
JWT in Nuxt3 (0) | 2024.09.03 |
---|---|
Caching in Nuxt3 (0) | 2024.07.30 |
[Nuxt3] 효율적인 API 호출 및 관리: Factory + Repository Pattern 적용기 (0) | 2024.07.22 |
[Nuxt3] 렌더링 모드의 핵심과 라이프사이클 이해하기 (1) | 2024.07.06 |