현록

Nuxt 3와 4에서 useFetch와 $fetch 차이

Nuxt에서 API를 호출할 때 useFetch$fetch를 모두 사용할 수 있다.
두 API는 HTTP 요청 옵션을 상당 부분 공유하지만 결과를 Nuxt 렌더링 과정에 연결하는 방식이 다르다.

초기 화면에 필요한 데이터는 useFetch가 기본 선택이다.
클릭이나 폼 제출로 실행하는 단발 요청은 $fetch가 더 단순하다.

이 글은 2026년 8월 현재 Nuxt 3.21과 Nuxt 4.5 문서를 함께 기준으로 한다.

$fetch의 직접 요청

$fetch는 Nuxt가 전역으로 제공하는 ofetch 기반 HTTP 요청 함수다.
파싱한 응답 데이터로 이행하는 Promise를 반환하므로 일반 async 함수처럼 사용할 수 있다.

<script setup lang="ts">
async function createTodo(title: string) {
  const todo = await $fetch('/api/todos', {
    method: 'POST',
    body: { title },
  })

  return todo
}
</script>

사용자가 버튼을 누르거나 폼을 제출할 때처럼 사용자 이벤트에 따라 실행하는 작업에 잘 맞는다.
server route나 별도 utility에서도 Nuxt의 반응형 상태가 필요하지 않다면 그대로 사용할 수 있다.

SSR이나 서버 코드에서 내부 /api route를 $fetch로 호출하면 Nuxt는 추가 HTTP 왕복 없이 해당 handler를 직접 실행할 수 있다.
외부 API를 호출할 때는 일반 네트워크 요청으로 동작한다.

useFetch의 SSR payload

useFetchuseAsyncData$fetch를 묶은 SSR 친화적인 composable이다.
응답을 data ref로 제공하고 요청 상태, 오류, 갱신 함수를 함께 반환한다.

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

const { data: posts, status, error, refresh } = await useFetch('/api/posts', {
  query: { page },
})
</script>

<template>
  <PostList v-if="posts" :posts="posts" />
  <p v-else-if="status === 'pending'">글을 불러오는 중입니다.</p>
  <p v-else-if="error">글을 불러오지 못했습니다.</p>
</template>

SSR에서 useFetch가 받은 데이터는 Nuxt payload에 들어가 브라우저로 전달된다.
클라이언트는 hydration 중 같은 초기 데이터를 다시 요청하지 않고 payload를 사용한다.

component setup에서 $fetch만 호출하면 같은 코드가 서버 렌더링과 클라이언트 hydration에서 각각 실행되어 요청이 두 번 발생할 수 있다.
초기 화면 데이터에 useFetch나 useAsyncData(() => $fetch(...))가 필요한 이유다.

초기 조회와 이벤트 요청

페이지 진입 시 필요한 조회는 useFetch로 가져오고 사용자 동작으로 생기는 변경은 $fetch로 보내면 역할이 분명해진다.
변경이 끝난 뒤 useFetch의 refresh()를 호출해 화면 데이터를 다시 맞출 수 있다.

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

async function handleSubmit(title: string) {
  await $fetch('/api/todos', {
    method: 'POST',
    body: { title },
  })

  await refresh()
}
</script>

useFetch도 POST 요청을 보낼 수 있지만 reactive option과 refresh가 연결된 composable이라는 점을 고려해야 한다.
이벤트 한 번에 정확히 한 요청을 실행하려면 $fetch가 의도를 더 직접적으로 드러낸다.

외부 SDK나 여러 요청 조합처럼 URL 하나로 표현하기 어려운 비동기 작업은 useAsyncData로 Nuxt payload와 상태를 붙일 수 있다.

const { data: users } = await useAsyncData('users', () => {
  return userClient.list()
})

API base URL과 비밀 값을 나누는 방법은 Nuxt 3와 4에서 runtimeConfig로 환경변수 관리하기에서 이어서 볼 수 있다.

반응형 상태와 요청 갱신

useFetch의 URL과 query, body 같은 option에는 ref나 computed 값을 넣을 수 있다.
기본적으로 이 값이 바뀌면 요청도 다시 실행된다.

검색 입력을 적용 버튼으로 확정한 뒤에만 요청하고 싶다면 watch: false를 사용하거나 immediate: falseexecute() 조합을 검토한다.
동시에 같은 key의 요청을 실행할 때는 dedupe 정책도 결과에 영향을 준다.

Nuxt 4.5에서는 useFetch를 await하지 않아도 서버가 렌더링 전에 요청 결과를 기다려 payload에 넣는다.
다만 client-side navigation에서는 await하면 데이터가 준비될 때까지 이동을 막고, await하지 않으면 먼저 이동한 뒤 statuserror로 로딩 상태를 직접 처리한다.

lazy: true는 client navigation을 막지 않겠다는 의도를 명시하는 별도 option이다.
await와 lazy를 단순히 같은 기능으로 취급하면 data가 아직 undefined인 시점을 놓칠 수 있다.

Nuxt 3 문서는 useFetch의 반환 타입과 예제를 await 중심으로 설명한다.
Nuxt 3와 4를 함께 지원하는 코드에서는 await 유무가 동일하게 동작한다고 가정하지 않고 각 설치 버전의 타입과 navigation 동작을 확인해야 한다.

쿠키 전달과 버전 차이

브라우저에서 같은 출처로 $fetch를 호출하면 브라우저의 cookie가 요청에 포함될 수 있다.
반대로 SSR 중 실행되는 일반 $fetch는 보안상 사용자의 요청 header와 cookie를 자동 전달하지 않는다.

상대 URL을 사용하는 useFetch는 서버에서 useRequestFetch를 사용해 host처럼 전달하면 안 되는 header를 제외하고 요청 정보를 내부 API로 전달한다.
직접 $fetch를 사용해야 한다면 useRequestFetch()를 사용하거나 필요한 header만 useRequestHeaders()로 골라서 전달한다.

외부 API에 사용자의 header 전체를 넘기면 인증 정보 유출과 SSRF 위험이 생길 수 있다.
내부 API가 응답한 Set-Cookie를 최종 브라우저 응답으로 다시 전달하는 작업도 자동이 아니므로 별도로 처리해야 한다.

Nuxt 3.21 문서는 같은 URL과 option의 useFetch가 여러 component에서 같은 상태 ref를 공유한다고 설명한다.
Nuxt 4.5는 자동 key에 호출 위치를 포함하므로 서로 다른 component의 같은 URL 호출은 기본적으로 별도 요청과 상태를 만들고, 공유하려면 같은 명시적 key를 지정해야 한다.

Nuxt 3 예제는 pages/, Nuxt 4 예제는 기본 app/pages/ 구조를 기준으로 작성하는 편이 정확하다.
초기 data 타입도 Nuxt 3 문서는 null, Nuxt 4 문서는 undefined를 기준으로 하므로 공통 component의 빈 값 검사를 느슨하게 작성하지 않는다.

참고 자료

관련 포스트
Nuxt 3와 4에서 runtimeConfig로 환경변수 관리하기 thumbnail
Nuxt 3와 4에서 runtimeConfig로 환경변수 관리하기
Nuxt 3와 Nuxt 4에서 runtimeConfig를 사용해 서버 전용 값과 클라이언트 공개 값을 나누고, NUXT_ 환경변수로 런타임 값을 덮어쓰는 방법을 정리합니다.
Nuxt 무중단 배포 구현기 thumbnail
Nuxt 무중단 배포 구현기
올해 1분기 쯔음 회사에서 처음으로 배포한 Nuxt 프로젝트가 있었다. 서비스가 운영 단계로 들어서면서 소스 수정이 매우 잦아졌고, 배포 과정마다 사용자는 정상적으로 서비스를 사용하기 어려운 상황을 마주했다. 원인을 분석한 결과, build 과정 초기에 .nuxt 폴더에 있는 번들파일을 제거하기 때문에 사용자들이 번들파일을 내려받지 못하여 벌어진 현상이었다.
nested router의 기본 route 지정해주기 thumbnail
nested router의 기본 route 지정해주기
안녕하세요. 오늘은 제가 올해 초 진행했었던 유튜브 뮤직 클론코딩 프로젝트를 리팩토링하면서.  개선이 필요한 코드를 발견하여 수정한 경험을 공유해보고자 합니다. 해당 페이지에서는 nested router를 통해 재생목록, 앨범, 노래, 아티스트, 구독 탭을 보여주고 있습니다. url에 child route를 입력해주지 않은 경우 탭에서 아무것도 보여주지 않아 리다이렉트를 시켜주고자 했었던 것 같습니다. 여기서 문제는 ... mounted 훅에서 검사를 시켜주고 있었네요 ...
Fetch API와 created hook에서 API 호출 시 차이 thumbnail
Fetch API와 created hook에서 API 호출 시 차이
안녕하세요. 오늘은 Nuxt의 Fetch API와 라이프사이클의 created 훅에서 API를 호출 했을 때 각각의 차이점에 대해 알아보겠습니다. SSR 기준으로, Nuxt 2.12 버전 이후 Fetch API는 Vue instance가 create 된 후에 실행 됩니다. 그러면 created 훅에서 실행하는 편이 더 빠른 API response를 가져오지 않을까 궁금해졌는데요. 이를 따로 비교한 글을 찾지 못해 제가 직접 한번 테스트를 해보았습니다.
Nuxt 프로젝트에서 환경변수 관리하기 (feat. dotenv) thumbnail
Nuxt 프로젝트에서 환경변수 관리하기 (feat. dotenv)
서비스를 배포하실 때 환경별로 포트번호 등이 달라서 소스코드를 직접 수정하셨던 경험이 있으신가요 ? 포트번호와 같이 환경별로 다른 값이나 DB 접속 정보 등 소스코드에 포함되면 안될 중요한 값을 dotenv 패키지를 통해서 환경변수로 관리한다면, 별도의 추가 작업을 할 일이 없으실 겁니다.