데이터 가져오기(Data Fetching)

Nuxt는 애플리케이션 내에서 데이터 가져오기를 처리하기 위한 composable들을 제공합니다.

Nuxt에는 브라우저나 서버 환경에서 데이터 가져오기를 수행하기 위한 두 개의 composable과 내장 라이브러리가 함께 제공됩니다: useFetch, useAsyncData, $fetch.

간단히 말하면:

  • $fetch는 네트워크 요청을 보내는 가장 단순한 방법입니다.
  • useFetch$fetch를 감싼 래퍼로, 유니버설 렌더링에서 데이터를 한 번만 가져옵니다.
  • useAsyncDatauseFetch와 비슷하지만 더 세밀한 제어를 제공합니다.

useFetchuseAsyncData는 공통된 옵션과 패턴을 공유하며, 이는 마지막 섹션에서 자세히 설명합니다.

useFetchuseAsyncData가 필요한 이유

Nuxt는 서버와 클라이언트 환경 모두에서 동형(isomorphic) (또는 유니버설) 코드를 실행할 수 있는 프레임워크입니다. 만약 Vue 컴포넌트의 setup 함수에서 데이터 가져오기를 수행하기 위해 $fetch 함수를 사용한다면, 이는 데이터를 두 번 가져오게 만들 수 있습니다. 한 번은 서버에서 HTML을 렌더링하기 위해, 또 한 번은 HTML이 하이드레이션될 때 클라이언트에서입니다. 이는 하이드레이션 문제를 일으키고, 상호작용 가능 시점을 늦추며, 예측 불가능한 동작을 유발할 수 있습니다.

useFetchuseAsyncData composable은 API 호출이 서버에서 이루어진 경우, 그 데이터를 페이로드를 통해 클라이언트로 전달되도록 보장함으로써 이 문제를 해결합니다.

페이로드는 useNuxtApp().payload를 통해 접근할 수 있는 JavaScript 객체입니다. 이는 하이드레이션 중 브라우저에서 코드가 실행될 때 동일한 데이터를 다시 가져오는 것을 피하기 위해 클라이언트에서 사용됩니다.

Nuxt DevTools를 사용해 Payload 탭에서 이 데이터를 확인할 수 있습니다.
app/app.vue
<script setup lang="ts">
const { data } = await useFetch('/api/data')

async function handleFormSubmit () {
  const res = await $fetch('/api/submit', {
    method: 'POST',
    body: {
      // 내 폼 데이터
    },
  })
}
</script>

<template>
  <div v-if="data == undefined">
    No data
  </div>
  <div v-else>
    <form @submit="handleFormSubmit">
      <!-- form input 태그들 -->
    </form>
  </div>
</template>

위 예시에서 useFetch는 요청이 서버에서 발생하고 브라우저로 올바르게 전달되도록 보장합니다. $fetch에는 이러한 메커니즘이 없으며, 요청이 오직 브라우저에서만 이루어질 때 사용하는 것이 더 좋은 선택입니다.

Suspense

Nuxt는 Vue의 <Suspense> 컴포넌트를 내부적으로 사용하여 모든 비동기 데이터가 뷰에 사용 가능해지기 전까지 네비게이션을 막습니다. 데이터 가져오기 composable들은 이 기능을 활용하고, 호출마다 가장 적합한 방식을 사용할 수 있도록 도와줍니다.

페이지 간 네비게이션 사이에 진행 표시줄을 추가하려면 <NuxtLoadingIndicator>를 추가할 수 있습니다.

$fetch

Nuxt는 ofetch 라이브러리를 포함하고 있으며, 애플리케이션 전역에서 $fetch 별칭으로 자동 임포트됩니다.

pages/todos.vue
<script setup lang="ts">
async function addTodo () {
  const todo = await $fetch('/api/todos', {
    method: 'POST',
    body: {
      // 내 todo 데이터
    },
  })
}
</script>
$fetch만 사용하면 네트워크 호출 중복 제거 및 네비게이션 방지를 제공하지 않는다는 점에 주의하세요.
초기 컴포넌트 데이터를 가져올 때는 $fetch를 클라이언트 측 상호작용(이벤트 기반)에 사용하거나 useAsyncData와 함께 사용하는 것이 권장됩니다.
$fetch에 대해 더 읽어보기.

클라이언트 헤더를 API로 전달하기

서버에서 useFetch를 호출할 때, Nuxt는 useRequestFetch를 사용하여 클라이언트 헤더와 쿠키를 프록시합니다 (host와 같이 전달되지 말아야 할 헤더는 예외).

<script setup lang="ts">
const { data } = await useFetch('/api/echo')
</script>
// /api/echo.ts
export default defineEventHandler(event => parseCookies(event))

또는, 아래 예시는 서버 측 요청(클라이언트에서 시작된)에서 쿠키에 접근하고 이를 API로 전송하기 위해 useRequestHeaders를 사용하는 방법을 보여줍니다. 동형 $fetch 호출을 사용함으로써, API 엔드포인트가 사용자의 브라우저에서 원래 전송된 것과 동일한 cookie 헤더에 접근할 수 있도록 보장합니다. 이는 useFetch를 사용하지 않을 때에만 필요합니다.

<script setup lang="ts">
const headers = useRequestHeaders(['cookie'])

async function getCurrentUser () {
  return await $fetch('/api/me', { headers })
}
</script>
useRequestFetch를 사용해 헤더를 호출에 자동으로 프록시할 수도 있습니다.
헤더를 외부 API로 프록시하기 전에 매우 주의해야 하며, 필요한 헤더만 포함해야 합니다. 모든 헤더가 안전하게 우회될 수 있는 것은 아니며, 원치 않는 동작을 초래할 수 있습니다. 다음은 프록시해서는 안 되는 일반적인 헤더 목록입니다:
  • host, accept
  • content-length, content-md5, content-type
  • x-forwarded-host, x-forwarded-port, x-forwarded-proto
  • cf-connecting-ip, cf-ray

useFetch

useFetch composable은 setup 함수에서 SSR-safe 네트워크 호출을 하기 위해 내부적으로 $fetch를 사용합니다.

app/app.vue
<script setup lang="ts">
const { data: count } = await useFetch('/api/count')
</script>

<template>
  <p>Page visits: {{ count }}</p>
</template>

이 composable은 useAsyncData composable과 $fetch 유틸리티를 감싼 래퍼입니다.

Read more in Docs > 4 X > API > Composables > Use Fetch.
Read and edit a live example in Docs > 4 X > Examples > Features > Data Fetching.

useAsyncData

useAsyncData composable은 비동기 로직을 감싸고, 해당 로직이 resolve된 후 결과를 반환하는 역할을 합니다.

useFetch(url)은 거의 useAsyncData(url, () => event.$fetch(url))와 동일합니다.
가장 일반적인 사용 사례를 위한 개발자 경험 설탕(sugar)입니다. (event.fetch에 대해 더 알고 싶다면 useRequestFetch를 참고하세요.)

useFetch composable을 사용하는 것이 적절하지 않은 경우가 있습니다. 예를 들어 CMS나 서드파티가 자체 쿼리 레이어를 제공하는 경우입니다. 이때는 useAsyncData를 사용해 호출을 감싸면서도 composable이 제공하는 이점을 유지할 수 있습니다.

app/pages/users.vue
<script setup lang="ts">
const { data, error } = await useAsyncData('users', () => myGetFunction('users'))

// 이렇게도 가능합니다:
const { data, error } = await useAsyncData(() => myGetFunction('users'))
</script>
useAsyncData의 첫 번째 인자는 두 번째 인자인 쿼리 함수의 응답을 캐시하는 데 사용되는 고유 키입니다. 이 키는 쿼리 함수를 직접 전달하여 무시할 수 있으며, 이 경우 키는 자동으로 생성됩니다.

자동 생성된 키는 useAsyncData가 호출된 파일과 줄 번호만 고려하기 때문에, 특히 useAsyncData를 감싼 커스텀 composable을 만들 때와 같이 원치 않는 동작을 피하기 위해 항상 직접 키를 만드는 것이 좋습니다.

키를 설정하면 useNuxtData를 사용해 컴포넌트 간에 동일한 데이터를 공유하거나 특정 데이터를 새로고침하는 데 유용합니다.
app/pages/users/[id].vue
<script setup lang="ts">
const { id } = useRoute().params

const { data, error } = await useAsyncData(`user:${id}`, () => {
  return myGetFunction('users', { id })
})
</script>

useAsyncData composable은 여러 $fetch 요청이 완료될 때까지 감싸서 기다린 다음, 그 결과를 처리하는 훌륭한 방법입니다.

<script setup lang="ts">
const { data: discounts, status } = await useAsyncData('cart-discount', async (_nuxtApp, { signal }) => {
  const [coupons, offers] = await Promise.all([
    $fetch('/cart/coupons', { signal }),
    $fetch('/cart/offers', { signal }),
  ])

  return { coupons, offers }
})
// discounts.value.coupons
// discounts.value.offers
</script>
useAsyncData는 데이터를 가져오고 캐시하기 위한 것이며, Pinia 액션 호출과 같은 사이드 이펙트를 트리거하는 용도가 아닙니다. 이는 nullish 값으로 반복 실행되는 등 의도치 않은 동작을 유발할 수 있습니다. 사이드 이펙트를 트리거해야 한다면, 이를 위해 callOnce 유틸리티를 사용하세요.
<script setup lang="ts">
const offersStore = useOffersStore()

// 이렇게 하면 안 됩니다
await useAsyncData(() => offersStore.getOffer(route.params.slug))
</script>
useAsyncData에 대해 더 읽어보기.

반환 값

useFetchuseAsyncData는 아래에 나열된 동일한 반환 값을 가집니다.

  • data: 전달된 비동기 함수의 결과.
  • refresh/execute: handler 함수가 반환한 데이터를 새로고침하는 데 사용할 수 있는 함수.
  • clear: dataundefined(또는 제공된 경우 options.default()의 값)로 설정하고, errorundefined로 설정하며, statusidle로 설정하고, 현재 진행 중인 요청을 취소된 것으로 표시하는 데 사용할 수 있는 함수.
  • error: 데이터 가져오기에 실패한 경우 에러 객체.
  • status: 데이터 요청의 상태를 나타내는 문자열 ("idle", "pending", "success", "error").
data, error, status<script setup>에서 .value로 접근할 수 있는 Vue ref입니다.

기본적으로 Nuxt는 refresh가 완료될 때까지 기다린 후에야 다시 실행할 수 있도록 합니다.

(예를 들어 server: false로) 서버에서 데이터를 가져오지 않은 경우, 하이드레이션이 완료될 때까지 데이터는 가져오지지 않습니다. 이는 클라이언트 측에서 useFetch를 await하더라도 <script setup> 내에서 data는 null 상태로 남는다는 의미입니다.

옵션

useAsyncDatauseFetch는 동일한 객체 타입을 반환하며, 마지막 인자로 공통된 옵션 집합을 받습니다. 이 옵션들은 네비게이션 차단, 캐싱, 실행 방식 등 composable의 동작을 제어하는 데 도움이 됩니다.

Lazy

기본적으로 데이터 가져오기 composable들은 Vue의 Suspense를 사용하여 비동기 함수가 resolve될 때까지 기다린 후에 새 페이지로 네비게이션합니다. 이 기능은 lazy 옵션을 사용해 클라이언트 측 네비게이션에서 무시할 수 있습니다. 이 경우 status 값을 사용해 로딩 상태를 수동으로 처리해야 합니다.

app/app.vue
<script setup lang="ts">
const { status, data: posts } = useFetch('/api/posts', {
  lazy: true,
})
</script>

<template>
  <!-- 로딩 상태를 직접 처리해야 합니다 -->
  <div v-if="status === 'pending'">
    Loading ...
  </div>
  <div v-else>
    <div v-for="post in posts">
      <!-- 무언가를 수행 -->
    </div>
  </div>
</template>

대안으로, 동일한 작업을 수행하기 위한 편의 메서드로 useLazyFetchuseLazyAsyncData를 사용할 수 있습니다.

<script setup lang="ts">
const { status, data: posts } = useLazyFetch('/api/posts')
</script>
useLazyFetch에 대해 더 읽어보기.
useLazyAsyncData에 대해 더 읽어보기.

클라이언트 전용 가져오기

기본적으로 데이터 가져오기 composable들은 클라이언트와 서버 환경 모두에서 비동기 함수를 실행합니다. server 옵션을 false로 설정하면 클라이언트 측에서만 호출을 수행합니다. 초기 로드 시에는 하이드레이션이 완료되기 전까지 데이터가 가져와지지 않으므로, 대기 상태를 처리해야 합니다. 하지만 이후 클라이언트 측 네비게이션에서는 페이지를 로드하기 전에 데이터가 await됩니다.

lazy 옵션과 함께 사용하면, 첫 렌더링에 필요하지 않은 데이터(예: SEO에 민감하지 않은 데이터)에 유용합니다.

/* 이 호출은 하이드레이션 전에 수행됩니다 */
const articles = await useFetch('/api/article')

/* 이 호출은 클라이언트에서만 수행됩니다 */
const { status, data: comments } = useFetch('/api/comments', {
  lazy: true,
  server: false,
})

useFetch composable은 setup 메서드에서 호출되거나, 라이프사이클 훅 내 함수의 최상위 레벨에서 직접 호출되도록 설계되었습니다. 그렇지 않은 경우에는 $fetch 메서드를 사용해야 합니다.

페이로드 크기 최소화

pick 옵션은 composable에서 반환받고자 하는 필드만 선택함으로써 HTML 문서에 저장되는 페이로드 크기를 최소화하는 데 도움이 됩니다.

<script setup lang="ts">
/* 템플릿에서 사용하는 필드만 선택 */
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 }))
  },
})
picktransform 모두 원치 않는 데이터가 처음에 가져와지는 것을 막지는 못합니다. 하지만 서버에서 클라이언트로 전송되는 페이로드에 원치 않는 데이터가 추가되는 것은 방지합니다.

캐싱과 재요청

useFetchuseAsyncData는 동일한 데이터를 다시 가져오는 것을 방지하기 위해 키를 사용합니다.

  • useFetch는 제공된 URL을 키로 사용합니다. 또는 마지막 인자로 전달되는 options 객체에 key 값을 제공할 수 있습니다.
  • useAsyncData는 첫 번째 인자가 문자열인 경우 이를 키로 사용합니다. 첫 번째 인자가 쿼리를 수행하는 handler 함수라면, useAsyncData 인스턴스의 파일 이름과 줄 번호에 고유한 키가 자동으로 생성됩니다.
키로 캐시된 데이터를 가져오려면 useNuxtData를 사용할 수 있습니다.

공유 상태와 옵션 일관성

여러 컴포넌트가 동일한 키로 useAsyncData 또는 useFetch를 사용할 경우, 동일한 data, error, status ref를 공유합니다. 이는 컴포넌트 간 일관성을 보장하지만, 일부 옵션은 일관되게 유지되어야 합니다.

다음 옵션들은 동일한 키를 사용하는 모든 호출에서 반드시 일관되어야 합니다:

  • handler 함수
  • deep 옵션
  • transform 함수
  • pick 배열
  • getCachedData 함수
  • default
// ❌ 이는 개발 환경에서 경고를 발생시킵니다
const { data: users1 } = useAsyncData('users', (_nuxtApp, { signal }) => $fetch('/api/users', { signal }), { deep: false })
const { data: users2 } = useAsyncData('users', (_nuxtApp, { signal }) => $fetch('/api/users', { signal }), { deep: true })

다음 옵션들은 경고 없이 안전하게 다르게 설정할 수 있습니다:

  • server
  • lazy
  • immediate
  • dedupe
  • watch
// ✅ 이는 허용됩니다
const { data: users1 } = useAsyncData('users', (_nuxtApp, { signal }) => $fetch('/api/users', { signal }), { immediate: true })
const { data: users2 } = useAsyncData('users', (_nuxtApp, { signal }) => $fetch('/api/users', { signal }), { immediate: false })

독립적인 인스턴스가 필요하다면, 다른 키를 사용하세요:

// 이들은 완전히 독립적인 인스턴스입니다
const { data: users1 } = useAsyncData('users-1', (_nuxtApp, { signal }) => $fetch('/api/users', { signal }))
const { data: users2 } = useAsyncData('users-2', (_nuxtApp, { signal }) => $fetch('/api/users', { signal }))

반응형 키

키로 computed ref, 일반 ref 또는 getter 함수를 사용할 수 있어, 의존성이 변경될 때 자동으로 업데이트되는 동적 데이터 가져오기가 가능합니다:

// computed 속성을 키로 사용
const userId = ref('123')
const { data: user } = useAsyncData(
  computed(() => `user-${userId.value}`),
  () => fetchUser(userId.value),
)

// userId가 변경되면 데이터가 자동으로 다시 가져와지고
// 다른 컴포넌트에서 사용하지 않는 경우 이전 데이터는 정리됩니다
userId.value = '456'

Refresh와 execute

데이터를 수동으로 가져오거나 새로고침하려면, composable이 제공하는 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 함수는 refresh의 별칭으로, 완전히 동일하게 동작하지만, 즉시 실행되지 않을 때 더 의미론적인 이름입니다.

전역적으로 캐시된 데이터를 다시 가져오거나 무효화하려면 clearNuxtDatarefreshNuxtData를 참고하세요.

Clear

어떤 이유로든 제공된 데이터를 지우고 싶지만, clearNuxtData에 전달할 특정 키를 알고 싶지 않은 경우, composable이 제공하는 clear 함수를 사용할 수 있습니다.

<script setup lang="ts">
const { data, clear } = await useFetch('/api/users')

const route = useRoute()
watch(() => route.path, (path) => {
  if (path === '/') {
    clear()
  }
})
</script>

Watch

애플리케이션 내 다른 반응형 값이 변경될 때마다 가져오기 함수를 다시 실행하려면, watch 옵션을 사용하세요. 하나 또는 여러 개의 watch 가능한 요소에 사용할 수 있습니다.

<script setup lang="ts">
const id = ref(1)

const { data, error, refresh } = await useFetch('/api/users', {
  /* id가 변경되면 다시 가져오기를 트리거합니다 */
  watch: [id],
})
</script>

반응형 값을 감시해도 가져오는 URL은 변경되지 않는다는 점에 유의하세요. 예를 들어, 아래 코드는 함수가 호출되는 시점에 URL이 구성되기 때문에 계속해서 동일한 초기 ID의 사용자를 가져옵니다.

<script setup lang="ts">
const id = ref(1)

const { data, error, refresh } = await useFetch(`/api/users/${id.value}`, {
  watch: [id],
})
</script>

반응형 값을 기반으로 URL을 변경해야 한다면, 대신 computed URL을 사용하는 것이 좋습니다.

반응형 fetch 옵션이 제공되면, 이들은 자동으로 감시되어 재요청을 트리거합니다. 어떤 경우에는 watch: false를 지정해 이 동작을 옵트아웃하는 것이 유용할 수 있습니다.

const id = ref(1)

// id가 변경되어도 자동으로 다시 가져오지 않습니다
const { data, execute } = await useFetch('/api/users', {
  query: { id }, // id는 기본적으로 감시됩니다
  watch: false, // id의 자동 감시를 비활성화
})

// 재요청을 트리거하지 않음
id.value = 2

Computed URL

때때로 반응형 값으로부터 URL을 계산하고, 이 값들이 변경될 때마다 데이터를 새로 가져와야 할 수 있습니다. 복잡하게 처리하는 대신, 각 파라미터를 반응형 값으로 연결할 수 있습니다. Nuxt는 반응형 값을 자동으로 사용하고, 값이 변경될 때마다 다시 가져옵니다.

<script setup lang="ts">
const id = ref(null)

const { data, status } = useLazyFetch('/api/user', {
  query: {
    user_id: id,
  },
})
</script>

더 복잡한 URL 구성이 필요한 경우, URL 문자열을 반환하는 computed getter 콜백을 사용할 수 있습니다.

의존성이 변경될 때마다 새로 구성된 URL을 사용해 데이터를 가져옵니다. 이를 not-immediate와 결합하면, 반응형 요소가 변경될 때까지 가져오기를 기다릴 수 있습니다.

<script setup lang="ts">
const id = ref(null)

const { data, status } = useLazyFetch(() => `/api/users/${id.value}`, {
  immediate: false,
})

const pending = computed(() => status.value === 'pending')
</script>

<template>
  <div>
    <!-- 가져오는 동안 입력을 비활성화 -->
    <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>

다른 반응형 값이 변경될 때 강제로 새로고침해야 한다면, 다른 값을 감시할 수도 있습니다.

즉시 실행 안 함

useFetch composable은 호출되는 즉시 데이터 가져오기를 시작합니다. 예를 들어 사용자 상호작용을 기다리기 위해, immediate: false를 설정해 이를 방지할 수 있습니다.

이 경우, 가져오기 라이프사이클을 처리하기 위한 status와, 데이터 가져오기를 시작하기 위한 execute가 모두 필요합니다.

<script setup lang="ts">
const { data, error, execute, status } = await useLazyFetch('/api/comments', {
  immediate: false,
})
</script>

<template>
  <div v-if="status === 'idle'">
    <button @click="execute">
      Get data
    </button>
  </div>

  <div v-else-if="status === 'pending'">
    Loading comments...
  </div>

  <div v-else>
    {{ data }}
  </div>
</template>

더 세밀한 제어를 위해, status 변수는 다음과 같습니다:

  • idle: 가져오기가 시작되지 않은 상태
  • pending: 가져오기가 시작되었지만 아직 완료되지 않은 상태
  • error: 가져오기에 실패한 상태
  • success: 가져오기가 성공적으로 완료된 상태

헤더와 쿠키 전달하기

브라우저에서 $fetch를 호출할 때, cookie와 같은 사용자 헤더는 API로 직접 전송됩니다.

일반적으로 서버 사이드 렌더링 중에는 보안상의 이유로 $fetch가 사용자의 브라우저 쿠키를 포함하지 않으며, fetch 응답의 쿠키를 전달하지도 않습니다.

그러나 서버에서 상대 URL로 useFetch를 호출할 때, Nuxt는 useRequestFetch를 사용해 헤더와 쿠키를 프록시합니다 (host와 같이 전달되지 말아야 할 헤더는 예외).

SSR 응답에서 서버 측 API 호출의 쿠키 전달하기

내부 요청에서 클라이언트로 쿠키를 전달/프록시하려면, 이를 직접 처리해야 합니다.

app/composables/fetch.ts
import { appendResponseHeader } from 'h3'
import type { H3Event } from 'h3'

export const fetchWithCookie = async (event: H3Event, url: string) => {
  /* 서버 엔드포인트로부터 응답을 가져옵니다 */
  const res = await $fetch.raw(url)
  /* 응답에서 쿠키를 가져옵니다 */
  const cookies = res.headers.getSetCookie()
  /* 각 쿠키를 들어오는 Request에 첨부합니다 */
  for (const cookie of cookies) {
    appendResponseHeader(event, 'set-cookie', cookie)
  }
  /* 응답의 데이터를 반환합니다 */
  return res._data
}
<script setup lang="ts">
// 이 composable은 쿠키를 클라이언트로 자동 전달합니다
const event = useRequestEvent()

const { data: result } = await useAsyncData(() => fetchWithCookie(event!, '/api/with-cookie'))

onMounted(() => console.log(document.cookie))
</script>

Options API 지원

Nuxt는 Options API 내에서 asyncData 가져오기를 수행하는 방법을 제공합니다. 이를 사용하려면 컴포넌트 정의를 defineNuxtComponent로 감싸야 합니다.

<script>
export default defineNuxtComponent({
  /* 고유 키를 제공하기 위해 fetchKey 옵션을 사용 */
  fetchKey: 'hello',
  async asyncData () {
    return {
      hello: await $fetch('/api/hello'),
    }
  },
})
</script>
Nuxt에서 Vue 컴포넌트를 선언하는 권장 방식은 <script setup> 또는 <script setup lang="ts">를 사용하는 것입니다.
Read more in Docs > 4 X > API > Utils > Define Nuxt Component.

서버에서 클라이언트로 데이터 직렬화하기

서버에서 가져온 데이터를 클라이언트로 전송하기 위해 useAsyncDatauseLazyAsyncData를 사용할 때(및 Nuxt payload를 활용하는 기타 모든 경우), 페이로드는 devalue로 직렬화됩니다. 이를 통해 기본 JSON뿐만 아니라 정규식, Date, Map, Set, ref, reactive, shallowRef, shallowReactive, NuxtError 등 더 고급 데이터 타입도 직렬화하고 복원/역직렬화할 수 있습니다.

Nuxt에서 지원하지 않는 타입에 대해 자체 serializer/deserializer를 정의하는 것도 가능합니다. 자세한 내용은 useNuxtApp 문서를 참고하세요.

이는 $fetch 또는 useFetch로 가져온 서버 라우트에서 전달되는 데이터에는 _적용되지 않는다_는 점에 유의하세요. 더 자세한 내용은 다음 섹션을 참고하세요.

API 라우트에서 데이터 직렬화하기

server 디렉터리에서 데이터를 가져올 때, 응답은 JSON.stringify를 사용해 직렬화됩니다. 하지만 직렬화는 JavaScript 원시 타입에만 제한되므로, Nuxt는 $fetchuseFetch의 반환 타입을 실제 값과 일치시키기 위해 최선을 다합니다.

JSON.stringify의 한계에 대해 더 알아보기.

예시

server/api/foo.ts
export default defineEventHandler(() => {
  return new Date()
})
app/app.vue
<script setup lang="ts">
// Date 객체를 반환했지만, `data`의 타입은 문자열로 추론됩니다
const { data } = await useFetch('/api/foo')
</script>

커스텀 serializer 함수

직렬화 동작을 커스터마이징하려면, 반환 객체에 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/app.vue
<script setup lang="ts">
// `data`의 타입은 다음과 같이 추론됩니다
// {
//   createdAt: {
//     year: number
//     month: number
//     day: number
//   }
// }
const { data } = await useFetch('/api/bar')
</script>

대체 serializer 사용하기

Nuxt는 현재 JSON.stringify를 대체하는 serializer를 지원하지 않습니다. 하지만 페이로드를 일반 문자열로 반환하고, 타입 안전성을 유지하기 위해 toJSON 메서드를 활용할 수 있습니다.

아래 예시에서는 superjson을 serializer로 사용합니다.

server/api/superjson.ts
import superjson from 'superjson'

export default defineEventHandler(() => {
  const data = {
    createdAt: new Date(),

    // 타입 변환을 우회하기 위한 방법
    toJSON () {
      return this
    },
  }

  // superjson을 사용해 출력값을 문자열로 직렬화
  return superjson.stringify(data) as unknown as typeof data
})
app/app.vue
<script setup lang="ts">
import superjson from 'superjson'

// `date`는 { createdAt: Date }로 추론되며, Date 객체 메서드를 안전하게 사용할 수 있습니다
const { data } = await useFetch('/api/superjson', {
  transform: (value) => {
    return superjson.parse(value as unknown as string)
  },
})
</script>

레시피

POST 요청을 통한 SSE(Server-Sent Events) 소비하기

GET 요청을 통해 SSE를 소비하는 경우, EventSource 또는 VueUse composable인 useEventSource를 사용할 수 있습니다.

POST 요청을 통해 SSE를 소비할 때는 연결을 수동으로 처리해야 합니다. 다음은 그 방법입니다:

// SSE 엔드포인트로 POST 요청 보내기
const response = await $fetch<ReadableStream>('/chats/ask-ai', {
  method: 'POST',
  body: {
    query: 'Hello AI, how are you?',
  },
  responseType: 'stream',
})

// 응답으로부터 TextDecoderStream을 사용해 텍스트 데이터로 가져오는 새로운 ReadableStream 생성
const reader = response.pipeThrough(new TextDecoderStream()).getReader()

// 데이터를 받는 대로 청크 단위로 읽기
while (true) {
  const { value, done } = await reader.read()

  if (done) { break }

  console.log('Received:', value)
}

병렬 요청 보내기

요청들이 서로 의존하지 않는다면, 성능 향상을 위해 Promise.all()로 병렬로 요청을 보낼 수 있습니다.

const { data } = await useAsyncData((_nuxtApp, { signal }) => {
  return Promise.all([
    $fetch('/api/comments/', { signal }),
    $fetch('/api/author/12', { signal }),
  ])
})

const comments = computed(() => data.value?.[0])
const author = computed(() => data.value?.[1])