Загружаем каталог…
Загружаем каталог…
apps/web-e2e 를 next dev (동적) 단일 기반에서 static-server.mjs (정적) 병행 기반으로 바꾸는 과정에서 겪은 이슈들과 대응, 남은 항목을 정리한다. 작업 진행 순서를 참고하되, 내용은 주제별로 묶었다. 1. 배경 — 왜 static 기반이 필요했나 이 프로젝트( output: "export" , 정적 익스포트)는 프로덕션엔 서버가 없다. 그런데 e2e는 지금까지 next dev (라이브 서버) 기준으로만 돌아갔다. 팀 내 피드백으로는: "이전에 이 프로젝트는 SSR 사용했었음 → 모든 e2e 환경은 여기에 맞춰져있음 → static으로 바꾸면서 e2e에서 오류가 보인 것" 즉 next dev (동적)로는 "실제 배포에서 사용자가 잘못된 URL로 들어왔을 때" 같은 시나리오를 정확히 재현할 수 없었고, 이게 not-found 관련 문제의 근본 원인이었다. 2. 구축한 것 — static 기반 e2e 파이프라인 파일 역할 apps/web-e2e/static-server.mjs apps/web/out (빌드 산출물)을 그대로 서빙하는 최소 정적 파일 서버. 없는 경로는 실제 404.html 을 404 상태코드로 반환 apps/web-e2e/playwright.config.ts E2E_STATIC=true 면 static-server.mjs , 아니면 기존 next dev 를 띄우도록 webServer.command 분기 apps/web-e2e/project.json e2e:local 타겟 추가 — E2E_STATIC=true 로 playwright 실행 루트 package.json pnpm e2e:local 스크립트 추가 검증 결과(static 기반) : 전체 스위트 반복 실행 시 not-found·navigator 포함 모두 안정적으로 통과(자세한 내용은 5절). 3. not-found(500 ↔ 404) 이슈 — 해결됨 3-1. 환경별로 다른 게 정답인 이유 환경 정답 이유 next dev 500 Next 자체 안전장치( next-dev-server.js )가 output:"export" + generateStaticParams() 에 없는 id를 감지해 의도적으로 런타임 에러를 던짐 static( static-server.mjs ) 404 파일이 실제로 없어서 나는 진짜 404 — 실제 배포와 동일 왜 dev에서는 404가 아니라 500이 뜨는가 — 상세 generateStaticParams() 는 빌드 시점에 "이 동적 라우트( [id] )에 대해 어떤 id들의 파일을 실제로 만들지" 목록을 정한다. 이 목록에 없는 id는 정적 빌드에서 파일 자체가 안 만들어지므로, 실제 배포에서는 그냥 파일이 없어서 404가 뜨는 게 정상이다. 문제는 next dev 가 이 상황을 다르게 처리한다는 점이다. next dev 는 기술적으로는 목록에 없는 id도 그냥 렌더링해서 보여줄 수 있다(코드 자체는 있으니까). 그런데 일부러 그렇게 하지 않고 런타임 에러를 던지도록 만들어져 있다: // node_modules/next/dist/server/dev/next-dev-server.js:585-594 (요약) if (this.nextConfig.output === 'export') { if (!prerenderedRoutes.some((item) => item.pathname === urlPathname)) { throw new Error(`Page "${page}" is missing param "${pathname}" in "generateStaticParams()"...`); } } 이 체크는 dynamicParams 설정과 무관하게, 오직 output === 'export' 인지만 보고 무조건 실행 된다. 의도는 이렇다 — 만약 dev가 이 상황에서도 친절하게 페이지를 보여주면, 개발자는 "잘 되네?"라고 착각하고 넘어간다. 근데 실제 빌드·배포하면 그 페이지는 파일 자체가 없어서 조용히 깨진다. 그래서 Next는 "나중에 조용히 깨지느니 지금 시끄럽게 알려주자"는 의도로, dev 단계에서 일부러 크게 에러(500)를 던지도록 설계했다. 즉 이 500은 코드 버그도, dev의 결함도 아니라 Next.js의 의도된 안전장치다. 테스트 스펙에 환경별 기대값을 분기하는 코드로 반영: const expectedStatus = process.env.E2E_STATIC === 'true' ? 404 : 500; expect(response?.status()).toBe(expectedStatus); 3-2. dev 전체 스위트에서만 나던 flaky — 원인과 해결 증상 : 이 테스트를 단독으로 돌리면 항상 500(정상). 근데 dev로 전체 스위트 를 같이 돌리면 가끔 404가 떴다(실패로 표시). 원인 : next dev 는 라우트를 요청 시점에 한 번만 컴파일하고 캐시한다. 다른 스펙( product-detail.spec.ts 등)이 같은 [id] 라우트를 유효한 id로 먼저 방문하면, 그 라우트가 컴파일되어버려서 — 그 뒤로는 잘못된 id로 접근해도 안전장치(500)가 안 걸리고 그냥 404가 남. 서버 프로세스의 영구적 상태 변화 라 재시도로 해결 불가능. 왜 "수직(격리) 구조"를 택했는가 — 다른 flaky 이슈(5절)에서 나온 개념을 여기 적용한 것 이 테스트는 CPU 타이밍 문제가 아니라 "다른 테스트가 먼저 그 라우트를 건드렸는가"라는 이진 상태 문제 다. 그래서 retries/workers/타임아웃 조정(5절 이슈에 썼던 방법들) 자체가 원천적으로 안 통한다 — 같은 서버에 재시도해봤자 여전히 같은 서버, 여전히 404. "수직구조로 떼어낸다"는 개념 자체는 원래 5절의 flaky 이슈를 놓고 나온 팀 내 피드백 이었다 — "이 테스트만 타임아웃을 주게 되면 다른건 병렬구조인데 얘만 수직구조가 될 것, 스케줄을 top/bottom 어디에 둘 것인지"라는 우려, 그리고 나중 피드백("진행 중인 것들 finish된 걸 보고, 환경 클리어한 이후 일정 시간 지나서 테스트")은 CPU 경합을 완화하려는 취지 였다(다른 작업이 다 끝나서 CPU 여유가 생긴 뒤에 타이밍 민감한 테스트를 돌리자는 것). 이 개념(전체와 분리해서 따로 돌린다)을 원인이 다른 not-found에도 같은 틀로 가져와 적용 한 것이다. not-found에선 왜 top/bottom 순서 자체가 중요하지 않은지 : 격리 실행이 실제로는 sh -c '...; ...' 로 완전히 별도의 playwright test 프로세스 를 띄우는 방식이라(첫 번째 명령이 끝나면 그 웹서버는 종료되고, 두 번째 명령이 자기 서버를 완전히 새로 켠다) — 이 테스트가 메인 스위트보다 먼저 도나 나중에 도나, 어차피 한 번도 안 건드려진 새 서버를 상대하는 건 똑같다. 그래서 "나중(bottom)"으로 둔 건 지시를 그대로 따른 게 아니라, 그냥 메인 스위트를 먼저 보여주고 이 격리 확인을 뒤에 덧붙이는 게 자연스러워서 고른 순서 일 뿐, 순서 자체가 결과에 영향을 주는 요인은 아니다(둘 다 서버가 항상 새것이라 어느 쪽이든 500이 보장됨, 실제로 검증도 이 순서로만 했다). 5절 이슈 쪽은 다르다 : "환경 클리어 후 나중에" 라는 그 원래 취지대로 테스트해봤지만(완전 격리, workers=1), 5절에서 보듯 여전히 60% 실패 가 나와 이 추론이 그쪽에서는 실제로 검증되지 않았다. 해결 : 이 테스트를 전체 스위트에서 완전히 분리 — e2e 실행 타겟을 2단계로 재구성: "command": "sh -c 'playwright test --grep-invert \"존재하지 않는 상품\"; e1=$?; playwright test --grep \"존재하지 않는 상품\"; e2=$?; exit $((e1>e2?e1:e2))'" 1단계: 이 테스트를 제외한 나머지 실행 2단계: 이 테스트만 단독 실행(항상 컴파일 안 된 새 서버 상대) sh -c 로 감싸 두 단계 모두 항상 실행되고, 둘 중 하나라도 실패하면 전체 종료 코드에 반영됨 pnpm e2e 한 번 으로 자동 실행됨(수동으로 두 번 돌릴 필요 없음). 검증 완료 — 반복 실행 시 이 테스트가 항상 500/passed로 격리 확인됨. 주의 : 이 격리 구조는 pnpm e2e (nx 타겟)를 통해서만 적용된다. pnpm exec playwright test --ui 처럼 nx를 거치지 않고 직접 실행하면 이 분리 효과가 없다 — 같은 효과를 원하면 --grep-invert "존재하지 않는 상품" 을 직접 붙여야 한다. 실제 사용 명령어 정리 상황 명령어 평소 전체 확인(자동, 신뢰 가능) pnpm e2e — 격리 구조 자동 적용됨 --ui 로 전체 다 보고 싶을 때(이 테스트는 가끔 빨갛게 뜰 수 있음, 이유 있는 것이니 무시 가능) pnpm exec playwright test --ui --ui 로 이 테스트만 빼고 나머지 안정적으로 보고 싶을 때 pnpm exec playwright test --ui --grep-invert "존재하지 않는 상품" 이 테스트만 단독 확인 pnpm exec playwright test --ui --grep "존재하지 않는" static 모드로 전체(둘 다 자동으로 안정적) E2E_STATIC=true pnpm exec playwright test --ui --ui 는 대화형 도구라 pnpm e2e 처럼 "끝나면 자동으로 이어서 실행"하는 체이닝이 안 된다 — 필요하면 직접 두 번(위 표의 grep-invert → grep) 나눠서 확인해야 한다. 4. goBackWithMarker 타이밍 버그 — 해결됨 증상 : static 모드로 전체 스위트를 돌릴 때만 웹 진입 시나리오 테스트의 "critical user journey" 항목에서 Execution context was destroyed, most likely because of a navigation 에러. 원인 : goBackWithMarker 헬퍼가 page.goBack() 직후 곧바로 이전 문서를 대상으로 page.evaluate() 를 호출했다. next dev 는 응답이 느려서 이 사이 시간이 우연히 확보됐는데, static-server.mjs 는 응답이 훨씬 빨라 그 여유가 사라지면서 원래 있던 타이밍 결함이 드러났다. 해결 : 이 evaluate() 는 판정 로직과 무관한 순수 시각 표시(Inspector 하이라이트 제거)라, 실패해도 무시하도록 처리: await page.evaluate(() => { ... }).catch(() => {}); 5. navigator.spec.ts CPU 경합 flaky — 미해결 5-1. 무엇이 문제인가 레이아웃 리플로우 감지 관련 테스트는 requestAnimationFrame 기반 리플로우 감지를 MutationObserver 로 관찰한다. 이건 "언젠가 참이 되면 통과"하는 일반 assert와 달리, 그 순간에 실제로 스케줄된 이벤트가 발화하는가 를 재는 방식이라 CPU 경합에 유난히 취약하다 — 브라우저 프로세스가 CPU를 못 받으면 이벤트 자체가 통째로 무산될 수 있다(그냥 "늦게 오는" 게 아니라). 참고로 CPU 경합 자체는 이 테스트만의 문제는 아니다 — 이 테스트를 붙잡고 있다가 알게 된 것뿐이고, 비슷한 실시간 타이밍 방식으로 짜인 테스트라면 다른 곳에서도 같은 현상이 생길 수 있다. 5-2. 시도했지만 안 됐던 것들 시도 결과 retries 0→1 부분 개선 retries 1→2 개선 없음/악화(재시도가 워커 점유 시간을 늘려 경합 창을 넓히는 역효과) workers 6→4 개선 없음 expect.poll 타임아웃 5초→10초 악화(같은 이유) 배경 앱(Cursor, Claude 데스크톱 앱, Slack) 종료 부분 개선(load average 27→11.5) — 완전 해결 아님 완전 격리(workers=1, 이 테스트만 단독) 여전히 5번 중 3번(60%) 완전 실패 — "격리하면 항상 성공"이라는 초반 결론 자체가 표본 부족으로 틀렸음이 나중에 드러남 5-3. 배경 CPU 조사 — 새로 발견한 것 테스트 도는 동안 load average가 유휴 대비 급증(4→27)했고, 이 중 상당 부분이 Playwright/Chromium이 아니라 회사 보안·관리 에이전트 , macOS 자체 서비스(Spotlight mds , system_profiler )였다. 유휴 상태에서는 이 프로세스들이 거의 안 보이다가, 테스트가 도는 동안에만 뚜렷하게 치솟는 패턴을 확인(우연이 아니라는 근거로 유휴-테스트중 비교 실측함). 이건 로컬(회사 보안 소프트웨어) 요인이라 직접 끄거나 조정할 수 없다. 5-4. 유일하게 통했던 것 — static 모드 static 모드( E2E_STATIC=true )로 전체 스위트를 반복 실행하면 이 테스트를 포함해 전부 안정적으로 통과했다(3회 이상 연속 클린 확인). 추정 이유: static은 응답이 거의 즉시라 전체 스위트가 훨씬 빨리 끝나고, dev처럼 "요청마다 컴파일하느라 오래 걸리는" 시간 동안 배경 프로세스들의 경합이 쌓일 기회 자체가 없다. 주의 : not-found를 격리해도 이 테스트의 실패율(60%)엔 변화가 없었다(5회 반복 검증) — 두 문제가 서로 얽혀있던 게 아니라 독립적이다. 5-5. 팀 피드백과의 관계 "workers=1로 잡으면 CPU 경합 자체가 안 생길 것"이라는 예상이 있었는데, 실측(완전 격리 후에도 60% 실패)이 이를 뒷받침하지 못했다 — Playwright 자체 워커 격리는 되지만, Playwright 바깥의 배경 프로세스(에디터, 보안 에이전트 등)까지는 격리 못 하기 때문 으로 추정. 6. 작업 진행 순서 (요약) 정적 빌드 산출물 기반 e2e 환경 추가( E2E_STATIC ) goBack 직후 실행 컨텍스트 파괴로 인한 flaky 수정 not-found 시나리오를 dev/static 양쪽 환경 자동 검증으로 확장 CPU 경합 flaky 완화용 로컬 retries 1회 적용 dev 전체 스위트에서 not-found 500 테스트를 격리 실행하도록 재구성 7. 남은 것 navigator CPU 경합 flaky — 최종 해결 방법 미정, 현재 실용적 대응: static 모드를 주력 검증 경로로 사용, dev 모드에서 이 테스트만 실패하면 재실행으로 확인
То, что RADAR обнаружил и классифицировал для этой возможности. Это опубликованный источником текст, а не подтверждение, что предложение ещё действует.
static 기반 e2e 전환 — 이슈·과정·남은 것 정리. apps/web-e2e 를 next dev (동적) 단일 기반에서 static-server.mjs (정적) 병행 기반으로 바꾸는 과정에서 겪은 이슈들과 대응, 남은 항목을 정리한다. 작업 진행 순서를 참고하되, 내용은 주제별로 묶었다. 1. 배경 — 왜 static 기반이 필요했나 이 프로젝트( output: "export" , 정적 익스포트)는 프로덕션엔 서버가 없다. 그런데 e2e는 지금까지 next dev (라이브 서버) 기준으로만 돌아갔다. 팀 내 피드백으로는: "이전에 이 프로젝트는 SSR 사용했었음 → 모든 e2e 환경은 여기에 맞춰져있음 → static으로 바꾸면서 e2e에서 오류가 보인 것" 즉 next dev (동적)로는 "실제…
Открыть источник