SCRUM-10616, 12163~12206 | 커밋 수: 41개+ | 기간: 2025.01


문제상황

브랜드 커뮤니티 기능을 신규로 도입해야 하는 상황이었습니다. 게시글, 댓글, 프로필 등 복합적인 기능을 함께 구현해야 했습니다.

Before (도메인별 프로필 분산)

장점

  • 각 도메인 독립 개발

고려할 점

  • 프로필 표시 로직 중복
  • 인터페이스 불일치
  • 새 도메인마다 재구현

After (전략패턴 Profile 컴포넌트)

장점

  • 단일 컴포넌트로 4개 도메인 커버
  • displayType만 변경하면 적용

고려할 점

  • 전략 설정 추가 시 config 확장 필요

주요 개선사항

스퀘어 메인, 상세, 게시글 작성 페이지를 구현했습니다. Profile 컴포넌트에 전략패턴을 적용하여 재사용성을 확보했고, 앱 메인의 스퀘어 영역도 함께 구현했습니다. 또한 댓글과 답글 시스템을 기존 인프라와 통합했습니다.

개발 규모
항목비교값 (개)
신규 페이지55 개
신규 컴포넌트1010 개
QA 이슈 해결2020 개
커밋4141 개

정량적 성과

신규 페이지 5개 이상을 구현하고, 컴포넌트 10개 이상을 새로 개발했습니다. QA 과정에서 발견된 이슈 20개 이상을 해결했습니다.

🎨

전략패턴

Profile 컴포넌트 4개 도메인 재사용

💬

댓글 통합

기존 인프라와 스퀘어 댓글 연결

🏪

브랜드 채널

브랜드-고객 소통 채널 확보

비즈니스 임팩트

브랜드와 고객 간의 소통 채널을 확보했습니다. 사용자 참여도가 증가했으며, 커뮤니티를 기반으로 한 마케팅이 가능해졌습니다.

  1. 요구사항 분석
  2. Profile 전략패턴 설계
  3. 5개 페이지 구현
  4. QA 20개+ 해결
  5. 런칭

기술적 구현 상세

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에 대응하는 전략을 배웠습니다.