Files

32 KiB
Raw Permalink Blame History

코드 리뷰 — refer_landing_page ("Issue 01" 디자인 시스템 적용)

리뷰 대상: main 브랜치, 893dd91 (Initial commit) 이후의 working-tree 변경분 (git status 기준 12 modified + 9 untracked files, +1,092 / -262 lines, 5개 라우트 + 5개 신규 컴포넌트 + 디자인 문서 1건) 검증 방법: git diff 정독, npx tsc --noEmit (clean), npm run build (clean, 8/8 정적 페이지), 디자인 정합성/HTML 유효성/접근성 정적 분석 작성일: 2026-06-16 (1차) · 재검증: 2026-06-17 (2차)

⚠️ 2차 재검증 노트 (2026-06-17)

1차 리뷰(2026-06-16) 이후 코드가 일부 수정되었습니다. 1차의 P0/P1 중 여러 건이 이미 working tree에 반영되어, 아래 본문의 일부 항목은 stale(이미 해결) 상태입니다. 2차 재검증 결과 상태를 갱신하고, 1차가 놓친 신규 버그 1건(display-sm 클래스 미정의 — §3 신규 A) 을 추가했습니다.

항목 1차 상태 2차 현재 상태
§3 #1 publications <ol>><div>><li> 🟠 미해결 해결됨 (as="li" 적용, publications/page.tsx:133)
§4 P1 #4 Marquee w-full max-w-full 미반영 해결됨 (Marquee.tsx:22)
§3 medium·P1 #3 HeroComposition 텍스트 겹침 🟡 미해결 해소됨 (주석을 상단 모서리 y=24로 이동)
§4 P0 #2 Next.js 14.2.5 보안 🟠 미해결 🟠 여전히 미해결 — 잔존 P0
신규 A display-sm 클래스 미정의 🔴 신규 High (1차 누락)
신규 B Counter rAF unmount 미취소 🟡 신규 Medium (1차 누락)

1. 개요 (Overview)

이번 변경은 "한 권의 인쇄 매거지(Issue 01)" 컨셉의 에디토리얼 디자인 시스템을 처음부터 적용한 대규모 시각·구조 리뉴얼입니다. 단순한 클래스 리네임이 아니라 레이아웃 언어·타이포그래피·모션·테마 토큰 전체를 재설계했습니다.

핵심 변화:

  • 테마 토큰 재설계: brand(KNU 블루) / accent 단일 톤 → ink / ivory / paper / line + 두 개의 에디토리얼 강조색 vermillion(MCM) · cobalt(QUIC) 시스템으로 교체. tailwind.config.tsapp/globals.css :root 양쪽에 미러링되어 있어 토큰 일관성 보장.
  • 타이포그래피 교체: 시스템 sans → Fraunces(영문 디스플레이 세리프) + Noto Serif KR(한글 세리프 헤드라인) + Inter(본문) 3-페어. next/font로 CSS 변수 주입.
  • 신규 모션·UI 프리미티브 4종: Reveal(IntersectionObserver 기반 스크롤 등장), Counter(rAF 카운트업, prefers-reduced-motion 존중), Marquee(CSS 키프레임 러닝 티커), SectionLabel("01 / 05" 스프레드 넘버링).
  • 페이지 단위 전면 재구성: 5개 라우트 모두 cover spread → figures → 메인 콘텐츠 → 보조 섹션의 잡지식 레이아웃으로 재작성. 카운터, 마키, 섹션 라벨이 페이지마다 다른 의미로 재사용됨.
  • 디자인 문서 정비: docs/DESIGN.md 정본 보강, docs/LAYOUT_AUDIT.md 17개 이슈 카탈로그(이번 변경으로 일부 해결), docs/TYPOGRAPHY_FIXES.md 작업 노트 추가.
  • 문서: README.md에 디자인 시스템 섹션·커스터마이징 훅 5종(색·폰트·모션·콘텐츠·공유 컴포넌트) 추가.

기술적으로 빌드/lint/타입 체크 모두 통과합니다 (npm run build → 8/8 정적 페이지 생성, 87.1 KB shared first-load JS, no warnings).

이전에 카탈로그화된 이슈 해결 현황

docs/LAYOUT_AUDIT.md 17개 이슈 중 이번 diff에서 다수가 해결되었습니다 (해결 항목 8, 잔존 5, 미해결 4, 검증 후 OK 3):

# 이슈 상태
1 border-current/* opacity modifier → fixed gray 해결 (border-ink/20 또는 border-vermillion/30로 교체)
2 HeroComposition transformOrigin 박스 미스매치 해결 (인라인 transformBox: "fill-box" 추가)
3 HeroComposition 우하단 텍스트와 4번 스트림 충돌 해결 (text y 356 → 334로 이동)
5 Footer mt-24 × <main flex-1> 이중 여백 해결 (mt-0 lg:mt-8)
6 PageHeader <h1> 한국어 단어 분리 해결 (break-keep + text-balance 추가)
7 "Standards" 9rem 캡 오버플로 해결 (display 캡 9rem → 7rem)
8 Members 3그룹 2명 → 3-col ghost cell 해결 (lg:grid-cols-2로 통일)
9 .reveal no-JS 폴백 부재 해결 (<noscript><style> 추가)
10 PageHeader display 미지정 시 en 풀사이즈 ⚠️ 부분 무효display ?? en 분기는 맞으나 폴백이 의존하는 display-sm 클래스가 미정의라 의도된 3.75rem이 아닌 ~16px로 렌더됨. 신규 A 참조
11 Counter "2026" 0→2026 카운트업 ⚠️ 부분 무효static prop은 정상이나 display-sm 스케일이 미적용(~16px). 신규 A 참조
17 corner-label 14px → 16px 해결 (text-smtext-base)

2. 변경된 파일 (Files changed)

파일 변경 종류 설명
tailwind.config.ts modified 컬러 토큰 전체 교체(ink/ivory/paper/line/vermillion/cobalt, brand는 back-compat 별칭), 폰트 패밀리 3종, display/display-sm 매거진 스케일, marquee/stream-dash/node-pulse 5종 keyframes·animation 추가.
app/globals.css modified :root CSS 변수 미러, 종이 그레인 배경, ::selection 강조, .display/.headline-ko/.kicker/.corner-label/.pull-quote/.rule/.spread-card/.draw-underline 에디토리얼 프리미티브, .reveal 4종 variant, .marquee-track, prefers-reduced-motion 폴백 추가. 기존 .card/.section-title/.section-subtitle 제거(어디서도 미사용).
app/layout.tsx modified next/font로 3개 폰트 로드 + CSS 변수 주입, <html className>에 변수 부착, <noscript> 폴백.
components/Header.tsx modified 데스크탑 nav에 01–05 섹션 번호 + 영문 라벨 + 활성 상태 애니언더라인, 모바일 햄버거 아이콘 6×6→5×5 + 테두리 토글, 이슈 스트립 ("Issue 01 — 2026"), sticky ivory 배경.
components/Footer.tsx modified 3-col 단순 링크 → 5-col 잡지 콜로폰(colophon/contact/index), <Marquee> 콜로폰 티커, 라벨별 인덱스 01–05, draw-underline 호버 효과, 세리프 산세리프if credit.
components/PageHeader.tsx modified 그라데이션 히어로 → ivory 잡지 마스트헤드, <SectionLabel>+<Reveal> 조합, 한국어 serif headline-ko + 영어 display 워드 + 우측 보더라인 데스크립션, display 폴백 안전장치.
app/intro/page.tsx modified Cover spread(헤드라인 + HeroComposition SVG) + 카운터 행(연구 축/표준화 기구/Issue) + 미션 풀쿼트 + 두 추력 비교 카드(vermillion × cobalt 연결 라인) + Focus Areas 4-col 그리드. thrusts 데이터에 no/tag/accent 필드 추가, 키워드/figures 데이터 추가.
app/members/page.tsx modified 카운터 3종(지도교수/대학원/학부) + advisor 7+5 콜룸 + research groups 3그룹(vermillion/cobalt/ink 액센트). 3번째 그룹(학부) 2명 → sm:grid-cols-2로 통일.
app/publications/page.tsx modified Venue <Marquee> + 카운터 3종(전체/저널/학회) + 잡지식 출판물 리스트(번호·연도·타입 좌측 rail + 제목·저자·venue 우측 본문), Journal → vermillion, Conference → cobalt.
app/standardization/page.tsx modified Org <Marquee> + 카운터 3종(표준화 기구/기여 항목/기술 영역) + 4개 표준화 기구 카드(oneM2M/W3C WoT/IETF QUIC/OMA LwM2M), 기구별 accent(vermillion/cobalt 교차) + 적응형 보더.
app/lectures/page.tsx modified 강의 3종을 3-col 카운터-스타일 카드로 재구성, 강의별 accent(vermillion/cobalt/ink), 우상단 0103 spread 번호, 호버 시 accent 배경 + ivory 텍스트.
README.md modified 디자인 시스템 섹션, 컴포넌트 트리 보강(SectionLabel/Reveal/Counter/Marquee 추가), 커스터마이징 훅 5종(색·폰트·모션·콘텐츠·공유 컴포넌트) 상세화.
신규 components/Reveal.tsx added (60 lines) IntersectionObserver 기반 스크롤 등장, `variant: up
신규 components/Counter.tsx added (78 lines) rAF 카운트업 (easeOutExpo), prefers-reduced-motion 존중 시 즉시 렌더, static prop으로 years/고정값 처리.
신규 components/Marquee.tsx added (41 lines) 콘텐츠를 2번 복제해 -50% 트랜슬레이트 루프, reverse prop으로 방향 반전, ✦ 세퍼레이터.
신규 components/SectionLabel.tsx added (28 lines) "01 / 05" 스프레드 넘버 + hairline + kicker.
신규 app/intro/_components/HeroComposition.tsx added (116 lines) 인라인 SVG 에디토리얼 일러스트, MCM 노드 메시(vermillion) + QUIC 멀티스트림(cobalt), node-pulse·stream-dash CSS 애니메이션.
신규 docs/DESIGN.md (17,872 B) added 디자인 시스템 정본(토큰·타이포·모션·컴포넌트 4종).
신규 docs/LAYOUT_AUDIT.md (22,936 B) added 17개 이슈 카탈로그(이번 리뷰 시점 기준 11개 해결).
신규 docs/TYPOGRAPHY_FIXES.md (4,322 B) added 타이포그래피 작업 노트.
신규 .antigravity-session.md, .kanban-*.md (4 files) added Antigravity CLI 세션 / 카반 작업 산출물 (리뷰 대상 아님, 무시).
신규 package-lock.json added 의존성 잠금 파일 (기존 package.json은 변경 없음 — dev install로 추정, 커밋 대상 검토 필요, §3 참조).

3. 발견된 이슈 (Issues)

🔴 신규 A (High) — display-sm 클래스가 정의되어 있지 않아 매거진 디스플레이 텍스트가 ~16px로 축소 렌더 (1차 누락)

위치: components/PageHeader.tsx:43, app/intro/page.tsx:53 (+ Counter 경유)

display-smtailwind.config.ts:77에서 fontSize로 정의되어 있습니다. Tailwind의 fontSize 키는 text-<key> 유틸리티만 생성하므로 실제로 만들어지는 클래스는 text-display-sm이고, 맨(bare) display-sm 클래스는 생성되지 않습니다. 한편 app/globals.css에는 .display 컴포넌트 클래스만 있고 .display-sm 컴포넌트 클래스는 없습니다.

검증 (컴파일된 CSS 기준):

$ grep -o "display-sm" .next/static/css/app/layout.css | wc -l
0          # ← display-sm 규칙이 전혀 없음
$ grep -o "\.display"  .next/static/css/app/layout.css | wc -l
1          # ← .display(컴포넌트 클래스)는 존재

따라서 className="display-sm ..."아무 폰트 크기도 적용하지 않는 no-op 클래스이고, 해당 텍스트는 브라우저 기본 크기(~16px)로 렌더됩니다.

영향:

  • PageHeaderdisplay prop을 넘기지 않으면 fallback en 단어가 display-sm으로 렌더됩니다. 현재 PageHeader를 사용하는 4개 페이지(members/publications/lectures/standardization) 모두 display를 넘기지 않으므로, "Members" · "Publications" · "Lectures" · "Standardization" 영문 디스플레이 단어가 의도된 clamp(2rem, 5.5vw, 3.75rem)이 아니라 ~16px 작은 vermillion 세리프로 표시됩니다 — 마스트헤드의 큰 영문 단어가 사실상 사라집니다.
  • app/intro/page.tsx:53의 연도 "2026" 피겨는 size: "display-sm"이라 Counterdisplay-sm으로 렌더 → ~16px. 같은 행의 "2" · "4"는 display(최대 7rem)라 극단적인 크기 불일치로 피겨 행이 깨져 보입니다.
  • 이 버그는 본 문서 §1 LAYOUT_AUDIT 표의 #10/#11(및 #6/#7 일부) "해결" 주장을 무효화합니다.

왜 1차에서 못 잡았나 / build가 clean한 이유: Tailwind/PostCSS는 미정의 유틸리티 클래스를 에러 없이 조용히 무시합니다. 따라서 npx tsc --noEmitnpm run build도 통과하며, "build clean"이 시각적 정합성을 보장하지 않습니다.

권장 수정 (택1):

/* (A안, 권장) app/globals.css 컴포넌트 레이어에 .display-sm 추가 */
.display-sm { @apply font-display text-display-sm font-light; }
/* → 추가 시 PageHeader의 중복 `font-display font-light`는 제거 가능 */

또는 사용처를 display-smtext-display-sm으로 바꾸되, 폰트 패밀리/굵기 클래스(font-display font-light)를 함께 명시 (Counter 사용처는 현재 폰트 클래스가 없으므로 누락 주의).

🟡 신규 B (Medium) — CounterrequestAnimationFrame 루프가 unmount 시 취소되지 않음 (1차 누락)

위치: components/Counter.tsx:52-69

const io = new IntersectionObserver(([entry]) => {
  ...
  let raf = 0;
  const tick = (now) => { ...; if (t < 1) raf = requestAnimationFrame(tick); };
  raf = requestAnimationFrame(tick);
  return () => cancelAnimationFrame(raf);   // ← IO 콜백의 반환값 → 아무 데도 안 쓰임
}, { threshold: 0.4 });
io.observe(el);
return () => io.disconnect();                // ← useEffect cleanup: 옵저버만 끊고 rAF는 안 멈춤

return () => cancelAnimationFrame(raf)IntersectionObserver 콜백의 반환값이라 호출되지 않습니다. useEffect의 cleanup은 io.disconnect()만 수행하므로, 진행 중인 rAF 카운트업 루프는 중단되지 않습니다. 카운트업 도중 컴포넌트가 언마운트되면(예: 빠른 라우트 전환) rAF가 언마운트된 컴포넌트에 setDisplay를 계속 호출합니다. React 18은 이 setState를 경고 없이 no-op 처리하므로 **실 피해는 경미(낭비 프레임 수 개)**하지만, 명백한 누수입니다.

권장: raf 변수를 useEffect 스코프로 끌어올려 cleanup에서 cancelAnimationFrame(raf)을 호출. 또는 isStatic/완료 플래그로 보호.


🟠 High — 잘못된 HTML 구조: <div><ol>/<ul> 안에서 <li>를 감쌈[2026-06-17 해결됨]

2차 재검증: 현재 코드는 app/publications/page.tsx:133에서 <Reveal as="li" ...>로 렌더되며 주석에 본 항목(REVIEW.md §3 #1)을 인용해 둠. <ol>의 직접 자식 계약(<li>만 허용)을 준수하므로 해결. 아래 설명은 이력 보존용.

위치: app/publications/page.tsx:131-160

<ol className="mt-12 border-t border-ink">
  {publications.map((p, i) => (
    <Reveal key={p.title} delay={(i % 3) * 80}>     {/* default as="div" */}
      <li className="group grid gap-6 border-b border-ink py-8 ...">
        ...
      </li>
    </Reveal>
  ))}
</ol>

Reveal은 기본 as: "div"로 렌더링됩니다. 결과 DOM은 <ol><div class="reveal"><li>...</li></div></ol>이 되어 HTML 명세 위반입니다(spec: <ol>의 자식은 0개 이상의 <li>/<script>/<template>만 허용). React는 콘솔 경고를 띄우지 않지만, 자동복구로 DOM이 재정렬될 수 있고(<li><ol> 밖으로 이동), <ol>start/type 카운터 의미가 깨질 수 있습니다.

나머지 페이지(lectures/standardization/members/intro)는 모두 <Reveal> 안에서 <article>을 감싸므로 안전합니다. 오직 publications만 <li>를 직접 감쌉니다.

권장 수정:

<Reveal as="li" key={p.title} delay={(i % 3) * 80}
        className="group grid gap-6 border-b border-ink py-8 ...">
  ...
</Reveal>

🟠 High — npm audit 1 critical + 6 high 보안 권고 미반영

위치: package.json / package-lock.json (Next.js 14.2.5)

.kanban-final-report.md §2에 따르면 의존성에 8개 권고(1 critical, 6 high, 1 moderate)가 있습니다. 핵심은 next@14.2.514.2.35 패치 한 줄로 21개가 해결됩니다(서버 컴포넌트 DoS, 캐시 포이즈닝, 미들웨어 SSRF, PostCSS XSS 등). 호환성: 같은 14.x 라인, React 18 락도 그대로.

권장 수정:

npm install next@14.2.35
npm audit

🟡 Medium — package-lock.json이 untracked 상태로 추가됨

위치: working tree 루트

package.json은 diff에 없는데 package-lock.json이 untracked로 들어왔습니다. 이전 커밋(893dd91)에도 lockfile이 없었다면 이번에 처음 도입된 것인데, 그 경우:

  • 의도된 커밋이면 그대로 두고, 향후 package.json 변경 시 함께 업데이트되는지 CI 또는 가이드로 명시.
  • 무심코 추가된 것이면(예: 로컬 npm install 부산물) .gitignore로 옮기는 것을 권장. 단, 215 KB인 lockfile을 추적하면 재현 가능한 빌드가 보장되므로 보통은 추적하는 편이 낫습니다.

권장 결정: lockfile 추적 유지(재현 가능한 빌드의 정석). 단, 의도가 불분명하면 git log --diff-filter=A -- package-lock.json으로 첫 등장 시점 확인.

🟡 Medium — Reveal의 IntersectionObserver cleanup 시점: disconnect이 unmount 시점에만 실행

위치: components/Reveal.tsx:47

useEffect(() => {
  ...
  io.observe(el);
  return () => io.disconnect();
}, []);

unobserve가 첫 intersection 시점에 호출되어(entry isIntersecting → unobserve, line 39) 이후 콜백이 다시 안 불리지만, dependency array가 []라서 variant/delay prop이 바뀌어도 옵저버가 재생성되지 않습니다. 현재 variant는 CSS attribute로 적용되므로 prop 변경이 옵저버 동작에는 영향이 없어 실 문제는 없지만, 마운트 이후 prop만 바뀌는 경우 stale observer가 됩니다(미세).

권장: useEffect deps에 [variant, delay]를 추가하거나, entry.targetdata-variant 속성을 read해서 비교. 다만 현재 사용 패턴(컴포넌트가 한 번 마운트되면 prop이 안 바뀌는 정적 리스트)에서는 영향 없음 — minor.

🟡 Medium — Reveal as prop 시그니처가 런타임 안전성을 보장하지 않음

위치: components/Reveal.tsx:19, 26

as?: ElementType이라 <Reveal as="ol">처럼 잘못된 부모(예: ol 안의 ol 금지)나, <Reveal as="li">를 잊고 wrapper를 list 안에 넣는 사용을 막을 수 없습니다. #1의 문제가 발생한 이유도 이 가드 부재입니다. 호출 컨벤션으로 잡혀 있지만(모든 페이지가 <Reveal> 안에 article/div를 둠), 실수 방지 차원에서 사용 시 li/ol/ul 컨텍스트일 땐 as를 강제하는 lint 룰(예: eslint-plugin-jsx-a11y custom rule) 또는 README에 "list 내부에서 쓸 땐 as=\"li\"" 규칙을 명시하면 좋겠습니다.

🟡 Medium — Marquee의 트랙이 flex + 콘텐츠가 inline-flex인데, width가 콘텐츠에 맞춰져 viewBox/parent overflow 계산이 viewport에 의존

위치: components/Marquee.tsx:22-38

<div className="marquee-track"> 안의 시퀀스가 [0, 1].map(dup)로 2번 복제됩니다. CSS는 translateX(0 → -50%)로 50% 이동 후 같은 위치로 와서 seamless loop. 하지만 marquee-trackinline-flex이고 콘텐츠가 flex인 점이 데스크탑에서만 동작합니다 — 부모에 overflow-hidden은 있으니 화면이 줄어들면 콘텐츠가 부모 너비에 의해 클립되지만, 부모 width가 명시되지 않은 경우(예: <Reveal>의 wrapper) inline-flex는 콘텐츠 폭만큼 자라서 페이지를 가로로 스크롤 가능하게 만들 수 있습니다. 현재는 모두 container-content 안에 있어 안전하지만, 다른 곳에서 단독 사용 시 가로 스크롤이 생길 수 있습니다.

권장: Marquee 최상위에 max-w-full 또는 w-full을 추가하고, 부모 wrapper에서도 가로 클립이 보장되는지 명시.

🟡 Medium — HeroComposition 4번 스트림이 우측 모서리 가까이 통과, 우하단 텍스트와의 간격이 6–10px[2026-06-17 해소됨]

2차 재검증: 현재 코드는 주석(annotation) 텍스트를 상단 두 모서리(y=24) 로 이동했습니다(HeroComposition.tsx:104,107). 스트림(y≥250)·노드 메시(y=60~240)와 수직으로 분리되어 겹침이 구조적으로 제거되었습니다. 1차의 y=334 우하단 배치 가정은 더 이상 유효하지 않습니다. 아래 설명은 이력 보존용.

위치: app/intro/_components/HeroComposition.tsx:99-113

LAYOUT_AUDIT #3은 y=356 → 334로 이동하여 해결되었지만, 스트림 곡선의 마지막 control point는 360, 320 (i=3) 입니다. 텍스트 baseline은 y=334이고 폰트 size 11 + letterSpacing 2이므로 텍스트 상단 y=323, 하단 y=336. 텍스트의 우측 끝(x=360, textAnchor="end")은 x=360에 anchor되므로 가로 폭이 좁지는 않지만, **스트림 4번은 x=240360 / y=300340 대역**을 지나가며 텍스트의 시작점(좌측)에 근접합니다. 텍스트 내용("QUIC · STREAMS")이 100px 폭이라면 우측 x≈260360에 그려지는데, 4번 스트림의 끝점 (360, 320)이 텍스트의 (260, 334) 근처를 지나갑니다.

해결된 것으로 보이지만 실제로는 텍스트가 스트림 곡선과 여전히 시각적으로 겹칠 수 있는 좁은 마진입니다. y=320 정도 또는 y=348 정도로 더 멀리 두는 편이 안전합니다.

🟡 Medium — text-balance + break-keep이 한국어에 대해 100% 안정적이지 않음

위치: components/PageHeader.tsx:32

<h1 className="headline-ko break-keep text-balance text-3xl leading-[1.05] text-ink sm:text-4xl lg:text-5xl">
  • text-balance (CSS text-wrap: balance)는 2024년 기준 Chrome 114+ / Safari 17.5+ / Firefox 121+에서 지원. 지원하지 않는 브라우저에서는 무시되며 fallback 없음 — 큰 문제 아님.
  • break-keep (word-break: keep-all)은 한국어/일본어/중국어 텍스트에서 단어 경계로 줄바꿈을 강제. 한국어는 공백이 거의 없어서 효과가 제한적입니다. "멀티에이전트 오케스트레이션" 같이 공백이 있는 제목은 효과가 있고, "사물인터넷표준연구실" 같이 공백이 없는 경우 여전히 글자 단위로 깨질 수 있습니다.

권장: 한 줄이 너무 길어질 가능성이 있는 한국어 헤드라인은 강제로 <br />을 박거나 whitespace-pre-line + 줄바꿈 문자(\n)를 사용.

🟡 Medium — Reveal로 감싼 <div>/<article> 안에 있는 <a> 링크가 reveal 애니메이션과 충돌할 가능성

Revealopacity: 0 → 1로 페이드 인하는데, prefers-reduced-motion이 설정되지 않은 환경에서 사용자가 링크를 빠르게 따라가려 할 때(약 0.8s transition + delay) 클릭 타깃이 시각적으로 보이지만 실제 클릭 가능 시점이 늦어지는 사각지대가 생깁니다. IntersectionObserver의 threshold: 0.12는 12%가 뷰포트에 들어와야 트리거되므로 화면 상단에서 스크롤 시 약 100ms 정도만 보입니다.

권장: prefers-reduced-motion 사용자에게만 즉시 표시 + 일반 사용자에게는 0.4s로 duration 단축 검토(현재 820ms는 약간 길음). 또는 threshold를 더 낮춰 0.05 정도로.

🟢 Low — key={p.title}가 title 충돌 시 깨짐

위치: app/publications/page.tsx:131, app/standardization/page.tsx

publication title이 unique하지 않은 경우(같은 제목이 여러 venue에 출판될 수 있음) React key 충돌이 일어납니다. 현재 데이터는 unique이지만, 향후 같은 title이 들어오면 오류. key={\${p.title}-${p.year}-${p.venue}`}` 등 composite key가 더 안전.

🟢 Low — accent 식별자가 색상 이름과 충돌

위치: app/lectures/page.tsx:18,26,34, app/members/page.tsx:25,34,44, app/standardization/page.tsx:20,30,40,50

각 페이지가 data field 이름으로 accent를 쓰고(accent: "vermillion" as const), accentText/accentDot 맵의 key로도 씁니다. 가독성은 양호하지만 accent는 Tailwind 유틸리티 클래스 이름과 충돌할 수 있고, 추후 CSS accent-color 속성과 헷갈릴 여지가 있습니다. 의미상 theme 또는 tint가 더 명확합니다.

🟢 Low — <Reveal>의 SSR/CSR hydration mismatch 가능성 (이론적)

Reveal"use client"이고 첫 렌더에서 is-visible 클래스를 아직 안 가진 상태로 마운트됩니다. HTML은 SSR 시에도 .reveal + data-variant + opacity: 0이 적용되어 전송되고, JS hydrate 후 IntersectionObserver가 is-visible을 토글합니다. 서버에서는 항상 opacity: 0 → 클라이언트에서는 항상 opacity: 0이라 hydration mismatch는 없음. 다만 no-JS 환경에서는 noscript 폴백(<noscript><style>.reveal { opacity: 1 !important; ... }</style></noscript>)이 의도대로 동작하므로 해결됨.

🟢 Low — Header 모바일 메뉴의 z-index와 <Marquee> band의 sticky 충돌 가능성

Headersticky top-0 z-50. <Marquee>는 absolute/sticky 없이 일반 flow. 모바일에서 햄버거 메뉴가 열렸을 때(<nav id="mobile-menu"> 펼쳐진 상태) <Marquee>가 위에 있으면 클릭이 가려질 수 있으나, <Marquee>aria-hidden이라 인터랙션 요소는 아닙니다. 실 문제 아님.

🟢 Low — app/intro/page.tsx:90-105에 중첩된 <Reveal> 4연타

<Reveal delay={60}>...</Reveal>
<Reveal delay={120}>...</Reveal>
<Reveal delay={200}>...</Reveal>
<Reveal delay={260}>...</Reveal>

같은 부모 영역에 4개 sibling Reveal이 60ms 간격으로 등장. Reveal 각각이 별도 IntersectionObserver 인스턴스를 생성하므로 브라우저 메모리에 observer 4개가 동시 만들어집니다(가벼우나 관용적이지 않음). 같은 영역이면 하나의 Reveal로 묶고 내부 stagger를 CSS transition-delay로 처리하는 편이 깔끔합니다.

🟢 Low — 접근성: Marqueeprefers-reduced-motion 폴백은 있으나 키보드/SR 사용자용 일시정지 토글 없음

prefers-reduced-motion 환경에서는 .marquee-track { animation: none }이 적용되어 멈춥니다. 그러나 키보드 사용자나 스크린리더 사용자에게 명시적인 정지 토글은 없습니다. aria-hidden이라 SR은 무시하므로 큰 문제는 아니나, 인지적으로 천천히 움직이는 콘텐츠가 본문 읽기를 방해할 수 있습니다(학습장애 사용자 등). WCAG 2.2.2 (Pause, Stop, Hide) 권고. 현재 컨텐츠는 "decorative ticker"로 분류 가능하므로 선택적 개선사항.

🟢 Low — HeroComposition SVG의 aria-label이 한글/영문 혼합

aria-label="MCM 상호운용 노드와 QUIC 멀티스트림을 형상화한 추상 일러스트레이션" — 영문+한글 혼합. SR 호환성을 고려하면 영문만 또는 한/영 둘 다 <title> + <desc>로 분리하는 게 표준. 현재 SR 환경에서 한글이 잘 읽히므로 minor.


4. 개선 제안 (Concrete Suggestions)

2차 갱신: 1차 P0 #1과 P1 #3/#4는 이미 해결되어 목록에서 제거(취소선)했고, 신규 A(display-sm 미정의) 를 P0로 승격했습니다.

P0 (반드시 머지 전 처리)

  1. 신규 A — display-sm 클래스 정의app/globals.css.display-sm { @apply font-display text-display-sm font-light; } 추가. 미적용 시 4개 PageHeader 페이지의 영문 디스플레이 단어와 intro "2026" 피겨가 ~16px로 깨짐. §3 신규 A 참조. 1분 작업.
  2. #2 npm audit 해결npm install next@14.2.35npm audit clean 확인. package.json은 여전히 14.2.5 (미반영). 1분 작업.
  3. #1 HTML invalidity 수정 이미 해결됨 (publications/page.tsx:133 <Reveal as="li">).

P1 (다음 PR에서)

  1. HeroComposition 우하단 텍스트 마진 해소됨 (주석을 상단 y=24로 이동).
  2. Marquee w-full max-w-full 명시 이미 적용됨 (Marquee.tsx:22).
  3. 신규 B — Counter rAF cleanupraf를 useEffect 스코프로 올려 cleanup에서 cancelAnimationFrame. §3 신규 B 참조. 5분 작업.
  4. Reveal 내부 stagger를 transition-delay로 통합 — intro 페이지 4연타는 단일 wrapper로 묶고 자식은 style={{ transitionDelay: ... }}로 처리. (미해결, minor) 10분 작업.
  5. Revealtransition-duration을 820ms → 600ms로 단축 — 모션 선호 off 환경에서 페이지 응답성 개선. (globals.css:20 아직 820ms) 1분 작업.

P2 (정리)

  1. README에 "List 안에서 <Reveal> 사용 시 as=\"li\" 필수" 규칙 추가docs/DESIGN.md 4절 보강.
  2. 데이터 필드명 accenttint 리네임 — lecture/member/standardization 페이지 3곳 + intro. 15분 작업.
  3. publications 키를 composite으로key={\${p.title}-${p.year}`}`. 1분 작업.
  4. package.json에 "engines": { "node": ">=20" } 명시 — Node 22 환경 명시. 1분.
  5. 마키에 명시적 정지 토글 (선택) — prefers-reduced-motion 외에 키보드/터치 사용자가 토글 가능하도록 <button aria-label="티커 일시정지"> 추가. 30분 작업.
  6. Counteraria-live="polite" 추가 — 카운트업이 끝났을 때 SR이 "전체 논문 4건" 식으로 알려줄 수 있음. 5분 작업.

5. 결론 (Verdict) — 2026-06-17 재검증 기준

Needs changes (잔존 P0 2건 머지 전 처리 필요)

매우 잘 정리된 변경입니다. 1차 리뷰 이후 코드가 갱신되어 1차 P0 #1(publications HTML)·P1 #3(HeroComposition)·P1 #4(Marquee) 3건이 이미 해결되었습니다. 다만 2차 재검증에서 1차가 놓친 시각 버그 1건을 발견했고, 1차의 보안 권고는 아직 미반영입니다.

머지 전 반드시 처리할 2건 (P0):

  1. display-sm 클래스 미정의 (globals.css.display-sm 추가) — 4개 PageHeader 페이지의 영문 디스플레이 단어와 intro "2026" 피겨가 의도된 ~3.75rem이 아니라 ~16px로 렌더되어 마스트헤드/피겨 행이 깨짐. build/tsc가 잡지 못하는 조용한 버그. §3 신규 A.
  2. Next.js 14.2.5 보안 권고 1 critical + 6 high — 동일 라인 14.2.35로 한 줄 업그레이드로 해결. (아직 package.json 미반영)

이 두 건을 처리하면 LGTM입니다. 그 후 P1(신규 B Counter cleanup, Reveal stagger/duration)은 다음 PR에서 정리하면 됩니다.

핵심 강점 (정리)

  • 토큰 일관성: tailwind.config.tsapp/globals.css :root 양쪽 미러, tailwind.config.ts 한 줄 변경이 사이트 전체에 전파.
  • 모션 일관성: 4개 페이지에서 같은 카운터/마키/리빌이 다른 의미로 재사용되어 통일된 리듬.
  • 접근성 기본기: aria-hidden (Marquee), aria-expanded (Header), aria-label (HeroComposition SVG), noscript 폴백, prefers-reduced-motion 존중.
  • 타입 안전성: 모든 prop이 optional/required 명시, as const 리터럴 타입으로 액센트 매핑, TypeScript 5.9 / strict 모드 clean.
  • 문서화: docs/DESIGN.md(정본), docs/LAYOUT_AUDIT.md(자체 점검), docs/TYPOGRAPHY_FIXES.md(작업 노트) 3종이 변경을 추적 가능하게 만듦.

결론 요약

항목 평가
빌드/타입/lint Clean (단, 미정의 클래스 display-sm는 빌드가 못 잡음 — §3 A)
디자인 일관성 ⚠️ 5개 라우트 잡지 미학은 일관되나, display-sm 미정의로 영문 마스트헤드/연도 피겨가 실제로는 깨짐
모션/타이포/테마 토큰 시스템 작동 (타이포 한 군데 display-sm 누락 제외)
1차 이슈 해결 (2차 확인) P0 #1·P1 #3·P1 #4 추가 해결됨
신규 발견 이슈 (2차) 🔴 1건(display-sm 미정의, High), 🟡 1건(Counter rAF cleanup)
잔존 P0 🟠 2건 — (a) display-sm 정의, (b) Next.js 14.2.35 업그레이드
머지 가능 여부 잔존 P0 2건 처리 후 가능

리뷰어 메모: 변경량 대비 코드 품질이 매우 높습니다. 특히 LAYOUT_AUDIT에 카탈로그된 이슈 중 핵심 8건을 한 번에 해결한 점, 그리고 <noscript> 폴백을 layout 단계에 추가한 점은 마이너 변경에서 놓치기 쉬운 디테일을 잘 챙긴 흔적입니다. 위 2건(P0)만 정리되면 본 변경은 그대로 main에 머지해도 좋을 수준입니다.