Next.js App Router E2E 커버리지와 istanbul ignore 주석 분석
배경
Next.js App Router 프로젝트에서 E2E 커버리지를 운영하다 보니 istanbul ignore 주석이 160개 가까이 쌓였다. 이 주석들이 왜 생기는지, 줄일 수 있는지, 지금 구조가 이 스택에서 할 수 있는 최선인지 분석했다.
1. 수집 파이프라인
build:e2e
→ webpack 빌드 (전체 소스맵 + 압축 끔)
start:e2e
→ NODE_V8_COVERAGE=.v8-coverage 서버 쪽 V8 커버리지 파일 기록
→ node --inspect=9230 CDP 포트 개방
E2E 테스트 실행
finalize-coverage
→ 브라우저 V8 커버리지 + 서버 V8 커버리지 수집
→ 소스맵 매핑, V8 → Istanbul 형식 변환
→ html / lcov / json / text-summary 리포트
// next.config.ts - 커버리지 빌드에서만 적용
if (!dev && process.env.E2E_COVERAGE) {
config.devtool = 'source-map';
config.optimization = { ...config.optimization, minimize: false };
}
Istanbul 계측 vs V8 수집
| 항목 | Istanbul(빌드 계측) | V8(nextcov) |
|---|---|---|
| 계측 시점 | 빌드 시 AST에 카운터 삽입 | 런타임에 엔진이 실행 범위 기록 |
| 수집 | window.__coverage__에 누적 | CDP, NODE_V8_COVERAGE |
| 형식 변환 | 불필요 | V8 → Istanbul 변환 필요 |
| 원본 매핑 | 계측 시점의 AST 기준 | 빌드 결과물 + 소스맵에 의존 |
Istanbul은 원본 AST에 직접 카운터를 넣기 때문에 분기·함수 단위가 원본 코드와 정확히 맞는다. V8 방식은 빌드된 코드의 실행 범위를 소스맵으로 되짚어 가므로, 번들러 변환과 소스맵 품질에 따라 정밀도가 달라진다. (최근 V8 → Istanbul 변환 도구들은 AST 기반으로 정밀도를 많이 끌어올렸지만, 이 프로젝트에서는 아래 패턴에서 매핑 오차가 남았다.)
2. istanbul ignore 주석 분류
| 그룹 | 사유 | 개수 |
|---|---|---|
| A | 소스맵 매핑 정밀도 한계 (인라인 JSX 콜백, async continuation) | 41 |
| B | 수집 한계로 보이는 패턴 (useEffect 본문, JSX prop의 화살표 함수) | 21 |
| C | 페이지 이탈 시 커버리지 소멸 (router.back() 직후 코드) | 4 |
| D | 하드웨어 의존 (카메라 등 실기기 전용) | 12 |
| E | 논리적으로 도달 불가 (UI나 Suspense 쿼리가 보장하는 방어 코드) | 82 |
A·B가 생기는 이유
onChange={(value) => doSomething(value)} // 인라인 JSX 화살표 함수
useEffect(() => {
(async () => {
const result = await fetchData();
setState(result); // await 이후 이어지는 코드
})();
}, []);
이런 코드는 번들 단계에서 주변 코드와 함께 변환·인라인되어, 소스맵으로 되짚을 때 실행 범위가 원본의 정확한 위치에 대응되지 않는 경우가 있었다. 실제로 실행되는 것은 확인했으므로 "실행 확인됨" 사유와 함께 ignore 처리했다.
3. 빌드 시점 Istanbul 계측을 쓰지 못한 이유
A·B를 없애는 가장 확실한 방법은 빌드할 때 Istanbul로 계측하는 것이다. 하지만 App Router에서는 막혔다.
- Babel로 전환하면 Server Actions 빌드가 실패한다.
babel-plugin-istanbul을 쓰려고.babelrc를 추가하면 Next.js가 SWC 대신 Babel을 사용하는데, App Router의 Server Actions는 SWC 변환에 의존한다. (참고 글) - SWC 계측 플러그인도 App Router에서 깨졌다.
swc-plugin-coverage-instrument를 app 디렉터리와 함께 쓰면 동작하지 않는다는 이슈가 있다. (vercel/next.js #42580) .babelrc없이 webpack 설정에서babel-loader를enforce: 'post'로 끼워 넣는 우회책은 이론상 가능하지만, App Router에서 검증된 사례를 찾지 못했다.
그래서 이 스택에서는 V8 기반 수집이 현실적인 선택이었다.
4. 결론
| 항목 | 판단 |
|---|---|
| nextcov(V8) | 유지. App Router + Server Actions에서 쓸 수 있는 방법 |
| A·B (62개) | 유지. 실행은 확인했고 도구 한계를 사유와 함께 기록 |
| C (4개) | 유지. 페이지 이탈 후 수집 불가 |
| D (12개) | 유지. 실기기 전용 기능 |
| E (82개) | 개별 재검토 대상 |
| 빌드 시점 Istanbul | 현재 불가 |
E 그룹은 다시 볼 여지가 있다. "정말 도달할 수 없는 코드"라면 커버리지에서 제외하기보다, 타입으로 불가능한 상태를 막거나 방어 코드를 지우는 편이 나을 수 있다. 반대로 API 응답 이상처럼 실제로 일어날 수 있는 방어 코드라면 모킹으로 E2E나 단위 테스트에서 검증하는 것이 맞다. ignore 주석은 사유를 반드시 함께 적어, 나중에 도구가 개선되면 다시 걷어낼 수 있게 한다.