0. 들어가며
2년째 멈춰 있던 내 개발 블로그 .... 반성 또 반성
벌써 만 3년 차 개발자가 되었는데(말도 안된다!!!!) 그동안 겪었던 여러 고민과 경험을 기록해 보려고 한다.
미래의 나에게 동기부여가 될 수 있고, 또 누군가에게는 작은 공감이 되길 바라면서!
이번 글에서는 프로젝트를 하면서 테스트 코드를 도입하게 된 배경과, 고민했던 전략들을 솔직하게 풀어내려고 했다.
글이 조금 길고 서술 위주일 수 있지만, 내 이야기를 나누며 함께 생각해 보는 시간이 되면 좋겠다.
1. 도입 배경
작년 초까지 플랫폼 서비스를 개발하면서, 서비스 특성상 안정성에 대한 고민이 깊어졌다. 이 고민은 자연스럽게 테스트 주도 개발(TDD) 방법론에 대한 관심으로 이어졌고, 그 결과 NextStep의 테스트 코드 수업을 수강하게 되었다. 처음엔 Jest를 사용해서 테스트 코드를 작성하는 과제 수행에 집중하느라 본질에 대해 깊이 생각하지 못했지만 TDD가 단순한 안정성을 넘어 지속 가능한 코드 품질 유지에 매우 중요한 방법임을 깨달았다. 특정 기능 요구사항에 맞는 테스트 케이스를 명확히 작성하고, 단일 책임 원칙에 따라 독립적인 함수를 만들면 컴포넌트에서 쉽게 재사용할 수 있고 이것이 곧 좋은 구조적 추상화로 이어진다는 생각이 들었다.
그래서 욕심만큼은 당장 테스트 코드를 도입하고 싶었다.
하지만 당시 개발 일정이 매우 촉박했고 여러 우선순위가 겹치다 보니 테스트 코드 도입은 자연스럽게 뒤로 미뤄졌다.
그러다 작년 중순부터 신규 서비스를 담당하면서 상황이 바뀌었다. 이 서비스는 고객 요구사항에 따라 기능과 API 응답 구조가 잦은 변화를 겪었고 변화가 빠른 만큼 공유가 원활하지 않은 부분도 발생해 필드 누락이나 계산 결과 불일치가 반복됐다. 이 과정에서 오는 피로도는 날이 갈수록 증가됐다.
나는 이 정보 격차를 해소하기 위해 내가 할 수 있는 것은 무엇일지 고민했고, 결국 지금이 테스트 코드 도입이 적기라고 확신했다.
그래서 바로 팀장님께 반복되는 위 문제를 위해 도입하려고 한다는 의견을 드렸고 감사하게도 팀장님은 내 의견에 동의해주셔서 도입할 수 있게 됐다. 🤩
또, 팀장님뿐만 아니라 나의 고민과 테스트 코드 도입에 대한 확신을 팀원들과 공유하기 위해 문서를 작성했고, 함께 고민하고 의견을 나누는 문화를 만들고자 했다.
2. 테스트 코드를 향한 나의 전략
그렇게 테스트 코드를 도입하게 되었지만, 이제 또 어떻게 작성할 것인지가 또 고민이었다.
(저처럼 고민이 많은 분들 계신가요? 😂)
‘유의미한 테스트 코드란 무엇일까?’
‘빠르게 진행되는 일정 속에서 테스트 코드 작성이 병목이 되진 않을까?’
하지만 반복되는 문제점이 명확했기에 더 이상 고민만 할 수 없었다.
‘일단 해보자!’라는 마음으로 빠르게 전략을 세웠다.
- 유의미한 테스트 코드 작성
- 지라 티켓을 통해 반복적으로 발생한 이슈에 대응할 수 있도록 한다.
- 백엔드 응답 구조 변경으로 인한 필드 누락 가능성이 높은 부분을 집중적으로 검증한다.
- 점진적 도입과 확대
- 모든 기능을 한꺼번에 적용하기보다 변동 가능성과 이슈가 큰 핵심 기능부터 우선 적용한다.
- 접속 요청 조회, 정책, 보고서 등 중요 기능을 중심으로 유틸 함수 단위 테스트부터 시작해 점차 범위를 넓혀나간다.
- 빠른 실행과 피드백
- 고민보다 실행에 집중하고, 빠르게 테스트 코드를 작성하며 팀원들의 피드백을 적극적으로 수용한다.
- 다양한 테스트 방법론을 적용하는 것보다 시행착오를 겪으면서 개선해 나가고 성장하는 자세를 갖는다.
- 도구 선택과 환경 최적화
- Vite 기반 프로젝트 특성에 맞게 Vitest를 선택해 빠르게 설정한다.
- API 모킹을 위해 MSW를 도입해 안정적인 테스트 환경을 구축한다.
3. 프로젝트 적용 예시
보고서와 통계 페이지의 계산 결과가 달라지는 문제를 해결하기 위해 동일한 계산 결과가 나오는지 검증하는 테스트 코드를 작성했다.
간단히 말해서 두 함수가 같은 값을 반환하는지를 비교하는 테스트 예시이다.
// example) Statistics.test.ts
describe('통계 페이지 통합테스트', () => {
const TOTAL_VERIFICATION_REQUESTS = 150;
const PASSED_REQUESTS = 40;
test('통계 페이지와 보고서의 탐지율 데이터는 동일해야함', () => {
const statisticsData: Pick<StatisticsItem, 'totalCount' | 'passedCount'> = {
totalCount: TOTAL_VERIFICATION_REQUESTS,
passedCount: PASSED_REQUESTS,
};
const reportData: Pick<ReportItem, 'totalCount' | 'passedCount'> = {
totalCount: TOTAL_VERIFICATION_REQUESTS,
passedCount: PASSED_REQUESTS,
};
const statisticsRatio = getBotDetectRatio(
statisticsData.totalCount - statisticsData.passedCount,
statisticsData.totalCount,
);
const reportRatio = calculatePercentages(
reportData.totalCount - statReportData.passedCount,
reportData.totalCount,
);
expect(statisticsData).toBe(reportRatio);
});
});
이 테스트가 조기에 이슈를 감지하는 데 유의미하다고 생각하지만 분명 아래와 같은 한계도 존재한다고 생각한다.
- getBotDetectionRatio와 calculatePercentages 함수는 이름만 다르고 내부 로직은 동일해서 유틸 함수 계산 값 일치 검증 자체는 크게 의미가 없을 수 있다. (이 부분은 동일한 유틸 함수를 사용하도록 변경이 필요하다.)
- 실제 API 응답 구조를 반영하지 않아, 데이터 변화에 따른 영향을 완벽히 검증하지 못한다.
- 타입에 필드가 추가돼도, 해당 필드가 실제 코드에서 참조되지 않으면 테스트 실패가 발생하지 않아 문제를 놓칠 위험이 있다.
특히, 2, 3번째 문제는 MSW를 도입해 실제 API 응답을 모킹하면 더 나아지지 않을까 생각이 들어서 개선해 나가보려고 한다!!
4. MSW 도입 후 테스트 코드 개선 (25.08.04)
import { setupServer } from 'msw/node';
import { statisticsHandlers } from '~/features/statistics/mocks/handlers';
import { reportHandlers } from '~/features/report/mocks/handlers';
export const mockServer = setupServer(...statisticsHandlers, ...reportHandlers);
나는 node 환경의 setupServer 함수를 사용했다. 그 이유는 간단했다.
DOM API가 필요하지 않았고 네트워크 요청 흐름을 모킹하여 비즈니스 로직이 올바르게 동작하는지 통합적으로 검증하는 데 초점을 두었기 때문이다. node 환경에서 브라우저 없이 서버 요청을 가로챌 수 있어서 내가 작성하려는 유닛 테스트와 통합 테스트에 적합하다고 생각했다.
import { http, HttpResponse } from 'msw';
// 통계 데이터 핸들러 예시
export const statisticsHandlers = [
http.get('*/v1/stat/:id', () => {
return HttpResponse.json({ data: statiticsResponse });
}),
];
// 보고서 데이터 핸들러 예시
export const reportHandlers = [
http.get('*/v1/report/:id', () => {
return HttpResponse.json({ data: reportResponse });
}),
];
위와 같이 핸들러를 설정한 후 미리 작성한 mock data를 넣어줬다.
그런 다음 MSW가 모킹한 데이터를 가지고 테스트 코드를 작성했다. 계산식에 필요한 데이터 필드가 있는지 검증하고, 그 값이 있을 때 계산한 값이 동일한지 보여주는 것으로 변경했다.
describe('봇 통계 통합 테스트', () => {
test('서로 다른 API에서 받아온 데이터로 계산한 탐지율이 일치한다', async () => {
const statData = await fetchStat('123');
const reportData = await fetchReport('123');
expect(statData).toHaveProperty('secondaryVerificationRequests');
expect(statData).toHaveProperty('passedRequests');
expect(reportData).toHaveProperty('secondaryVerificationRequests');
expect(reportData).toHaveProperty('passedRequests');
const statRatio = getBotDetectRatio(
statData.secondaryVerificationRequests - statData.passedRequests,
statData.secondaryVerificationRequests,
);
const reportRatio = calculatePercentages(
reportData.secondaryVerificationRequests - reportData.passedRequests,
reportData.secondaryVerificationRequests,
);
expect(statRatio).toBe(reportRatio);
});
});
API 응답 구조와 동일한 목데이터를 MSW로 모킹하여 테스트에 활용했다는 점이 이전 코드와 가장 큰 차이점이다.
이를 통해 데이터 필드가 변경되었을 때도 빠르게 감지할 수 있고, 실제 API 응답과 유사한 환경에서 받아온 값을 기반으로 계산 결과를 검증할 수 있다.
즉, 하드코딩된 데이터가 아닌 서버에서 오는 데이터 흐름을 모방하여 더 신뢰성 높은 통합 테스트가 가능해졌다! 🥹🥹
5. 마무리
테스트 코드를 도입한 목적은 기능별로 반복적으로 발생하는 이슈와 유관 부서와의 정보 누락 격차를 줄이기 위함이었다.
아직 도입 초기 단계라 주요 기능 일부에만 적용된 상태여서 큰 효과를 체감하기엔 이르지만, QA 과정에서 발생할 수 있는 문제 의존도를 낮추고 서비스 안정성을 확보하는 데 큰 도움이 될 것이라고 기대하고있다.
테스트 작성 과정에서 테스트 시나리오 작성 방식에 대해 고민하는 계기가 됐고, 프로젝트 개선의 첫걸음을 내딛은 것 같아 의미가 컸다.
앞으로도 많은 시행착오가 있겠지만, 완벽해야 한다는 욕심을 버리고 꾸준히 테스트 코드를 작성하면서 더 빠르고 안정적인 테스트 환경을 구축하는 것을 목표로 삼았다.
'개발회고📚' 카테고리의 다른 글
2022년, 지난 날의 회고 (0) | 2023.02.12 |
---|---|
[패스트캠퍼스] Rubber duck debugging : 두 번째 (3) | 2021.08.06 |
[패스트캠퍼스] Rubber duck debugging : 첫 번째 (0) | 2021.07.30 |