본문 바로가기
항해 플러스

항해 플러스 프론트엔드 7~8주차 회고(테스트 코드와 TDD)

by 이의찬 2025. 8. 30.

회고에 앞서...

사실 테스트 코드 챕터는 '테스트 코드 작성하기'와 'TDD' 두 가지 주제로 이루어져 있지만, 나는 지난 주에 오픈 소스 기여와 개인 사정이 겹치는 바람에 첫 주제인 '테스트 코드 작성하기' 과제를 정상적으로 진행하지 못했다... 고로 TDD 주차에 테스트 코드와 TDD를 모두 학습하려고 노력했다.

그 덕분에 이번 주차에서 TDD를 경험하는 것 외에도 이슈를 해결하며 '적절한 테스트 작성 방법'과 '접근성을 고려해서 좀 더 의미 있는 테스트를 작성하는 방법'등에 대해서 배울 수 있었다.

혹시 왜 테스트코드를 써야하는지 모르겠다면? 아래의 글을 참고해주세요.
 

코드 가꾸기 - 테스트 코드

1. Why?프론트엔드에서 테스트 코드를 작성하는 비율은 상대적으로 높지 않다. 삼성청년SW아카데미 내에서 3차례 프로젝트를 진행하며 그 때마다 10팀 씩, 총 30팀을 봤지만 한 팀도 테스트를 도입

cksxkr5193.tistory.com

기술적으로 새로 알게 된 부분

TDD 경험

이번 과제의 핵심 중 하나가 테스트를 먼저 작성하고 이를 기반으로 개발하는 TDD를 체험해보는 것이었는데, 구현도 없는 상태에서 테스트부터 작성한다는 게 어색하게 느껴질 수 있지만 TDD는 AI 시대에 정말 유용한 개발 방법론이라고 생각한다.

요구사항만 명확히 정리되어 있다면 이를 기반으로 "이런 테스트를 작성해줘"라고 요청하면 되니까 테스트를 작성하는 건 AI의 도움을 받기가 상대적으로 쉽고, 그리고 그 테스트를 기반으로 구현을 하는 것도 더 명확한 방향성을 가지기 때문이다.

// 1단계: 실패하는 테스트 작성 (Red)
it('31일에 매월 반복을 선택하면 매월 31일에만 생성된다', async () => {
  // 요구사항을 명확히 한 테스트
  expect(eventList.getByText('2025-01-31')).toBeInTheDocument();
  expect(eventList.queryByText('2025-02-31')).not.toBeInTheDocument(); // 2월 31일은 없음
});

의미 있는 테스트 작성

접근성 고려

data-testid를 제거하고 접근성 속성 기반으로 마이그레이션하는 과정에서, 테스트하기 쉬운 코드가 실제로 사용하기도 쉬운 코드라는 걸 체감했다. aria-label, role 같은 속성들이 단순히 테스트를 위한 게 아니라 실제 사용자 경험 향상에 기여한다는 점이 인상적이었다.

도메인 로직의 복잡성 고려

달력 관련 로직에서 31일 매월 반복, 윤년 2월 29일 처리 같은 것들이 단순해 보이지만 실제로는 정말 많은 예외 상황을 고려해야 한다.

특히 이런 도메인 로직을 테스트할 때는 일반적인 케이스보다 예외 케이스가 더 중요할 수 있다는 걸 배웠다. 윤년이 4년에 한 번 오는 특수한 상황인데, 그런 상황에서의 사용자 경험이 실제로는 더 중요할 것 같다.

테스트 구현 전략

우선 이번 과제에서 어떤 전략을 사용할 것인가에 대한 내 주장은 테스트 트로피 전략이였는데, 그 이유는 다음과 같다. 

  1. 프론트엔드는 "기능 바꿔주세요"라고 하면 화면에서 바로 그 변화가 보여야 하는데, 그런 사용자 경험은 통합 테스트 단계에서 명시되기 때문에 이 방식이 더 적절하다고 생각했다. 또한 테오가 말했듯이 결국 문제는 액션에서 생기는데, 유닛 테스트만으로는 "진짜 잘 작동하나?"에 대한 확신을 얻기 어렵다.
  2. 특히 과제 환경 자체가 새로운 반복 일정 기능이 추가되는 등 요구사항이 계속 변경되는 상황이라 E2E나 시각적 회귀 테스트를 작성하는 것은 비효율적이고, 실제 개발 환경에서도 당장 기능이 정상적으로 동작하는 것이 우선이므로 유닛 테스트를 우선적으로 작성하는 것보다는 통합테스트로 요구사항이 적절하게 동작하는지부터 확인하는 것이 더 합리적이라고 생각했다.

합의된 테스트 전략과 그 이유

다른 팀원들과 현재 프로젝트 상황을 고려하여 테스트 트로피 전략에서 E2E 테스트를 제외하고 진행하는 것이 적절하다고 판단했다. 다음과 같은 근거들을 바탕으로 결정했다.

개발 단계 특성상의 고려사항

현재 개발 중인 프로덕트로서 요구사항과 기능 명세가 지속적으로 변경될 가능성이 높다. 이런 환경에서 E2E 테스트는 변경사항에 대한 유지보수 비용이 과도하게 높아질 수 있어 투자 대비 효율성이 떨어진다.

테스트 시나리오의 중복성 문제

애플리케이션의 복잡도 수준에서는 통합 테스트와 E2E 테스트가 검증하는 시나리오가 크게 다르지 않다. 동일한 테스트 케이스를 다른 레이어에서 중복으로 작성하는 것보다, 통합 테스트에 집중하여 테스트 코드 작성의 효율성을 높이는 것이 바람직하다.

사용자 여정의 단순성

아직 프로덕트가 성숙 단계에 이르지 않아 복잡한 멀티 스텝 사용자 플로우나 크로스 피처 시나리오가 많지 않다. 현재로서는 개별 기능 단위의 검증만으로도 충분한 커버리지를 확보할 수 있다.

모킹 인프라의 성숙도

MSW를 통한 API 모킹이 잘 구축되어 있어, 통합 테스트 레벨에서도 실제 네트워크 의존성 없이 안정적인 테스트 환경을 제공할 수 있다. 이는 E2E 테스트의 주요 장점 중 하나인 실제 환경 시뮬레이션의 필요성을 크게 줄여준다.

외부 의존성의 단순함

현재 외부 시스템과의 연동 포인트가 복잡하지 않아, 통합 테스트 레벨에서의 모킹으로도 충분히 엣지 케이스들을 커버할 수 있다. 따라서 실제 브라우저 환경에서의 E2E 검증이 추가로 제공하는 가치가 제한적이다.

결론

이러한 상황에서는 효율성을 고려하여 정적 분석 도구와 통합 테스트에 집중하면서, 향후 프로덕트가 성숙해지고 복잡한 사용자 여정이 등장할 때 E2E 테스트 도입을 재검토하는 것이 현실적인 접근법이라고 생각했다.

문제 해결하기

Testing Library와 현실적 타협점 찾기

우선 테스트를 작성하면서 처음에는 각각의 반복 인스턴스가 개별 이벤트로 생성될 것이라고 예상하고 테스트를 작성했다.

expect(eventList.getByText('2025-01-31')).toBeInTheDocument();
expect(eventList.getByText('2025-03-31')).toBeInTheDocument();
expect(eventList.queryByText('2025-02-31')).not.toBeInTheDocument();

하지만 실제 구현에서는 반복 일정이 하나의 이벤트 객체로 관리되고 있었고, 이를 수정하기보다는 현재 구현에 맞춰 테스트를 조정하는 것이 더 현실적이라는 걸 배웠다.

expect(eventList.getByText('2025-01-31')).toBeInTheDocument();
expect(eventList.getByText(/반복:/)).toBeInTheDocument();
expect(eventList.getByText(/월마다/)).toBeInTheDocument();

"Found multiple elements" 해결하기

FormControlLabel과 Checkbox가 둘 다 같은 라벨을 가지면서 MUI 컴포넌트들이 복잡한 DOM 구조를 만들면서 발생하는 "Found multiple elements" 에러가 발생해 Testing Library가 혼란스러워했다.

이 과정에서 쿼리 우선순위를 체계화할 수 있었다.

1. getByRole + 정규식

MUI는 접근성을 잘 지켜서 role이 명확하고, 정규식을 쓰면 텍스트 변화에도 유연하게 대응할 수 있다. 실제 사용자가 스크린 리더로 접근하는 방식과 동일해서 가장 안정적이다.

2. getElementById

ID를 사용해서 확실하게 하나만 선택할 수 있다. 하지만 MUI 컴포넌트들은 기본적으로 ID를 자동 생성하거나 없는 경우가 많아서 개발자가 직접 줘야 하는 추가 작업이 필요하다.

3. getByLabelText

MUI의 복잡한 DOM 구조 때문에 라벨 텍스트가 여러 곳에 중복으로 나타나서 Testing Library가 혼란스러워한다. 가장 예측하기 어렵고 불안정하다.

그리고 특정 DOM 요소 안에서만 검색 범위를 제한하는 within()로 스코프를 제한하는 방법도 학습할 수 있었다.

// 특정 컨테이너를 먼저 찾기
const eventItem = screen.getByText('특정 이벤트 제목').closest('[role="article"]');

// 그 컨테이너 안에서만 검색하기
const deleteButton = within(eventItem).getByLabelText('Delete event');

이런 방법들을 통해서 반복 일정이나 여러 개의 동일한 컴포넌트가 있을 때 발생하는 "Found multiple elements" 에러를 해결했다.