SCRUM-14442 | 커밋 수: 37개 | 기간: 2025.07


문제상황

신규 회원을 대상으로 한 프로모션 배너가 존재하지 않아 유입 후 전환을 유도할 수단이 없었습니다. 또한 테스트 환경이 불안정하여 기존 fireEvent 기반 테스트가 실제 사용자 행위와 괴리가 있었고, 테스트 유틸리티가 중복되어 있는 상태였습니다.

주요 개선사항

NewMemberBanner 컴포넌트를 신규 구현했습니다. 쿠키 기반 노출 제어 기능을 추가하여 "오늘 하루 닫기"를 지원하고, dayjs를 이용한 날짜 기반 조건부 노출 로직을 적용했습니다. 테스트 측면에서는 @testing-library/user-event 패키지를 도입하여 실제 사용자 행위를 시뮬레이션하도록 개선했습니다.

정량적 성과

NewMemberBanner에 대한 테스트 코드를 240줄 이상 작성했습니다. 로그인 상태와 날짜 기반 조건부 렌더링을 구현하여 4가지 케이스를 커버했으며, 테스트 유틸리티인 renderComponent를 리팩토링하여 재사용성을 확보했습니다.

테스트 개선 지표
항목비교값 (개)
테스트 코드(줄)240240 개
커밋 수3737 개
조건부 렌더링 케이스44 개

비즈니스 임팩트

신규 회원 전환율 향상을 기대할 수 있게 되었습니다. 웰컴 기프트 프로모션을 배너를 통해 지원하게 되었고, user-event 도입으로 테스트 안정성이 전반적으로 향상되었습니다.


기술적 구현 상세

AS-IS (fireEvent)

장점

  • 간단한 이벤트 시뮬레이션

고려할 점

  • 실제 사용자 행위와 차이
  • 불안정한 테스트
  • 테스트 유틸리티 중복

TO-BE (user-event)

장점

  • 실제 사용자 행위 시뮬레이션
  • 안정적 테스트
  • renderComponent 유틸리티 통합

고려할 점

  • setup 필요

AS-IS

기존에는 신규 회원 대상 프로모션 배너가 없었고, 테스트 환경도 불안정했습니다.

문제점:

신규 회원 유입 후 전환을 유도할 수단이 부재했습니다. fireEvent 기반 테스트는 실제 사용자 행위와 차이가 있어 불안정했고, 테스트 유틸리티가 여러 곳에 중복되어 있었습니다.

TO-BE

NewMemberBanner 컴포넌트를 구현하고, user-event 기반 테스트로 안정성을 확보했습니다.

  1. 로그인 확인
  2. 신규회원 확인
  3. 프로모션 기간 확인
  4. 쿠키(오늘닫기) 확인
  5. 배너 노출

핵심 구현

1. NewMemberBanner 컴포넌트

<!-- components/layout/NewMemberBanner.vue -->
<script setup lang="ts">
import { useBenefitStore } from '@/stores/modules/benefit';
import { useCookies } from '@/hooks/common';
import dayjs from 'dayjs';

const benefitStore = useBenefitStore();
const { getCookie, setCookie } = useCookies();

const props = defineProps<{
  promotionEndDate: string;
}>();

// 배너 노출 조건: 로그인 + 신규회원 + 프로모션 기간 + 오늘 닫기 여부
const shouldShowBanner = computed(() => {
  // 1. 로그인 상태 확인
  // 2. 신규 회원 여부 확인
  // 3. 프로모션 기간 확인 (dayjs 활용)
  // 4. 오늘 닫기 쿠키 확인
});

const closeBanner = () => {
  // 오늘 하루 닫기
  setCookie('newMemberBannerClosed', dayjs().format('YYYY-MM-DD'), {
    expires: 1, // 1일
  });
};
</script>

<template>
  <div v-if="shouldShowBanner" class="new-member-banner">
    <div class="banner-content">
      <p>웰컴 기프트를 받아가세요!</p>
      <button @click="navigateToWelcomeBenefit">
        혜택 받기
      </button>
    </div>
    <button class="close-button" @click="closeBanner">
      닫기
    </button>
  </div>
</template>

2. 테스트 코드 (user-event 기반)

// test/integration/components/layout/NewMemberBanner.test.ts
import { render, screen } from '@testing-library/vue';
import userEvent from '@testing-library/user-event';
import NewMemberBanner from '@/components/layout/NewMemberBanner.vue';
import { createTestingPinia } from '@pinia/testing';
import dayjs from 'dayjs';

describe('NewMemberBanner', () => {
  const user = userEvent.setup();

  const renderBanner = (options = {}) => {
    return render(NewMemberBanner, {
      props: {
        promotionEndDate: dayjs().add(7, 'day').format('YYYY-MM-DD'),
        ...options.props,
      },
      global: {
        plugins: [
          createTestingPinia({
            initialState: {
              benefit: {
                isLoggedIn: true,
                isNewMember: true,
                ...options.initialState?.benefit,
              },
            },
          }),
        ],
      },
    });
  };

  describe('배너 노출 조건', () => {
    it('로그인한 신규 회원에게 배너가 표시된다', () => {
      // Given & When
      renderBanner();

      // Then
      expect(screen.getByText('웰컴 기프트를 받아가세요!')).toBeInTheDocument();
    });

    it('비로그인 상태에서는 배너가 표시되지 않는다', () => {
      // Given & When
      renderBanner({
        initialState: {
          benefit: { isLoggedIn: false, isNewMember: true },
        },
      });

      // Then
      expect(screen.queryByText('웰컴 기프트를 받아가세요!')).not.toBeInTheDocument();
    });

    it('기존 회원에게는 배너가 표시되지 않는다', () => {
      // Given & When
      renderBanner({
        initialState: {
          benefit: { isLoggedIn: true, isNewMember: false },
        },
      });

      // Then
      expect(screen.queryByText('웰컴 기프트를 받아가세요!')).not.toBeInTheDocument();
    });

    it('프로모션 기간이 지나면 배너가 표시되지 않는다', () => {
      // Given & When
      renderBanner({
        props: {
          promotionEndDate: dayjs().subtract(1, 'day').format('YYYY-MM-DD'),
        },
      });

      // Then
      expect(screen.queryByText('웰컴 기프트를 받아가세요!')).not.toBeInTheDocument();
    });
  });

  describe('배너 닫기', () => {
    it('닫기 버튼 클릭 시 배너가 사라진다', async () => {
      // Given
      renderBanner();

      // When
      await user.click(screen.getByText('닫기'));

      // Then
      expect(screen.queryByText('웰컴 기프트를 받아가세요!')).not.toBeInTheDocument();
    });
  });
});

3. 테스트 유틸리티 리팩토링

// test/setup-mocks/util.ts
import { render, type RenderOptions } from '@testing-library/vue';
import { createTestingPinia } from '@pinia/testing';
import { createRouter, createMemoryHistory } from 'vue-router';

interface RenderComponentOptions extends RenderOptions {
  initialState?: Record<string, unknown>;
  route?: string;
}

export const renderComponent = (
  component: Component,
  options: RenderComponentOptions = {}
) => {
  const { initialState, route = '/', ...renderOptions } = options;

  const router = createRouter({
    history: createMemoryHistory(),
    routes: [{ path: '/:pathMatch(.*)*', component: { template: '<div />' } }],
  });
  router.push(route);

  return render(component, {
    ...renderOptions,
    global: {
      plugins: [
        createTestingPinia({ initialState }),
        router,
      ],
      ...renderOptions.global,
    },
  });
};

4. Layout에서 배너 조건부 렌더링

<!-- components/layout/Layout.vue -->
<script setup lang="ts">
import { NewMemberBanner } from '@/components/layout';
import { useRoute } from 'vue-router';

const route = useRoute();

// 홈 페이지에서만 배너 표시
const showNewMemberBanner = computed(() => {
  return route.path === '/home' || route.path === '/';
});
</script>

<template>
  <div class="layout">
    <Header />
    <NewMemberBanner
      v-if="showNewMemberBanner"
      promotion-end-date="2025-12-31"
    />
    <main>
      <slot />
    </main>
    <Footer />
  </div>
</template>


회고

잘한 점

user-event를 도입하여 테스트 안정성을 크게 향상시켰습니다. dayjs를 활용한 날짜 기반 조건부 렌더링을 구현했고, 테스트 유틸리티인 renderComponent를 리팩토링하여 재사용성을 확보했습니다.

개선할 점

초기에 '웰컴 키프트'라는 오타를 발견하는 것이 지연되었습니다. 또한 홈 페이지 외 다른 페이지에서의 노출 조건에 대한 검토가 필요합니다.

배운 점

@testing-library/user-event가 fireEvent 대비 실제 사용자 행위에 더 가깝게 동작한다는 것을 확인했습니다. 쿠키 기반 노출 제어 패턴과 조건부 렌더링 테스트 방법을 체득했습니다.