웹 인터페이스의 반응성을 개선하는 것은 종종 프론트엔드가 백엔드와 통신하는 방식을 최적화하는 것에서 시작됩니다. Vue.js 기반의 Semaphore UI에서는 사용자에게 데이터 로딩을 더 빠르고 부드럽게 만들기 위해 여러 가지 기술을 도입했습니다. 이 포스트에서는 버전 2.14에서 구현한 세 가지 가장 영향력 있는 변경 사항을 공유합니다.


API 사용 최적화의 이유

API는 동적 웹 앱의 중추 역할을 하지만, 잘 관리되지 않은 요청은 병목 현상을 초래할 수 있습니다. 순차 호출에 대한 긴 대기 시간이나 중복 데이터 가져오기는 사용자 경험을 저하시킵니다. 이러한 문제를 정면으로 해결해 보겠습니다.


1. Promise.all()을 사용한 병렬 요청

문제: 순차 요청

일반적인 함정은 API 호출을 하나씩 하는 것입니다. 예를 들어, 사용자 세부 정보를 가져온 다음, 그들의 주문, 그들의 선호도를 가져오는 경우입니다:

// 순차적 접근 (느림 😞)
async function fetchData() {
  const user = await getUser();
  const inventory = await getInventory(projectID);
  const templates = await getTemplates(projectID);
  return { user, inventory, templates };
}

여기서 각 요청은 이전 요청이 완료될 때까지 기다립니다. 각 호출이 200ms가 걸린다면, 총 시간은 600ms—상당한 지연입니다.

해결책: 병렬 실행

Vue.js는 JavaScript의 비동기 기능을 활용하므로 Promise.all()을 사용하여 여러 요청을 동시에 실행할 수 있습니다:

// 병렬 접근 (빠름 ⚡)
async function fetchDataParallel() {
  const [user, inventory, templates] = await Promise.all([
    getUser(),
    getInventory(projectID),
    getTemplates(projectID),
  ]);
  return { user, inventory, templates };
}

독립적인 요청을 그룹화함으로써 세 가지 호출이 동시에 해결됩니다. 각 호출이 200ms가 걸린다면, 총 시간은 200ms로 줄어듭니다!

Vue와 통합

Vue 컴포넌트에서 이 패턴을 생명주기 훅이나 메서드에서 사용하세요:

export default {
  props: {
    projectID: Number,
  },
  watch: {
    async projectID() {
      await this.fetchDataParallel();
    },
  },
  async created() {
    await this.fetchDataParallel();
  },
  methods: {
    async fetchDataParallel() {
      try {
        const [
          this.user, 
          this.inventory,
          this.templates
        ] = await Promise.all([
          getUser(),
          getInventory(this.projectID),
          getTemplates(this.projectID),
        ]);
      } catch (error) {
        console.error("데이터 로딩 실패:", error);
      }
    };
  }

팁: 데이터가 로드되는 동안 사용자를 참여시키기 위해 스켈레톤 로더와 결합하세요.


2. 캐시된 API 응답 재사용

문제: 중복 가져오기

앱은 종종 여러 컴포넌트에서 동일한 데이터를 다시 가져옵니다(예: 여러 뷰에서 요청된 사용자 프로필 데이터). 이는 대역폭을 낭비하고 인터페이스를 느리게 만듭니다.

해결책: 캐시 및 재사용

API 응답을 저장하고 무효화될 때까지 재사용합니다. Vue의 반응성 시스템은 중앙 집중식 스토어(예: Pinia 또는 Vuex)를 사용하여 이를 쉽게 만듭니다:

1단계: 캐시 스토어 생성

// stores/apiCache.js
import { defineStore } from 'pinia';

export const useApiCache = defineStore('apiCache', {
  state: () => ({
    project: null,
    templates: {},
  }),
  actions: {
    async fetchProject() {
      if (!this.project) {
        this.project = await getProject();
      }
      return this.project;
    },
    async fetchTemplates(id) {
      if (!this.templates[id]) {
        this.templates[id] = await fetchTemplates(id);
      }
      return this.templates[id];
    },
  },
});

2단계: 컴포넌트에서 스토어 사용

// Component.vue
import { useApiCache } from '@/stores/apiCache';

export default {
  setup() {
    const apiCache = useApiCache();

    // 캐시된 사용자 재사용 또는 한 번만 가져오기
    const user = apiCache.fetchUser();

    return { user };
  },
};

팁: Semaphore UI에서는 아직 Pinia를 사용하지 않지만, 곧 마이그레이션할 계획입니다.

캐시 무효화 전략

3. 부드러운 전환을 위한 스켈레톤 로더 사용

문제: 로딩 중 비응답 UI

최적화된 API 호출이 있더라도, 사용자는 데이터가 로드되는 동안 짧은 지연을 경험합니다. 이 시간 동안 빈 화면이나 멈춘 인터페이스는 실제 가져오기 시간이 최소화되더라도 앱이 다듬어지지 않았거나 느리게 느껴질 수 있습니다.

해결책: 스켈레톤 로더

스켈레톤 플레이스홀더는 데이터가 로드되는 동안 콘텐츠의 레이아웃을 모방하여 시각적 피드백을 제공하고 사용자의 참여를 유지합니다. 병렬 요청 및 캐싱과 결합하면 속도의 지각을 원활하게 만듭니다.


Vue.js에서 스켈레톤 로더 추가 방법

1단계: 스켈레톤 컴포넌트 선택

Semaphore UI에서는 Vuetify 프레임워크를 사용합니다. 이 프레임워크는 이미 스켈레톤 로더 컴포넌트를 포함하고 있습니다.

<v-skeleton-loader
  type="
    table-heading,
    image,
    list-item-two-line,
    list-item-two-line,
    list-item-two-line"
></v-skeleton-loader>

2단계: 비동기 데이터 가져오기와 통합 로딩 상태를 사용하여 스켈레톤과 실제 콘텐츠 간에 전환합니다:

// Component.vue
export default {
  data() {
    return {
      isLoading: true,
      user: null,
      templates: [],
    };
  },
  async created() {
    this.isLoading = true;
    try {
      [this.user, this.templates] = await Promise.all([
        fetchUser(),
        fetchTemplates(),
      ]);
    } finally {
      this.isLoading = false;
    }
  },
};

3단계: 스켈레톤 조건부 렌더링

<template>
  <div class="user-profile">
    <v-skeleton-loader
      v-if="isLoading"
      type="
        table-heading,
        image,
        list-item-two-line,
        list-item-two-line,
        list-item-two-line"
    ></v-skeleton-loader>
    <v-card v-else>
      <h2>{{ user.name }}</h2>

      <v-card v-for="tpl in templates" :key="tpl.id">
        <v-card-title>{{ tpl.name }}</v-card-title>
        <v-card-text>{{ tpl.description }}</v-card-text>
      </v-card>
    </v-card>
  </div>
</template>

로더 디자인을 위한 고급 팁

  1. 콘텐츠 구조 일치: 스켈레톤을 실제 레이아웃(예: 이미지 플레이스홀더, 텍스트 라인)에 맞게 디자인합니다.
  2. 위치 우선 콘텐츠: 배경 데이터가 완료되는 동안 보이는 콘텐츠에 대한 로더를 먼저 표시합니다.
  3. 캐싱과 결합: 캐시된 데이터가 존재하는 경우, 스켈레톤을 건너뛰고 즉시 데이터를 표시합니다.

이것이 중요한 이유


모든 것을 종합하기

병렬 요청, 스마트 캐싱, 스켈레톤 로더를 결합함으로써 우리는 상당한 개선을 이룰 수 있었습니다:

  1. 더 빠른 초기 로드: 병렬 요청이 데이터 로드 시간을 크게 단축했습니다.
  2. 부드러운 탐색: 재사용된 응답이 중복 네트워크 호출을 줄였습니다.
  3. 향상된 사용자 참여: 스켈레톤 로더가 시각적 피드백과 지각된 속도를 개선했습니다.

API 사용 최적화는 단순한 성능 향상뿐만 아니라 사용자 경험을 개선하는 것입니다. 우리는 Semaphore UI를 더 빠르고 친근하게 만들기 위해 매 업데이트마다 프론트엔드를 계속 다듬어 나갈 것입니다.