Vue는 특정 HTTP 라이브러리를 강제하지 않습니다. fetch API나 Axios 등 프로젝트에 맞는 도구를 선택하면 됩니다.

면접에서 "API 호출은 어떻게 관리하나요?"라고 물으면, 단순히 "Axios 씁니다"가 아니라 계층 분리와 에러 핸들링 전략 을 설명할 수 있어야 합니다.


fetch API 기본

VUE
<script setup lang="ts">
import { ref, onMounted } from 'vue'

interface Post {
  id: number
  title: string
  body: string
}

const posts = ref<Post[]>([])
const isLoading = ref(false)
const error = ref<string | null>(null)

const fetchPosts = async () => {
  isLoading.value = true
  error.value = null

  try {
    const response = await fetch('https://jsonplaceholder.typicode.com/posts')

    // fetch는 HTTP 에러에서 reject하지 않음 — 수동 체크 필요
    if (!response.ok) {
      throw new Error(`HTTP ${response.status}: ${response.statusText}`)
    }

    posts.value = await response.json()
  } catch (e) {
    error.value = (e as Error).message
  } finally {
    isLoading.value = false
  }
}

onMounted(fetchPosts)
</script>

<template>
  <div v-if="isLoading">로딩 중...</div>
  <div v-else-if="error">에러: {{ error }}</div>
  <ul v-else>
    <li v-for="post in posts" :key="post.id">{{ post.title }}</li>
  </ul>
</template>

Axios 설정

TYPESCRIPT
// api/client.ts
import axios from 'axios'

const apiClient = axios.create({
  baseURL: import.meta.env.VITE_API_BASE_URL || '/api',
  timeout: 10000,
  headers: {
    'Content-Type': 'application/json'
  }
})

// 요청 인터셉터 — 토큰 자동 첨부
apiClient.interceptors.request.use(
  (config) => {
    const token = localStorage.getItem('access_token')
    if (token) {
      config.headers.Authorization = `Bearer ${token}`
    }
    return config
  },
  (error) => Promise.reject(error)
)

// 응답 인터셉터 — 에러 핸들링
apiClient.interceptors.response.use(
  (response) => response,
  async (error) => {
    if (error.response?.status === 401) {
      // 토큰 갱신 시도
      try {
        const refreshToken = localStorage.getItem('refresh_token')
        const { data } = await axios.post('/api/auth/refresh', { refreshToken })
        localStorage.setItem('access_token', data.accessToken)

        // 원래 요청 재시도
        error.config.headers.Authorization = `Bearer ${data.accessToken}`
        return apiClient(error.config)
      } catch {
        // 갱신 실패 — 로그아웃
        localStorage.clear()
        window.location.href = '/login'
      }
    }
    return Promise.reject(error)
  }
)

export default apiClient

API 모듈 계층 분리

TYPESCRIPT
// api/users.ts
import apiClient from './client'

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

export const usersApi = {
  getAll: () =>
    apiClient.get<User[]>('/users').then(res => res.data),

  getById: (id: number) =>
    apiClient.get<User>(`/users/${id}`).then(res => res.data),

  create: (data: Omit<User, 'id'>) =>
    apiClient.post<User>('/users', data).then(res => res.data),

  update: (id: number, data: Partial<User>) =>
    apiClient.patch<User>(`/users/${id}`, data).then(res => res.data),

  delete: (id: number) =>
    apiClient.delete(`/users/${id}`)
}
VUE
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { usersApi, type User } from '@/api/users'

const users = ref<User[]>([])

onMounted(async () => {
  users.value = await usersApi.getAll()
})
</script>

HTTP 통신 Composable

TYPESCRIPT
// composables/useApi.ts
import { ref, type Ref } from 'vue'

interface UseApiReturn<T> {
  data: Ref<T | null>
  error: Ref<string | null>
  isLoading: Ref<boolean>
  execute: (...args: any[]) => Promise<void>
}

export function useApi<T>(
  apiFn: (...args: any[]) => Promise<T>
): UseApiReturn<T> {
  const data = ref<T | null>(null) as Ref<T | null>
  const error = ref<string | null>(null)
  const isLoading = ref(false)

  const execute = async (...args: any[]) => {
    isLoading.value = true
    error.value = null

    try {
      data.value = await apiFn(...args)
    } catch (e: any) {
      error.value = e.response?.data?.message || e.message || '알 수 없는 에러'
    } finally {
      isLoading.value = false
    }
  }

  return { data, error, isLoading, execute }
}
VUE
<script setup lang="ts">
import { onMounted } from 'vue'
import { useApi } from '@/composables/useApi'
import { usersApi } from '@/api/users'

const { data: users, error, isLoading, execute } = useApi(usersApi.getAll)

onMounted(() => execute())
</script>

요청 취소 패턴

TYPESCRIPT
import { ref, watch, onUnmounted } from 'vue'

export function useCancelableRequest() {
  let controller: AbortController | null = null

  const fetchData = async (url: string) => {
    // 이전 요청 취소
    controller?.abort()
    controller = new AbortController()

    const response = await fetch(url, {
      signal: controller.signal
    })
    return response.json()
  }

  onUnmounted(() => {
    controller?.abort()
  })

  return { fetchData }
}

면접 팁

  • fetch와 Axios의 차이: fetch는 HTTP 에러에서 reject하지 않고, Axios는 자동으로 reject합니다. 인터셉터도 Axios만의 기능입니다
  • API 모듈 계층 분리 를 설명하면 코드 구조에 대한 이해도를 보여줄 수 있습니다
  • 토큰 갱신(refresh) 인터셉터 패턴은 실무에서 반드시 필요한 패턴입니다

요약

Vue의 HTTP 통신은 fetch API나 Axios를 활용하며, API 모듈을 계층적으로 분리하는 것이 권장됩니다. Axios 인터셉터로 토큰 자동 첨부와 에러 핸들링을 중앙화하고, composable로 컴포넌트의 비동기 상태를 깔끔하게 관리할 수 있습니다.

댓글 로딩 중...