8주차 과제 체크포인트
기본 과제
필수
- 반복 유형 선택
- 일정 생성 또는 수정 시 반복 유형을 선택할 수 있다.
- 반복 유형은 다음과 같다: 매일, 매주, 매월, 매년
- 31일에 매월을 선택한다면 -> 매월 마지막이 아닌, 31일에만 생성하세요.
- 윤년 29일에 매년을 선택한다면 -> 29일에만 생성하세요!
- 반복 일정 표시
- 캘린더 뷰에서 반복 일정을 시각적으로 구분하여 표시한다.
- 아이콘을 넣든 태그를 넣든 자유롭게 해보세요!
- 캘린더 뷰에서 반복 일정을 시각적으로 구분하여 표시한다.
- 반복 종료
- 반복 종료 조건을 지정할 수 있다.
- 옵션: 특정 날짜까지, 특정 횟수만큼, 또는 종료 없음 (예제 특성상, 2025-06-30까지)
- 반복 일정 단일 수정
- 반복일정을 수정하면 단일 일정으로 변경됩니다.
- 반복일정 아이콘도 사라집니다.
- 반복 일정 단일 삭제
- 반복일정을 삭제하면 해당 일정만 삭제합니다.
심화 과제
- 이 앱에 적합한 테스트 전략을 만들었나요?
각 팀원들의 테스트 전략은?
합의된 테스트 전략과 그 이유는 무엇인가요?
- 이번 과제에서는 코드의 안정성을 단계별로 튼튼하게 쌓아 올리는 피라미드 테스트 전략을 채택했습니다. TDD로 과제를 진행해야 했기 때문에, 가장 작고 예측 가능한 순수 함수부터 검증하는 단위 테스트로 시작했습니다. 이 튼튼한 기반 위에서, 훅과 컴포넌트 등의 각 부품들이 서로 잘 연결되어 동작하는지 확인하는 통합 테스트를 진행했고, 마지막으로 실제 사용자와 같은 환경에서 전체 시나리오를 검증하는 E2E 테스트로 마무리하며 각 계층이 서로를 보완하도록 설계했습니다.
추가로 작성된 테스트 코드는 어떤 것들이 있나요?
- 단위 테스트 📍 recurringEvents.spec.ts
- 목표: UI나 다른 로직과 완전히 분리된, 반복 일정 생성의 핵심 비즈니스 로직을 검증하는 데 집중했습니다.
- 주요 검증 내용:
- generateRecurringEvents 함수가 규칙에 따라 정확한 날짜 배열을 반환하는지 테스트했습니다.
- 엣지 케이스 커버: "31일에 '매월' 반복 시 31일이 없는 달 건너뛰기", "윤년 2월 29일에 '매년' 반복 시 다음 윤년에만 생성" 등 실패하기 쉬운 경계값들을 집중적으로 검증하여 로직의 안정성을 확보했습니다.
- TDD 사이클: 실패하는 테스트(Red)를 먼저 작성하고, 이를 통과시키는 최소한의 코드(Green)를 구현한 뒤, 코드를 개선(Refactor)하는 과정을 반복하며 기능의 완성도와 신뢰도를 높였습니다.
- 통합 테스트 📍 app.integration.spec.tsx
- 목표: TDD로 만든 유틸 함수와 각종 커스텀 훅, 그리고 UI 컴포넌트들이 함께 조립되었을 때 의도대로 상호작용하는지 검증했습니다.
- 주요 검증 내용:
- 사용자 행동 시뮬레이션: userEvent를 사용해 "사용자가 '반복 일정' 체크박스를 누르고, 폼을 채운 뒤 '저장' 버튼을 누른다"와 같은 전체 기능 흐름을 테스트했습니다.
- 상태 변경 확인: saveEvent가 호출된 후, filteredEvents 상태가 올바르게 업데이트되어 최종적으로 캘린더 UI에 새로운 반복 일정들이 정확하게 렌더링되는지 확인하며, 각 계층 간의 연결성을 검증했습니다.
- E2E 테스트 📍 calendar.cy.ts
- 목표: 실제 개발 서버에서 실행되는 App 전체를 대상으로, 가장 중요한 핵심 사용자 시나리오가 처음부터 끝까지 완벽하게 동작하는지 최종적으로 검증했습니다.
- 주요 검증 내용:
- 실제 브라우저 환경: Cypress를 사용하여 실제 Chrome 브라우저에서 테스트를 실행, jsdom 환경에서는 발견하기 어려운 렌더링 타이밍 이슈등을 검증했습니다.
- 핵심 시나리오 검증: "새로운 반복 일정을 생성하고, 그중 하나를 수정하면 단일 일정으로 바뀌고, 원본을 삭제하면 모든 가상 일정이 사라진다"와 같은 복잡한 사용자 여정을 테스트하여 기능 동작 정확도를 검증했습니다.
- 전체 검증 내용:
- 기본 CRUD (Create, Read, Update, Delete)
- 시나리오: 사용자가 새 단일 일정을 성공적으로 생성한다
- 시나리오: 사용자가 기존 일정을 성공적으로 수정한다
- 시나리오: 사용자가 기존 일정을 삭제한다
- 반복 일정 기능
- 시나리오: 사용자가 '매주' 반복 일정을 생성한다
- 시나리오: 사용자가 반복 일정의 '가상 인스턴스'만 수정한다
- 시나리오: 사용자가 반복 일정의 '원본'을 삭제한다
- 캘린더 뷰 및 검색
- 시나리오: 사용자가 뷰를 전환하고 날짜를 이동한다
- 시나리오: 사용자가 키워드로 일정을 검색한다
- 유효성 검사 및 예외 처리
- 시나리오: 사용자가 필수 필드를 비우고 일정을 생성하려고 한다
- 시나리오: 사용자가 겹치는 시간에 일정을 생성하려고 한다
과제 셀프회고
기술적 성장
- 이번 과제를 통해 테스트 주도 개발의 실제 사이클을 처음부터 끝까지 온전히 경험한 것이 가장 큰 기술적 성장이었습니다. 이전에는 막연하게만 알던 Red-Green-Refactor 사이클을, 실패하는 단위 테스트를 먼저 작성하고, 이를 통과시키는 가장 간단한 코드를 구현한 뒤, 통합 테스트라는 안전망 위에서 실제 UI와 로직을 연결하며 리팩토링하는 전 과정을 직접 수행해 볼 수 있었습니다.
코드 품질
-
가장 만족스러운 부분은 TDD로 완성한 recurringEvents.ts 유틸리티 파일입니다. TDD 사이클로 개발을 해본 것이 처음이라 의미있게 느껴지기도 하고, 엣지 케이스까지 검증된 신뢰도 있는 함수들을 먼저 구현해 놓았더니 훅에 이식하여 기능 구현으로까지 이어지는 과정이 수월했습니다.
-
반면, App.tsx 컴포넌트는 초기의 거대한 단일 컴포넌트에서 많이 발전했지만 여전히 리팩토링이 필요한 부분이 많다고 생각합니다. 현재 App은 여러 커스텀 훅을 호출하고 그 결과들을 하위 컴포넌트에 props로 전달하는 역할을 하고 있습니다. 여기서 더 나아가 EventFormPanel이나 CalendarView 같은 자식 컴포넌트들이 props 대신 전역 상태 관리 도구를 통해 필요한 상태와 함수를 직접 가져가도록 만든다면, App의 역할을 더욱 단순화하고 컴포넌트 간의 결합도를 더 낮출 수 있을 것 같습니다.
학습 효과 분석
-
가장 큰 배움이 있었던 부분은 단위 테스트와 통합 테스트의 역할을 명확히 구분하게 된 점인 것 같습니다. 처음에는 모든 것을 통합 테스트로 한 번에 검증하면 되는 거 아닐까? 생각했지만, 순수한 로직을 검증하는 단위 테스트가 얼마나 강력한 기반이 되는지 깨달았습니다. 촘촘히 만들어진 단위 테스트 덕분에, 복잡한 UI를 조립하는 통합 테스트 단계에서는 모듈 간 연결의 문제에만 집중할 수 있었습니다.
-
추가 학습이 필요한 영역은 안정적인 E2E 테스트 작성법입니다. 문법이 미숙해서 부족한 부분이 여럿 있는 것 같습니다. force 옵션을 쓰지 않고도 애니메이션이나 비동기 UI 업데이트를 안정적으로 기다리게 하는 Cypress의 고급 기법들이나, 각 테스트가 서로에게 영향을 주지 않도록 데이터를 완벽하게 격리하는 패턴에 대해 더 자세히 공부해서 실무에 적용해 보고 싶습니다!
과제 피드백
- 반복 일정이라는 요구사항이 처음에는 간단한 기능 추가 정도라고 생각했지만, 요구 사항이 명확하지 않아서 헤매게 된 시점이 있습니다. 그래서 냅다 요구사항부터 다시 정리했습니다.
## 반복 일정 기능 요구사항
### 1. 반복 일정 생성
### 1.1. 반복 유형
- 사용자는 일정을 생성하거나 수정할 때 반복 유형을 선택할 수 있다.
- 지원되는 반복 유형은 다음과 같다:
- 매일: 지정된 종료일까지 매일 일정이 생성된다.
- 매주: 지정된 종료일까지 매주 같은 요일에 일정이 생성된다.
- 매월: 지정된 종료일까지 매월 같은 날짜에 일정이 생성된다.
- 매년: 지정된 종료일까지 매년 같은 날짜와 월에 일정이 생성된다.
### 1.2. 엣지 케이스 처리
- 일자 고정 원칙: 반복 규칙은 시작일의 '일자'를 기준으로 한다.
- 예시 1: 1월 31일에 '매월' 반복을 설정하면, 31일이 없는 달(2월, 4월 등)에는 해당 월의 마지막 날이 아닌, 일정이 생성되지 않아야 한다.
- 예시 2: 윤년 2월 29일에 '매년' 반복을 설정하면, 윤년이 아닌 해에는 일정이 생성되지 않고 다음 윤년의 2월 29일에 생성되어야 한다.
### 2. 반복 일정 표시
- 시각적 구분: 캘린더 뷰에서 반복 일정(원본 및 가상 인스턴스 포함)은 일반 일정과 명확히 구분될 수 있도록 반복 아이콘과 함께 표시되어야 한다.
- 일반(반복이 아닌) 일정에는 반복 아이콘이 표시되지 않아야 한다.
### 3. 반복 종료 조건
### 3.1. 종료일 지정
- 사용자는 반복 일정에 대해 '특정 날짜까지'라는 종료 조건을 설정할 수 있다.
- 반복 일정은 지정된 종료일까지만 생성되며, 그 이후 날짜에는 생성되지 않아야 한다.
### 3.2. 유효성 검사
- 종료일은 시작일 이후여야 한다: 사용자가 시작일보다 이전 또는 같은 날짜를 종료일로 선택할 경우, 에러 메시지를 표시하고 저장을 막아야 한다.
- 최대 종료일 제한: 시스템은 예제 특성상 최대 반복 종료일을 2025년 10월 30일로 제한한다. 사용자가 이 날짜를 초과하여 선택하면 에러 메시지를 표시해야 한다.
### 4. 반복 일정 개별 수정
- 단일 일정으로 전환: 사용자가 캘린더 뷰에서 특정 날짜의 반복 일정 인스턴스를 수정하면, 해당 일정은 더 이상 반복 규칙의 일부가 아닌 독립적인 단일 일정으로 변경되어야 한다.
- 반복 아이콘 제거: 단일 일정으로 전환된 이벤트에서는 반복 아이콘이 사라져야 한다.
- 예외 처리: 원본 반복 규칙에는 수정된 날짜가 예외(exception) 목록에 추가되어, 해당 날짜에 더 이상 가상의 반복 일정이 중복으로 표시되지 않도록 해야 한다.
### 5. 반복 일정 개별 삭제
- 단일 인스턴스만 삭제: 사용자가 캘린더 뷰에서 특정 날짜의 반복 일정 인스턴스를 삭제하면, 해당 날짜의 이벤트만 삭제되어야 한다.
- 예외 처리: 원본 반복 규칙에는 삭제된 날짜가 예외(exception) 목록에 추가되어야 한다.
- 다른 인스턴스 유지: 삭제된 날짜 외의 다른 모든 과거 및 미래의 반복 일정은 영향을 받지 않고 그대로 유지되어야 한다.
- TDD랑 테스트랑 .. 그리고 ai와 (?) 많이 친해질 수 있었던 재미있는 과제였던 것 같습니다. 헤헤
리뷰 받고 싶은 내용
-
테스트의 경계: recurringEvents.ts 같은 유틸리티 함수에 대해 꼼꼼한 단위 테스트를 작성했습니다. 이 함수를 사용하는 useEventOperations 같은 커스텀 훅을 테스트할 때도, 이 유틸리티 함수의 모든 엣지 케이스를 다시 테스트해야 할까요? 아니면 훅 레벨에서는 유틸 함수는 이미 검증되었으니, 훅이 유틸 함수를 올바르게 호출하는지만! 테스트하는 것이 더 효율적인지 조언을 듣고 싶습니다.
-
E2E 테스트의 독립성: 현재 작성한 E2E 테스트의 CRUD 시나리오는 생성 테스트가 성공해야 수정 테스트가 이어서 실행될 수 있는 의존적인 구조입니다. 각 테스트가 서로에게 영향을 주지 않도록 완벽하게 독립적으로 만드려면, beforeEach에서 cy.request()를 사용해 매번 테스트에 필요한 데이터를 직접 생성하고 시작해야 하거나 인터셉트 처리로 api를 모킹해야 하는 것으로 공부했습니다. 하지만 그렇게 되면 실제 api 테스트가 되지 않아서 E2E 테스트의 의미가 흐려진다고 느껴집니다. 반면에 실제 데이터를 테스트 용도로 조작하는 것이 위험하게 느껴지기도 합니다. 보통 실무에서 사용하는 일반적인 패턴은 어떤 것인지 궁금합니다!
과제 피드백
테스트의 경계: recurringEvents.ts 같은 유틸리티 함수에 대해 꼼꼼한 단위 테스트를 작성했습니다. 이 함수를 사용하는 useEventOperations 같은 커스텀 훅을 테스트할 때도, 이 유틸리티 함수의 모든 엣지 케이스를 다시 테스트해야 할까요? 아니면 훅 레벨에서는 유틸 함수는 이미 검증되었으니, 훅이 유틸 함수를 올바르게 호출하는지만! 테스트하는 것이 더 효율적인지 조언을 듣고 싶습니다.
저는 이럴 때 그냥 훅에서도 검증하는 편인데요, "무엇을 테스트 해야 좋을까?"에 대해 판단하는 시간보다 그냥 테스트를 작성하는게 더 빠르다고 느꼈기 때문이랍니다 ㅎㅎ 이걸 선별하는것도 무척... 귀찮고 어려워요. 물론 이론상 유틸에 대해 검증하는 부분을 훅에서 하지 않아도 무방하죠. 그런데 이걸 구분하는게 쉽냐는 다른 문제라서요! 그냥 해버려~ 가 저의 기조입니다.
E2E 테스트의 독립성: 현재 작성한 E2E 테스트의 CRUD 시나리오는 생성 테스트가 성공해야 수정 테스트가 이어서 실행될 수 있는 의존적인 구조입니다. 각 테스트가 서로에게 영향을 주지 않도록 완벽하게 독립적으로 만드려면, beforeEach에서 cy.request()를 사용해 매번 테스트에 필요한 데이터를 직접 생성하고 시작해야 하거나 인터셉트 처리로 api를 모킹해야 하는 것으로 공부했습니다. 하지만 그렇게 되면 실제 api 테스트가 되지 않아서 E2E 테스트의 의미가 흐려진다고 느껴집니다. 반면에 실제 데이터를 테스트 용도로 조작하는 것이 위험하게 느껴지기도 합니다. 보통 실무에서 사용하는 일반적인 패턴은 어떤 것인지 궁금합니다!
이건 팀 내에서 어떻게 합의하냐에 따라 다르다고 생각해요 ㅎㅎ 가령 CRUD를 하나의 테스트 스펙에서 진행할 수도 있답니다! 혹은 테스트 자체를 함수로 만들어서 진행한다거나!?
describe('CURD 테스트', () => {
생성테스트검증 = () => {}
수정테스트검증 = () => {}
읽기테스트검증 = () => {}
삭제테스트검증 = () => {}
test('생성 테스트', () => {
생성테스트검증();
})
test('수정 테스트', () => {
생성테스트검증();
수정테스트검증();
})
})
요로코롬...
여튼 중요한건 팀에서 어떤 전략을 토대로 테스트를 진행하냐에 따라 다르다는 점이랍니다! 저는 테스트에서 제일 중요한게 결국 "사이드 이펙트(문제)를 찾아내는 것" 이라고 생각하고, 이를 찾아낼 수 있다면 뭐든 좋다고 생각해요!