Nuxt 3에서 useAsyncData 예시를 찾아보면 두 가지 코드가 섞여 나온다. 하나는 브라우저 표준 fetch를, 다른 하나는 $fetch를 쓴다. 둘 다 useAsyncData의 두 번째 인자로 들어가 있어서 얼핏 보면 같은 일을 하는 것 같다.
두 코드를 나란히 놓고 무엇이 다른지, $fetch를 왜 useAsyncData로 감싸야 하는지 정리한다.
같은 자리에 들어간 두 함수
먼저 fetch를 쓴 예시다.
// Nuxt 3 정식판에서는 useAsyncData가 auto-import되므로 이 import 줄은 없어도 된다
import { useAsyncData } from 'nuxt3'
export default {
setup() {
const { data, pending, error, refresh } = useAsyncData('user', () =>
fetch('http://localhost:8080/api/user/1')
.then(response => response.json()) // JSON 파싱을 직접 한다
)
return { data, pending, error, refresh }
}
}
$fetch 쪽은 이렇다.
const { data, pending, error, refresh } = await useAsyncData(
'mountains', // key
() => $fetch('https://api.nuxtjs.dev/mountains') // 파싱된 결과가 바로 온다
)
두 코드 모두 useAsyncData의 두 번째 인자로 함수를 넘긴다. 이 함수가 데이터를 가져오는 비동기 작업을 맡고, 함수가 돌려준 Promise의 결과값이 useAsyncData의 data가 된다.
차이는 응답을 다루는 부분에 있다. 첫 번째 코드는 fetch로 요청을 보낸 뒤 .then에서 response.json()을 직접 부른다. response.json()은 Fetch API가 제공하는 메서드로, 응답 본문을 JSON으로 파싱한다. 두 번째 코드에는 이 호출이 없다. $fetch는 내부적으로 fetch를 쓰면서 응답을 JSON으로 파싱하는 일까지 대신 한다.
fetch는 표준, $fetch는 그 위에 얹은 것
fetch는 웹 브라우저와 Node.js에서 모두 쓸 수 있는 HTTP 요청 함수다. 서버에 데이터를 요청하거나 보낼 때 쓰고, Promise를 반환하므로 async/await로 다룰 수 있다.
$fetch는 Nuxt 3가 전역으로 제공하는 함수다. 내부에서 fetch로 요청을 보내고, 그 위에 몇 가지 처리를 더한다. 자동 JSON 파싱이 그중 하나이고, 에러 처리도 들어 있다. 표준 fetch는 서버가 404나 500을 돌려줘도 Promise를 reject하지 않아서 response.ok를 직접 확인해야 한다. $fetch는 이런 응답을 에러로 던져 준다.
서버에서 부를지 브라우저에서 부를지는 SSR이 정한다
API를 어디서 부르느냐에 따라 페이지가 만들어지는 흐름이 달라진다.
브라우저에서 부르는 경우:
Nuxt 웹서버가 클라이언트에게 페이지 제공→브라우저에서 API 호출→API 결과로 CSRSSR 중 서버에서 부르는 경우:
Nuxt 웹서버가 직접 API 호출→API 결과값으로 SSR된 페이지를 클라이언트에 제공
두 번째 흐름에서는 클라이언트가 API를 따로 부를 필요 없이 데이터가 채워진 페이지를 받으니 첫 로딩이 빠르다.
다만 서버에서 호출되는 건 $fetch만의 성질이 아니다. SSR 동안 <script setup>은 서버에서 먼저 실행되니, 그 안에 있는 fetch든 $fetch든 서버에서 한 번 돈다. $fetch가 서버 쪽에서 더 해 주는 일은 따로 있다. 서버에서 /api/... 같은 Nuxt 내부 API 경로를 부르면 HTTP 요청을 새로 보내지 않고 해당 핸들러 함수를 바로 호출한다.
useAsyncData로 감싸지 않으면 두 번 호출된다
문제는 SSR이 끝난 뒤다. 컴포넌트에서 $fetch를 useAsyncData로 감싸지 않고 쓰면, 처음에는 서버에서 한 번, 하이드레이션 중에 클라이언트에서 또 한 번 데이터를 가져온다. $fetch는 서버에서 가져온 결과를 클라이언트로 넘기지 않기 때문이다. 서버에서 가져온 결과를 페이지에 실어 클라이언트로 넘기는 일은 useAsyncData가 한다.
sequenceDiagram participant S as Nuxt 서버 participant B as 브라우저 participant A as API S->>A: $fetch (SSR) A-->>S: 데이터 S->>B: 렌더링된 HTML Note over B: 하이드레이션 B->>A: $fetch 한 번 더 A-->>B: 같은 데이터
컴포넌트에서 $fetch를 쓸 때는 useAsyncData로 감싼다. useFetch는 이 조합의 줄임말이다.
<script setup lang="ts">
// SSR 동안 데이터를 서버에서 한 번, 클라이언트에서 한 번, 총 두 번 가져온다
const dataTwice = await $fetch('/api/item')
// SSR 중에는 서버에서만 가져오고, 결과를 클라이언트로 넘긴다
const { data } = await useAsyncData('item', () => $fetch('/api/item'))
// useAsyncData + $fetch의 줄임말인 useFetch를 써도 된다
// (한 파일에 그대로 두면 data가 중복 선언되니, 셋 중 하나만 쓴다)
const { data } = await useFetch('/api/item')
</script>
useFetch와 useAsyncData가 서로 어떻게 다른지는 앞선 글에서 따로 다뤘다.
정리하면,
$fetch는fetch위에 JSON 파싱과 에러 처리를 얹은 함수라서response.json()을 따로 부르지 않아도 된다.SSR 중 서버에서 호출되는 건
setup이 서버에서 먼저 돌기 때문이고, 결과를 클라이언트로 넘기는 건useAsyncData의 몫이다.컴포넌트에서는
$fetch를useAsyncData로 감싸거나useFetch를 쓴다. 그냥 쓰면 같은 요청이 두 번 나간다.