10. 데이터 가져오기(Data fetching)

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

참고) Nuxt-Utils > $fetch

 

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
  • 네이버 블로그 공유
  • 네이버 밴드 공유
  • 페이스북 공유