Vue API 호출을 관리하는 구조 – async/await과 useXXX 패턴(컴포저블)

2025. 5. 28. 23:01Front-End/Vue.js

반응형

Vue 프로젝트에서 서버 데이터를 가져오는 API 호출은 필수적인 작업이다.

하지만 단순히 fetch()나 axios를 매번 직접 호출하는 방식은 금방 복잡해지고,

컴포넌트 내부가 로직 + 화면 처리 + 예외 처리까지 섞이게 되면서 유지보수가 어려워진다.

 

그래서 다음과 같은 흐름으로 API 호출을 관리한다:

  • API 호출은 async/await로 처리
  • API 함수는 /api/모듈.js로 분리
  • 상태 관리를 포함하고 싶을 땐 useXXX() composable 함수로 구성

1. async/await는 Promise를 동기 코드처럼 다루기 위한 문법이다

자바스크립트는 비동기 작업을 Promise로 처리한다. 예를 들어, 서버에서 데이터를 받아오는 코드는 다음과 같이 작성할 수 있다.

function getData() {
  return new Promise((resolve, reject) => {
    setTimeout(() => resolve("완료!"), 1000)
  })
}

async/await는 위 코드를 더 읽기 쉽게 만든 문법이다.

async function loadData() {
  const result = await getData()
  console.log(result) // "완료!"
}

에러 처리는 try/catch로 깔끔하게 처리할 수 있다.

async function loadData() {
  try {
    const result = await getData()
    console.log(result)
  } catch (e) {
    console.error("에러:", e)
  }
}

Promise와 async/await 비교

항목  Promise  async/await
코드 스타일 체이닝 (.then()) 동기 흐름처럼 작성
에러 처리 .catch() 필요 try/catch로 처리
가독성 중첩되기 쉬움 깔끔하고 직관적
병렬 처리 .then() 체이닝 Promise.all()로 가능

 

2. 실무에서는 API 호출 함수를 따로 분리한다

항상 axios.get(...)을 컴포넌트 안에서 직접 쓰면, 재사용도 어렵고 테스트도 불편하다. 그래서 API 함수는 api/ 폴더에 모듈별로 나눈다.

// api/user.js
import api from './axiosInstance'

export async function fetchUser(id) {
  const { data } = await api.get(`/users/${id}`)
  return data
}

export async function updateUser(id, payload) {
  const { data } = await api.put(`/users/${id}`, payload)
  return data
}

3. axios 설정도 별도로 구성한다

// api/axiosInstance.js
import axios from 'axios'

const api = axios.create({
  baseURL: '/api',
  timeout: 5000
})

api.interceptors.response.use(
  res => res,
  err => {
    if (err.response?.status === 401) {
      alert("세션이 만료되었습니다. 다시 로그인해주세요.")
    }
    return Promise.reject(err)
  }
)

export default api

4. 상태와 로직을 함께 관리하고 싶다면 useXXX() 컴포저블을 만든다

useXXX() 컴포저블 패턴은 Vue의 Composition API와 궁합이 잘 맞는 방식이다.

비즈니스 로직과 상태를 함께 관리할 수 있어 컴포넌트를 더 깔끔하게 유지할 수 있다.

예를 들어 사용자 정보를 불러오는 로직은 다음처럼 정리할 수 있다:

// composables/useUser.js
import { ref } from 'vue'
import { fetchUser, updateUser } from '@/api/user'

export function useUser() {
  const user = ref(null)
  const loading = ref(false)
  const error = ref(null)

  const loadUser = async (id) => {
    loading.value = true
    error.value = null
    try {
      user.value = await fetchUser(id)
    } catch (e) {
      error.value = e
    } finally {
      loading.value = false
    }
  }

  const saveUser = async (id, payload) => {
    await updateUser(id, payload)
  }

  return {
    user,
    loading,
    error,
    loadUser,
    saveUser
  }
}

5. 컴포넌트에서는 매우 깔끔하게 사용할 수 있다

<script setup>
import { useUser } from '@/composables/useUser'

const { user, loading, error, loadUser } = useUser()

onMounted(() => {
  loadUser(1)
})
</script>

<template>
  <div v-if="loading">로딩 중...</div>
  <div v-else-if="error">에러: {{ error.message }}</div>
  <div v-else>
    <p>이름: {{ user?.name }}</p>
    <p>이메일: {{ user?.email }}</p>
  </div>
</template>

6. 전체 구조 예시

src/
├── api/                    # API 호출 함수들
│   ├── axiosInstance.js   # axios 기본 설정과 인터셉터
│   ├── user.js            # 사용자 관련 API 함수
├── composables/           # 상태 관리 로직 (Composition 함수)
│   └── useUser.js         # 사용자 상태 + API 로직 묶은 컴포저블
├── components/            # 화면 구성 요소
│   └── UserView.vue       # 사용자 정보를 보여주는 화면 컴포넌트

마무리 요약

개념 설명

async/await 비동기 코드를 깔끔하게 처리
API 함수 분리 중복 제거, 테스트, 재사용에 유리
axiosInstance 공통 설정, 인터셉터 처리
useXXX() 컴포저블 상태와 로직을 함께 관리
컴포넌트 UI와 사용자 이벤트 처리만 담당

Vue에서 구조를 제대로 잡고 싶다면, 비즈니스 로직은 API와 composable로 분리하고, 컴포넌트는 최대한 가볍게 유지하는 것이 핵심이다. 이 패턴을 익히면 API 호출이 많은 화면도 복잡하지 않게 유지할 수 있다.

 

더보기

지피티에게 물어봄

 

좋아, 그건 정말 useXXX() 컴포저블을 써야 할 대표적인 상황이야.
왜냐하면 이제 단순 데이터 조회가 아니라:

  • **검색 조건(searchParams)**이라는 상태가 생기고
  • 그에 따라 API를 다시 호출하고 갱신해야 하며
  • 그 과정을 컴포넌트마다 반복하면 코드가 지저분해지기 때문이야.

아래에 “검색 조건이 바뀔 때마다 데이터를 다시 조회하는” 구조를
**useUserHistoryList()**라는 이름의 컴포저블로 정리해볼게.


✅ 예: 검색 조건 기반 유저 이력 조회용 useXXX 컴포저블

// composables/useUserHistoryList.js
import { ref, watch } from 'vue'
import { fetchUserHistoryList } from '@/api/user'

export function useUserHistoryList() {
  const users = ref([])
  const loading = ref(false)
  const error = ref(null)

  const searchParams = ref({
    keyword: '',
    dateRange: null
  })

  const loadUsers = async () => {
    loading.value = true
    error.value = null
    try {
      users.value = await fetchUserHistoryList(searchParams.value)
    } catch (e) {
      error.value = e
    } finally {
      loading.value = false
    }
  }

  // 검색 조건이 바뀌면 자동으로 재조회
  watch(searchParams, loadUsers, { deep: true })

  return {
    users,
    loading,
    error,
    searchParams,
    loadUsers
  }
}

✅ API 함수 예시 (params 전달)

// api/user.js
export async function fetchUserHistoryList(params) {
  const { data } = await api.get('/users/history', { params })
  return data
}

✅ 컴포넌트에서 사용

<script setup>
import { useUserHistoryList } from '@/composables/useUserHistoryList'

const { users, searchParams, loading, error } = useUserHistoryList()

// 예: 검색 버튼 클릭 시
function search(keyword) {
  searchParams.value.keyword = keyword
}
</script>

<template>
  <div>
    <input v-model="searchParams.keyword" @keyup.enter="search(searchParams.keyword)" />
    <button @click="search(searchParams.keyword)">검색</button>

    <div v-if="loading">로딩 중...</div>
    <div v-else-if="error">에러 발생: {{ error.message }}</div>
    <ul>
      <li v-for="user in users" :key="user.id">
        {{ user.name }} - {{ user.createdAt }}
      </li>
    </ul>
  </div>
</template>

✅ 왜 이게 좋을까?

항목 설명
상태 분리 검색 조건(searchParams)과 결과(users)가 깔끔히 관리됨
재사용 용이 여러 화면에서 동일한 패턴으로 재사용 가능
UI 로직 분리 컴포넌트는 UI에만 집중, 로직은 useXXX()에 집중
watch + ref 검색 조건이 바뀌면 자동으로 API 호출됨

 

반응형