다크 모드
Appendix B. 사내 표준 UI 라이브러리(Ant Design Vue) 브릿지 가이드
"우리가 14개 챕터에 걸쳐 완성한 코어 아키텍처(Composable, Store, Repository)는 사내에서 어떤 UI 라이브러리를 쓰더라도 100% 그대로 재사용된다. 엔터프라이즈 백오피스 표준인 Ant Design Vue와의 1:1 완벽 결합법을 마스터한다."
B.1 왜 비즈니스 로직과 UI 라이브러리를 분리했는가?
현업 실무에서는 엔터프라이즈 사내 백오피스 및 관리자 포털 표준으로 **Ant Design Vue (ant-design-vue)**를 매우 널리 사용합니다.
초보 개발자들은 흔히 UI 라이브러리의 컴포넌트(a-table, a-pagination, a-form) 템플릿과 이벤트 핸들러 안에 직접 Axios 호출과 도메인 상태 로직을 마구 뒤섞어버립니다.
하지만 우리는 앞서 5장과 9장에서 비즈니스 상태와 알고리즘을 usePagination, usePostStore, postRepository로 완전히 독립시켜 두었습니다.
그렇기 때문에 현업에서 사내 디자인 시스템이나 Ant Design Vue를 도입하더라도, 기존 비즈니스 로직을 단 한 줄도 고치지 않고 UI 컴포넌트만 갈아 끼워 10분 만에 최고급 엔터프라이즈 프로덕션 화면을 조립할 수 있습니다!
B.2 Ant Design Vue 설치 및 프로젝트 설정
먼저 프로젝트 프론트엔드에 Ant Design Vue를 추가합니다:
bash
cd frontend
npm install ant-design-vuemain.ts에 Ant Design Vue와 스타일 시트를 등록합니다 (Ant Design Vue 4.x 기준):
typescript
// frontend/src/main.ts
import { createApp } from 'vue';
import Antd from 'ant-design-vue';
import 'ant-design-vue/dist/reset.css'; // Ant Design 4.x 글로벌 리셋 CSS
import App from './App.vue';
import router from './router';
import { pinia } from './stores';
const app = createApp(App);
app.use(pinia);
app.use(router);
app.use(Antd);
app.mount('#app');B.3 실무 케이스: Ant Design Vue와의 1:1 결합
사내 표준인 Ant Design Vue의 <a-table>, <a-card>, <a-popconfirm>, <a-pagination>에 우리가 14개 장에 걸쳐 완성한 코어 아키텍처를 결합해 보겠습니다.
[코드 B-1] Ant Design Vue 게시판 화면 (src/views/AntdBoardView.vue)
html
<template>
<div class="antd-board-container">
<a-card :bordered="false" class="board-card">
<!-- 카드 헤더 영역 -->
<template #title>
<div class="header-title">
<span class="icon">💼</span>
<span>사내 기술 Q&A (Ant Design Vue 버전)</span>
</div>
</template>
<template #extra>
<a-button type="primary" @click="router.push('/write')">
✏️ 새 글 작성
</a-button>
</template>
<!-- 1. Ant Design Vue 테이블에 usePostStore 및 컬럼 명세서 바인딩 -->
<a-table
:columns="columns"
:data-source="posts"
:loading="isLoading"
:pagination="paginationConfig"
row-key="id"
size="middle"
>
<!-- 커스텀 바디 셀 슬롯 -->
<template #bodyCell="{ column, record }">
<!-- 제목 열: 상세 페이지 링크 및 인기 태그 렌더링 -->
<template v-if="column.key === 'title'">
<a class="post-title-link" @click="router.push(`/post/${record.id}`)">
{{ record.title }}
</a>
<a-tag v-if="record.likeCount >= 5" color="error" style="margin-left: 8px;">
🔥 인기
</a-tag>
</template>
<!-- 좋아요 열: 10장의 낙관적 업데이트(Optimistic Update) 액션 결합 -->
<template v-if="column.key === 'likeCount'">
<a-button type="text" size="small" @click="toggleLikeOptimistic(record.id)">
❤️ {{ record.likeCount }}
</a-button>
</template>
<!-- 관리 열: Ant Design Popconfirm을 통한 안전한 삭제 확인 -->
<template v-if="column.key === 'action'">
<a-popconfirm
title="정말 이 게시글을 삭제하시겠습니까?"
ok-text="삭제"
cancel-text="취소"
ok-type="danger"
@confirm="removePost(record.id)"
>
<a-button type="link" danger size="small">삭제</a-button>
</a-popconfirm>
</template>
</template>
</a-table>
</a-card>
</div>
</template>
<script setup lang="ts">
import { onMounted, computed } from 'vue';
import { useRouter } from 'vue-router';
import { storeToRefs } from 'pinia';
import type { TableColumnsType } from 'ant-design-vue';
import { usePostStore } from '@/stores/usePostStore';
import { usePagination } from '@/composables/usePagination';
import type { Post } from '@/types/post';
const router = useRouter();
// 🌟 1. 9장에서 구축한 Pinia 스토어 주입 (비즈니스 상태 재사용)
const postStore = usePostStore();
const { posts, isLoading, totalCount } = storeToRefs(postStore);
const { fetchPosts, toggleLikeOptimistic, removePost } = postStore;
// 🌟 2. 5장에서 분리한 usePagination과 Pinia fetchPosts 액션 조립
const { currentPage, pageSize, setPage } = usePagination({
totalCount,
onPageChange: (newPage) => fetchPosts({ page: newPage })
});
// 🌟 3. Ant Design Vue 테이블 컬럼 명세 (7장의 Post DTO와 엄격한 타입 계약 체결)
const columns: TableColumnsType<Post> = [
{
title: 'ID',
dataIndex: 'id',
key: 'id',
width: 80,
align: 'center'
},
{
title: '제목',
dataIndex: 'title',
key: 'title',
ellipsis: true
},
{
title: '작성자',
dataIndex: 'author',
key: 'author',
width: 140
},
{
title: '작성일',
dataIndex: 'createdAt',
key: 'createdAt',
width: 130,
align: 'center'
},
{
title: '좋아요',
dataIndex: 'likeCount',
key: 'likeCount',
width: 100,
align: 'center'
},
{
title: '관리',
key: 'action',
width: 90,
align: 'center'
}
];
// 🌟 4. Ant Design Table 내장 페이지네이션 설정 바인딩
const paginationConfig = computed(() => ({
current: currentPage.value,
pageSize: pageSize.value,
total: totalCount.value,
showTotal: (total: number) => `총 ${total}개의 게시글`,
showSizeChanger: false,
onChange: (page: number) => setPage(page)
}));
onMounted(() => {
fetchPosts();
});
</script>
<style scoped>
.antd-board-container {
max-width: 1100px;
margin: 32px auto;
padding: 0 16px;
}
.board-card {
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.06);
border-radius: 12px;
}
.header-title {
display: flex;
align-items: center;
gap: 8px;
font-weight: 600;
font-size: 1.15rem;
}
.post-title-link {
color: #1677ff;
font-weight: 500;
cursor: pointer;
transition: color 0.2s ease;
}
.post-title-link:hover {
color: #4096ff;
text-decoration: underline;
}
</style>B.4 실무 케이스 2: Tailwind CSS를 이용한 유틸리티 스타일링
만약 사내 프로젝트가 UI 컴포넌트 라이브러리 대신 Tailwind CSS를 스타일링 표준으로 채택하고 있다면, 8장에서 제작한 순수 제네릭 BoardTable.vue에 유틸리티 클래스를 바로 입혀서 활용할 수 있습니다:
html
<!-- Tailwind CSS가 적용된 제네릭 행 스타일 예시 -->
<tr class="border-b border-gray-200 hover:bg-gray-50 transition-colors duration-150">
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 font-mono">{{ item.id }}</td>
<td class="px-6 py-4 text-sm font-medium text-blue-600 hover:text-blue-800 cursor-pointer">
{{ item.title }}
</td>
<td class="px-6 py-4 text-sm text-gray-700">{{ item.author }}</td>
<td class="px-6 py-4 text-sm text-gray-400">{{ item.createdAt }}</td>
</tr>B.5 현장 투입을 앞둔 개발자를 위한 최종 요약
- 컴포넌트 껍데기는 변하지만, 아키텍처의 알맹이는 결코 변하지 않는다.
- 사내 프로젝트에 Ant Design Vue, Element Plus, Tailwind 등 어떤 UI 체계가 도입되든, 여러분이 교재를 통해 완성한 TypeScript DTO, Composable 로직, Pinia 스토어, Repository 네트워크 계층은 100% 그대로 재사용됩니다.
- 이제 여러분은 현업 소스 코드를 마주했을 때 낯설어하지 않고, 껍데기(UI 라이브러리)와 알맹이(아키텍처)를 명확히 분리하여 바라볼 수 있는 진정한 시야를 가지게 되었습니다!