chan9yu 님의 상세페이지[5팀 여찬규] Chapter 3-2. 프론트엔드 테스트 코드

8주차 과제 체크포인트

기본 과제

필수

  • 반복 유형 선택
    • 일정 생성 또는 수정 시 반복 유형을 선택할 수 있다.
    • 반복 유형은 다음과 같다: 매일, 매주, 매월, 매년
      • 31일에 매월을 선택한다면 -> 매월 마지막이 아닌, 31일에만 생성하세요.
      • 윤년 29일에 매년을 선택한다면 -> 29일에만 생성하세요!
  • 반복 일정 표시
    • 캘린더 뷰에서 반복 일정을 시각적으로 구분하여 표시한다.
      • 아이콘을 넣든 태그를 넣든 자유롭게 해보세요!
  • 반복 종료
    • 반복 종료 조건을 지정할 수 있다.
    • 옵션: 특정 날짜까지, 특정 횟수만큼, 또는 종료 없음 (예제 특성상, 2025-06-30까지)
  • 반복 일정 단일 수정
    • 반복일정을 수정하면 단일 일정으로 변경됩니다.
    • 반복일정 아이콘도 사라집니다.
  • 반복 일정 단일 삭제
    • 반복일정을 삭제하면 해당 일정만 삭제합니다.

선택

  • 반복 간격 설정
    • 각 반복 유형에 대해 간격을 설정할 수 있다.
    • 예: 2일마다, 3주마다, 2개월마다 등
  • 예외 날짜 처리:
    • 반복 일정 중 특정 날짜를 제외할 수 있다.
    • 반복 일정 중 특정 날짜의 일정을 수정할 수 있다.
  • 요일 지정 (주간 반복의 경우):
    • 주간 반복 시 특정 요일을 선택할 수 있다.
  • 월간 반복 옵션:
    • 매월 특정 날짜에 반복되도록 설정할 수 있다.
    • 매월 특정 순서의 요일에 반복되도록 설정할 수 있다.
  • 반복 일정 전체 수정 및 삭제
    • 반복 일정의 모든 일정을 수정할 수 있다.
    • 반복 일정의 모든 일정을 삭제할 수 있다.

심화 과제

  • 이 앱에 적합한 테스트 전략을 만들었나요?

각 팀원들의 테스트 전략은?

페어 1팀은 두 그룹으로 나누어서 각자 전략을 공유하고 이야기를 하면서 최적의 테스트 전략을 정했습니다.

이지훈

  • 전략: 테스트 트로피 전략
  • 핵심 관점: 사용자 관점에서의 정상 동작 확인이 핵심
  • 접근법: 통합 테스트 중심으로 사용자 시나리오 검증, 훅과 같이 데이터와 상태를 다루는 로직은 단위 테스트로 보완

이은지

  • 전략: 테스트 트로피 전략 + Outside-in TDD
  • 핵심 관점: 오버엔지니어링 방지와 큰 그림부터 접근
  • 접근법: E2E 테스트 → 통합 테스트 → 단위 테스트 → 구현 순서로 사용자 시나리오 위주의 통합테스트부터 시작해 점진적으로 구현 범위 축소
  • 비율: 단위 20-30%, 통합 50-60%, E2E 5-15%
  • 특징: 중요한 단위 로직들은 통합 테스트를 통해 간접 검증 가능

허정석

  • 전략: 다이아몬드 형 (단위 테스트 중심)
  • 핵심 관점: 빠른 피드백과 비용 효율성 중시
  • 접근법: 단위 테스트로 빠른 버그 감지 (UI 컴포넌트, 유틸 함수, 상태 관리), 통합 테스트로 실제 환경과 유사한 검증 (MSW 활용), E2E는 핵심 유저 플로우만 선별적 적용
  • 비율: 단위 60-70%, 통합 20-30%, E2E 5-10%

주산들

  • 전략: 단위 테스트 중심 + 트로피 (TDD 개발 방식 고려)
  • 핵심 관점: TDD 개발에 적합한 점진적 접근
  • 접근법: 작은 유닛 테스트부터 시작해 점진적으로 비용이 큰 테스트로 확장, Green Zone 진입 후 통합 테스트로 검증, 과제의 단일 기능 특성상 유닛 테스트 접근이 용이
  • 순서: 유닛 → 통합 → E2E

윤영서

  • 전략: 테스트 트로피 전략
  • 핵심 관점: 비용 대비 신뢰도의 균형점 추구
  • 접근법: 통합 테스트가 핵심으로 실제 유저 플로우와 흡사하면서도 구현 변경에 영향 적음, 단위 테스트는 저렴하지만 리팩토링 시 취약성 존재, 리액트 컴포넌트 간 통합의 중요성 강조
  • 참고 자료: Kent C. Dodds의 Testing Trophy 개념

여찬규

  • 전략: 단위 테스트 중심의 피라미드 구조
  • 핵심 관점: 개발 속도와 비용 효율성 우선
  • 접근법: 빠르고 저렴한 단위 테스트 중심, 느리고 비싼 E2E 테스트는 최소화
  • 비율: 단위 70%, 통합 20%, E2E 10%

합의된 테스트 전략과 그 이유는 무엇인가요?

결론은 테스트 트로피 전략을 채택하게 되었습니다.

그 이유는

  1. 실용적 균형점: 비용 대비 효과를 고려했을 때 통합 테스트가 가장 실용적

    • 사용자 관점의 테스트를 통해 실제 동작 확인 가능
    • 구현 세부사항 변경에 상대적으로 덜 민감
  2. 리액트 생태계 특성: 컴포넌트 간 상호작용과 통합이 중요한 프론트엔드 특성 고려

    • 개별 컴포넌트보다는 화면 단위의 사용자 시나리오가 더 중요
    • React Testing Library 등을 활용한 통합 테스트가 효과적
  3. 오버엔지니어링 방지: 과도한 단위 테스트로 인한 비효율성 방지

    • 구현이 확정되지 않은 초기 단계에서 과도한 단위 테스트는 비효율적
    • 통합 테스트를 통해 여러 단위 로직을 간접적으로 검증 가능
  4. 유지보수성: 리팩토링과 구현 변경에 대한 안정성 확보

    • 인터페이스가 동일하면 내부 구현 변경에 영향을 받지 않음
    • 하나의 테스트로 여러 기능을 동시에 검증 가능

적용 방향으로는

  • 통합 테스트 중심: 사용자 시나리오와 주요 플로우 검증에 집중
  • 선별적 단위 테스트: 복잡한 비즈니스 로직, 유틸 함수, 훅 등 핵심 로직만 선별
  • 최소한의 E2E: 가장 중요한 사용자 여정만 선별하여 적용

이러한 전략을 통해 개발 효율성과 테스트 신뢰성의 최적 균형점을 찾고자 했습니다

추가로 작성된 테스트 코드는 어떤 것들이 있나요?

기본과제에서 진행한 테스트코드는 basic.*.spec.ts로 새로만들어 진행했고, 심화과제에서 진행한 테스트 코드는 advanced.*.spec.ts에서 진행했습니다

단위 테스트

  • src/__tests__/unit/advanced.repeatUtils.spec.ts: 윤년 처리, 월말 날짜 조정, 반복 일정 메타데이터 생성 등 핵심 비즈니스 로직 테스트

통합 테스트

  • src/__tests__/integration/advanced.integration.spec.tsx: 애플리케이션 전체 렌더링, 데이터 로딩 완료 확인, 에러 상태 처리, 사용자 인터랙션 등 통합 시나리오 검증

E2E 테스트

  • src/__tests__/e2e/calendar-crud.spec.ts: 기본 UI 요소 확인, 폼 입력 기능, 앱 기본 동작, 일정 생성 등 실제 사용자 플로우 테스트
  • 일정 겹침 경고 팝업 자동 처리 로직 포함

https://github.com/user-attachments/assets/8d3496fd-4656-4640-962f-785658ec59ce


과제 셀프회고

사실 지금까지는 테스트에 대해 별로 생각이 없었던 것 같아요. 8주차 과제를 통해 여러 테스트 케이스를 작성해보고 TDD 방법론과 테스트 전략을 결정하는 과정에서 많은 부분을 배울 수 있어서 좋았습니다!

기술적 성장

Red-Green-Refactor 사이클을 경험하며 테스트 우선 개발이 왜 좋은지 알게 되었습니다. 구현 전에 요구사항을 테스트로 명세화하는 과정을 통해 설계 품질이 향상되었고, 리팩토링할 때도 훨씬 편해졌어요.

그리고 실제 사용자 환경에서의 브라우저 자동화 테스트를 구축해볼 수 있었습니다. 팝업 처리, 폼 입력, 동적 콘텐츠 대기 등 실제 시나리오를 다루는 UI 인터랙션 테스트를 구현했습니다. 또한 CI/CD를 고려한 headless 브라우저 환경 설정까지 경험하며 실무에서 사용할 수 있는 테스트 환경에 대해 이해할 수 있었습니다.

코드 품질

통합 테스트에서는 지난 주차에 했던 내용을 참고해서 setup 함수을 구현해 재사용 가능한 테스트 환경을 만들었습니다. 실제 애플리케이션과 동일한 Context를 제공해서 일관된 테스트 실행 환경을 만들 수 있었습니다.

E2E 테스트에서는 일정 겹침 경고 팝업이 나타나는 경우와 나타나지 않는 경우를 모두 고려한 안정적인 예외 처리 로직을 구현했습니다.

테스트 트로피 전략을 실제로 적용해보면서 통합 테스트에 집중하되, 핵심 비즈니스 로직에 대해서만 선별적으로 단위 테스트를 작성하는 방식으로 진행했습니다. 다만 RepeatHelper 클래스를 보면 반복 일정 생성, 날짜 계산 로직(일/주/월/년별), 윤년 처리, 월말 날짜 조정 등 여러 책임이 한 곳에 집중되어 있습니다. 각 반복 타입별로 별도 클래스를 만들거나 패턴을 적용해서 책임을 분산시키면 테스트하기도 쉽고 유지보수도 편할 것 같아요.

class RepeatHelper {
  private static readonly DEFAULT_END_DATE = '2025-10-30';

  public createRepeatEvents(event: Event): Event[] {
    if (event.repeat.type === 'none') {
      return [event];
    }

    const startDate = new Date(event.date);
    const endDate = event.repeat.endDate
      ? new Date(event.repeat.endDate)
      : new Date(RepeatHelper.DEFAULT_END_DATE);

    if (endDate < startDate) {
      return [event];
    }

    const events: Event[] = [{ ...event, isRecurring: true }];
    const originalDay = startDate.getDate();

    let currentDate = new Date(startDate);
    let eventCount = 1;

    const collectNext = (date: Date) => {
      const nextDate = this.getNextDate(date, event.repeat, originalDay);

      if (nextDate > endDate) return;

      events.push({
        ...event,
        id: `${event.id}-${eventCount}`,
        date: formatDate(nextDate),
        originalId: event.id,
        isRecurring: true,
      });

      eventCount = eventCount + 1;
      collectNext(nextDate);
    };

    collectNext(currentDate);

    return events;
  }

  private getNextDate(currentDate: Date, repeat: Event['repeat'], originalDay: number): Date {
    switch (repeat.type) {
      case 'daily':
        return this.addDays(currentDate, repeat.interval);
      case 'weekly':
        return this.addDays(currentDate, 7 * repeat.interval);
      case 'monthly':
        return this.addMonths(currentDate, repeat.interval, originalDay);
      case 'yearly':
        return this.addYears(currentDate, repeat.interval, originalDay);
      default:
        return currentDate;
    }
  }

  private addDays(date: Date, days: number): Date {
    const result = new Date(date);
    result.setDate(result.getDate() + days);

    return result;
  }

  private addMonths(date: Date, months: number, originalDay: number): Date {
    const result = new Date(date);
    const newYear = result.getFullYear();
    const newMonth = result.getMonth() + months;

    const lastDayOfTargetMonth = new Date(newYear, newMonth + 1, 0).getDate();
    const targetDay = Math.min(originalDay, lastDayOfTargetMonth);
    result.setFullYear(newYear, newMonth, targetDay);

    return result;
  }

  private addYears(date: Date, years: number, originalDay: number): Date {
    const result = new Date(date);
    const originalMonth = result.getMonth();

    result.setFullYear(result.getFullYear() + years);
    result.setMonth(originalMonth);

    if (originalMonth === 1 && originalDay === 29) {
      const isLeapYear = this.isLeapYear(result.getFullYear());
      result.setDate(isLeapYear ? 29 : 28);
    } else {
      result.setDate(originalDay);
    }

    return result;
  }

  private isLeapYear(year: number) {
    return year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0);
  }
}

export const repeatHelper = new RepeatHelper();

학습 효과 분석

TDD의 실질적 이점도 직접 느낄 수 있었습니다. 요구사항을 명확히 하는 과정에서 설계 품질이 좋아지고, 리팩토링할 때도 편하게 진행했던 거 같습니다 디버깅 시간도 줄어들고 회귀 버그 방지 효과까지 체험할 수 있었습니다.

아직 테스트 관련해서 더 학습이 필요한 영역들이 많다는 것도 알게 되었습니다. 그래도 테스트 전략 수립, E2E 테스트 도입 등 실무에 도움이 될 정도로 경험을 쌓을 수 있었습니다.

과제 피드백

TDD 프로세스를 적용할 수 있는 구조가 너무 좋았습니다. 테스트 작성부터 실패 확인, 구현, 통과, 리팩토링까지의 전체 사이클을 경험하면서 각 단계별 목표와 의의를 명확히 이해할 수 있었던 거 같아요!

팀 심화 과제도 재미있었습니다 개인별 테스트 전략을 검토한 후 팀 합의를 결정하는 과정은 실무에서의 기술적 의사결정 과정과 비슷한 경험이었습니다

리뷰 받고 싶은 내용

현재 구현한 테스트 구성에서 테스트 트로피 전략에 맞게 했는지 아직 헷갈립니다 특히 단위 테스트가 생각보다 많은 편이라 생각이 드는데 테스트 트로피 전략에 맞게 구현을 잘 한건지? 그리고 이 중에서 통합 테스트로 대체 가능한 부분이 있는지가 궁금합니다!

E2E 테스트에서 일정 겹침 경고 팝업 처리 로직에 대해서도 궁금한 점이 있습니다. 상황에 따라 팝업이 나타날 때도 있고 안 나타날 때도 있는데 (최초 1회에 진행했을 때는 팝업 미노출, 그 후에 이미 같은 데이터로 일정을 저장을 시도하기 때문에 일정 겹침 경고 팝업이 노출하게 됨), 이걸 try-catch로 처리하고 있습니다. 이런 방식이 괜찮은 접근인지 궁금합니다.

// calendar-crud.spec.ts
const handleOverlapWarning = async (page: Page) => {
  try {
    const continueButton = page
      .getByRole('button', { name: /계속 진행|계속|continue|확인|yes/i })
      .first();

    if (await continueButton.isVisible({ timeout: 2000 })) {
      await continueButton.click();
      await page.waitForTimeout(500);
    }
  } catch {
    // 팝업이 없으면 무시하고 계속 진행
  }
};

과제 피드백

찬규님 고생하셨어요! 기능도 다 잘 동작하고 설정에 따른 유틸 함수들도 잘 만들어주셨네요! 당연히 e2e 테스트에 대한 부분도 잘 해주셨구요. 다만, 저 코드가 어떻게 들어가게 되었는지 모르겠지만 테스트 코드내에서 catch등으로 에러를 처리하는건 엄청 특이한 케이스가 아니면 불필요하거든요! 저렇게 예외처리를 두는 것보다 보통의 경우 에러를 해결하는게 필요하니 그 케이스에 해당하는지 고민해봐도 좋을것 같습니다. (아 이부분을 질문을 주셨군요. 추가로 해당 상황에 맞춰 작성해보면 해당 케이스별로 환경을 구성하는게 필요합니다. 명확한 노출 조건이 있다면 상황에 따라 함수를 다르게 처리하는게 나은것 같아요. 어쨋든 catch 처리 방식은 좋은 방식은 아닙니다!)

그리고 e2e도 wairfor를 작성하는것보다 명시적인 처리가 있는게 좋으니까 그 방식도 고민해보세요! 그리고 반복일정에 대한 테스트도 테스트 케이스에 맞춰 코드들이 생성되는데 TDD를 하다보면 구현 집약적인 테스트들이 많이 생길수밖에 없거든요. 이 부분에 대해서도 좀 더 보강하는 형태로 좀 느슨하게 개발이 된것 같은 느낌이긴 하네요 ㅎㅎ

질문 주신 부분 답변 드려볼게요!

현재 구현한 테스트 구성에서 테스트 트로피 전략에 맞게 했는지 아직 헷갈립니다 특히 단위 테스트가 생각보다 많은 편이라 생각이 드는데 테스트 트로피 전략에 맞게 구현을 잘 한건지? 그리고 이 중에서 통합 테스트로 대체 가능한 부분이 있는지가 궁금합니다!

딱 명확하게 나눠지기는 어려울거에요. 이미 강제로 작성되어있는 테스트들도 많이 있고 앱의 특성상 기능 자체가 많이 없을수도 있구요. 지금의 단계에서는 저라면 단위테스트를 꽤 많이 제거하고 통합테스트를 더 작성할 수 있는 부분들이 있을 것 같아요. 날짜 로직들이 엄청 복잡한 케이스들이 아니면 통합테스트에서 충분히 커버가 가능할 것들도 있는 것 같구요.

고생하셨고 앞으로 즐거운 테스트 생활하시길 바랍니다! 계속 시도해보세요~