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
useFetch는 useAsyncData와 $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: false와 execute() 조합을 검토한다.
동시에 같은 key의 요청을 실행할 때는 dedupe 정책도 결과에 영향을 준다.
Nuxt 4.5에서는 useFetch를 await하지 않아도 서버가 렌더링 전에 요청 결과를 기다려 payload에 넣는다.
다만 client-side navigation에서는 await하면 데이터가 준비될 때까지 이동을 막고, await하지 않으면 먼저 이동한 뒤 status와 error로 로딩 상태를 직접 처리한다.
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의 빈 값 검사를 느슨하게 작성하지 않는다.
참고 자료




