다크 모드
Chapter 12. HTTP 레이어 격리와 프론트엔드 에러 바운더리
"11장까지 작성된 코드 곳곳에 흩어져 있는
axios.get을 보라. 컴포넌트가 네트워크 주소와 HTTP 상태 코드를 직접 아는 죄악을 멈추고, Repository 계층과 에러 바운더리로 격리하라."
12.1 컴포넌트에서 Axios를 완전히 몰아내자
1장부터 11장까지 우리는 편의상 컴포넌트와 스토어 내부에서 axios.get('/api/posts')를 직접 호출해 왔습니다.
만약 백엔드 팀에서 다음과 같은 변경 사항을 통보해 온다면 어떻게 될까요?
- "인증 토큰이 만료되면 401이 떨어지니 Refresh Token으로 자동 재발급받아 재요청하세요."
- "모든 API 기본 주소 앞에
/api/v2가 붙습니다."
지금 구조에서는 수십 군데의 axios 호출 코드를 열어 일일이 고쳐야 합니다.
백엔드에서 Controller와 DB 사이에 Service와 Repository를 두듯이, 프론트엔드도 HTTP 클라이언트 인프라와 비즈니스 Repository를 명확히 분리해야 합니다.
12.2 [1단계] 중앙 집중식 httpClient.ts와 인터셉터 AOP
토큰 자동 주입과 만료 시 재발급(Refresh Token Rotation)을 전담하는 싱글톤 HTTP 클라이언트를 만듭니다.
[코드 12-1] src/api/httpClient.ts
typescript
import axios, { type AxiosError, type InternalAxiosRequestConfig } from 'axios';
export const httpClient = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL || '',
timeout: 10000,
headers: { 'Content-Type': 'application/json' }
});
// 1. 요청 인터셉터: 모든 발신 요청에 JWT 토큰 자동 첨부
httpClient.interceptors.request.use((config: InternalAxiosRequestConfig) => {
const token = localStorage.getItem('ACCESS_TOKEN');
if (token && config.headers) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
// 2. 응답 인터셉터: 401 에러 시 백그라운드 토큰 자동 갱신 AOP
httpClient.interceptors.response.use(
(res) => res,
async (error: AxiosError) => {
const originalRequest = error.config as InternalAxiosRequestConfig & { _retry?: boolean };
if (error.response?.status === 401 && !originalRequest._retry) {
originalRequest._retry = true;
try {
const refreshRes = await axios.post('/api/auth/refresh', {
refreshToken: localStorage.getItem('REFRESH_TOKEN')
});
const newToken = refreshRes.data.accessToken;
localStorage.setItem('ACCESS_TOKEN', newToken);
originalRequest.headers.Authorization = `Bearer ${newToken}`;
return httpClient(originalRequest); // 이전 실패 요청 재전송!
} catch (refreshErr) {
localStorage.clear();
window.location.href = '/login?expired=true';
return Promise.reject(refreshErr);
}
}
return Promise.reject(error);
}
);12.3 [2단계] 비즈니스 postRepository.ts 추상화
이제 컴포넌트와 Pinia 스토어는 Axios나 HTTP URL을 전혀 알지 못합니다. 오직 Repository의 메서드만 호출합니다.
[코드 12-2] src/api/postRepository.ts
typescript
import { httpClient } from './httpClient';
import type { Post, CreatePostRequest, PageResult } from '@/types/post';
export const postRepository = {
async getPosts(params: { page: number; size: number; category?: string; keyword?: string }) {
const res = await httpClient.get<PageResult<Post>>('/api/posts', { params });
return res.data;
},
async getPostById(id: number) {
const res = await httpClient.get<Post>(`/api/posts/${id}`);
return res.data;
},
async createPost(payload: CreatePostRequest) {
const res = await httpClient.post<Post>('/api/posts', payload);
return res.data;
},
async deletePost(id: number) {
await httpClient.delete(`/api/posts/${id}`);
},
async toggleLike(id: number) {
const res = await httpClient.post<Post>(`/api/posts/${id}/like`);
return res.data;
}
};이로써 usePostStore.ts의 axios.get(...)은 모두 postRepository.getPosts(...)로 깔끔하게 교체되었습니다.
12.4 프론트엔드 에러 바운더리: onErrorCaptured
서버가 예기치 않게 500 에러를 뿜거나 렌더링 중 오류가 터졌을 때, 화면 전체가 하얗게 죽어버리는 현상(White-out)을 방지하기 위해 에러 바운더리로 감쌉니다.
[코드 12-3] src/components/common/ErrorBoundary.vue
html
<template>
<div v-if="hasError" class="error-fallback-box">
<h3>⚠️ 일시적인 시스템 오류가 발생했습니다</h3>
<p>{{ errorMessage }}</p>
<button @click="resetError">다시 시도하기</button>
</div>
<slot v-else></slot>
</template>
<script setup lang="ts">
import { ref, onErrorCaptured } from 'vue';
const hasError = ref(false);
const errorMessage = ref('');
onErrorCaptured((err, instance, info) => {
hasError.value = true;
errorMessage.value = err.message || '알 수 없는 오류가 발생했습니다.';
console.error(`💥 [Error Boundary 포착] 위치: ${info}`, err);
// 에러가 상위 부모로 계속 전파되어 앱 전체가 크래시되는 것을 방지!
return false;
});
function resetError() {
hasError.value = false;
errorMessage.value = '';
}12.5 다음 챕터로 이어지는 질문
이제 게시판 시스템은 완벽한 인프라 분리와 에러 안전망을 갖추었습니다.
하지만 실제 사내 운영에 배포하자마자 생각지 못한 병목이 터졌습니다:
사내 인기 기술 아티클에 사우들의 댓글이 10,000개 넘게 달리자, 상세 페이지를 열 때마다 브라우저가 5초 동안 굳어버리고 스크롤이 버벅이는 현상이 발생한 것입니다!
"화면에 10,000개의 댓글이 존재하더라도, 60fps로 매끄럽게 스크롤되도록 만들 수는 없을까?"
이어지는 Chapter 13에서는 shallowRef를 통한 반응형 오버헤드 삭감과, **60줄로 직접 만드는 초경량 가상 스크롤(Virtual Scroll)**을 우리 게시판에 장착해 보겠습니다.