Skip to content

Fix(a11y): Production Lighthouse 기반 전체 페이지 접근성 완성 — A11y 100점#7

Merged
mayrang merged 6 commits intorefactor/phase-6-auth-uxfrom
refactor/phase-6-a11y-production
Mar 30, 2026
Merged

Fix(a11y): Production Lighthouse 기반 전체 페이지 접근성 완성 — A11y 100점#7
mayrang merged 6 commits intorefactor/phase-6-auth-uxfrom
refactor/phase-6-a11y-production

Conversation

@mayrang
Copy link
Copy Markdown
Owner

@mayrang mayrang commented Mar 30, 2026

Summary

  • landmark-one-mainAppShell.tsx, Layout.tsx 내부 콘텐츠 래퍼 <div><main> 교체. 앱 전체에 <main> 랜드마크가 없던 구조적 문제 완전 해소.
  • color-contrast--color-keycolor 다크닝(4.11:1→5.41:1), BoxLayoutTag 하드코딩 제거, Navbar 비활성 탭(1.58:1→5.24:1), TripListPage arbitrary value 교체
  • button-name / target-sizeShareIcon 중첩 버튼 구조 해소(className/ariaLabel prop 추가), TripDetailHeader width 고정값 제거
  • label-content-name-mismatch — 동행자 버튼 aria-label 제거 (WCAG 2.5.3)
  • document-title/trip/list, /trip/apply/[id] metadata 추가
  • 기타 button-name — Header 뒤로가기, CreateTripButton AddIcon, TripListPage AlarmIcon

최종 Lighthouse 수치 (Production 빌드)

페이지 Performance Accessibility FCP
/ 89 100 (+21) 213ms
/trip/list 76 100 (+28) 336ms
/trip/detail/1 89 100 (+18) 213ms

변경 파일

파일 변경 내용
src/components/AppShell.tsx 내부 <div><main>
src/components/Layout.tsx auth route 내부 <div><main>
src/app/globals.css --color-keycolor #3e8d00 → #2d7a00
src/shared/ui/tag/BoxLayoutTag.tsx 하드코딩 색상 → var(--color-text-muted)
src/widgets/home/Navbar.tsx inactiveColorvar(--color-text-muted)
src/page-views/trip/TripListPage.tsx arbitrary value 교체, AlarmIcon divbutton
src/components/icons/ShareIcon.tsx className, ariaLabel prop 추가
src/page-views/trip/TripDetailHeader.tsx 공유 버튼 wrapper 제거, width: "auto"
src/page-views/trip/TripDetailPage.tsx 동행자 버튼 aria-label 제거
src/shared/ui/layout/Header.tsx 뒤로가기 aria-label 추가
src/widgets/home/CreateTripButton.tsx aria-label, aria-expanded 추가
src/app/trip/list/page.tsx metadata 추가
src/app/trip/apply/[travelNumber]/page.tsx metadata 추가
docs/refactoring/phase-6.md §13 추가 (배경/분석/수치/트러블슈팅)
docs/progress.md Phase 6 완료 처리, 최종 수치 반영

Test plan

  • yarn build && yarn start 후 Lighthouse로 /, /trip/list, /trip/detail/1 A11y 100 확인
  • axe DevTools로 각 페이지 위반 0건 확인
  • 공유 버튼 클릭 → URL 복사 Toast 정상 동작
  • 스크린리더로 <main> 랜드마크 이동(단축키) 동작 확인
  • Navbar 비활성 탭 색상 시각적으로 충분히 구분되는지 확인

🤖 Generated with Claude Code

mayrang and others added 6 commits March 30, 2026 00:00
## 주요 변경사항

### Bug Fix
- TripDetailHeader: 호스트 버튼 직접 접근 시 깜빡임 버그 수정
  - 서버 프리페치(null 토큰) → AppShell 토큰 복구 전 hostUserCheck=false 덮어쓰기 문제
  - isAuthResolved(!!accessToken || isGuestUser) guard 추가
  - isGuestUser 변수 섀도잉 버그 수정 (isGuestUser: isGuestUserStore 별칭)
- useAuth: loginPath redirect 미동작 버그 수정
  - localStorage.getItem("loginPath") 읽기 전 removeItem 하던 문제

### Error Handling
- useTripDetail: createMutationOptions 적용 (update/delete mutation)
- useEnrollment: cancelMutation에 createMutationOptions 적용

### Accessibility (WCAG 2.1 AA)
- TripDetailPage: 스켈레톤 UI 추가 (auth 복구 대기 중 animate-pulse)
- TripDetailPage: 스크롤 컨테이너 role="region" + tabIndex=0 (scrollable-region-focusable 해결)
- TripDetailPage: 동행자 행 div→button 전환 + aria-label
- TripDetailPage: 댓글 FAB div→button 전환 + aria-label
- TripDetailHeader: 알림/공유/더보기 div→button 전환 + aria-label
- ApplyListButton: 북마크 버튼 aria-label 추가 (button-name critical 위반 해결)

### E2E / MSW
- tripDetail.spec.ts: 호스트 버튼 + 삭제 + axe 측정 스펙 작성
- enrollment.spec.ts, trip.spec.ts: MSW stateful mock 기반 재작성
- MSW routes 확장 (trip, misc)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
## 변경 내용

### 접근성 (axe WCAG 2 AA)
- globals.css: --color-text-muted #848484(3.67:1) → #6b6b6b(5.24:1), --color-text-muted2 #ababab(2.25:1) → #717171(4.80:1)
- EmailLoginForm: '처음 오셨나요?' inline #848484 → var(--color-text-muted) 클래스로 전환
- InfoText: 기본 색상 #ABABAB 하드코딩 → var(--color-text-muted2) CSS 변수 참조
- auth E2E axe 검사에 .exclude('nextjs-portal') 추가 (dev overlay 뱃지 제외)

### react-hook-form ref 연결 수정
- ValidationInputField: forwardRef 미적용으로 RHF ref가 DOM input에 미연결되어
  getValues().email = undefined → zod "Required" 에러 발생하던 구조적 버그 수정
- forwardRef + onBlur prop 추가, ref를 StateInputField → <input> 까지 전달

### E2E 테스트
- fillReactInput 헬퍼: nativeInputValueSetter 우회 → locator.fill() 으로 단순화
- '로그인' 버튼 locator에 exact: true 추가 (소셜 로그인 버튼 strict mode 위반 수정)
- 결과: 33 passed, axe 전 페이지 "위반 없음 ✓"

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
## 변경 내용

### RegisterDone 자동 로그인
- 회원가입 완료 시 /login 리다이렉트 → / 홈으로 변경
- /api/users/sign-up 응답에 accessToken이 포함되어 있어 이미 로그인 상태임이 확인됨
  (useAuth.registerEmailMutation.onSuccess에서 setLoginData 호출)
- 백엔드 협의 불필요 — MSW 검증으로 확인 완료

### MSW 서버 테스트 격리
- db/store.ts: db.reset() 메서드 추가 (모든 데이터 초기화 + 시드 재등록)
- http.ts: POST /api/test/reset 엔드포인트 추가
- auth.spec.ts: 이메일 회원가입 beforeEach db 리셋 추가

### E2E 테스트 안정화
- auth.spec.ts: test.describe.configure({ mode: 'serial' }) 추가
  — fullyParallel: true 환경에서 공유 MSW db 상태 충돌 방지
- goToVerifyEmail/goToRegisterPassword: email 파라미터 추가 (기본값 유지)
- goToRegisterDone: 고유 이메일(reg{timestamp}@test.com) 사용 — new@test.com 중복 방지
- 전체 플로우 E2E 테스트 추가: RegisterEmail → RegisterDone → / 이동 확인
- 결과: 34 passed, 0 failed

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
## 변경 내용

### landmark-one-main (전 페이지 공통)
- AppShell.tsx: 내부 콘텐츠 래퍼 <div> → <main>
- Layout.tsx: auth route 내부 <div> → <main>
- 앱 전체에 <main> 랜드마크가 없던 구조적 문제 해소

### color-contrast (WCAG 2.1 AA 4.5:1)
- globals.css: --color-keycolor #3e8d00(4.11:1) → #2d7a00(5.41:1)
- BoxLayoutTag: 하드코딩 rgba(132,132,132,1) → var(--color-text-muted)
- Navbar: inactiveColor var(--color-muted3)(1.58:1) → var(--color-text-muted)(5.24:1)
- TripListPage: text-[#3e8d00] → text-[var(--color-keycolor)]

### button-name / target-size
- ShareIcon: className/ariaLabel prop 추가, inner button에 직접 적용
- TripDetailHeader: 중첩 button wrapper 제거, width: "auto" 고정값 제거

### label-content-name-mismatch (WCAG 2.5.3)
- TripDetailPage: 동행자 버튼 aria-label 제거 (visible text로 충분)

### document-title
- /trip/list/page.tsx: metadata 추가
- /trip/apply/[travelNumber]/page.tsx: metadata 추가

### button-name (기타)
- Header: 뒤로가기 버튼 aria-label="뒤로 가기" 추가
- CreateTripButton: AddIcon aria-label + aria-expanded 추가
- TripListPage: AlarmIcon div → button 전환 + aria-label

### 문서화
- phase-6.md: §13 추가 (배경/위반 분석/수정 목록/수치/트러블슈팅/회고)
- progress.md: Phase 6 완료 처리, 최종 수치 반영 (A11y 100/100/100)

## 수치
- A11y: / 79→100, /trip/list 72→100, /trip/detail 82→100
- Performance 유지: / 89, /trip/list 76, /trip/detail 89
- FCP 유지: 213ms / 336ms / 213ms

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@vercel
Copy link
Copy Markdown

vercel bot commented Mar 30, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
release-back Ready Ready Preview, Comment Mar 30, 2026 7:48am
release-test Ready Ready Preview, Comment Mar 30, 2026 7:48am

@mayrang
Copy link
Copy Markdown
Owner Author

mayrang commented Mar 30, 2026

코드 리뷰

Blocking (머지 전 수정 필요)

없음 ✅


Non-blocking (권장 개선)

1. AppShell.tsx<main>id 또는 aria-label 권장

// 현재
<main className="relative h-full ...">

// 권장: skip navigation 링크 타겟으로 활용 가능
<main id="main-content" className="relative h-full ...">

<main id="main-content">을 추가하면 나중에 "콘텐츠로 바로가기" 스킵 링크(<a href="#main-content">)를 달 때 별도 수정 없이 연결할 수 있다. 현재 없어도 동작하는데 지장은 없음.


2. Layout.tsx auth route — <main> 보다 <div role="main"> 검토 불필요 (현재 구현이 올바름)

확인 사항: auth 경로(/login, /register*)에도 <main>을 쓰는 것이 맞는가?

WCAG landmark 규칙상 <main>하나의 페이지에 하나만 있으면 된다. auth 페이지도 별도 페이지이므로 각자 <main>을 갖는 것이 올바르다. AppShell과 Layout이 각각 독립적 경로에서만 렌더링되므로 동시에 두 개 <main>이 존재하는 상황은 발생하지 않는다. 현재 구현 유지가 정답.


3. ShareIcon.tsxtypeof window 가드 위치

// 현재: hooks (useState) 이후에 window 가드
export default function ShareIcon(...) {
  const [isToastShow, setIsToastShow] = useState(false);
  if (typeof window === "undefined") {
    return null;  // ← hooks 이후 조건부 return
  }

React 규칙상 hooks 이후 조건부 return은 허용되지만, SSR guard는 가능하면 hooks 앞에 두거나 useEffect + 상태로 처리하는 것이 관례다. 현재 코드에서 CopyToClipboardwindow.location을 참조하기 때문에 SSR guard가 필요한데, "use client" 지시자가 이미 있어 실제로는 SSR에서 실행되지 않는다. 가드 자체가 dead code일 가능성이 높음.

typeof window === "undefined" 가드 제거 가능 여부 확인 권장.


4. TripListPage.tsx--color-keycolor 참조 방식 일관성

// 수정된 코드
<span className="text-[var(--color-keycolor)] font-bold">

// 다른 파일들 (일반적 패턴)
className="text-[var(--color-keycolor)]"

현재 프로젝트에 text-keycolor처럼 Tailwind config에 CSS 변수를 등록했다면 arbitrary value 없이 text-keycolor로 쓸 수 있다. tailwind.config.ts를 확인해서 이미 등록돼 있으면 통일하는 게 좋음. 등록이 안 돼 있다면 text-[var(--color-keycolor)] 방식이 맞음.


5. Navbar.tsx — inactive 색상이 너무 진해질 가능성

var(--color-text-muted) = #6b6b6b (5.24:1 on white)

접근성 수치는 해결됐지만, 비활성 탭이 active 탭(var(--color-text-base))과 시각적으로 충분히 구분되는지 디자인 관점에서 확인 필요. 두 색상 간 대비가 너무 낮으면 사용자가 현재 탭을 인식하기 어려울 수 있음. 시각적 QA 권장.


참고: 트러블슈팅 하이라이트

중첩 버튼 문제가 교훈적임. ShareIcon 내부에 <button>이 있다는 사실을 컴포넌트 외부에서 알 방법이 없었고, 래퍼 <button>을 추가하는 순간 axe가 button-nametarget-size 두 위반을 동시에 리포트했다. 재사용 컴포넌트가 자체 인터랙티브 요소를 포함할 때는 prop drilling(className, ariaLabel)으로 외부 제어권을 열어야 한다는 패턴을 확립했다.


Reviewer: Claude Sonnet 4.6

@mayrang mayrang merged commit aa13b84 into refactor/phase-6-auth-ux Mar 30, 2026
2 of 3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant