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:67lang={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) 견고 - 브랜드 좋아요 낙관적 캐시:
getQueriesDataprefix 매칭으로 languageCode 포함 키 정상 처리 decodeJWT,maskAccount: 빈 값/구분자 누락 가드 + try/catch 완비- 타입 안정성:
as any/as unknown/: any사용처 0건 robots.ts: 계정/비공개 경로 와일드카드 차단 + sitemap/host 지정 — 정상sitemap.ts: 정적·동적 경로 +alternates.languages(x-default 포함) — 정상- news/article 상세:
NewsArticle/ArticleJSON-LD + canonical/hreflang/OG(type:article) 구비 — 모범 사례 - LCP 이미지(홈 MainBanner, 상세 DetailMain):
priority지정 정상 (단,sizes미지정·alt 공백은 위 항목 참조) - 인터랙티브 요소:
<div onClick>오용 0건,aria-label32곳 사용 — 양호