Nuxt에는 브라우저 또는 서버 환경에서 데이터 가져오기를 수행하기 위한 두 개의 컴포저블 useFetch, useAsyncData 와 내장 라이브러리 $fetch 가 함께 제공된다.
간단히 말해서:
- useFetch 는 컴포저블은 컴포넌트에 셋업 함수에서 데이터 가져오는 가장 간단한 방법이다.
- $fetch 는 사용자 상호 작용을 기반으로 네트워크 요청을 할때 좋다.
- useAsyncData 와 $fetch 를 결합하면 더욱 세밀한 제어가 가능하다.
두 가지 useFetch, useAsyncData 모두 마지막 섹션에서 공통 옵션 및 패턴 세트와 함께 자세히 설명하겠다.
그 전에 먼저 이러한 컴포저블이 존재하는 이유를 아는 것이 중요하다.
특정 컴포저블을 사용하는 이유는 무엇일까?
클라이언트와 서버 환경 모두에서 호출을 수행하고 페이지를 렌더링할 수 있는 Nuxt와 같은 프레임워크를 사용할 때 아래와 같이 몇 가지 문제를 해결해야 하는데, 이것이 바로 Nuxt가 쿼리를 래핑하기 위한 컴포저블 $fetch 을 제공하는 이유이다. SSR 지원 시 발생할 수 있는 아래의 문제를 살펴보자.
네트워크 중복 호
useFetch 및 useAsyncData 컴포저블은 서버에서 API 호출이 이루어지면 데이터 페이로드가 클라이언트에 올바르게 전달되도록 보장한다.
페이로드는 useNuxtApp().payload 를 통해 액세스할 수 있는 JavaScript 개체이다. 브라우저에서 코드가 실행될 때 동일한 데이터를 다시 가져오는 것을 방지하기 위해 클라이언트에서 사용된다.
Nuxt DevTools를 사용하여 페이로드 탭 에서 이 데이터를 검사할 수 있다.
Suspense
Nuxt는 모든 비동기 데이터를 뷰에 사용할 수 있기 전에 탐색을 방지하기 위해 내부적으로 Vue의 <Suspense> 컴포넌트를 사용한다. 데이터 가져오기 컴포저블은 이 기능을 활용하고 호출별로 가장 적합한 기능을 사용하는 데 도움이 될 수 있다.
useFetch
useFetch 컴포저블은 데이터 가져오기를 수행하는 가장 간단한 방법이다.
<!--
app.vue
-->
<script setup lang="ts">
const { data: count } = await useFetch('/api/count')
</script>
<template>
Page visits: {{ count }}
</template>
이 컴포저블은 useAsyncData 컴포저블과 $fetch 유틸리티를 둘러싼 래퍼이다.
참고) Docs > API > Composable > Use Fetch
참고) Docs > Examples > Feature > Data Fetching
$fetch
Nuxt에는 ofetch라이브러리가 포함되어 있으며 애플리케이션 전체에 걸쳐 $fetch 별칭으로 자동 임포트 된다. useFetch 가이것으로 구현되었다.
const users = await $fetch('/api/users').catch((error) => error.data)
$fetch 만을 사용하면 네트워크 중볼 호출 제거 및 탐색 방지 기능이 제공되지 않는다 . 이벤트 핸들러에 데이터를 게시할 때, 클라이언트측 전용 로직을 수행할 때, 또는 useAsyncData 와 조합해서 쓸 때 사용하기를 권장한다.
ofetch 라이브러리는 fetch API 위에 구축되었으며 여기에 다음과 같은 편리한 기능을 추가한다.
- 브라우저, Node 또는 작업자 환경에서 동일한 방식으로 작동한다.
- 자동 응답 구문 분석
- 오류 처리
- 자동 재시도
- 인터셉터
참고) ofetch github
useAsyncData
useAsyncData 컴포저블은 비동기 로직을 래핑하고 해결된 결과를 반환하는 역할을 담당한다.
실제로 가장 일반적인 사용 사례 useAsyncData(url, () => $fetch(url)) 와 useFetch(url) 는 거의 동일하다.
useFetch 컴포저블 을 사용하는 것이 적절하지 않은 경우가 있다 (예: CMS 또는 타사에서 자체 쿼리 레이어를 제공하는 경우). 이 경우 useAsyncData 를 사용하여 호출을 래핑하면서도 컴포저블에서 제공하는 이점을 계속 유지할 수 있다.
useAsyncData 의 첫 번째 인수는 두 번째 인수인 쿼리 함수의 응답을 캐시하는 데 사용되는 고유 키 이다. 쿼리 함수를 직접 전달하면 이 인수를 무시할 수 있다. 이 때는 자동 생성된다.
const { data, error } = await useAsyncData('users', () => myGetFunction('users'))
자동 생성된 키는 호출된 파일과 줄만 고려하므로 래핑되는 자체 커스텀 useAsyncData 컴포저블을 만드는 경우 원치 않는 동작을 방지하려면 항상 자체 키를 만드는 것이 좋다.
const id = ref(1)
const { data, error } = await useAsyncData(`user:${id.value}`, () => {
return myGetFunction('users', { id: id.value })
})
useAsyncData 컴포저블은 여러 useFetch 작업이 완료될 때까지 래핑하고 기다린 후 각 작업의 결과를 검색하는 방법을 제공한다.
const { data: discounts, pending } = await useAsyncData('cart-discount', async () => {
const [coupons, offers] = await Promise.all([$fetch('/cart/coupons'), $fetch('/cart/offers')])
return {
coupons,
offers
}
})
참고) useAsyncData
옵션
useAsyncData 와 useFetch sms 동일한 객체 유형을 반환하고 공통 옵션 세트를 마지막 인수로 받아들인다. 이 공통 옵션은 탐색 차단, 캐싱, 실행과 같은 컴포저블 동작을 제어하는 데 도움이 될 수 있다.
Lazy
기본적으로 데이터 가져오기 컴포저블은 Vue의 Suspense를 사용하여 새 페이지로 이동하기 전에 비동기 함수가 해결될 때까지 기다린다. 이 기능은 lazy 옵션을 사용하여 클라이언트 측 탐색에서 무시할 수 있다 . 이 경우 해당 pending 값을 사용하여 로드 상태를 수동으로 처리해야 한다.
<!--
app.vue
-->
<script setup lang="ts">
const { pending, data: posts } = useFetch('/api/posts', {
lazy: true
})
</script>
<template>
<!-- you will need to handle a loading state -->
<div v-if="pending">
Loading ...
</div>
<div v-else>
<div v-for="post in posts">
<!-- do something -->
</div>
</div>
</template>
또는 동일한 작업을 수행하기 위한 편리한 방법으로 useLazyFetch 및 useLazyAsyncData 를 사용할 수도 있다 .
const { pending, data: posts } = useLazyFetch('/api/posts')
클라이언트 전용 가져오기
기본적으로 데이터 가져오기 컴포저블은 클라이언트 및 서버 환경 모두에서 비동기 기능을 수행한다. 클라이언트 측에서만 호출을 수행하려면 server옵션을 false 로 설정하면 된다. 초기 로드시 하이드레이션이 완료되기 전에 데이터를 가져오지 않으므로 보류 상태를 처리해야 하지만 후속 클라이언트 측 탐색에서는 페이지를 로드하기 전에 데이터가 대기된다.
lazy 옵션과 결합하면 첫 번째 렌더링에 필요하지 않은 데이터(예: SEO에 민감하지 않은 데이터)에 유용할 수 있다.
/* This call is performed before hydration */
const { article } = await useFetch('api/article')
/* This call will only be performed on the client */
const { pending, data: posts } = useFetch('/api/comments', {
lazy: true,
server: false
})
useFetch 컴포저블은 설정 메서드에서 호출되거나 수명 주기 후크에 있는 함수의 최상위 수준에서 직접 호출되도록 되어 있다. 그렇지 않으면 $fetch 함수를사용해야 한.
페이로드 크기 최소화
pick 옵션을 사용하면 컴포저블에서 반환하려는 필드만 선택하여 HTML 문서에 저장된 페이로드 크기를 최소화하는 데 도움이 된다.
<script setup lang="ts">
/* only pick the fields used in your template */
const { data: mountain } = await useFetch('/api/mountains/everest', { pick: ['title', 'description'] })
</script>
<template>
<h1>{{ mountain.title }}</h1>
<p>{{ mountain.description }}</p>
</template>
여러 개체에 대해 더 많은 제어나 매핑이 필요한 경우 이 transform기능을 사용하여 쿼리 결과를 변경할 수 있다.
const { data: mountains } = await useFetch('/api/mountains', {
transform: (mountains) => {
return mountains.map(mountain => ({ title: mountain.title, description: mountain.description }))
}
})
pick transform 둘 다 처음에 원하지 않는 데이터를 가져오는 것을 방지하지 않는다. 그러나 서버에서 클라이언트로 전송되는 페이로드에 원치 않는 데이터가 추가되는 것을 방지해준다.
캐싱 및 다시 가져오기
Keys
useFetch 와 useAsyncData 사용 시 동일한 데이터를 다시 가져오는 것을 방지하려면 키를 사용한다.
- useFetch 는 제공된 URL을 키로 사용한다. 또는 마지막 인수로 전달된 options 개체에 key 값을 제공할 수 있다 .
- useAsyncData 는 문자열인 경우 첫 번째 인수를 키로 사용한다. 첫 번째 인수가 쿼리를 수행하는 핸들러 함수인 경우 useAsyncData 인스턴스의 파일 이름과 줄 번호를 이용한 고유한 키가 생성된다.
키로 캐시된 데이터를 얻으려면 useNuxtData 을 사용할 수 있다.
새로 고침 및 실행
데이터를 수동으로 가져오거나 새로 고치려면 컴포저블에서 제공하는 execute 또는 refresh 함수를 사용한다.
<script setup lang="ts">
const { data, error, execute, refresh } = await useFetch('/api/users')
</script>
<template>
<div>
<p>{{ data }}</p>
<button @click="refresh">Refresh data</button>
</div>
</template>
이 execute 함수는 정확히 동일한 방식으로 작동하지만 fetch 가 즉각적이지 않은 경우에 더 의미가 있는 refresh 의 별칭 이다.
캐시된 데이터를 전체적으로 다시 가져오거나 무효화하려면 clearNuxtData 및 refreshNuxtData 를 참조한다.
Watch
애플리케이션의 다른 반응 값이 변경될 때마다 가져오기 기능을 다시 실행하려면 watch 옵션을 사용한다. 하나 이상의 감시 가능한 요소에 사용할 수 있다.
const id = ref(1)
const { data, error, refresh } = await useFetch('/api/users', {
/* Changing the id will trigger a refetch */
watch: [id]
})
반응형 값을 관찰해도 가져온 URL은 변경되지 않는다 . 예를 들어, 함수가 호출되는 순간 URL이 구성되므로 사용자의 동일한 초기 ID를 계속 가져온다.
const id = ref(1)
const { data, error, refresh } = await useFetch(`/api/users/${id.value}`, {
watch: [id]
})
반응 값을 기반으로 URL을 변경해야 하는 경우 Computed URL을 대신 사용할 수 있다.
Computed URL
때로는 반응 값에서 URL을 계산하고 이러한 값이 변경될 때마다 데이터를 새로 고쳐야 할 수도 있다. 저글링 대신 각 매개변수를 반응 값으로 연결할 수 있다. Nuxt는 반응 값을 자동으로 사용하고 변경될 때마다 다시 가져온다.
const id = ref(null)
const { data, pending } = useLazyFetch('/api/user', {
query: {
user_id: id
}
})
더 복잡한 URL 구성의 경우 URL 문자열을 반환하는 계산된 getter 로 콜백을 사용할 수 있다.
종속성이 변경될 때마다 새로 구성된 URL을 사용하여 데이터를 가져온다. 이것을 not-immediate 와 결합하면 가져오기 전에 반응 요소가 변경될 때까지 기다릴 수 있다.
<script setup lang="ts">
const id = ref(null)
const { data, pending, status } = useLazyFetch(() => `/api/users/${id.value}`, {
immediate: false
})
</script>
<template>
<div>
<!-- disable the input while fetching -->
<input v-model="id" type="number" :disabled="pending"/>
<div v-if="status === 'idle'">
Type an user ID
</div>
<div v-else-if="pending">
Loading ...
</div>
<div v-else>
{{ data }}
</div>
</div>
</template>
다른 반응 값이 변경될 때 강제로 새로 고쳐야 하는 경우 다른 값을 볼 수도 있다(Watch).
Not immediate
useFetch 컴포저블 호출되는 순간 데이터 가져오기를 시작다. 사용자 상호 작용을 기다리도록 immediate: false 로 설정하여 이를 방지할 수 있다 .
이를 위해, status 는 가져오기 수명주기를 처리하고 execute 는 데이터 가져오기를 시작하는 필요하다 .
<script setup lang="ts">
const { data, error, execute, pending, status } = await useLazyFetch('/api/comments')
</script>
<template>
<div v-if="status === 'idle'">
<button @click="execute">Get data</button>
</div>
<div v-else-if="pending">
Loading comments...
</div>
<div v-else>
{{ data }}
</div>
</template>
보다 세부적인 제어를 위해 status변수는 다음과 같다.
- idle 가져오기가 시작되지 않은 경우
- pending 가져오기가 시작되었지만 아직 완료되지 않은 경우
- error 가져오기에 실패했을 때
- success 가져오기가 성공적으로 완료되면
헤더 및 쿠키 전달
브라우저에서 $fetch 가 호출되면 cookie 같은 사용자 헤더가 API로 직접 전송된다. 그러나 서버 측 렌더링 중에는 $fetch요청이 서버 내에서 '내부적으로' 발생하므로 사용자의 브라우저 쿠키를 포함하지 않으며 가져오기 응답에서 쿠키를 전달하지도 않는다.
클라이언트 헤더를 API에 전달
서버 측에서 API에 액세스하고 쿠키를 프록시하는데 useRequestHeaders 를 사용할 수 있다.
아래 예에서는 $fetch 호출에 요청 헤더를 추가하여 API 엔드포인트가 원래 사용자가 보낸 동일한 cookie 헤더에 액세스할 수 있도록 한다.
<script setup lang="ts">
const headers = useRequestHeaders(['cookie'])
const { data } = await useFetch('/api/me', { headers })
</script>
헤더를 외부 API로 프록시하기 전에 매우 주의해야하고 필요한 헤더만 포함해야한다. 모든 헤더가 우회해도 안전한 것은 아니며 원치 않는 동작이 발생할 수 있다. 다음은 프록시되지 않는 일반적인 헤더 목록이다.
- host,accept
- content-length, content-md5,content-type
- x-forwarded-host, x-forwarded-port,x-forwarded-proto
- cf-connecting-ip,cf-ray
SSR 응답 시 서버 측 API 호출에서 쿠키 전달
서버사이드 요청에서 결과로 전송된 프록시된 쿠키를 전달하려면 이를 직접 처리해야 한다.
// composables/fetch.ts
import { appendResponseHeader, H3Event } from 'h3'
export const fetchWithCookie = async (event: H3Event, url: string) => {
/* Get the response from the server endpoint */
const res = await $fetch.raw(url)
/* Get the cookies from the response */
const cookies = (res.headers.get('set-cookie') || '').split(',')
/* Attach each cookie to our incoming Request */
for (const cookie of cookies) {
appendResponseHeader(event, 'set-cookie', cookie)
}
/* Return the data of the response */
return res._data
}
<script setup lang="ts">
// This composable will automatically pass cookies to the client
const event = useRequestEvent()
const result = await fetchWithCookie(event, '/api/with-cookie')
onMounted(() => console.log(document.cookie))
</script>
옵션 API 지원
Nuxt 3는 asyncData를 수행하기 위해서 옵션 API 내에서 데이터 가져오기를 수행하는 방법을 제공한다 . 이것이 작동하려면 컴포넌트 정의를 defineNuxtComponent 안에서 래핑해야 한다.
<script>
export default defineNuxtComponent({
/* Use the fetchKey option to provide a unique key */
fetchKey: 'hello',
async asyncData () {
return {
hello: await $fetch('/api/hello')
}
}
})
</script>
Nuxt 3에서 Vue 컴포넌트를 선언하는데 권장되는 방법은 <script setup lang="ts">를 사용하는 것이다.
직렬화
server 디렉터리에서 데이터를 가져올 때 응답은 JSON.stringify 를 사용한 결과다. 그러나 직렬화는 JavaScript 기본 유형으로만 제한되므로 Nuxt는 $fetch 와 useFetch 의 반환 유형을 변환하고 실제 값과 최대한 일치시킨다.
직렬화 예
// server/api/foo.ts
export default defineEventHandler(() => {
return new Date()
})
<!--
app.vue
-->
<script setup lang="ts">
// Type of `data` is inferred as string even though we returned a Date object
const { data } = await useFetch('/api/foo')
</script>
커스텀 직렬화 기능
직렬화 동작을 사용자 정의하려면 반환된 객체에 toJSON 함수를 정의하면 된다 . toJSON 함수를 정의하면 Nuxt는 함수의 반환 유형을 존중하고 유형 변환을 시도하지 않는다.
// server/api/bar.ts
export default defineEventHandler(() => {
const data = {
createdAt: new Date(),
toJSON() {
return {
createdAt: {
year: this.createdAt.getFullYear(),
month: this.createdAt.getMonth(),
day: this.createdAt.getDate(),
},
}
},
}
return data
})
<!--
app.vue
-->
<script setup lang="ts">
// Type of `data` is inferred as
// {
// createdAt: {
// year: number
// month: number
// day: number
// }
// }
const { data } = await useFetch('/api/bar')
</script>
대체 직렬 변환기 사용
Nuxt는 현재 JSON.stringify 에 대한 대체 직렬 변환기를 지원하지 않는다. 그러나 페이로드를 일반 문자열로 반환하고 이 toJSON메서드를 활용하여 유형 안전성을 유지할 수 있다.
아래 예에서는 superjson을 직렬 변환기로 사용한다.
// server/api/superjson.ts
import superjson from 'superjson'
export default defineEventHandler(() => {
const data = {
createdAt: new Date(),
// Workaround the type conversion
toJSON() {
return this
}
}
// Serialize the output to string, using superjson
return superjson.stringify(data) as unknown as typeof data
})
<!--
app.vue
-->
<script setup lang="ts">
import superjson from 'superjson'
// `date` is inferred as { createdAt: Date } and you can safely use the Date object methods
const { data } = await useFetch('/api/superjson', {
transform: (value) => {
return superjson.parse(value as unknown as string)
},
})
</script>
'Nuxt 공식문서 번역 > 개요' 카테고리의 다른 글
12. 오류 처리(Error Handling) (0) | 2023.12.11 |
---|---|
11. 상태 관리 (0) | 2023.12.11 |
9. Transitions (0) | 2023.12.11 |
8. SEO 와 Meta (0) | 2023.12.10 |
7. 라우팅 (0) | 2023.12.10 |