8주차 과제 체크포인트
기본 과제
필수
- 반복 유형 선택
- 일정 생성 또는 수정 시 반복 유형을 선택할 수 있다.
- 반복 유형은 다음과 같다: 매일, 매주, 매월, 매년
- 31일에 매월을 선택한다면 -> 매월 마지막이 아닌, 31일에만 생성하세요.
- 윤년 29일에 매년을 선택한다면 -> 29일에만 생성하세요!
- 반복 일정 표시
- 캘린더 뷰에서 반복 일정을 시각적으로 구분하여 표시한다.
- 아이콘을 넣든 태그를 넣든 자유롭게 해보세요!
- 캘린더 뷰에서 반복 일정을 시각적으로 구분하여 표시한다.
- 반복 종료
- 반복 종료 조건을 지정할 수 있다.
- 옵션: 특정 날짜까지, 특정 횟수만큼, 또는 종료 없음 (예제 특성상, 2025-06-30까지)
- 반복 일정 단일 수정
- 반복일정을 수정하면 단일 일정으로 변경됩니다.
- 반복일정 아이콘도 사라집니다.
- 반복 일정 단일 삭제
- 반복일정을 삭제하면 해당 일정만 삭제합니다.
선택
- 반복 간격 설정
- 각 반복 유형에 대해 간격을 설정할 수 있다.
- 예: 2일마다, 3주마다, 2개월마다 등
- 예외 날짜 처리:
- 반복 일정 중 특정 날짜를 제외할 수 있다.
- 반복 일정 중 특정 날짜의 일정을 수정할 수 있다.
- 요일 지정 (주간 반복의 경우):
- 주간 반복 시 특정 요일을 선택할 수 있다.
- 월간 반복 옵션:
- 매월 특정 날짜에 반복되도록 설정할 수 있다.
- 매월 특정 순서의 요일에 반복되도록 설정할 수 있다.
- 반복 일정 전체 수정 및 삭제
- 반복 일정의 모든 일정을 수정할 수 있다.
- 반복 일정의 모든 일정을 삭제할 수 있다.
심화 과제
- 이 앱에 적합한 테스트 전략을 만들었나요?
각 팀원들의 테스트 전략은?
테스트 전략
각자 논의에 앞서서 테스트 전략에 대해 다음과 같이 논의했습니다.
이지훈
- 전략: 테스트 트로피 전략
- 핵심 관점: 사용자 관점에서의 정상 동작 확인이 핵심
- 접근법:
- 통합 테스트 중심으로 사용자 시나리오 검증
- 훅과 같이 데이터와 상태를 다루는 로직은 단위 테스트로 보완
이은지
- 전략: 테스트 트로피 전략 + 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%
합의된 테스트 전략과 그 이유는 무엇인가요?
최종적으로는 테스트 트로피 전략을 선택했습니다.
선택 이유
선택의 이유는 일단 좀 더 다수가 트로피 전략을 생각하기도 했고 (피라미드파가 팔랑귀...) 물론 값싼 단위 테스트를 많이 작성하여 테스트 작성의 속도를 높일 수 있다는 장점이 있지만 프론트엔드의 특성상 유저에 좀 더 가까운(그렇지만 너무 비싸지는 않은) 통합테스트를 많이 작성하는것이 어떤가 하는 생각에 모두가 동의했던 것 같습니다.
그래서 결론적으로는
- 사용자 관점에서 기능단위로 실제 동작을 확인 가능하고
- 비용 대비 효과(신뢰도)가 높으며
- 컴포넌트간의 상호작용과 통합이 중요한 프론트엔드의 특성을 고려하고
- 리팩토링시에 내부 구현 변경에 영향을 받지 않는(물론 기능의 변경시에는 영향을 받겠지만)
- 또한 여러 로직의 집약체로 간접적으로 로직의 검증이 가능한
통합 테스트를 주로 작성하되, 값비싼 e2e 테스트는 정말 우리 서비스에서 중요한 유저 플로우에 대해서만 작성하자고 논의했습니다.
추가로 작성된 테스트 코드는 어떤 것들이 있나요?
전반적으로 반복일정에 대한 통합테스트를 추가적으로 작성했습니다.
그리고 트로피 전략에 알맞게, 그나마 이 프로젝트에서 중요하다고 생각되는 유저 시나리오에 대하여 e2e를 작성했습니다.
그리고 잘 찾아보시면 제가 그렇게 신경쓰지 않았지만 비매드가 소중하게 만들어준 유틸들이...있습니다....
과제 셀프회고
왜 매번 나는 새벽까지 고생을 하는 가에 대한 고찰... 매번 쉽지 않았다고 쓰기도 손 아프네요 그치만 이번에도 쉽지 않았습니다...
이번 과제의 부제는 비매드와 친해지기 입니다. (인간개입을 곁들인...)
기술적 성장
사실 테스트 전략에 대해서는 이전에도 면접이라거나, 4기를 했었기 때문에... 새로 학습했다고 보기엔 어렵고, 또 e2e도 회사에서 작성을 해본 경험이 있었기때문에 학습했다고 보기는 어려운 것 같습니다.
이번 과제에서 진짜 학습을 했다하면... 모두(?)가 극찬했던 비매드...가 아닐까 싶습니다. 코치님께서 처음에 이번 과제에는 풀 ai를 사용해도 된다고 말씀주셔서 '진짜 게르마늄 팔찌' 한 번 사봐? 라는 생각을 했던 것 같습니다.
영서에게... '비매드'란?
저에게 비매드란 사실 '게르마늄 팔찌'였습니다. 아니 뭐 다들 좋다는데...진짜 보기에는 좋은건 모르겠고... 잡솨보라는데... 오히려 좋다고만하니까 진짜 좋은가? 싶은 생각이 드는...그런 존재가 비매드였어요. (약간 게르마늄 팔찌보다는 풀리오 안마기같기도) 암튼 이번 과제에 뛰어들면서 적(?)에 대해서 알면 욕하기 쉽다는 생각으로 '아 내가 써보니까 별로던데 ㅋㅋ'라는 멘트를 칠 생각하며 비매드를 설치했습니다.
일요일 오후 3시 쯤에 설치를 했고, 모델은 GPT-5을 사용했습니다. 문서양이 상당할 것으로 생각이 되는데 클로드 소넷 모델을 사용하면서 토큰을 살살 녹이기에는 너무 부담이 될 것 같았습니다.
그런데... 처음 비매드를 설치하고, 비매드에게 절차적으로 프롬프팅을 하려고 하니 비매드가 제 생각처럼 움직여 주지 않았습니다. 제가 여태까지 봤던 비매드의 폴더 구조와, 제가 에이전트에게 요청을 해서 만들어준 폴더구조가 다른것을 보고 '뭔가 잘못됐다'고 생각했습니다.
뭔가 잘못되었다고 생각을해서 일단 쉼호흡을 한 번하고 비매드를 삭제하고 다시 설치하고 커서를 껐다 켰습니다.
그런데도 원하는대로 문서를 생성해주지 않았고, 두번의 삭제 후 세번째 트라이때 갑자기 '한 번 존댓말을 해볼까?'라는 생각이 들어 존댓말을 해보니 너무 잘해주더라고요...?
약 세시간이 지난뒤에야 원하는 형태의 문서를 얻을 수 있었습니다...(이제 시작)
이때부터 미칠듯이 화가나기 시작합니다. 아니 비매드 왜 쓰는거임? 저는 프롬프팅을 너무나도 잘했다고 생각했기 때문에... 갑자기 제가 고용한 Sarah 대신 '윤진우'씨가 오신것이 이해가 되지 않았습니다.
주먹을 꽉 쥐고 그때 질문을 하게 되는데요.
그 비매드라는 빨간약 외 드세요? 그냥 ai한테 야 나 이런기능 구현 하고싶은데 테스트코드 짜고 그에 맞춰서코드 짜줘 하면 더 빠를거같은데
이때 준형님께서 좋은 답변을 해주셨습니다.
프레임워크를 왜 쓰는지 부터 잘 생각해보면 좋을듯 프레임워크란 무엇인가? 정해져있는 규약 안에서 제품을 만들어낸다 정해져있지 않은 방법을 사용하기 어렵다
여기서 관통하는건 정해져있는 규약이라는것임
Bmad Method같은 Ai native workflow 프레임워크는 결국 우리 사람들이 일하는 방식에 규약을 걸어서 강제하는 행위일거임
사람들은 너무나 자유롭기떄문에 업무를 하는 방법도 너무나 천차만별임 많은 사람들이 이 자유떄문에 골머리를 썩힘 그렇기떄문에 코드컨벤션이라든가 지라라든가 업무에 도움을 주는 규칙드를 정하는것
그럼 Bmad Method는 여기서 멀 강제하느냐? 컨텍스트엔지니어링에서 답을 찾아냄 거대한 컨텍스트를 가져감에따라 거대한 문서를 작성해가며, 그 문서가 SSOT가 되도록 만드는것임 개발자뿐만 아니라 비개발 직군까지 이 거대한 컨텍스트 위에서 함께 일하는 방법을 찾아서, 사람들의 자유를 박탈해가며 일하는 방식을 정해줘버리는것임
단순히 엔지니어링관점에선 머리가 아플수있음. 그런데 조직관점에서보면 너무 훌륭한 관점으로 볼수있음
그러니까 요약을 하자면, 단순히 AI를 사용해서 작업을 하는 것보다 비매드를 통해서 문서화를 하고, 해당 컨텍스트를 통해 모든 직군이 같은 컨텍스트를 바라보도록 강제(?)해서 효율성의 극대화를 하자는...요런 얘기 같았는데요.
암튼 납득이 되는 이유라서 화를 다시 꾹 참고 다시 재개하기 시작했습니다.
bmad의 시스템 아키텍쳐는 다음과 같은데요.
여러 페르소나들이 있지만 저는 pm, architect, po, dev, qa를 사용해서 작업을 하도록 구현했습니다.
- pm을 통해 브라운필드 prd 작성
- architect를 통해 아키텍쳐 문서화
- po를 통해 epic 및 story 문서 작성
- dev를 통해 red-green 테스트 작성 및 기능 개발
- qa를 통해 위 내용 검증 후 문서화 이렇게 1에서 5를 순차적으로 문서화를 진행했고, 만약 제가 문서화를 봤을때 납득가지 않는 내용이 있다면 다시 재문서화를 하거나 아니면 에이전트들과 토론을 하는 형태로 문서를 보정했습니다.
또 이번 과제의 경우에는 tdd를 했어야 했기때문에, dev 페르소나인 제임스에게 나름의 행동 강령을 추가했습니다.
- TDD_MANDATORY: ALWAYS follow Red-Green-Refactor cycle for ALL new features (Kent Beck & Kent C. Dodds principles)
그리고 tdd 행동강령을 만들어주고, 이 친구가 길을 잃으려고 할때마다 복창하도록 했습니다.
tdd 행동강령
# 테스트 코드 & 개발 행동 강령Kent Beck과 Kent C. Dodds의 TDD 원칙 기반
🎯 핵심 원칙
"테스트가 없으면 기능이 아니다. 클린하지 않으면 완성이 아니다."
모든 코드 작성은 **신뢰성(Confidence)**를 높이는 것이 목표입니다. 테스트와 코드 모든 결정은 "이것이 사용자에게 더 나은 경험을 제공하는가?"라는 질문에 답할 수 있어야 합니다.
🔴🟢🔵 Red-Green-Refactor 사이클
1. 🔴 RED Phase: 실패하는 테스트 작성
// ✅ 좋은 예: 명확한 의도를 가진 실패 테스트
describe('반복 일정 생성', () => {
test('사용자가 매주 반복 일정을 생성할 수 있다', async () => {
// Given: 사용자가 일정 생성 폼에 있을 때
render(<Calendar />);
// When: 반복 일정을 설정하고 저장하면
await user.click(screen.getByRole('button', { name: /일정 추가/i }));
await user.type(screen.getByLabelText(/제목/i), '팀 미팅');
await user.selectOptions(screen.getByLabelText(/반복 유형/i), 'weekly');
await user.click(screen.getByRole('button', { name: /저장/i }));
// Then: 반복 일정이 생성되고 시각적으로 구분된다
expect(screen.getByText('팀 미팅')).toBeInTheDocument();
expect(screen.getByLabelText('반복 일정 아이콘')).toBeInTheDocument();
});
});
RED Phase 체크리스트:
- 테스트 이름이 사용자 시나리오를 명확히 설명하는가?
- Given-When-Then 구조로 작성했는가?
- 구현 세부사항이 아닌 사용자 관점에서 작성했는가?
- 테스트를 실행하면 실패하는가?
2. 🟢 GREEN Phase: 테스트를 통과시키는 최소 구현
// ✅ 좋은 예: 테스트만 통과시키는 최소 구현
export const useRecurringEvents = () => {
const createRecurringEvent = async (eventData) => {
// 가장 단순한 구현으로 시작
if (eventData.repeat.type === 'weekly') {
return { success: true, id: 'temp-id' };
}
return { success: false };
};
return { createRecurringEvent };
};
GREEN Phase 체크리스트:
- 테스트가 통과하는가?
- 가장 간단한 구현인가? (복잡한 로직 금지)
- 다른 테스트를 깨뜨리지 않는가?
- 하드코딩이어도 괜찮다 (리팩터링에서 개선)
3. 🔵 REFACTOR Phase: 클린 코드로 개선
// ✅ 좋은 예: 단일 책임 원칙을 따르는 리팩터링
// 📁 utils/recurringDateCalculator.ts - 날짜 계산만 담당
export const calculateWeeklyDates = (startDate: string, endDate: string): string[] => {
// 순수 함수: 입력이 같으면 출력도 같음
const dates: string[] = [];
let current = new Date(startDate);
const end = new Date(endDate);
while (current <= end) {
dates.push(current.toISOString().split('T')[0]);
current.setDate(current.getDate() + 7);
}
return dates;
};
// 📁 hooks/useRecurringEvents.ts - 상태 관리만 담당
export const useRecurringEvents = () => {
const createRecurringEvent = async (eventData) => {
if (eventData.repeat.type === 'weekly') {
const dates = calculateWeeklyDates(eventData.date, eventData.repeat.endDate);
return await createEventsBatch(dates.map((date) => ({ ...eventData, date })));
}
return { success: false };
};
return { createRecurringEvent };
};
REFACTOR Phase 체크리스트:
- 함수/클래스가 하나의 책임만 가지는가? (SRP)
- 함수명이 하는 일을 정확히 설명하는가?
- 중복 코드를 제거했는가? (DRY)
- 모든 테스트가 여전히 통과하는가?
🚫 Kent C. Dodds - 피해야 할 실수들
실수 1: 구현 세부사항 테스트 (HIGH 위험)
// ❌ 나쁜 예: 내부 상태와 메서드 테스트
test('increment 메서드가 count를 증가시킨다', () => {
const wrapper = mount(<Counter />);
expect(wrapper.instance().state.count).toBe(0); // 구현 세부사항!
wrapper.instance().increment(); // 구현 세부사항!
expect(wrapper.instance().state.count).toBe(1);
});
// ✅ 좋은 예: 사용자 관점에서 테스트
test('사용자가 버튼을 클릭하면 숫자가 증가한다', async () => {
render(<Counter />);
const button = screen.getByRole('button');
expect(button).toHaveTextContent('0');
await user.click(button);
expect(button).toHaveTextContent('1');
});
실수 2: 100% 코드 커버리지 강박 (MEDIUM 위험)
// ❌ 나쁜 예: 커버리지만 늘리는 무의미한 테스트
test('About 페이지가 렌더링된다', () => {
render(<AboutPage />);
expect(screen.getByText('About Us')).toBeInTheDocument();
});
// ✅ 좋은 예: 핵심 비즈니스 로직에 집중
test('결제 프로세스가 성공적으로 완료된다', async () => {
// 중요한 비즈니스 로직 테스트
render(<CheckoutPage />);
await completePaymentFlow();
expect(screen.getByText('결제가 완료되었습니다')).toBeInTheDocument();
});
실수 3: React Testing Library 잘못된 사용
// ❌ 나쁜 예들
import { render, screen, cleanup } from '@testing-library/react'; // cleanup 불필요
import { fireEvent } from '@testing-library/react'; // user-event 사용해야 함
afterEach(cleanup); // ❌ 자동으로 처리됨
test('잘못된 테스트 패턴', () => {
const wrapper = render(<Component />); // ❌ wrapper 변수명
const { getByTestId } = render(<Component />); // ❌ screen 사용해야 함
expect(wrapper.queryByRole('button')).toBeInTheDocument(); // ❌ query* 잘못 사용
fireEvent.click(getByTestId('submit')); // ❌ fireEvent + testId
});
// ✅ 좋은 예
import { render, screen } from '@testing-library/react';
import { user } from '@testing-library/user-event';
test('올바른 테스트 패턴', async () => {
render(<Component />);
const submitButton = screen.getByRole('button', { name: /제출/i });
expect(submitButton).toBeInTheDocument();
await user.click(submitButton);
expect(screen.getByText('제출되었습니다')).toBeInTheDocument();
});
📋 React Testing Library 체크리스트
쿼리 우선순위 (중요도 순)
getByRole- 접근성 기반 (최우선)getByLabelText- 폼 요소getByPlaceholderText- 입력 필드getByText- 표시되는 텍스트getByTestId- 최후의 수단
// ✅ 쿼리 우선순위 준수 예시
test('사용자 등록 폼 테스트', async () => {
render(<RegisterForm />);
// 1순위: getByRole 사용
const submitButton = screen.getByRole('button', { name: /가입하기/i });
// 2순위: getByLabelText 사용
const emailInput = screen.getByLabelText(/이메일/i);
const passwordInput = screen.getByLabelText(/비밀번호/i);
// 3순위: getByPlaceholderText 사용 (label이 없는 경우만)
const searchInput = screen.getByPlaceholderText(/검색어를 입력하세요/i);
// 실제 사용자 상호작용 시뮬레이션
await user.type(emailInput, 'test@example.com');
await user.type(passwordInput, 'password123');
await user.click(submitButton);
// 명시적 assertion
expect(screen.getByText('가입이 완료되었습니다')).toBeInTheDocument();
});
ESLint 플러그인 필수 사용
// .eslintrc.js
{
"extends": ["@testing-library/react", "@testing-library/jest-dom"]
}
waitFor 올바른 사용법
// ❌ 잘못된 waitFor 사용
await waitFor(() => {
fireEvent.click(button); // side-effect in waitFor!
expect(screen.getByText('로딩 중')).toBeInTheDocument();
expect(screen.getByText('완료')).toBeInTheDocument(); // 여러 assertion!
});
// ✅ 올바른 waitFor 사용
fireEvent.click(button); // side-effect는 밖에서
await waitFor(() => expect(screen.getByText('완료')).toBeInTheDocument()); // 단일 assertion
expect(screen.getByText('상태 메시지')).toBeInTheDocument(); // 추가 검증은 밖에서
🎯 클린 코드 원칙
1. 단일 책임 원칙 (SRP)
// ❌ 나쁜 예: 여러 책임을 가진 함수
function processRecurringEvent(eventData) {
// 1. 날짜 계산
const dates = [];
let current = new Date(eventData.startDate);
while (current <= new Date(eventData.endDate)) {
dates.push(current.toISOString().split('T')[0]);
current.setDate(current.getDate() + 7);
}
// 2. API 호출
return fetch('/api/events-list', {
method: 'POST',
body: JSON.stringify({ events: dates.map((date) => ({ ...eventData, date })) }),
});
// 3. 에러 처리
// ...
}
// ✅ 좋은 예: 각각 하나의 책임만 담당
// 📁 utils/dateCalculator.ts - 날짜 계산만 담당
export const calculateWeeklyDates = (startDate: string, endDate: string): string[] => {
const dates: string[] = [];
let current = new Date(startDate);
const end = new Date(endDate);
while (current <= end) {
dates.push(current.toISOString().split('T')[0]);
current.setDate(current.getDate() + 7);
}
return dates;
};
// 📁 api/eventsApi.ts - API 호출만 담당
export const createEventsBatch = async (events: EventData[]): Promise<BatchResponse> => {
const response = await fetch('/api/events-list', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ events }),
});
if (!response.ok) {
throw new Error(`API Error: ${response.status}`);
}
return response.json();
};
// 📁 hooks/useRecurringEvents.ts - 상태 관리와 오케스트레이션만 담당
export const useRecurringEvents = () => {
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const createRecurringEvents = useCallback(async (eventData: EventForm) => {
setIsLoading(true);
setError(null);
try {
const dates = calculateWeeklyDates(eventData.date, eventData.repeat.endDate);
const eventsToCreate = dates.map((date) => ({ ...eventData, date }));
const result = await createEventsBatch(eventsToCreate);
return result;
} catch (err) {
setError(err instanceof Error ? err.message : '알 수 없는 오류');
throw err;
} finally {
setIsLoading(false);
}
}, []);
return { createRecurringEvents, isLoading, error };
};
2. 의미있는 이름 사용
// ❌ 나쁜 예: 의미 없는 이름
const processData = (d, t) => {
const result = [];
let curr = new Date(d);
while (curr <= new Date(t)) {
result.push(curr.toISOString().split('T')[0]);
curr.setDate(curr.getDate() + 7);
}
return result;
};
// ✅ 좋은 예: 의도가 명확한 이름
const calculateWeeklyRecurringDates = (startDate: string, endDate: string): string[] => {
const recurringDates: string[] = [];
let currentDate = new Date(startDate);
const finalDate = new Date(endDate);
while (currentDate <= finalDate) {
const dateString = currentDate.toISOString().split('T')[0];
recurringDates.push(dateString);
currentDate.setDate(currentDate.getDate() + 7);
}
return recurringDates;
};
3. 순수 함수 우선 사용
// ✅ 좋은 예: 순수 함수 (테스트하기 쉬움)
export const calculateMonthlyDates = (
startDate: string,
endDate: string,
dayOfMonth: number
): string[] => {
// 입력이 같으면 출력도 항상 같음
// 부작용(side effect) 없음
const dates: string[] = [];
let current = new Date(startDate);
const end = new Date(endDate);
// 첫 번째 유효한 날짜 찾기
current.setDate(dayOfMonth);
if (current < new Date(startDate)) {
current.setMonth(current.getMonth() + 1);
current.setDate(dayOfMonth);
}
while (current <= end) {
// 31일 특수 처리: 해당 월에 31일이 없으면 건너뛰기
if (dayOfMonth === 31 && current.getDate() !== 31) {
current.setMonth(current.getMonth() + 1);
current.setDate(dayOfMonth);
continue;
}
dates.push(current.toISOString().split('T')[0]);
current.setMonth(current.getMonth() + 1);
current.setDate(dayOfMonth);
}
return dates;
};
// 순수 함수 테스트 예시
describe('calculateMonthlyDates', () => {
test('31일 매월 반복 시 31일이 없는 달은 건너뛴다', () => {
const result = calculateMonthlyDates('2024-01-31', '2024-06-30', 31);
// 2월, 4월, 6월은 31일이 없으므로 제외
expect(result).toEqual(['2024-01-31', '2024-03-31', '2024-05-31']);
});
test('동일한 입력에 대해 항상 같은 출력을 반환한다', () => {
const input = ['2024-01-01', '2024-12-31', 15] as const;
const result1 = calculateMonthlyDates(...input);
const result2 = calculateMonthlyDates(...input);
expect(result1).toEqual(result2);
});
});
✅ 코드 리뷰 체크리스트
테스트 코드 리뷰
- Red-Green-Refactor 사이클을 따랐는가?
- 테스트 이름이 사용자 시나리오를 설명하는가?
- Given-When-Then 구조로 작성되었는가?
- 구현 세부사항이 아닌 동작을 테스트하는가?
- screen.getByRole을 우선 사용했는가?
- @testing-library/user-event를 사용했는가?
- waitFor을 올바르게 사용했는가? (단일 assertion, side-effect 분리)
- 명시적 assertion을 사용했는가? (.toBeInTheDocument() 등)
프로덕션 코드 리뷰
- 각 함수가 하나의 책임만 가지는가? (SRP)
- 함수/변수명이 의도를 명확히 드러내는가?
- 순수 함수로 작성 가능한 로직은 순수 함수로 분리했는가?
- 복잡한 로직을 작은 단위로 분해했는가?
- 중복 코드를 제거했는가? (DRY)
- 타입 안전성을 확보했는가? (TypeScript strict mode)
🎯 결론
"코드는 컴퓨터가 이해할 수 있도록 작성하는 것이 아니라, 사람이 이해할 수 있도록 작성하는 것이다." - Kent Beck
모든 코드와 테스트는 다음 개발자(미래의 나 포함)가 쉽게 이해하고 수정할 수 있도록 작성해야 합니다. 테스트는 코드의 살아있는 문서이자 안전망입니다.
기억할 핵심 메시지
- 테스트 우선 - 구현보다 테스트를 먼저 작성
- 사용자 관점 - 구현이 아닌 사용자가 경험하는 것을 테스트
- 단일 책임 - 하나의 함수는 하나의 일만
- 의미있는 이름 - 코드가 스스로 설명하도록
- 지속적인 개선 - 리팩터링을 통한 끊임없는 품질 향상
참고 자료:
또 작업을 하다가, 기존의 epic 혹은 story가 잘못 작성된 것 같거나, 코드 구현에 있어서 이상함이 느껴지면 에이전트 친구들과 토론을 시작했습니다.
인간의 개입이 없이 전체적으로 프로그래밍을 맡기기에는 아직은 부족함이 있지 않나 하는 생각을 했던 것 같습니다.
그렇다면 비매드의 욜로옵션은...어떻게 써야하는 걸까...라는 생각이 들은...
그리고 po한테 epic을 만들어 달라고 한뒤에 해당 epic으로 story를 만들면 story단위가 너무 커서 하지 않은 일도 했다고 몇번이고 박박 우기는 상황이 있었는데요.
그럴때 story를 좀 더 세분화해서 만들어달라고하니 잘되었던 것 같습니다.
e2e
e2e의 경우에 앞서서 말한것처럼 엄청 비싼 테스트기 때문에 주요 시나리오에 대해서 작성할 필요가 있다고 생각했습니다.
그래서 우선 architect에게 e2e 시나리오를 식별해달라고 요구했습니다.
그렇게 길게 대화한 내용을 문서화하도록 요구한뒤, dev에게 작업에 들어가 달라고 했습니다. 그러다 문제가 하나 발생했는데요... 발생한 문제는 하위의 학습효과 분석에서 확인하실 수 있습니다
암튼 e2e 시나리오는 아키텍쳐를 기반으로 에이전트인 제임스가 작성해주었는데요, 제임스가 작성해준 e2e가 시원치 않아서 완벽한 인간의 개입이 있어야했습니다. (사실 지금도 만족스럽진 않지만...하위를 보시면 왜 다른 부분에 더 힘을 썼는지 조금은...이해해주실듯...우하하)
억까...
나만보기 너무 억울해서...
계속 타임아웃이 나길래 제가 뭐 잘못했나 싶어서 테스트간 간섭이 있나 코드 다 뜯어 고쳐보고 원래 짠 코드 원복하고 했는데...
결론은 커서+크롬이 너무 무거워서 감당을 못해서 타임아웃이 간헐적으로 일어난 것 같았음요..
다른 맥 켜서 확인해보니까 잘되길래 타임아웃을 10000으로 늘려서 테스트해보니까 통과함... 4시간동안 억까당하고 엉엉울뻔했습니다
코드 품질
코드 품질은 처음엔 ai가 짜준것이 많다보니 마음에 들지 않는 것이 많았습니다. 그래서 프롬프팅을 통해 해결하거나, 제가 리팩토링을 직접하고 ai에게 '나 이러이렇게 고쳤어'라고 일러주는 방식으로 진행했습니다.
그렇게해도 기본과제가 끝난다음에 마음에 들지 않는 부분들이 있어서, 리액트 행동 강령을 클로드를 통해서 만든뒤 이를 에이전트들에게 알려주었습니다.
architect를 불러서 아키텍쳐를 분석해서 리팩토링을 어떻게하면 좋을지 문서화 해달라고 했습니다...만 갑자기 아키텍쳐인 윈스턴이 삘받아서 지가 리팩토링까지 다해버리는 바람에 진정시켯습니다...
암튼 시키고 있는데 epic -story로 나눠서 그런가 정말 너무너무 잘게 잘게 리팩토링을 하길래... 끊어버리고 직접 james에게 요청했더니 그제서야 제 의도를 파악하고 제가 원하는 크기로 리팩토링을 해줬습니다.
학습 효과 분석
e2e 작성시에 다음과 같은 문제 상황이 발생했는데요.
프로젝트를 진행하면서 E2E 테스트를 구축하는 과정에서 예상치 못한 문제에 직면했습니다. 논리적으로는 일정 겹침이 발생하지 않아야 하는 테스트 시나리오임에도 불구하고, 이전에 실행된 테스트의 데이터가 남아있어 예기치 않은 일정 충돌이 발생하는 것이었습니다.
기존 프로젝트에서는 realEvents.json 파일을 통해 일정 데이터를 관리하고 있었는데, E2E 테스트도 동일한 데이터소스를 참조하다 보니 테스트 실행 순서나 이전 테스트의 부작용에 따라 결과가 달라지는 불안정한 상황이 발생했습니다. 이를 위한 근본적인 해결책이 필요하다고 생각했습니다.
환경구성
문제의 핵심은 서로 다른 성격의 테스트들이 동일한 데이터소스를 공유한다는 점이었습니다. 따라서 다음과 같이 환경을 분리했습니다.
기존 환경:
Frontend ← MSW (Mock Service Worker) ← 단위/통합 테스트
E2E 환경:
Frontend ← Playwright API Interceptor ← E2E 테스트
Playwright의 page.route() 기능을 활용하여 E2E 테스트만의 독립적인 API 모킹 시스템을 구축하는 것이었습니다.
interceptorApi
tests/e2e/utils/api-interceptor.ts - 중앙 인터셉터 관리
/**
* API 인터셉팅 유틸리티
* 모든 API 인터셉터를 한 번에 적용
*/
export const interceptApi = async (page: Page, customApiList: ((page: Page) => Promise<void>)[] = []) => {
await Promise.all([...mockApis, ...customApiList].flat().map((api) => api(page)));
};
먼저 모든 API 인터셉터를 통합 관리하는 유틸리티를 구성했습니다. 이를 통해 테스트 코드에서는 간단한 한 줄의 호출로 모든 API 모킹을 적용할 수 있게 되었습니다.
실제 모킹 인터셉터
가장 중요한 부분은 실제 백엔드처럼 상태를 유지하는 API 인터셉터를 구현하는 것이었는데,
// tests/e2e/api/events.ts
class EventApiInterceptor {
private events: Record<string, unknown>[] = [];
reset() {
this.events = []; // 테스트 간 완전한 격리
}
loadSampleData() {
this.events = [ /* 미리 정의된 테스트 데이터 */ ];
}
async interceptAllEventApis(page: Page) {
await page.route('/api/events', async (route) => {
const method = route.request().method();
if (method === 'GET') {
// 현재 메모리 상태 반환
await route.fulfill({
body: JSON.stringify({ events: this.events })
});
} else if (method === 'POST') {
// 새 데이터를 메모리에 추가
const newEvent = await route.request().postDataJSON();
this.events.push({ id: `test-${Date.now()}`, ...newEvent });
await route.fulfill({
body: JSON.stringify(newEvent)
});
}
// PUT, DELETE도 마찬가지로 메모리 조작
});
}
}
사실 e2e를 적게 구현할 생각이라서 저렇게 만능 인터셉터로 구현했지만, 시간이 더 있다면 분리된 인터셉터로 구현하지 않을까 싶습니다. 핵심은 POST로 생성한 데이터가 즉시 GET 요청에서 조회 가능하다는 점입니다.(일정 추가시에 저장한 데이터를 바로 확인할 수 있어야 하기 때문) 마치 실제 백엔드처럼 상태가 유지되면서도, 테스트가 끝나면 깨끗하게 초기화됩니다.
vitest와 plawright 환경 분리
// vite.config.ts - 단위/통합 테스트
test: {
setupFiles: './src/setupTests.ts', // MSW 설정
include: ['src/**/*.{test,spec}.{js,ts,tsx}'],
exclude: ['tests/**/*'], // E2E 테스트 제외
}
// playwright.config.ts - E2E 테스트
testMatch: /.*\.spec\.ts$/, // E2E 전용 패턴
webServer: {
command: 'npm run dev', // 실제 개발 서버 사용
}
위와 같이 구성해서 msw 기반의 단위/통합 테스트와 독립적인 환경을 구성하도록 했습니다.
e2e를 ci에서 돌아가게 구현하기
name: E2E Tests
on:
push:
branches: [main, develop]
pull_request:
branches: [main, develop]
jobs:
e2e-tests:
runs-on: ubuntu-latest
env:
CI: true
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup pnpm
uses: pnpm/action-setup@v2
with:
version: latest
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'pnpm'
- name: Install dependencies
run: pnpm install
- name: Install Playwright Browsers
run: npx playwright install chromium
- name: Install system dependencies for Playwright
run: npx playwright install-deps chromium
- name: Run E2E tests
run: pnpm run test:e2e
env:
CI: true
위 처럼 ci시 워크플로우를 작성해서 ci환경에서도 e2e가 돌아가도록 작성했습니다.
과제 피드백
여태까지 과제중에서 제일 열려있는 요구사항의 과제였다고 생각하는데요, 의도 자체가 모호한 요구사항을 구체화해서 기능 구현을 하는 것이기 때문에 아쉽진 않았고 오히려 자유도가 높아져서 구현하기 재밌었던 것 같습니다.
소요시간은..예상했던 20시간(순시간)에 비해서...삽질하는 시간이 길었던것같고 순시간으로 따지면 30시간정도..걸린것 같습니다. 다행인건 비매드를 사용하면서 지금도 리팩토링을 돌려두고 이렇게 pr을 작성하고 회사일을 할 수 있었다는 것인데요. 어떻게 보면 비매드의 소중함(?)을 알 수 있었던 과제였다고 생각합니다.
리뷰 받고 싶은 내용
- bmad의 yolo옵션 사용해보셨나요? 이번 pr에서 제가 초반에도 인간의 개입을 언급했는데요, yolo옵션이 알아서해준다고는 하지만 아무리 비매드 에이전트가 절차적으로 잘 구성되어있다고 해도 인간의 개입없이는 명확하게 원하는 요구사항을 수정할 수 없을거 같아서요. 저는 비매드가 처음이기도 하고 돈 없는 거지라서... 함부로 사용하기 어려운데 코치님은 연봉 3억이셔서 써보셨을수 있을거같아 여쭤봅니다.
과제 피드백
한편의 아름다운 글이네요. 비매드에 흠뻑 빠지셨던 것 같아서 좋습니다.
bmad의 yolo옵션 사용해보셨나요? 이번 pr에서 제가 초반에도 인간의 개입을 언급했는데요,
저는 따로 쓰지는 않아봤습니다! 근데 종종 어느정도 작업이 안정적이게 실행된다라는 것을 인지한다면 인간의 피드백 없이 자유롭게 하라고 뒀던 적들은 있었던 것 같아요.
하지만 아무리 비매드 에이전트가 절차적으로 잘 구성되어있다고 해도 인간의 개입없이는 명확하게 원하는 요구사항을 수정할 수 없을거 같아서요.
이 부분은 저도 동일한 생각입니다. 그래서 작업의 단위를 작게 갖고 유저의 피드백이 명확하게 있는 상태에서 사용하는게 필요하다고 생각했어요. 결국 '전부 다 한번에 못해주니까 의미없어' 보다는 '이정도만 개입해도 이만큼이나 해주잖아' 관점 때문에 AI의 중요성이 올라가는거 아닐까 싶어요.
전반적으로 테스트와 기능 모두 잘 작성되어 있고 필요한 테스트도 잘 작성되어 있는것 같습니다! 문서들도 잘 정리했고, 체크리스트로 만들어서 사용되기 좋게 작성된 것 같아요. e2e테스트도 잘 작성되었구요.
작업의 단위단위를 작게 가져가는게 아직은 필요한 핵심인것 같아요! 앞으로도 회사에서나 과제하시는데 있어서 잘 활용하고 좋은 의견 나눠보면 좋을것 같습니다~ 고생하셨어요~