2025. 5. 28. 23:01ㆍFront-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 호출됨 |
'Front-End > Vue.js' 카테고리의 다른 글
| Capacitor (0) | 2025.10.19 |
|---|---|
| Vue Composition API - setup() 정리 기준 (1) | 2025.07.10 |
| async/await 완벽 이해하기 – JavaScript 비동기 처리의 핵심 (0) | 2025.05.28 |
| Vue Composition API를 쓰면서 헷갈리는 this, 화살표 함수, Promise, async/await 정리 (0) | 2025.05.28 |
| Vue 프로젝트 폴더 구조 정리 (0) | 2025.05.18 |