FrontEND/vue

Pinia에서 비동기 API 호출을 관리하는 실전 패턴 (with Axios + TypeScript)

mingmingIT 2025. 5. 1. 10:41

Vue 프로젝트에서 비동기 API를 호출할 때, 대부분의 데이터는 상태로 저장되고 여러 컴포넌트에서 공유됩니다.
이때 Pinia를 통해 상태를 관리하면서 동시에 API 호출과 데이터 가공까지 맡긴다면 훨씬 깔끔하고 유지보수하기 쉬운 구조를 만들 수 있습니다.

이번 포스팅에서는 Pinia 스토어에서 비동기 API를 안전하게 호출하고 상태를 관리하는 방법을 아래와 같은 흐름으로 정리합니다:


✅ 목표 구조

  1. 상태: data, isLoading, error 분리
  2. 액션: Axios + DTO 처리
  3. 컴포넌트에서는 store만 불러 사용

1. 📦 기본 구조 예시: 유저 리스트

// src/stores/userStore.ts
import { defineStore } from 'pinia'
import axios from 'axios'

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

export const useUserStore = defineStore('user', {
  state: () => ({
    users: [] as User[],
    isLoading: false,
    error: '' as string | null,
  }),

  actions: {
    async fetchUsers() {
      this.isLoading = true
      this.error = null

      try {
        const res = await axios.get<User[]>('/api/users')
        this.users = res.data
      } catch (e: any) {
        this.error = e?.message ?? '알 수 없는 오류'
      } finally {
        this.isLoading = false
      }
    },
  },
})
 

 

💡 상태를 users, isLoading, error로 분리하면 컴포넌트에서 UI 조건 분기 처리하기 쉽습니다.


2. 🧱 DTO 패턴 함께 사용하기 (선택적)

// src/dto/UserDTO.ts
export class UserDTO {
  id: number
  name: string
  email: string

  constructor(data: any) {
    this.id = data.id ?? -1
    this.name = data.name ?? '이름없음'
    this.email = data.email ?? ''
  }

  get displayName() {
    return this.name.toUpperCase()
  }
}

그리고 store에서는 아래와 같이 DTO로 가공:

import { UserDTO } from '@/dto/UserDTO'

const res = await axios.get('/api/users')
this.users = res.data.map((u: any) => new UserDTO(u))

3. 🧩 컴포넌트에서 사용하는 방식

<script setup lang="ts">
import { onMounted } from 'vue'
import { useUserStore } from '@/stores/userStore'
import { storeToRefs } from 'pinia'

const userStore = useUserStore()
const { users, isLoading, error } = storeToRefs(userStore)

onMounted(() => {
  userStore.fetchUsers()
})
</script>

<template>
  <div>
    <p v-if="isLoading">로딩 중...</p>
    <p v-else-if="error">오류: {{ error }}</p>
    <ul v-else>
      <li v-for="user in users" :key="user.id">
        {{ user.displayName ?? user.name }}
      </li>
    </ul>
  </div>
</template>

✅ 실무에서 이 패턴을 써야 하는 이유

이유 설명
💡 책임 분리 API 호출 로직이 컴포넌트 밖(Pinia)에 있어서 재사용성과 가독성 향상
🔄 로딩/에러 상태 분리 UI 제어가 쉬움 (v-if 조건 처리 용이)
✅ 타입 안전성 DTO 또는 Axios 제네릭으로 타입 추론 가능
👥 여러 컴포넌트 공유 하나의 store로 상태를 전역 관리

🏁 마무리

Pinia 상태에서 비동기 호출을 직접 다루는 것은 실무에서 매우 흔한 패턴입니다.
아래와 같은 기능들을 앞으로 확장할 수 있습니다:

  • 페이지네이션 (page, limit 상태 추가)
  • 검색 필터 (keyword, sort 등 상태로 관리)
  • 캐싱 (최초 호출만 요청 등)

✅ 컴포넌트는 최대한 UI 역할만 담당하고, 데이터 요청은 Pinia 스토어에 위임하세요. 유지보수가 쉬운 Vue 아키텍처가 완성됩니다!