인트로 페이지 제거 → 루트가 메인 직행 + AuthGuard 스피너 게이트 제거로 SEO 색인 해결 — POP-SPOT v2.23 (+v2.23.1/2)
발견형 서비스인데 모든 방문자가 5 섭션 인트로→ENTER 거쳐야 메인 도달. 이탈을 키우는 구조라 제거. v2.23.1·2 은 SEO 색인 이시합 해결 — /popups/[slug] 이 크롤러를 /login 으로 튕긴게 수정하고, AuthGuard 의 스피너 게이트를 제거해 공개 페이지가 실제 HTML로 서버 렌더링 되도록.
출시 후 3 개월 Google Search Console 색인 1 건. "팝스팟"·"팝업스토어" 검색에 도메인이 안 뜨고, 유일한 클릭 한 건이 메인이 아닌 /popups/art 슬라이스 랜딩. 원인 추적 해보니 두 가지가 겹쳐 있었다. v2.23 에서 인트로 제거, v2.23.1·2 에서 크롤러 하이라이키 수정.
이 글에서 다루는 것
모르는 단어 한 줄로
| 용어 | 한 줄 설명 |
|---|---|
| SSG | Static Site Generation. 빌드 시점에 HTML 완성. 크롤러에게 가장 친화적 |
| AuthGuard | 로그인 필요 경로를 감싸 토큰을 검증하는 프론트 컴포넌트 |
| PUBLIC_PATHS | AuthGuard 가 "로그인 안 되어도 통과" 하는 경로 목록 |
| Yeti | 네이버 크롤러. JavaScript 실행없이 HTML만 읽음 |
| Suspense | React 의 지연 로딩 경계. useSearchParams 등이 필수로 요구함 |
v2.23 — 인트로 제거
의사결정
발견형 서비스의 핵심 — "들어오자마자 명확한 가치 (팝업 목록·지도)" 를 보여주는 것. 그런데 인트로가 있는 한 새 방문자 전원이:
이 구조는 광고처럼 처음 몇 초 안에 닫는 방문자에게 아무것도 안 남긴다. AuthGuard 가 이미 "/"를 공개 로 명시되어 있으면 인트로 게이트만 넘기면 비로그인 사용자도 메인의 공개 콘텐츠 (팝업·지도·캘린더·랭킹) 을 바로 볼 수 있음. 찜·코스·스탬프·메이트 등 행동 시에만 로그인 유도 (기존 동작 그대로 유지).
삭제 항목
파일: 아래 경로들을 git rm 으로 삭제·커밋 (일반 코드 수정이 아닌 파일 삭제 명령)
삭제 대상:
- app/intro/page.tsx
- app/intro/layout.tsx
- public/intro-background.mp4 (17 MB)
- middleware.ts (인트로 리다이렉트가 유일한 역할이라 이제 필요 없음)참조 정리
// page.tsx — 탈퇴 후 이동
// 갈아엎기 전
await fetch('/api/v1/users/me', { method: 'DELETE' });
router.replace('/intro'); // 인트로로
// 갈아엎은 후
await fetch('/api/v1/users/me', { method: 'DELETE' });
router.replace('/login');파일: popspot-frontend/src/components/AuthGuard.tsx (v2.22 양식)
// v2.22 까지 — AuthGuard.tsx 의 PUBLIC_PATHS
const PUBLIC_PATHS = ['/', '/login', '/signup', '/intro', '/popups/'];
// ^^^^^^^^ /intro 경로 존재 시절도 포함파일: 같은 AuthGuard.tsx (v2.23 수정 후)
// v2.23 — /intro 제거 (페이지 자체 삭제)
const PUBLIC_PATHS = ['/', '/login', '/signup', '/popups/'];파일: app/sitemap.ts, app/feed.xml/route.ts, src/app/terms/page.tsx, 각종 admin 컴포넌트들
// sitemap.ts · feed.xml/route.ts · terms/page.tsx — 모두 /intro 링크 제거
// admin 컴포넌트 — 비관리자가 접근 시 /intro 대신 / 로해석
v2.23.1 — /popups/[slug] 이 크롤러를 /login 으로 튕김
진단 과정
파일: 코드 파일 아닌 Google Search Console 대시보드 고츠
Google Search Console:
- "팝스팟" 검색 → 0 노출
- "팝업스토어" 검색 → 0
- 최근 3개월 노출 1 회, 클릭 1 회
→ 유일하게 클릭된 페이지는 /popups/art (메인 아님)/popups/art 는 v2.21-S3 에서 만든 랜딩 페이지. 서버 렌더링 (SSG) 이라 크롤러 친화적이어야 하는데 왜 색인이 이렇게 느릴까?
근본 원인
파일: popspot-frontend/src/app/layout.tsx (루트 레이아웃)
// app/layout.tsx — 루트 레이아웃
export default function RootLayout({ children }) {
return (
<html>
<body>
<AuthGuard> {/* 모든 경로에 적용 */}
{children}
</AuthGuard>
</body>
</html>
);
}AuthGuard 가 토큰 없이 접근한 건을 PUBLIC_PATHS 에 있는지 검사. 문제는 PUBLIC_PATHS 가 정확 매칭이었고 "/popups/" 프리픽스 안 들어있었음. 크롤러가 /popups/seongsu 접근 → PUBLIC_PATHS 에 없음 → /login 으로 리다이렉트. 결국 만든 SEO 랜딩을 아무도 볼 수 없었다.
수정
파일: popspot-frontend/src/components/AuthGuard.tsx (v2.23.1 수정 후)
// v2.23.1 — AuthGuard.tsx. 정확 매칭 vs 프리픽스 매칭 이원화
const PUBLIC_PATHS = ['/', '/login', '/signup'];
const PUBLIC_PREFIXES = ['/popups/']; // 프리픽스 — /popups/seongsu, /popups/art 등 23개 하위 구간 일괄 커버
function isPublicPath(pathname: string | null): boolean {
if (!pathname) return false;
if (PUBLIC_PATHS.includes(pathname)) return true;
return PUBLIC_PREFIXES.some(prefix => pathname.startsWith(prefix));
}해석
v2.23.2 — AuthGuard 의 스피너 게이트 제거
더 심각한 문제
v2.23.1 핫픽스 후도 네이버 서치어드바이저 에서는 여전히 색인 0. 구글 계속 느림.
차이점 추적: 구글은 JavaScript 실행 후 DOM 읽을 수 있지만, 네이버 Yeti 는 JavaScript 안 돌린다. 서버가 돌려주는 HTML 만 읽는다.
파일: 같은 AuthGuard.tsx (v2.23.1 까지의 상태)
// v2.23.1 까지 — AuthGuard. 서버가 돌려주는 HTML 이 스피너만 될 수 있음
'use client';
export function AuthGuard({ children }) {
const [verified, setVerified] = useState(false);
// 위: 초기 false. 클라이언트 JS 가 돌아야 verified 가 true 됨
// …
if (!verified) {
return <Spinner />;
// 위: 네이버 Yeti 는 JS 안 돌리므로 HTML 을 봐도 <Spinner /> 만 보임 → "본문 없음" 으로 판단
}
return <>{children}</>;
}이 구조는 서버 렌더링 HTML 이 항상 스피너만. JavaScript 가 돌아야만 verified=true 되어 children 을 별도 렌더링. Yeti 는 스피너 이하를 볼 때까지 멈춰있으니 "명확한 본문 없음" 으로 판단.
수정
파일: 같은 AuthGuard.tsx (v2.23.2 수정 후. 현재 운영 장의 최종본)
// v2.23.2 — 항상 children 먼저 렌더링. 리다이렉트는 클라이언트에서 useEffect 로 처리
export function AuthGuard({ children }) {
return (
<Suspense fallback={null}>
{/* useSearchParams 는 Suspense 래퍼 필수 (Next.js 15 권장 구조) */}
<AuthGuardInner>{children}</AuthGuardInner>
</Suspense>
);
}
function AuthGuardInner({ children }) {
const pathname = usePathname();
// 항상 children 먼저 렌더링
return (
<>
{children}
<PathBasedRedirector pathname={pathname} />
</>
);
}
function PathBasedRedirector({ pathname }) {
useEffect(() => {
if (isPublicPath(pathname)) return;
// 토큰 검증 실패 시 리다이렉트
verifyToken().catch(() => router.replace('/login'));
}, [pathname]);
return null;
}해석
결과
생성된 정적 HTML 직접 확인 (next build 후 .next/server/app/popups/seongsu.html):
네이버/구글이 SEO 랜딩·정보 페이지 색인 가능.
남은 한계
메인("/") 은 "use client" + useSearchParams 쓴다. 본문은 여전히 fallback (스피너) 으로 SSR 됨. 단 <head> 의 title·description·keywords·JSON-LD 는 서버 렌더되어 브랜드 "팝스팟" 검색에는 잡힌.
메인 본문까지 완전 SSR 하려면 useSearchParams → window.location 패턴 전환 필요. 하지만 app/page.tsx 가 1,592 줄이라 리팩터 그 자체가 별도 파일 서빘 (v2.22 감사의 클린코드 백로그 #4).
빌드 이슈
패치 프로덕션 빌드에서 두 번 실패:
세 번째에 41/41 성공.