임팩트: 상태관리 아키텍처 전환 — 번들 사이즈, DX, 마이그레이션 전략 문서화
SCRUM-14457 | 커밋 수: 45개 | 기간: 2025.07
문제상황
Vuex 레거시 코드로 인해 타입 안전성이 부족했습니다. 상태 관리 코드의 복잡성이 계속 증가하고 있었으며, Vue 3 Composition API와의 통합이 어려운 상황이었습니다.
주요 개선사항
전체 Vuex 모듈을 Pinia로 마이그레이션하여 100% 전환을 달성했습니다. benefit, mollys, product, promotion, wine, review 등 전 도메인의 마이그레이션을 완료했으며, store 파일들의 dispatch를 Pinia 방식으로 변경했습니다.
정량적 성과
Vuex 모듈 15개 이상을 Pinia 스토어로 전환했습니다. shims-vuex.d.ts를 삭제하고 타입 안전성을 100% 확보했습니다.
| 항목 | 비교 | 값 (개/%) |
|---|---|---|
| Vuex 모듈 수 | 15 개/% | |
| 커밋 수 | 45 개/% | |
| 전환율(%) | 100 개/% |
비즈니스 임팩트
IDE 자동완성이 개선되어 개발 생산성이 향상되었습니다. 런타임 에러를 사전에 방지할 수 있게 되었으며, Vue 3 생태계에 완전히 통합되었습니다.
타입 안전성
100% 확보, 런타임 에러 사전 방지
DX 향상
IDE 자동완성, dispatch 문자열 제거
레거시 제거
shims-vuex.d.ts 삭제
기술적 구현 상세
Vuex (AS-IS)
장점
- 안정적 생태계
- 기존 코드베이스 호환
고려할 점
- 문자열 기반 dispatch
- 타입 추론 불가
- mutations 보일러플레이트
- IDE 자동완성 미지원
Pinia (TO-BE)
장점
- 완전한 타입 안전성
- IDE 자동완성
- mutations 불필요
- Composition API 통합
고려할 점
- 마이그레이션 비용 (45커밋)
AS-IS
기존 Vuex 코드는 타입 추론이 어렵고, dispatch 문자열 기반으로 동작했습니다.
// 기존 Vuex 구조 (더미 예시)
// store/modules/benefit.ts
export default {
namespaced: true,
state: () => ({
benefits: [],
isLoading: false,
}),
mutations: {
SET_BENEFITS(state, benefits) {
state.benefits = benefits;
},
SET_LOADING(state, isLoading) {
state.isLoading = isLoading;
},
},
actions: {
async fetchBenefits({ commit }) {
commit('SET_LOADING', true);
const response = await api.getBenefits();
commit('SET_BENEFITS', response.data);
commit('SET_LOADING', false);
},
},
};
// 컴포넌트에서 사용
this.$store.dispatch('benefit/fetchBenefits');
문제점:
문자열 기반 dispatch로 타입 안전성이 부족했습니다. IDE 자동완성이 지원되지 않았으며, 런타임에서만 에러를 발견할 수 있었습니다.
TO-BE
Pinia로 마이그레이션하여 완전한 타입 안전성을 확보했습니다.
// stores/modules/benefit.ts
import { defineStore } from 'pinia';
export const useBenefitStore = defineStore('benefit', {
state: () => ({
benefits: [] as Benefit[],
isLoading: false,
}),
actions: {
async fetchBenefits() {
this.isLoading = true;
try {
const response = await benefitApi.getBenefits();
this.benefits = response;
} finally {
this.isLoading = false;
}
},
},
});
핵심 구현
1. Pinia 스토어 정의
// stores/modules/benefit.ts
import { defineStore } from 'pinia';
import { benefitApi } from '@/stores/api/benefit';
import type { Benefit, BenefitState } from '@/types/benefit';
export const useBenefitStore = defineStore('benefit', {
state: (): BenefitState => ({
benefits: [],
isLoading: false,
error: null,
}),
getters: {
activeBenefits: (state) =>
state.benefits.filter(b => b.isActive),
totalBenefitAmount: (state) =>
state.benefits.reduce((sum, b) => sum + b.amount, 0),
},
actions: {
async fetchBenefits() {
this.isLoading = true;
this.error = null;
try {
this.benefits = await benefitApi.getBenefits();
} catch (e) {
this.error = e as Error;
} finally {
this.isLoading = false;
}
},
clearBenefits() {
this.benefits = [];
},
},
});
2. common.js에서 Pinia 방식으로 마이그레이션
// public/js/common.js
// 기존 Vuex 방식
// window.emart.store.dispatch('benefit/fetchBenefits');
// Pinia 방식으로 변경
const getBenefitStore = () => {
const pinia = window.emart.pinia;
return useBenefitStore(pinia);
};
const fetchBenefits = async () => {
const benefitStore = getBenefitStore();
await benefitStore.fetchBenefits();
};
3. 컴포넌트에서 사용
<!-- components/benefit/BenefitList.vue -->
<script setup lang="ts">
import { useBenefitStore } from '@/stores/modules/benefit';
import { storeToRefs } from 'pinia';
const benefitStore = useBenefitStore();
const { benefits, isLoading, activeBenefits } = storeToRefs(benefitStore);
onMounted(() => {
benefitStore.fetchBenefits();
});
</script>
<template>
<div v-if="isLoading">로딩 중...</div>
<ul v-else>
<li v-for="benefit in activeBenefits" :key="benefit.id">
{{ benefit.name }}
</li>
</ul>
</template>
4. shims-vuex.d.ts 삭제 및 pinia.d.ts 정의
// src/pinia.d.ts
import 'pinia';
import type { Router } from 'vue-router';
declare module 'pinia' {
export interface PiniaCustomProperties {
router: Router;
}
}
// 전역 타입 확장
declare global {
interface Window {
emart: {
pinia: Pinia;
// ...
};
}
}
5. App.vue에서 inject 방식 변경
<!-- App.vue -->
<script setup lang="ts">
import { inject } from 'vue';
import type { VueCookies } from 'vue-cookies';
// 기존: this.$cookies
// 변경: inject를 통한 접근
const cookies = inject<VueCookies>('$cookies');
const getMallType = () => {
return cookies?.get('mallType') ?? 'emart';
};
</script>
회고
잘한 점
전체 모듈 100% 마이그레이션을 완료했습니다. shims-vuex.d.ts 삭제로 레거시 코드를 완전히 제거했으며, IDE 자동완성 및 타입 체크가 활성화되었습니다.
개선할 점
마이그레이션 중 일부 런타임 에러가 발생했습니다. 테스트 커버리지가 부족했던 부분이 원인이었으며, common.js의 Pinia 접근 방식도 개선이 필요합니다.
배운 점
Vuex에서 Pinia로의 마이그레이션 전략을 수립하고 실행하는 경험을 얻었습니다. Pinia의 타입 안전성 장점을 실감했으며, 레거시 JavaScript 파일에서 Pinia에 접근하는 방법을 학습했습니다.