SCRUM-10616, 12163~12206 | 커밋 수: 41개+ | 기간: 2025.01
문제상황
브랜드 커뮤니티 기능을 신규로 도입해야 하는 상황이었습니다. 게시글, 댓글, 프로필 등 복합적인 기능을 함께 구현해야 했습니다.
Before (도메인별 프로필 분산)
장점
- 각 도메인 독립 개발
고려할 점
- 프로필 표시 로직 중복
- 인터페이스 불일치
- 새 도메인마다 재구현
After (전략패턴 Profile 컴포넌트)
장점
- 단일 컴포넌트로 4개 도메인 커버
- displayType만 변경하면 적용
고려할 점
- 전략 설정 추가 시 config 확장 필요
주요 개선사항
스퀘어 메인, 상세, 게시글 작성 페이지를 구현했습니다. Profile 컴포넌트에 전략패턴을 적용하여 재사용성을 확보했고, 앱 메인의 스퀘어 영역도 함께 구현했습니다. 또한 댓글과 답글 시스템을 기존 인프라와 통합했습니다.
| 항목 | 비교 | 값 (개) |
|---|---|---|
| 신규 페이지 | 5 개 | |
| 신규 컴포넌트 | 10 개 | |
| QA 이슈 해결 | 20 개 | |
| 커밋 | 41 개 |
정량적 성과
신규 페이지 5개 이상을 구현하고, 컴포넌트 10개 이상을 새로 개발했습니다. QA 과정에서 발견된 이슈 20개 이상을 해결했습니다.
전략패턴
Profile 컴포넌트 4개 도메인 재사용
댓글 통합
기존 인프라와 스퀘어 댓글 연결
브랜드 채널
브랜드-고객 소통 채널 확보
비즈니스 임팩트
브랜드와 고객 간의 소통 채널을 확보했습니다. 사용자 참여도가 증가했으며, 커뮤니티를 기반으로 한 마케팅이 가능해졌습니다.
- 요구사항 분석
- Profile 전략패턴 설계
- 5개 페이지 구현
- QA 20개+ 해결
- 런칭
기술적 구현 상세
AS-IS
기존에는 브랜드 커뮤니티 기능이 없었고, 프로필 컴포넌트가 각 도메인마다 다르게 구현되어 있었습니다.
<!-- 기존 구조 (더미 예시) -->
<!-- 각 도메인마다 프로필 표시 방식이 다름 -->
<template>
<!-- 리뷰 프로필 -->
<div class="review-profile">
<img :src="user.profileImage" />
<span>{{ user.nickname }}</span>
</div>
<!-- 댓글 프로필 -->
<div class="comment-profile">
<img :src="writer.avatar" />
<span>{{ writer.name }}</span>
</div>
</template>
프로필 표시 로직이 중복되어 있었고, 도메인별로 프로필 인터페이스가 일치하지 않았습니다. 새로운 도메인을 추가할 때마다 프로필 컴포넌트를 재구현해야 하는 문제가 있었습니다.
TO-BE
Profile 컴포넌트에 전략 패턴을 적용하여 다양한 도메인에서 재사용 가능하게 구현했습니다.
// 통합된 프로필 인터페이스
interface ProfileInfo {
id: string;
nickname: string;
profileImageUrl: string;
webUserId?: string;
isFollowing?: boolean;
}
type ProfileDisplayType = 'review' | 'comment' | 'feed' | 'square';
핵심 구현
1. Profile 컴포넌트 전략패턴
<!-- components/common/Profile.vue -->
<script setup lang="ts">
import { useProfileStrategy } from '@/hooks/user';
const props = defineProps<{
profile: ProfileInfo;
displayType: ProfileDisplayType;
showFollowButton?: boolean;
}>();
const { getDisplayConfig } = useProfileStrategy();
const config = computed(() =>
getDisplayConfig(props.displayType)
);
</script>
<template>
<div :class="['profile', config.containerClass]">
<img
:src="profile.profileImageUrl"
:class="config.imageClass"
:alt="profile.nickname"
/>
<div class="profile-info">
<span :class="config.nicknameClass">
{{ profile.nickname }}
</span>
<span v-if="profile.webUserId" class="web-user-id">
@{{ profile.webUserId }}
</span>
</div>
<FollowButton
v-if="showFollowButton"
:is-following="profile.isFollowing"
@toggle="handleFollowToggle"
/>
</div>
</template>
2. 프로필 전략 훅
// hooks/user.ts
interface ProfileDisplayConfig {
containerClass: string;
imageClass: string;
nicknameClass: string;
showWebUserId: boolean;
}
export const useProfileStrategy = () => {
const displayConfigs: Record<ProfileDisplayType, ProfileDisplayConfig> = {
review: {
containerClass: 'profile--review',
imageClass: 'profile__image--small',
nicknameClass: 'profile__nickname--bold',
showWebUserId: false,
},
comment: {
containerClass: 'profile--comment',
imageClass: 'profile__image--medium',
nicknameClass: 'profile__nickname--normal',
showWebUserId: false,
},
feed: {
containerClass: 'profile--feed',
imageClass: 'profile__image--large',
nicknameClass: 'profile__nickname--bold',
showWebUserId: true,
},
square: {
containerClass: 'profile--square',
imageClass: 'profile__image--large',
nicknameClass: 'profile__nickname--bold',
showWebUserId: true,
},
};
const getDisplayConfig = (type: ProfileDisplayType): ProfileDisplayConfig => {
return displayConfigs[type];
};
return { getDisplayConfig };
};
3. 스퀘어 피드 아이템
<!-- components/esquare/FeedItem.vue -->
<script setup lang="ts">
import { Profile } from '@/components/common';
import { useEsquareStore } from '@/stores/modules/esquare';
const props = defineProps<{
feed: SquareFeed;
}>();
const esquareStore = useEsquareStore();
const handleLike = async () => {
await esquareStore.toggleLike(props.feed.id);
};
</script>
<template>
<article class="feed-item">
<Profile
:profile="feed.author"
display-type="square"
:show-follow-button="!feed.author.isOwner"
/>
<div class="feed-content">
<p>{{ feed.content }}</p>
<ImageGallery
v-if="feed.images.length"
:images="feed.images"
/>
</div>
<div class="feed-actions">
<LikeButton
:is-liked="feed.isLiked"
:count="feed.likeCount"
@click="handleLike"
/>
<CommentButton :count="feed.commentCount" />
<ShareButton />
</div>
</article>
</template>
4. 스퀘어 메인 페이지
<!-- views/esquare/EsquareMain.vue -->
<script setup lang="ts">
import { useEsquareStore } from '@/stores/modules/esquare';
import { FeedList, ScrollBanner } from '@/components/esquare';
const esquareStore = useEsquareStore();
const { feeds, recommendedSquares } = storeToRefs(esquareStore);
onMounted(async () => {
await Promise.all([
esquareStore.fetchFeeds(),
esquareStore.fetchRecommendedSquares(),
]);
});
</script>
<template>
<div class="esquare-main">
<ScrollBanner :squares="recommendedSquares" />
<FeedList :feeds="feeds" />
</div>
</template>
5. 홈 화면 브랜드 스퀘어 영역
<!-- components/home/BrandSquare.vue -->
<script setup lang="ts">
import { usePromotionStore } from '@/stores/modules/promotion';
const promotionStore = usePromotionStore();
const { visitedBrands } = storeToRefs(promotionStore);
const setVisitedBrand = (brandId: string) => {
promotionStore.setVisitedBrand(brandId);
};
</script>
<template>
<section class="brand-square">
<h2>브랜드 스퀘어</h2>
<ul class="brand-list">
<li
v-for="brand in visitedBrands"
:key="brand.id"
@click="setVisitedBrand(brand.id)"
>
<img :src="brand.logoUrl" :alt="brand.name" />
<span>{{ brand.name }}</span>
</li>
</ul>
</section>
</template>
회고
잘한 점
Profile 컴포넌트에 전략패턴을 적용하여 재사용성을 극대화한 점이 좋았습니다. 스퀘어 도메인 구조를 명확히 분리했고, 댓글 및 답글 시스템을 기존 인프라와 성공적으로 통합했습니다.
개선할 점
초기 Profile 컴포넌트 설계 시점부터 전략패턴을 적용했다면 Revert 없이 진행할 수 있었을 것입니다. 팔로우 기능의 실시간 동기화도 개선이 필요합니다.
배운 점
전략 패턴을 Vue 컴포넌트에 적용하는 방법을 익혔습니다. 팔로우, 좋아요, 댓글 등 커뮤니티 기능이 가진 복잡성을 체감했으며, 대규모 신규 기능 개발 시 QA에 대응하는 전략을 배웠습니다.