apps/web 개선점 진단 리포트

점검 대상: apps/web (Next.js 16 App Router, i18n: ko/en/zh-TW, FSD) 관점: SEO/접근성, 버그/정합성 작성일: 2026-07-13 방식: 코드베이스 실제 확인 항목만 기재 (추측성 제외)


요약

심각도 건수 대표 항목
🔴 P0 1 루트 레이아웃 canonical 하드코딩 → 다수 페이지가 홈으로 정규화
🟠 P1 9 hreflang 부재, 문의 폼 센티넬 버그, 팝업 빈 배열 크래시 등
🟡 P2 11 i18n 하드코딩, alt 공백, nav 랜드마크, 데드 코드 등

추천 처리 순서: ① P0 canonical → ② 문의 폼 버그·팝업 크래시 → ③ SEO P1 일괄(hreflang/h1/Twitter/갤러리) → ④ i18n 하드코딩 일괄 정리


🔴 P0 — 최우선

1. 루트 레이아웃 canonical 하드코딩 → 대부분 페이지가 "홈"을 canonical로 선언

  • 파일: src/app/[locale]/layout.tsx:34-36
  • 문제: generateMetadata가 alternates.canonical을 https://seoulmoment.com.tw로 고정 반환. Next.js는 하위 페이지가 alternates를 자체 지정하지 않으면 이 값을 그대로 상속함. 결과적으로 홈·about·product/news 리스트·policy·terms·magazine·brand 상세가 전부 canonical을 루트 도메인(=홈)으로 선언 → 서로 다른 페이지가 홈으로 통합/색인 제외될 위험.
  • 개선: 레이아웃의 canonical 하드코딩 제거 → 각 페이지가 buildLocalizedAlternates(locale, path)(src/i18n/metadata.ts)로 자기참조 canonical을 설정하도록 통일. 상세 3종(product/news/article)이 이미 모범 사례.
  • 비고: AdSense 재심사 대응(색인/크롤 품질)과 직결.

🟠 P1 — 높은 우선순위

SEO

2. 홈·리스트·정적 페이지에 hreflang(alternates.languages) 부재

  • 파일: src/app/[locale]/(home)/page.tsx(메타데이터 자체 없음), product/page.tsx:5-18, news/page.tsx:5-18, about/page.tsx:5-18, policy/page.tsx, terms/page.tsx
  • 문제: hreflang이 detail 3종(product/news/article [id])에만 존재. 트래픽이 큰 홈·목록·정적 페이지에 언어별 대체 URL이 없어 ko/en/zh-TW 상호 연결이 안 됨. sitemap엔 alternates가 있으나 페이지 <head>엔 없음.
  • 개선: 위 페이지들의 generateMetadata에 buildLocalizedAlternates(locale, path) 추가.

3. brand/[id] 상세만 canonical·hreflang 누락

  • 파일: src/app/[locale]/brand/[id]/page.tsx:37-45
  • 문제: 다른 상세와 달리 alternates가 없어 P0의 홈 canonical을 상속. sitemap엔 brand 동적 경로가 포함(sitemap.ts:183-188)되어 색인 대상인데 canonical이 홈으로 감.
  • 개선: alternates: buildLocalizedAlternates(locale, \/brand/${brandId}`)` 추가.

4. 홈 페이지에 h1 부재

  • 파일: src/features/home/ui/PrimeSection/MainBanner.tsx:49-56 (+ views/home/ui/HomePage.tsx)
  • 문제: 대표 텍스트 "Seoul Moment"가 <p className="text-[80px]">로 마크업. 홈에 <h1>이 없고 첫 헤딩이 하위 섹션의 <h2> → 문서 개요/키워드 신호 손실.
  • 개선: 히어로 브랜드/캐치프레이즈를 <h1>으로 승격(시각 스타일 유지).

5. 상품 갤러리 이미지: alt 공백 + unoptimized

  • 파일: src/widgets/product-gallery/ui/ProductGallery.tsx:49-56, 72-79
  • 문제: 콘텐츠 이미지가 alt=""로 스크린리더/이미지검색에서 누락 + unoptimized로 next/image 최적화 우회 → 상세페이지 LCP 악화.
  • 개선: alt={product.name} 등 의미 있는 대체텍스트, unoptimized 제거(도메인이 next.config에 등록되어 있으면 최적화 가능), 첫 슬라이드 sizes 지정.

6. Twitter Card 메타데이터 전무

  • 파일: src/app/ 전체 (twitter 키 0건)
  • 문제: OpenGraph만 있고 twitter 필드가 없어 X(트위터) 공유 시 카드 미표시.
  • 개선: 레이아웃/상세 generateMetadata에 twitter: { card: "summary_large_image", title, description, images } 추가.

7. magazine/[id] 상세에 generateMetadata 자체 없음

  • 파일: src/app/[locale]/magazine/[id]/page.tsx
  • 문제: title/description/canonical/OG 전무 → 레이아웃 상속으로 title=홈, canonical=홈. robots 미차단이라 크롤 대상. (레거시 슬라이스로 보이나 라우트 생존)
  • 개선: 상세 메타데이터 추가 또는 미사용이면 robots disallow/라우트 제거.

버그/정합성

8. 문의 폼: "기타" 센티넬 값 비교 버그

  • 파일: src/features/inquiry/ui/InquiryForm.tsx:62, :132
  • 문제: Select 실제 값은 "partnership_inquiry" | "listing_inquiry" | "etc"이고 emailSubject도 이 값으로만 세팅되는데, 두 곳에서 하드코딩 한국어 "기타"와 비교 → 비교가 항상 실패. (line 235는 올바르게 === "etc"로 되어 있어 리팩터링 중 2곳만 누락)
  • 실패 시나리오: "기타(etc)" 선택 후 커스텀 제목 입력 → line 132 emailSubject === "기타" ? subject : emailSubject가 항상 false 분기 → 입력한 제목 대신 리터럴 "etc"가 메일 제목으로 전송. 또한 line 62 submit 판정에서 subject가 항상 "채워짐" 처리되어 etc 선택 시 빈 제목이어도 버튼 활성화(검증 우회).
  • 개선: 두 리터럴을 "etc"로 교체.

9. 오프라인 팝업 섹션: 빈 배열일 때 초기 렌더 크래시

  • 파일: src/features/promotion/ui/BrandOfflinePopup/index.tsx:23
  • 문제: useState(popupList[0].id) — 옵셔널 체이닝 없이 첫 요소 접근. 호출부 views/promotion/ui/PromotionPage.tsx:43는 길이 검사 없이 전달하고 섹션은 항상 렌더.
  • 실패 시나리오: 오프라인 팝업 없는 프로모션(popupList: []) → popupList[0]이 undefined → .id 접근에서 Cannot read properties of undefined → 프로모션 페이지 전체 렌더 실패.
  • 개선: 호출부에서 popupList.length > 0 가드 또는 popupList[0]?.id + fallback. (백엔드가 빈 배열 반환 가능한지 확인 필요)

🟡 P2 — 개선 권장

i18n 하드코딩 (ko/en/zh-TW 3개 로케일인데 한국어/영어 고정)

위치 내용
views/product/ui/ProductDetailPage.tsx:140 "판매가" 리터럴 (주변 라벨은 모두 t())
features/search/ui/SearchBody.tsx:42,61,92 placeholder "검색어를 입력해주세요."(2곳), "전체 삭제"
features/search/ui/SearchHeader.tsx:70 "를 눌러 검색하세요"
shared/lib/hooks/useNicknameValidate.ts:35-37 닉네임 검증 결과 3종 한국어 고정
entities/product/model/hooks/useProductLikeToggle.ts:68 toast.error("로그인이 필요합니다.")
features/promotion/ui/BrandOfflinePopup/index.tsx:33 toast.success("Address Copied") (영어 고정)
features/mypage/lib/formatLikeCount.ts:7 "…만" 단위 → en 사용자에 부정확, Intl.NumberFormat 미사용

SEO/a11y

  • 콘텐츠 이미지 alt 공백: widgets/detail/ui/DetailMain.tsx:42(상세 히어로, LCP), :75(아바타), views/brand/ui/BrandDetailPage.tsx:102,131,192,232,264
  • 메인 내비 <nav> 랜드마크 부재: widgets/header/ui/Header.tsx:76,104 (<ul>만 사용). 코드베이스 전체 <nav>는 features/promotion/ui/BrandTab.tsx:38 한 곳뿐
  • brand 리스트가 빈 프래그먼트 반환: src/app/[locale]/brand/page.tsx (return <></>) → soft-404 성격
  • metadataBase 미설정: src/app/[locale]/layout.tsx → 상대경로 유입 시 localhost 해석 위험
  • <html lang> 폴백 불일치: layout.tsx:67 lang={locale ?? "ko"}이나 routing.defaultLocale은 "en"

데드 코드

  • features/mypage/ui/SummaryStats.tsx:21-23: label 필드("좋아요"/"리뷰"/"공지사항") 미사용. 실렌더는 t(stat.key)(line 57). 오용 방지 위해 제거 권장.

✅ 점검했으나 문제없음

  • 최근 "stray 0" 유사 falsy-render 패턴: 나머지 {x && (…)}는 모두 string/object 대상이라 0 노출/크래시 없음
  • 좋아요 토글(useProductLikeToggle/useBrandLikeToggle): 낙관적 업데이트·재시드 가드·stale closure 방지(ref) 견고
  • 브랜드 좋아요 낙관적 캐시: getQueriesData prefix 매칭으로 languageCode 포함 키 정상 처리
  • decodeJWT, maskAccount: 빈 값/구분자 누락 가드 + try/catch 완비
  • 타입 안정성: as any/as unknown/: any 사용처 0건
  • robots.ts: 계정/비공개 경로 와일드카드 차단 + sitemap/host 지정 — 정상
  • sitemap.ts: 정적·동적 경로 + alternates.languages(x-default 포함) — 정상
  • news/article 상세: NewsArticle/Article JSON-LD + canonical/hreflang/OG(type:article) 구비 — 모범 사례
  • LCP 이미지(홈 MainBanner, 상세 DetailMain): priority 지정 정상 (단, sizes 미지정·alt 공백은 위 항목 참조)
  • 인터랙티브 요소: <div onClick> 오용 0건, aria-label 32곳 사용 — 양호