당신 시간의 알찬 소비  ·  당신 주변의 변화를 관찰합니다

Tech

바이브코딩으로 하는 SEO 대응 실패와 교훈

애들빙자여행러 · 2026년 6월 21일

블로그 재구성을 위한 정리 문서. 두 서비스의 SEO 전략, 실제 대응 방식, 3차에 걸친 장애 대응, 그리고 시사점을 시간순으로 정리했다.

0. 배경

  • 지금여기(now) : 2026-03-26 시작. 성수·홍대 팝업스토어/공연/축제 정보를 다루는 hyperlocal 미디어. 네이버 검색 노출이 꾸준히 잘 됨.
  • 네모네AIM(matmatch) : 2026-05-30 시작. 미식·문화·라이프·테크를 다루는 라이프스타일 매거진. 검색 유입이 트래픽의 거의 100%인데, 네이버 검색 노출이 계속 부진. 2026-05-31부터 급락 이후 여러 차례 원인을 찾아 수정했지만 좀처럼 회복되지 않았음.

두 서비스 모두 Next.js(App Router) 기반이고, 같은 인프라(서버, pm2, nginx)에서 운영 중. SEO 대응 방식과 시행착오를 비교하면 같은 실수를 반복하지 않을 수 있다고 판단해 전체 히스토리를 정리한다.


1. 공통 SEO 기본 전략

두 서비스 모두 다음 기본 요소는 처음부터 갖추고 시작:

항목 내용
sitemap.xml app/sitemap.ts로 동적 생성, 백엔드 API에서 게시물 목록을 가져와 URL 생성
robots.txt 네이버/구글 봇 허용, admin/auth 등 비공개 경로 차단
메타데이터 Next.js generateMetadata/metadata export로 title, description, OG, canonical 설정
사이트 인증 naver-site-verification, Google Search Console 인증 메타 태그
구조화 데이터(JSON-LD) WebSite, Organization, Article/Event 등 schema.org 마크업

이 정도 "기본기"는 두 서비스 모두 처음부터 잘 갖춰져 있었다. 즉, 두 서비스의 노출 격차는 "기본 SEO 셋업의 차이"가 아니라 더 깊은 곳에 있었다.


2. 지금여기의 SEO 대응 타임라인

날짜 작업
2026-03-26 서비스 시작
2026-06-01 Next.js 14.2.29 업그레이드, robots.ts 동적 전환, noindex 레이아웃 정리, Supabase SSR 인증 도입과 함께 SEO 메타데이터 정비
2026-06-10 상세 페이지(posts/[id]) title/description 중복 문제 수정 — 장소 전체 목록을 매번 불러오던 것을 GET /places/{id} 단건 조회로 교체, 폴백 메타데이터도 고유화
2026-06-17 랭킹/테마 UX 개선과 함께 홈 타이틀·OG 타이틀 SEO 강화, 만료된 장소(팝업스토어 등) 자동 DB 정리 로직 추가
2026-06-17 네이버 애널리틱스 + GA4 추적 추가

지금여기는 큰 장애 없이 점진적으로 메타데이터 품질을 개선하는 방식으로 작업해왔다. 핵심 콘텐츠(홈/상세 페이지)는 처음부터 클라이언트 렌더링(CSR) 구조였지만, 이것이 노출에 큰 문제가 되지 않은 것으로 보인다 (3장 참고).


3. 네모네AIM의 SEO 대응 타임라인 — 3차에 걸친 장애

네모네AIM은 5/31 급락 이후 세 번의 별개 원인을 찾아 수정했는데, 그중 세 번째가 가장 까다로웠고 오늘(6/21)에야 완전히 해결됐다.

1차 원인: 페이지네이션으로 인한 링크 소멸 (5/31 발생 → 6/14 수정)

  • 5/31, 서버 부하 절감을 위해 홈페이지 포스트 목록을 전체 노출 → useState(10) 페이지네이션으로 변경.
  • 결과: 서버가 응답하는 홈 HTML에 포스트 링크가 10개만 포함됨. 나머지 90여 개는 "더보기" 버튼을 클릭해야 노출되는데, 크롤러는 버튼을 클릭하지 않으므로 그 포스트들을 발견할 경로가 사라짐.
  • 검색 유입이 거의 전부인 서비스에서, 홈에서 끊긴 링크는 사이트맵만으로는 크롤 우선순위가 낮아짐.

6/14 수정 시도 (세 단계로 진행됨, 같은 날 저녁 1시간 사이):

  1. 645fdcf (20:27) — useState(10)useState(initialPosts.length)로 변경해 SSR 시 전체 포스트가 HTML에 포함되게 하고, hydration 후 useEffect로 다시 10개로 줄임 (사용자 UX는 유지).
  2. de61d6d (20:45) — 위 방식 대신, page.tsx(서버 컴포넌트)에 <div className="h-0 overflow-hidden" aria-hidden="true">화면엔 안 보이지만 크롤러에게는 보이는 숨김 링크 목록을 추가하는 방식으로 교체. HomeContent.tsx는 다시 useState(10)으로 원복.
  3. 6d333c8 (21:28) — 위 변경들과 revalidate=60(ISR 캐싱) 시도가 충돌해 빌드 에러 발생. 급한 대로 force-dynamic을 명시해 캐싱을 끄고 매 요청마다 새로 렌더링하도록 롤백.

2차 원인: 의도치 않은 봇 차단 설정 (6월 초 발견 → 긴급 제거)

  • robots.txt 또는 관련 설정에 크롤러를 막는 규칙이 들어가 있던 것을 발견, 긴급 제거.
  • 단, 발견 시점이 주말이라 "네이버가 주말에 크롤링을 덜 했을 수도 있다"는 가능성도 있어 실제 영향 범위는 불확실.

3차 원인: ISR 캐싱 + useSearchParams + Suspense의 충돌 (6/16 발생 → 6/21 완전 해결)

가장 교묘했던 원인. 발생 경위:

  • 6/14 1차 원인 수정 직후, "force-dynamic이 메모리 부담이 크다"는 판단 아래 6/16 da20d53 커밋에서 다시 ISR(revalidate=60)로 전환을 시도. 이번엔 빌드 에러를 피하려고 검색 기능에 쓰던 useSearchParams()(HomeContent.tsx)를 <Suspense fallback={null}>로 감쌈.
  • 커밋 메시지: "크롤러용 전체 포스트 링크 노출 효과는 그대로 유지" — 하지만 실제로는 정반대 결과가 났다.

실제로 벌어진 일: Next.js는 ISR(정적 캐시) 페이지에서 useSearchParams()를 쓰는 컴포넌트의 실제 렌더링 결과를 빌드/캐시 시점에 알 수 없다. 그래서 해당 Suspense 영역은 정적 HTML에는 fallback={null}만 굽고, 실제 콘텐츠는 브라우저에서 JS가 실행된 뒤에만 렌더링한다. 그런데 홈페이지의 메인 그리드(HomeContent.tsx) 전체가 이 useSearchParams() 호출 하나에 묶여 있었기 때문에, 홈페이지 전체 콘텐츠가 5일 동안(6/16~6/21) 크롤러에게 빈 화면으로 노출되고 있었다.

이건 로컬 코드만 봐서는 알아내기 어려웠다. page.tsxasync function으로 서버에서 데이터를 fetch하고, 백엔드 호출도 정상이었기 때문에 코드상으로는 "SSR이 잘 되고 있다"고 판단하기 쉬운 구조였다. 실제로 운영 서버에 curl -A "Yeti" https://nemoneai.com/로 직접 요청을 보내 응답 HTML을 까봐야만 발견할 수 있었다:

포스트 링크: 0개, 이미지: 0개, 본문 텍스트: nav/footer 199자뿐

같은 방식으로 useSearchParams()를 쓰지 않는 /category/Taste 페이지를 검사하자 링크가 80개 정상 노출되어, 문제가 홈페이지 한 곳에 국한된 구조적 결함임이 확인됐다.

6/21 최종 수정:

  • 검색 기능을 /search?q=...라는 별도 페이지로 완전히 분리 (서버 컴포넌트가 searchParams를 prop으로 받아 처리, robots: {index:false} + robots.txt에 Disallow: /search 추가로 인덱싱 대상에서도 제외).
  • HomeContent.tsx에서 useSearchParams()를 완전히 제거 — 홈은 검색 여부와 무관하게 항상 정적으로 전체 그리드를 렌더링.
  • 결과: 빌드 시 / 라우트가 ○ (Static)으로 표시됨 (이전엔 보이지 않던 동적 우회 없이 정상 정적 생성). 배포 후 운영 서버에서 직접 확인한 결과 포스트 링크 10개, 이미지 12개, 제목 10개가 정상적으로 서버 HTML에 포함됨.

4. 진단 방법론 — 무엇이 달랐나

이번에 3차 원인을 찾아낸 방법은 이전 두 차례와 달랐다는 점이 중요하다.

  • 1·2차 원인: 로컬 코드를 읽고 "이 부분이 문제겠다"를 추론한 뒤 수정 → 검증은 빌드 성공 여부로만 확인.
  • 3차 원인: now와 matmatch의 코드를 나란히 비교 분석했지만, 코드 분석만으로는 결론이 나지 않았다 (오히려 코드상으로는 matmatch가 now보다 SSR/구조화 데이터 면에서 더 우수해 보였다). 실제로 curl -A "Yeti" <운영 URL>운영 서버가 크롤러에게 무엇을 실제로 응답하는지 직접 확인하면서, 코드의 "의도"와 배포된 "실제 동작"이 정반대였던 지점을 발견.

"로컬 코드가 SSR처럼 보인다"는 것과 "운영 서버가 크롤러에게 실제로 콘텐츠를 내려준다"는 것은 다른 문제였다.


5. 지금여기는 왜 잘 되고 있었나 — 남은 질문

기술적으로만 보면 now의 홈/상세 페이지는 지금도 완전한 CSR이다 (상세 페이지는 초기 HTML에 "Loading..." 문구만 있고, 실제 본문은 클라이언트 렌더링 이후에만 나타남). 이는 일반적인 SEO 기준으로는 matmatch보다 불리한 구조다. 그런데도 now는 검색 노출이 잘 되고 있다. 가능한 설명:

  1. 서비스 시작 시점 차이: 네이버의 신뢰도/누적 지수(C-Rank 류) 알고리즘에서 사이트 연령과 누적 신호가 유리하게 작용했을 가능성.
  2. 콘텐츠 주제의 경쟁 구도 차이: 지금여기는 특정 팝업스토어명·축제명 등 고유명사 기반의 hyperlocal·시의성 콘텐츠를 다룬다. 경쟁 콘텐츠가 거의 없는 롱테일 키워드라 노출이 쉽다. matmatch는 "우롱차의 역사", "북경오리 과학" 같은 일반 교양/정보성 콘텐츠를 다루는데, 이는 위키·대형 미디어·블로그가 이미 점유한 초경쟑 카테고리라 같은 기술 수준이어도 노출이 훨씬 어렵다.
  3. 기술적 요인은 일부일 뿐: 이번에 고친 3차 원인(홈 콘텐츠 누락)은 명백한 결함이었지만, 그것만으로 now와의 격차 전체를 설명하긴 어렵다. 콘텐츠 전략(주제 선택)도 함께 검토할 필요가 있다.

이 부분은 아직 결론이 아니라 가설이며, 추후 검색 반영 추이를 보며 검증이 필요하다.


6. 시사점

  1. "의도"와 "실제 배포 결과"는 다를 수 있다. 6/16 커밋은 "크롤러용 링크 노출 효과를 유지"하려는 의도였지만 정반대로 작동했다. 코드 리뷰만으로는 이런 종류의 버그(프레임워크 차원의 렌더링 분기)를 잡기 어렵다.
  2. 운영 서버를 크롤러 시점에서 직접 검증해야 한다. curl -A "Yeti" <URL>처럼 실제 크롤러 User-Agent로 운영 환경의 응답을 까보는 것이, 로컬 코드 분석보다 훨씬 빠르고 확실하게 진짜 원인을 드러냈다.
  3. "서버 속도 최적화"와 "크롤러 노출"은 자주 충돌하는 목표다. 페이지네이션 축소(1차), ISR 캐싱 전환(3차) 모두 서버 부하를 줄이려는 시도였고, 그 과정에서 매번 크롤러 노출이 희생됐다. 두 목표를 동시에 만족시키려면 "검색 의존적 로직(useSearchParams 등)을 캐싱 가능한 정적 콘텐츠와 철저히 분리"하는 설계가 필요하다 (오늘 적용한 /search 라우트 분리가 그 예).
  4. 기술적 SEO가 전부는 아니다. matmatch는 now보다 기술적으로(SSR, JSON-LD, sitemap 세분화 등) 더 잘 갖춰져 있었는데도 검색 노출은 더 나빴다. 콘텐츠 주제의 경쟁 강도, 사이트 연령, 도메인 신뢰도 같은 비-기술적 요인이 동등하게, 혹은 더 크게 작용할 수 있다.
  5. 같은 인프라 위의 두 서비스를 비교하는 것 자체가 진단 도구가 된다. "왜 안 되지"를 한 서비스 안에서만 파다 보면 추측에 머물기 쉬운데, 잘 되고 있는 비교 대상(now)이 있었기에 "기술 vs 콘텐츠" 구분이 비로소 선명해졌다.
# 바이브코딩# 바이브코딩seo전략# seo전략# seo실수# isr# ssg# seo교훈# 비개발자# 네이버 SEO# 네이버 검색 노출 안됨# 네이버 색인 누락# 네이버 검색엔진 최적화

COMMENTS

의견을 남기려면 로그인이 필요합니다.

NO COMMENTS YET.
Nemone Store Banner