[Nuxt.js-5] Nuxt.js 데이터 가져오기 (Data Fetching)

  • 카카오톡 공유하기
  • 네이버 블로그 공유하기
  • 네이버 밴드에 공유하기
  • 페이스북 공유하기
  • 트위터 공유하기
  • 링크 복사하기

지난 포스팅에는 에셋과 정적 파일 관리에 대해 알아보았는데요, 이번에는 Nuxt.js의 핵심 기능 중 하나인 데이터 가져오기(Data Fetching)에 대해 자세히 알아보겠습니다.

웹 애플리케이션에서 외부 API와의 연동은 필수적인 요소입니다. Nuxt.js는 이러한 데이터 가져오기를 위한 강력하고 직관적인 도구들을 제공하는데, 오늘은 그 중에서도 useFetchuseAsyncData를 중심으로 살펴보겠습니다.

1. Nuxt.js의 데이터 가져오기 특징

Nuxt.js의 데이터 가져오기 기능은 다음과 같은 특징을 가지고 있습니다:

  • 자동 중복 제거(Deduplication): 동일한 요청은 한 번만 실행
  • SSR 지원: 서버 사이드에서 데이터를 미리 가져와 초기 HTML에 포함
  • 클라이언트 사이드 하이드레이션: 서버에서 가져온 데이터를 클라이언트에서 재사용
  • 타입 안전성: TypeScript와 완벽하게 통합
  • 자동 로딩 상태 관리: 데이터 로딩 중 상태를 쉽게 처리

2. useFetch – 가장 간단한 데이터 가져오기 방법

useFetch는 Nuxt.js에서 외부 API로부터 데이터를 가져오는 가장 간단하고 직관적인 방법입니다.

기본 사용법

// pages/users.vue
<script setup>
const { data: users, pending, error, refresh } = await useFetch('https://jsonplaceholder.typicode.com/users')
</script>

<template>
  <div>
    <p v-if="pending">데이터를 불러오는 중...</p>
    <p v-else-if="error">에러가 발생했습니다: {{ error }}</p>
    <div v-else>
      <h2>사용자 목록</h2>
      <ul>
        <li v-for="user in users" :key="user.id">
          {{ user.name }} ({{ user.email }})
        </li>
      </ul>
      <button @click="refresh">새로고침</button>
    </div>
  </div>
</template>

위 코드에서 useFetch는 다음과 같은 값들을 반환합니다:

  • data: API 응답 데이터
  • pending: 데이터 로딩 중 여부
  • error: 오류 발생 시 오류 정보
  • refresh: 데이터를 다시 가져오는 함수

옵션 설정하기

useFetch는 다양한 옵션을 제공하여 요청을 세밀하게 제어할 수 있습니다:

const { data } = await useFetch('https://api.example.com/posts', {
  method: 'POST',
  body: { title: '새 포스트', content: '내용...' },
  headers: {
    'Authorization': `Bearer ${token}`
  },
  query: {
    limit: 10,
    page: 1
  },
  // 캐시 키 설정
  key: 'posts-page-1',
  // 캐시 시간 설정 (밀리초)
  server: true, // 서버 사이드에서만 실행
  lazy: false, // 즉시 실행 (기본값)
})

3. useAsyncData – 더 세밀한 제어가 필요할 때

useAsyncDatauseFetch보다 더 세밀한 제어가 필요한 경우에 사용합니다. 외부 API뿐만 아니라 복잡한 비동기 작업을 처리할 때 유용합니다.

기본 사용법

// pages/posts.vue
<script setup>
const { data: posts, pending, error, refresh } = await useAsyncData('posts', async () => {
  const response = await $fetch('https://jsonplaceholder.typicode.com/posts')
  return response.slice(0, 10) // 처음 10개 포스트만 반환
})
</script>

<template>
  <div>
    <p v-if="pending">데이터를 불러오는 중...</p>
    <p v-else-if="error">에러가 발생했습니다: {{ error }}</p>
    <div v-else>
      <h2>포스트 목록</h2>
      <div v-for="post in posts" :key="post.id" class="post">
        <h3>{{ post.title }}</h3>
        <p>{{ post.body }}</p>
      </div>
      <button @click="refresh">새로고침</button>
    </div>
  </div>
</template>

useAsyncData는 첫 번째 인자로 고유 키를, 두 번째 인자로 비동기 함수를 받습니다. 이 키는 내부적으로 데이터 캐싱에 사용됩니다.

useFetch vs useAsyncData

두 함수의 주요 차이점은 다음과 같습니다:

  • useFetch: URL을 직접 받아 HTTP 요청을 처리하는 간단한 래퍼
  • useAsyncData: 더 일반적인 비동기 작업을 처리할 수 있는 저수준 API

실제로 useFetch는 내부적으로 useAsyncData$fetch를 사용하여 구현되어 있습니다.

// useFetch의 간략화된 내부 구현
function useFetch(url, options) {
  return useAsyncData(
    options?.key || url,
    () => $fetch(url, options),
    options
  )
}

4. 데이터 변환 (Transformation)

API에서 받아온 데이터를 바로 사용하기보다는 변환하여 사용하는 경우가 많습니다. useFetchuseAsyncData 모두 transform 옵션을 제공합니다.

const { data: users } = await useFetch('https://jsonplaceholder.typicode.com/users', {
  transform: (users) => {
    return users.map(user => ({
      id: user.id,
      name: user.name,
      email: user.email,
      // 필요한 속성만 선택
    }))
  }
})

5. 로딩 및 에러 상태 처리

데이터를 가져올 때는 로딩 상태와 에러 처리가 중요합니다. Nuxt.js는 이를 쉽게 처리할 수 있는 방법을 제공합니다.

로딩 상태 처리

<template>
  <div>
    <div v-if="pending" class="loading-spinner">
      <!-- 로딩 스피너 컴포넌트 -->
      로딩 중...
    </div>
    <div v-else>
      <!-- 데이터 표시 -->
    </div>
  </div>
</template>

에러 처리

<template>
  <div>
    <div v-if="error" class="error-message">
      <h3>오류가 발생했습니다</h3>
      <p>{{ error.message }}</p>
      <button @click="refresh">다시 시도</button>
    </div>
    <div v-else-if="pending">로딩 중...</div>
    <div v-else>
      <!-- 데이터 표시 -->
    </div>
  </div>
</template>

6. 실전 예제: 영화 정보 API 연동하기

이제 배운 내용을 활용하여 영화 정보 API를 연동하는 실전 예제를 만들어 보겠습니다.

// pages/movies.vue
<script setup>
const searchQuery = ref('')
const page = ref(1)

const { data: movies, pending, error, refresh } = await useFetch(() => {
  return `https://api.themoviedb.org/3/search/movie?api_key=YOUR_API_KEY&query=${searchQuery.value}&page=${page.value}`
}, {
  watch: [searchQuery, page], // 이 값들이 변경되면 자동으로 다시 요청
  default: () => ({ results: [] }), // 기본값 설정
})

function nextPage() {
  page.value++
}

function prevPage() {
  if (page.value > 1) {
    page.value--
  }
}
</script>

<template>
  <div class="container">
    <h1>영화 검색</h1>
    
    <div class="search-box">
      <input 
        v-model="searchQuery" 
        placeholder="영화 제목을 입력하세요" 
        @keyup.enter="page = 1"
      >
    </div>
    
    <div v-if="pending" class="loading">
      검색 중...
    </div>
    
    <div v-else-if="error" class="error">
      <p>오류가 발생했습니다: {{ error.message }}</p>
      <button @click="refresh">다시 시도</button>
    </div>
    
    <div v-else-if="movies.results.length === 0" class="no-results">
      검색 결과가 없습니다.
    </div>
    
    <div v-else class="movie-list">
      <div v-for="movie in movies.results" :key="movie.id" class="movie-card">
        <img 
          :src="movie.poster_path 
            ? `https://image.tmdb.org/t/p/w200${movie.poster_path}` 
            : '/placeholder.png'"
          :alt="movie.title"
        >
        <div class="movie-info">
          <h3>{{ movie.title }}</h3>
          <p>{{ movie.release_date }}</p>
          <p>평점: {{ movie.vote_average }}/10</p>
        </div>
      </div>
      
      <div class="pagination">
        <button :disabled="page === 1" @click="prevPage">이전</button>
        <span>{{ page }} 페이지</span>
        <button @click="nextPage">다음</button>
      </div>
    </div>
  </div>
</template>

<style scoped>
.container {
  max-width: 1200px;
  margin: 0 auto;
  padding: 20px;
}

.search-box {
  margin-bottom: 20px;
}

.search-box input {
  width: 100%;
  padding: 10px;
  font-size: 16px;
}

.movie-list {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
  gap: 20px;
}

.movie-card {
  border: 1px solid #ddd;
  border-radius: 8px;
  overflow: hidden;
}

.movie-card img {
  width: 100%;
  height: 300px;
  object-fit: cover;
}

.movie-info {
  padding: 10px;
}

.pagination {
  margin-top: 20px;
  display: flex;
  justify-content: center;
  gap: 10px;
  align-items: center;
}

.loading, .error, .no-results {
  text-align: center;
  padding: 40px;
  font-size: 18px;
}
</style>

위 예제에서는 useFetchwatch 옵션을 사용하여 검색어나 페이지가 변경될 때마다 자동으로 API를 다시 호출합니다.

7. 서버와 클라이언트에서의 데이터 가져오기 차이점

Nuxt.js는 기본적으로 서버 사이드 렌더링(SSR)을 지원합니다. 이는 데이터 가져오기에도 영향을 미칩니다.

서버 사이드 데이터 가져오기

  • 초기 페이지 로드 시 서버에서 데이터를 가져옴
  • SEO에 유리함 (검색 엔진이 데이터가 포함된 HTML을 볼 수 있음)
  • 초기 로딩 성능이 향상됨 (사용자는 데이터가 이미 포함된 HTML을 받음)

클라이언트 사이드 데이터 가져오기

  • 페이지가 로드된 후 브라우저에서 데이터를 가져옴
  • 사용자 상호작용에 따른 데이터 갱신에 적합
  • 서버 부하를 줄일 수 있음

Nuxt.js에서는 server 옵션을 통해 데이터를 어디서 가져올지 제어할 수 있습니다:

// 서버에서만 데이터 가져오기
const { data } = await useFetch('/api/data', { server: true, client: false })

// 클라이언트에서만 데이터 가져오기
const { data } = await useFetch('/api/data', { server: false, client: true })

// 서버와 클라이언트 모두에서 데이터 가져오기 (기본값)
const { data } = await useFetch('/api/data')

8. 데이터 캐싱과 재검증

Nuxt.js는 데이터 가져오기 작업의 결과를 캐싱하여 성능을 최적화합니다.

캐시 키 설정

// 커스텀 캐시 키 설정
const { data } = await useFetch('/api/posts', {
  key: `posts-page-${page.value}`
})

캐시 무효화 및 새로고침

const { data, refresh } = await useFetch('/api/data')

// 버튼 클릭 시 데이터 새로고침
function handleRefresh() {
  refresh()
}

9. 실전 팁과 모범 사례

API 클라이언트 중앙화하기

대규모 애플리케이션에서는 API 호출을 중앙화하는 것이 좋습니다:

// composables/useApi.js
export function useApi() {
  const config = useRuntimeConfig()
  
  return {
    async getUsers() {
      return useFetch(`${config.public.apiBase}/users`)
    },
    
    async getUser(id) {
      return useFetch(`${config.public.apiBase}/users/${id}`)
    },
    
    async createUser(userData) {
      return useFetch(`${config.public.apiBase}/users`, {
        method: 'POST',
        body: userData
      })
    }
  }
}

// 사용 예시
const { getUser } = useApi()
const { data: user } = await getUser(1)

환경 변수 활용하기

API URL과 같은 설정은 환경 변수로 관리하세요:

// nuxt.config.js
export default defineNuxtConfig({
  runtimeConfig: {
    // 서버에서만 접근 가능한 키
    apiSecret: process.env.API_SECRET,
    
    // 클라이언트에서도 접근 가능한 키
    public: {
      apiBase: process.env.API_BASE || 'https://api.example.com'
    }
  }
})

// 사용 예시
const config = useRuntimeConfig()
const { data } = await useFetch(`${config.public.apiBase}/posts`)

타입 안전성 확보하기

TypeScript를 사용하여 API 응답의 타입을 정의하세요:

interface User {
  id: number
  name: string
  email: string
}

const { data: users } = await useFetch<User[]>('/api/users')

// 이제 users.value는 User[] 타입으로 추론됩니다

10. 결론

이번 시간에는 Nuxt.js의 데이터 가져오기 기능에 대해 살펴보았습니다. useFetchuseAsyncData를 활용하면 서버와 클라이언트 모두에서 효율적으로 API 데이터를 가져올 수 있습니다. 이러한 기능은 Nuxt.js의 강력한 장점 중 하나로, 실제 프로젝트에서 매우 유용하게 활용될 수 있습니다.

다음 포스팅에는 상태 관리(State Management)와 useState에 대해 알아보겠습니다. 컴포넌트 간 상태를 공유하고 관리하는 방법, 반응형 상태 관리의 기본 개념, 그리고 전역 상태 관리의 필요성에 대해 작성할 예정입니다. Nuxt.js에서 제공하는 useState 컴포저블을 활용하여 효율적인 상태 관리에 대해 설명 해보겠습니다.


게시됨

카테고리

작성자

댓글

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다