Skip to content

[9팀 임두현] Chapter 3-2. 프런트엔드 테스트 코드 #25

Open
ldhldh07 wants to merge 60 commits intohanghae-plus:mainfrom
ldhldh07:main
Open

[9팀 임두현] Chapter 3-2. 프런트엔드 테스트 코드 #25
ldhldh07 wants to merge 60 commits intohanghae-plus:mainfrom
ldhldh07:main

Conversation

@ldhldh07
Copy link

@ldhldh07 ldhldh07 commented Aug 27, 2025

8주차 과제 체크포인트

기본 과제

필수

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

선택

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

심화 과제

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

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

저희팀은 팀원별로 테스트 전략에 대한 의견을 주고 받기 보다는 피그잼을 통해 함께 브레인스토밍하며 전략을 좁혀 나갔습니다.

따라서, 하나의 PR에 테스트 전략을 수립하여 모두 페어코딩을 진행했습니다.
결론적으로 9팀은 심화과제 PR부분과 코드 전략을 함께 작성하여 그 내용이 같습니다.

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

저희 9팀은 TDD 기반으로 반복 일정 기능을 구현하면서 윤년, 월말 날짜 등 다양한 엣지 케이스를 마주할 수 있다고 판단했습니다.
따라서 테스트 전략의 목표를 엣지 케이스를 최소화하는 것으로 잡고, 이를 검증하기 위한 방향을 고민했습니다.

이 과정에서 단순히 “어떻게 테스트할까” 라는 관점에 머무르지 않고,
“어떻게 하면 코드를 테스트하기 좋은 구조로 만들까” 라는 질문에 집중했습니다.
코치님께서도 이 점을 강조해주셨고, 그 조언을 바탕으로 레이어를 먼저 추상화한 뒤,
테스트할 부분과 그렇지 않은 부분을 의도적으로 구분하여 선택적으로 검증하는 전략을 수립했습니다.

아키텍처 정의하기

[기존 구조]

  • 역할 기반 디렉토리 구조
  • 유닛 테스트 비중이 높음 (피라미드 구성)

뿐만 아니라 utils 디렉토리는 아래와 같은 서로 다른 역할과 관심사가 혼재하고 있었습니다.

  • repeatEventUtils : 반복 일정의 다음 날짜를 계산하는 순수 계산 로직
  • notificationUtils : 사용자에게 알림을 띄우는 UI 관련 사이드 이펙트
  • eventUpdateUtils: 이벤트 관련 도메인 레이어

이처럼 역할이 다른 코드들이 한 폴더에 섞여있어 반복 일정 계산 로직을 구별하기 어려웠습니다.


[레이어 도출]

  • 반복 일정의 엣지 케이스를 판단하는 핵심 로직은 대부분 repeatEventUtils 같은 순수 함수 안에 있다고 판단했습니다.
  • 이 함수들을 사이드 이펙트가 있는 함수로부터 분리하여 레이어를 나눈다면 테스트가 쉬워질 것이라고 의견을 모았습니다. 이 부분을 도메인 유틸 레이어(repeat) 로 정의하고 기존에 작성한 유닛 테스트를 활용하기로 했습니다.
  • 전략적 선택과 집중
    • 집중: "도메인 레이어" 에 유닛 테스트를 집중하기로 했습니다.
    • 선택: 도메인 로직 테스트에 집중하는 대신, 변경이 잦고 테스트 비용이 비싼 UI 관련 테스트의 비중은 낮추기로 결정했습니다.
    • 가정: msw 핸들러와 같은 라이브러리나 Date 객체, Javascript 내장 함수 관련 로직은 자체의 동작을 신뢰하기로 가정했습니다. 대신 API 요청/응답에 따라 앱이 어떻게 반응하는지를 검증하는 통합 테스트를 추가하기로 했습니다.

[새로운 레이어 구조]
관심사의 분리와 테스트 용이성이라는 두 가지 키워드로 다음과 같이 레이어를 세분화하고자 했습니다.

1. utils

repeat이라는 기능(feature)단위로 디렉토리를 묶고, 그 안에서 역할을 세분화 했습니다.

📦repeat
 ┣ 📜actions.ts       # 상태를 변경하는 로직 (도메인)
 ┣ 📜constants.ts     # 기능 관련 상수
 ┣ 📜formats.ts       # 문자열 포맷팅 등 (도메인/UI 보조)
 ┗ 📜helpers.ts       # 핵심 계산 로직 (순수 함수, 도메인)

2. test

📦repeat
 ┣ 📂e2e
 ┣ 📂integration
 ┗ 📂unit
 ┃ ┣ 📜actions.spec.ts
 ┃ ┗ 📜helpers.spec.ts

레이어 별 테스트 전략 수립

아키텍처가 명확하게 정의되니 코치님 말씀대로 각 디렉토리의 역할에 맞는 테스트 전략을 구체화 할 수 있었습니다.

반복 일정 생성에서는 기준에 맞는 일정 생성 및 엣지 케이스 판단이 중요합니다. 반면 UI 구현의 비중은 적습니다.
이를 테스트 전략에도 반영했습니다. 일정 생성과 기준에 맞는 일정일지 판단하는 함수의 유닛 테스트의 비중을 높였습니다.
UI 구현을 검증하는 통합 테스트와 e2e테스트는 최소한의 사용성을 보장하는 정도로 최소화했습니다.

저희는 '전략적 선택과 집중' 원칙에 따라 테스트의 종류와 범위를 다음과 같이 결정했습니다.

[Unit Test] - 도메인 로직 (Domain Logic)

  • 대상: utils/repeat/helpers.ts, utils/repeat/actions.ts
  • 전략: 테스트의 절반 이상을 집중합니다. helpers의 순수 계산 함수들을 테스트하며 엣지 케이스를 검증하고, actions의 상태 변경 로직이 의도대로 동작하는지 확인합니다.
  • 이유: 반복 일정을 생성함에 있어서 윤년, 월별 일수와 같은 예외적 케이스를 고려하는 것은 큰 비중을 차지합니다. 그에 따라 테스트의 비중도 가장 높습니다.

[Integration Test] - 계층 간 통합 (Layer Integration)

  • 대상: UI 컴포넌트, API 레이어, 도메인 로직의 통합 동작
  • 전략: MSW를 활용하여 API의 성공, 실패, 비정상 응답 등 다양한 시나리오에 따라 UI 상태와 도메인 로직이 올바르게 연동되어 동작하는지 검증합니다.
  • 이유: "API 요청/응답에 따라 앱이 어떻게 반응하는가" 를 확인하는 것이 목적입니다.
    실제 서버나 fetch 라이브러리를 테스트 하는 것이 아니라, 각 레이어들이 올바르게 통합되어 데이터의 흐름을 잘 처리하는지 확인하는 목적입니다.

[E2E Test] - 사용자 시나리오 (User Scenario)

  • 대상: 반복 일정 폼 등록, 삭제
  • 전략: "사용자가 반복 일정을 생성하고 저장할 수 있다" 와 같은 중요한 시나리오 위주로 검증합니다.
  • 이유: UI는 변경이 잦고 테스트 유지보수 비용이 아이콘 및 이벤트 리스트를 추가하는 UI구현의 중요도가 적습니다. 따라서 사용자에게 최종적으로 전달되는 핵심 기능이 문제없이 동작하는지만을 보장하기로 "선택" 했습니다.

과제 셀프회고

이번 과제에서는 테스트 주도 개발을 경험했습니다.

기술적 성장

MUI를 쓰면 DOM이 복잡해져 RTL에서 요소 찾기가 어렵다

지난 주차때 고생했던 것이 리액트 테스팅 라이브러리를 사용하여 ui요소를 가져오는 부분이었습니다.
특히 mui와 같은 ui라이브러리를 사용할 때 더 어려웠습니다.

최근 feconf를 다녀왔습니다. 그중 wai-aria와 웹 접근성에 대한 세션을 들었습니다.
특히 테스트 코드 과제를 한 직후라 관심이 가는 내용이 있었습니다.
aria-label을 통해 RTL 사용시 권장되는 getByRole를 유도할 수 있다는 내용이었습니다.

    <IconButton
      aria-label={`Delete event ${event.date}`}
      onClick={() => deleteEvent(event.id)}
    >
      <Delete data-testid="DeleteIcon" aria-hidden={false} role="img" />
    </IconButton>

테스트

screen.getByRole('button', { name: `일정 삭제 ${date}` }).click();

이번 과제에 관련 내용을 적극적으로 적용하고자 했습니다.
aria-label로 버튼 자체에 접근 가능한 이름을 제공하고, 다른 aria 관련 속성들로 유연한 태그 접근이 가능했습니다.

특히 위 방식의 코드는 날짜를 동적으로 라벨에 넣었습니다.
이 경우 상위 컴포넌트의 데이터(날짜)와 관련된걸 찾아서 그 내에서 한번더 찾아야 할 필요없이 직접적으로 관리 가능하단 점에서 매력적이었습니다.

코드 품질

테스트 주도 개발의 Red-Green-Refactor 구조와 그 구조의 이유를 이해했습니다.
특히 이 구조는 테스트 코드의 장점과도 연결됩니다.

처음에는 실패하는 테스트를 만든다는 것이 무엇을 먼저 해야할지 감이 안왔습니다.

특히 과제에서 제시된 요구사항이 의도된 애매한 요구사항이었습니다.
그래서 먼저 명확한 요구사항으로 세분화하고자 했습니다.

주 단위로 반복일정을 만든다

  • 하나의 날짜를 기준으로 주 단위의 다음 날짜를 찾는다
  • 날짜의 유효성을 판단한다
  • 판단한 날짜들 각각에 일정을 생성한다
   it('2025-10-15부터 1주 반복이면 10/22가 나와야 한다', () => {  ...  });
   it('excludedDates에 2025-10-22가 있으면 10/22는 제외되어야 한다', () => {  ...  });

해당 테스트를 위해 필요한 동사까지 만든 후 그 동사가 정의하는 동작은 비워뒀습니다.
테스트를 해결하는데 집중하고 기본적인 동사에 들어갈 동작에 대해서는 의식하지 않습니다.

추상적인 동작 구현이 아닌 테스트 코드 통과를 위한 코드를 작성합니다. 그 경우 서비스를 작은 단위에 쪼개서 볼 수 있습니다.
그리고 그 단위들이 다 합쳐져서 커다란 기능과 애플리케이션을 요구사항에 맞게 만들어냅니다.

refactor 단계의 중요성도 느꼈습니다.

Red-Green-Refactor의 관계 속에서 리팩토링의 중요성은 상대적으로 덜 드러납니다.
자칫 그냥 추가로 이뤄지는 동작으로 판단될 수 있습니다.
하지만 테스트 코드와 그 추상화 단계의 중요성을 고려했을 때 테스트에 맞는 단위로 쪼개고 분리하는 작업 또한 중요합니다.

import { expandWeeklyInstances } from './helpers';

export const generateInstances = (event, rangeStart, rangeEnd) => {
  if (event.repeat.type === 'weekly') {
    return expandWeeklyInstances(event, rangeStart, rangeEnd);
  }
  // daily/monthly/yearly는 분류 후 각 함수로 추상화
  return [event];
};

리팩토링을 통해 테스트를 해야하는 함수와 아닌 함수를 분리할 수 있습니다.
그 뿐 아니라 리팩토링 과정을 두고 Green 단계에서는 테스트 통과에 집중하는 코드를 생성해내는 것이 생산성이 좋았습니다.

기술적인 문서로서의 테스트코드

테스트 코드는 요구사항을 개발자의 언어로 대치하여 제시합니다.
테스트 주도 개발을 통해 개발을 진행하면서 요구사항에 부합하는지 반복해서 검증할 수 있습니다.
또한 테스트를 작성하면서 요구사항에 대해 인지하고 개발하면서 그 요구사항을 구현하는 방향성을 유지할 수 있습니다.

개발자는 코더가 아니라 사용자 경험을 향상시키고자 하는 서비스 제공자로서의 마음을 가져야 합니다.

테스트 주도 개발은 그 목적과 부합합니다.

describe('generateInstances - weekly', () => {
  it('주간 범위 내에서만 weekly(같은 요일) 인스턴스를 생성한다', () => {
    const base = makeEvent({
      id: 'e5',
      title: 'Weekly',
      date: '2025-01-01', // 수요일
      repeat: { type: 'weekly', interval: 1, endDate: '2025-01-30' },
    });

    const rangeStart = new Date('2025-01-05T00:00:00Z');
    const rangeEnd = new Date('2025-01-20T00:00:00Z');

    const instances = generateInstances(base, rangeStart, rangeEnd);
    expect(instances.map((e) => e.date)).toEqual(['2025-01-08', '2025-01-15']);
  });
});

describe('generateInstances - monthly', () => {
  it('31일 시작 monthly는 31일이 없는 달(Feb)을 건너뛰고 3월 31일만 생성한다', () => {
    const base = makeEvent({
      id: 'e6',
      title: 'Monthly-31',
      date: '2025-01-31',
      repeat: { type: 'monthly', interval: 1, endDate: '2025-03-31' },
    });

    const rangeStart = new Date('2025-02-01T00:00:00Z');
    const rangeEnd = new Date('2025-03-31T00:00:00Z');

    const instances = generateInstances(base, rangeStart, rangeEnd);
    expect(instances.map((e) => e.date)).toEqual(['2025-03-31']);
  });

위 코드들을 보면 이 기능을 구현하기 위해 어떤 로직이 필요한지
함수들의 진행과 그리고 테스트명으로 알 수 있습니다.

이는 팀간 내용을 공유하거나 전달할 때 유용합니다.
단순히 텍스트로만 공유되는 것이 아니라 개발의 언어로 서술되기 때문에 더욱 컴퓨터적인 사고로 요구사항을 받아들이고 수행할 수 있습니다.

학습 효과 분석

유닛 테스트, 통합 테스트, e2e 테스트에 대한 각각의 역할과 장단점을 인식했습니다.
이는 항해 플러스의 교육과정이 전부 뒷받침되었기에 가능하기도 합니다.

순수 함수와 추상화 레벨에 따른 분류를 인지한 후, 테스트 코드와 TDD를 익혔습니다.
그 흐름속에서 함수의 독립성과 테스트 용이함의 인과관계를 파악했습니다.
테스크 코드의 장점이 무엇이고 그걸 활용하는 맥락에서의 TDD를 이해할 수 있었습니다.

AI로 테스트코드를 잘 작성하는법

현재의 AI를 통해 코딩할 때는 한번에 많은 양이 아닌 요구사항을 세분화하는 것이 효과적입니다.

이런점이 TDD와 AI코딩이 맞아떨어집니다.
테스트의 요구사항을 정립하고 테스트 내의 흐름으로 만들어내는 과정에서 Todo 과제의 세분화가 효과적이고 유저 친화적으로 구성됩니다.

그리고 이를 통과시키도록 AI에게 요구했을 떄 자연스럽게 가장 최적화된 순서로 기능 구현을 진행합니다.

  it('excludedDates에 2025-10-22가 있으면 2025-10-22는 제외되어야 한다', () => {
    ...
  });

  it('endDate가 2025-10-20이면 2025-10-22는 생성되지 않아야 한다', () => {
    ...
  });

  it('월 31일 시작이면 다음 달 불가능한 날짜는 보정되어야 한다', () => {
    ...
  });

특히 경계 지점을 예민하게 구분해서 테스트내 로직내 규칙이 필요한 지점에 적용하는 부분에서는
인지 부하를 압도적으로 절약할 수 있었습니다.

그중 어떤 케이스를 설정해서 시나리오를 적용할지에 대해서는 AI에 온전히 맡기기보다는 어느정도 큰 가닥을 잡아주는 경우 더 퀄리티 높은 테스트 코드를 생성했습니다.

정리

테스트 코드 개발은 어떤 면에서는 일반적이지 않은 개발 방식으로 언급됩니다.
하지만 요구사항을 반영한 제한사항을 정해두고 그 제한을 충족하기 위한 기능을 구현한다는 것은 어찌보면 가장 자연스러운 방식의 개발일 수 있겠다는 생각이 들었습니다.

리뷰 받고 싶은 내용

유닛 테스트 전략

각 테스트는 단일 기능만 검증하도록 구성했습니다. 외부 의존성과 다른 기능이 개입되지 않도록 고립시켜, 실패 원인이 명확히 드러나게 했습니다.

통합 테스트 전략

실제 사용자 흐름을 따라 여러 모듈이 협력하는 과정을 검증하되, 중간 단계는 “체크포인트” 수준으로 최소 핵심만 확인했습니다.
한 번에 모든 것을 검증하려는 거대한 시나리오 대신, 의미 있는 하위 시나리오로 분할하고 각 시나리오에서 사용자 관점의 결과를 점검해 테스트의 안정성을 높였습니다.

하나의 흐름으로 모든 것을 한 번에 테스트하는 방식은 실제 사용자 경험에 가깝다는 장점이 있습니다.
하지만 지나치게 많은 중간 테스팅을 하나의 테스팅 유닛에 담을 경우 불안정하다 느꼈습니다.
그래서 통합 테스트는 흐름의 목표 달성 여부와 핵심 중간 상태만 검증하고, 세부 로직은 유닛 테스트로 분리했습니다.

정리

유닛/통합의 경계가 적절한지, 통합 테스트에서 확인하는 중간 체크포인트의 수와 수준이 과하거나 부족하지 않은지 피드백을 부탁드립니다.

@ldhldh07 ldhldh07 force-pushed the main branch 3 times, most recently from 2f94c63 to 24443b0 Compare August 28, 2025 08:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant