From 0dd118e0e7618a77a8f3dbf5a9b552127256c461 Mon Sep 17 00:00:00 2001 From: Lee Sanghyun <59863112+dev-ant@users.noreply.github.com> Date: Fri, 13 Nov 2026 00:23:14 +0900 Subject: [PATCH] fix: avoid unused route CSS preloads on hero auctions --- docs/css-preload-warning-r1.md | 24 +++++++++++++ package-lock.json | 4 +-- package.json | 2 +- scripts/test-production-workflow.mjs | 2 +- scripts/workflow-contract.test.mjs | 2 +- src/app/(main)/hero-auctions/page.tsx | 6 +++- .../navigation/ThreeTierNav.test.tsx | 34 +++++++++++++++++++ src/components/navigation/ThreeTierNav.tsx | 12 +++++-- src/components/page/hero/HeroPages.test.tsx | 18 ++++++++-- 9 files changed, 94 insertions(+), 10 deletions(-) create mode 100644 docs/css-preload-warning-r1.md diff --git a/docs/css-preload-warning-r1.md b/docs/css-preload-warning-r1.md new file mode 100644 index 0000000..2177b52 --- /dev/null +++ b/docs/css-preload-warning-r1.md @@ -0,0 +1,24 @@ +# 다른 페이지 CSS 프리패치 경고 수정 r1 + +- 기준: `main`의 `9ebe63f`, 브랜치 `fix/css-preload-warning`, 버전 `1.0.30`. +- `/hero-auctions`에서 사용하지 않은 CSS preload 경고의 두 파일은 길드 레이아웃의 `guild.css` (`2345695d322d7922.css`)와 로그인 페이지의 `AuthPanel.module.css` (`675c1bd902d00ab7.css`)다. 로컬 빌드 manifest 및 CSS 내용으로 소유 페이지를 확인했다. 해시는 진단 당시 빌드 기준이다. +- 운영 Chromium 요청 추적에서 `/guilds?_rsc`, `/sign-in?_rsc`의 `next-router-prefetch: 1`을 확인했다. 두 RSC 요청만 임시 제외한 대조 실험에서 CSS preload가 0개였고 실험 설정은 원복했다. 인증 확인 중 상단 로그인 링크가 먼저 렌더링되는 경우도 동일한 경로를 사용한다. +- 승인된 수정은 상단 길드 링크, 상단 로그인 링크, 경매 비로그인 안내 로그인 링크 세 곳에만 `prefetch={false}`를 적용한다. 공통 NavigationItem은 선택 prop을 전달하며 기본값은 기존 Next 동작을 유지한다. CSS 파일·import·다른 링크·패키지는 변경하지 않는다. +- [Next.js 15 공식 프리패치 문서](https://nextjs.org/docs/15/app/guides/prefetching)에 따라 특정 링크 프리패치를 비활성화한다. 해당 목적 페이지의 미리 다운로드 이점은 포기하고 클릭할 때 필요한 리소스를 로드한다. 클라이언트 이동과 목적 페이지 스타일은 유지한다. +- 완료 기준: 세 링크의 prefetch prop과 나머지 메뉴 기본값 회귀, 로그인 안내·메뉴 기존 테스트, Node 20 전체 `npm run verify`, workflow 계약 테스트 2종, 실제 Chromium에서 경고 제거 및 길드·로그인 클릭 후 스타일 확인. 커밋·PR·운영 병합은 담당 리더의 별도 단계다. + +## 구현 검증 + +- Node 20.20.2 대상 테스트 2 suites / 30 tests 통과, workflow 계약 테스트 9개 통과. +- `npm run verify` 종료 코드 0: 포맷·lint·typecheck·deadcode·112 suites / 540 tests·프로덕션 빌드 49개 정적 페이지 모두 통과. 범위 밖 기존 lint 경고는 유지했다. +- `git diff --check` 통과. 실제 Chromium에서 변경 이후 경고 및 목적 페이지 스타일 확인은 검증 담당 리더에게 인계한다. + +## Chromium 검증 + +- 로컬 프로덕션 빌드 + 실제 로컬 Auth/MySQL, 응답 모킹 없이 검증했다. +- 비로그인 데스크톱, 로그인 데스크톱 및 모바일(390×844) 경매 화면에서 로드 후 9초 대기: CSS preload 0개, console warning 0건. +- 모바일 경매 규칙 팝업 표시 및 가로 넘침 없음 확인. +- 길드 메뉴 클릭 후 guild.css가 stylesheet로 로드되고 제목의 32px / 750 스타일 적용 확인. +- 로그인 안내 클릭 후 AuthPanel.module.css 로드 및 library 배경 이미지 적용 확인. +- 로컬 Community 서비스는 연결하지 않아 길드 데이터 API 3개는 404였다. 길드 데이터 기능은 이번 검증 범위 밖이며 CSS 적용 검증과 구분한다. 익명 인증 확인 401은 예상 응답이다. +- 독립 검토 결과 ACCEPT / CLEAR. 운영에는 아직 반영하지 않았다. diff --git a/package-lock.json b/package-lock.json index 9d2818f..ef5159b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "devnogi-front", - "version": "1.0.29", + "version": "1.0.30", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "devnogi-front", - "version": "1.0.29", + "version": "1.0.30", "dependencies": { "@hookform/resolvers": "^5.1.1", "@radix-ui/react-checkbox": "^1.3.3", diff --git a/package.json b/package.json index e8cd102..a1601ad 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "devnogi-front", - "version": "1.0.29", + "version": "1.0.30", "private": true, "scripts": { "dev": "next dev", diff --git a/scripts/test-production-workflow.mjs b/scripts/test-production-workflow.mjs index 95e92cd..0aac0cf 100644 --- a/scripts/test-production-workflow.mjs +++ b/scripts/test-production-workflow.mjs @@ -64,7 +64,7 @@ test("production Compose requires the immutable image reference", () => { test("package and lockfile agree on the next release version", () => { const pkg = JSON.parse(read("package.json")); const lock = JSON.parse(read("package-lock.json")); - assert.equal(pkg.version, "1.0.29"); + assert.equal(pkg.version, "1.0.30"); assert.equal(lock.version, pkg.version); assert.equal(lock.packages[""].version, pkg.version); }); diff --git a/scripts/workflow-contract.test.mjs b/scripts/workflow-contract.test.mjs index 0ae58bb..9fb5754 100644 --- a/scripts/workflow-contract.test.mjs +++ b/scripts/workflow-contract.test.mjs @@ -68,7 +68,7 @@ test("release version and immutable image contract are fixed", () => { ); const pkg = JSON.parse(read("package.json")); const lock = JSON.parse(read("package-lock.json")); - assert.equal(pkg.version, "1.0.29"); + assert.equal(pkg.version, "1.0.30"); assert.equal(lock.version, pkg.version); assert.equal(lock.packages[""].version, pkg.version); assert.match( diff --git a/src/app/(main)/hero-auctions/page.tsx b/src/app/(main)/hero-auctions/page.tsx index 274b872..4adc33c 100644 --- a/src/app/(main)/hero-auctions/page.tsx +++ b/src/app/(main)/hero-auctions/page.tsx @@ -397,7 +397,11 @@ export default function HeroAuctionsPage() {

로그인이 필요합니다

광고 경매는 로그인 후 이용할 수 있습니다.

- + 로그인하기
diff --git a/src/components/navigation/ThreeTierNav.test.tsx b/src/components/navigation/ThreeTierNav.test.tsx index 48c834a..667a7d7 100644 --- a/src/components/navigation/ThreeTierNav.test.tsx +++ b/src/components/navigation/ThreeTierNav.test.tsx @@ -9,6 +9,15 @@ import userEvent from "@testing-library/user-event"; import ThreeTierNav from "./ThreeTierNav"; +jest.mock("next/link", () => { + const Link = jest.requireActual("next/link").default; + return function ObservedLink( + props: React.ComponentProps, + ) { + return ; + }; +}); + let pathname = "/"; const toggleTheme = jest.fn(); let mockAuthState: { @@ -37,6 +46,31 @@ beforeEach(() => { toggleTheme.mockClear(); }); +it("길드와 로그인 진입점만 자동 프리패치를 끄고 나머지 메뉴 기본값을 유지한다", () => { + render(); + expect(screen.getByRole("link", { name: "길드" })).toHaveAttribute( + "data-prefetch", + "false", + ); + expect(screen.getByRole("link", { name: "로그인" })).toHaveAttribute( + "data-prefetch", + "false", + ); + expect(screen.getByRole("link", { name: "커뮤니티" })).toHaveAttribute( + "data-prefetch", + "auto", + ); + expect(screen.getByRole("link", { name: "의장" })).toHaveAttribute( + "data-prefetch", + "auto", + ); + fireEvent.click(screen.getByRole("button", { name: "도감" })); + expect(screen.getByRole("link", { name: "아이템 도감" })).toHaveAttribute( + "data-prefetch", + "auto", + ); +}); + it("주요 메뉴를 요청된 순서로 제공하고 검색폼은 노출하지 않는다", () => { render(); diff --git a/src/components/navigation/ThreeTierNav.tsx b/src/components/navigation/ThreeTierNav.tsx index 135b43f..e370640 100644 --- a/src/components/navigation/ThreeTierNav.tsx +++ b/src/components/navigation/ThreeTierNav.tsx @@ -48,12 +48,14 @@ function NavigationItem({ href, label, pathname, -}: NavigationLink & { pathname: string }) { + prefetch, +}: NavigationLink & { pathname: string; prefetch?: boolean }) { const isActive = isCurrentPath(pathname, href); return ( ) : null} - + 로그인 diff --git a/src/components/page/hero/HeroPages.test.tsx b/src/components/page/hero/HeroPages.test.tsx index b0d0775..0c31810 100644 --- a/src/components/page/hero/HeroPages.test.tsx +++ b/src/components/page/hero/HeroPages.test.tsx @@ -19,6 +19,15 @@ jest.mock("@/contexts/AuthContext", () => ({ useAuth: jest.fn() })); jest.mock("@/hooks/useExperience", () => ({ useExperience: jest.fn() })); jest.mock("@/lib/hero/request", () => ({ heroRequest: jest.fn() })); +jest.mock("next/link", () => { + const Link = jest.requireActual("next/link").default; + return function ObservedLink( + props: React.ComponentProps, + ) { + return ; + }; +}); + const request = jest.mocked(heroRequest); const auth = jest.mocked(useAuth); const experience = jest.mocked(useExperience); @@ -136,13 +145,18 @@ it.each([false, true])( ).not.toBeInTheDocument(); expect(request).not.toHaveBeenCalled(); expect(experience).not.toHaveBeenCalled(); - if (isLoading) + if (isLoading) { expect(screen.getByRole("status")).toHaveTextContent("로그인 확인 중"); - else + } else { expect(screen.getByRole("link", { name: "로그인하기" })).toHaveAttribute( "href", "/sign-in", ); + expect(screen.getByRole("link", { name: "로그인하기" })).toHaveAttribute( + "data-prefetch", + "false", + ); + } }, );