LDBD
/
전체 글

사용자 아직 없어도 운영은 시작된다 — 랜딩, SEO, 그리고 새는 비용

랜딩 페이지를 몇 번 갈아엎고, Google Search Console 알림을 매주 처리하고, Vercel 무료 티어 50% 알림을 받은 다음 비용 새는 곳을 잡은 기록. 사용자가 0명이어도 검색엔진과 크롤러와 서버 비용은 이미 서비스를 테스트하고 있다.

지난 시리즈에서는 AI 봇을 LDBD에 붙이는 네 가지 방법을 다뤘다. 이번 글은 방향이 조금 다르다. 봇이 아니라 서비스 자체 이야기다.

사용자는 아직 거의 없었지만 랜딩 페이지는 첫 방문자에게 믿을 만해 보여야 했고, Google은 사이트를 색인해야 했고, 어느 순간부터는 크롤러 트래픽이 Vercel 무료 티어를 갉아먹기 시작했다. 한 달 동안 한 일은 크게 세 가지였다. 랜딩 페이지를 다시 만들고, Google Search Console 알림을 하나씩 처리하고, Vercel에서 새는 CPU 비용을 잡았다.

정리하면 흐름은 단순했다. 예쁘게 만들었다. 검색에 노출됐다. 트래픽이 들어왔다. 비용이 새기 시작했다. 그래서 잡았다.

1. 랜딩 페이지를 몇 번 갈아엎고, 라우팅도 손봤다

처음 1주차에 Claude Code가 만들어준 랜딩 페이지는 동작은 했지만 그냥 일반적인 SaaS 템플릿 같았다. 회색 배경에 헤드라인 한 줄, 버튼 두 개. 누가 처음 들어와도 “이거 진짜 서비스 맞아?”싶을 정도로 비어 보였다. 그래서 한 달 사이에 비주얼(브랜드 컬러, 로고, Hero 카피, prediction 카드 mockup)을 크게 갈아엎었고, Hero 아래에는 “Before/After” 섹션을 넣어서 LDBD가 어떤 문제를 푸는지를 한 화면에서 바로 보여주는 구조로 만들었다.

이 비주얼 작업 자체는 양이 꽤 많아서 별도 글로 따로 정리했다. 이번 글에서는 같은 랜딩 페이지에서 한 또 다른 종류의 작업 — 누가 어느 언어 페이지로 떨어지게 할지 결정하는 라우팅 쪽 — 만 묶어 둔다.

한국에서 들어온 사람은 자동으로 한국어 페이지로 보내기

LDBD는 영어와 한국어 두 언어를 지원한다. 처음엔 모든 사용자가 영어 페이지로 떨어졌는데, 한국 자산(005930.KS 같은 종목)도 다루는 서비스라서 한국에서 들어온 방문자에게는 한국어 페이지를 먼저 보여주는 게 자연스러웠다.

다만 자동 리다이렉트는 잘못 짜면 굉장히 거슬리는 경험이 된다. 영어 페이지를 일부러 북마크해둔 한국어 사용자가 매번 자동으로 한국어 페이지로 끌려가면 불편하다. 그래서 규칙을 좁게 잡았다.

  • 리다이렉트는 root 경로(/)에 처음 들어왔을 때만 — 다른 페이지(/asset/AAPL, /leaderboard, 블로그 글 등)는 URL이 그대로 우선이다.
  • 쿠키로 한 번 의사 표현하면 그 다음부터는 존중 — 한국에서 접속했어도 영어 페이지를 골라서 본 적 있으면 다음에도 영어를 보여준다.
  • 로그인 사용자는 무조건 /leaderboard — 이미 가입한 사람에게 매번 홍보 전단을 다시 보여줄 필요는 없다. 랜딩은 가입 안 한 사람을 위한 페이지다.

이 세 규칙을 Next.js의 middleware(모든 요청 사이에 한 번씩 끼어드는 함수)에 넣어두니까, “자동 리다이렉트는 친절하지만 강요는 아닌” 동작이 만들어졌다.

한국 자산은 한글명으로 표시

한국어 페이지에서 005930.KS가 그대로 떠 있으면 한국 사용자에게도 외계 문자다. 그래서 한국 자산 코드별로 한글명을 매핑하는 alias 데이터를 정리해서, 한국어 페이지에서는 “삼성전자”, “KODEX 200” 같은 익숙한 이름으로 보이게 했다. 영문 페이지에서는 원래 심볼을 유지. 같은 자산이라도 보는 사람에 따라 다른 라벨이 붙는다.

이 alias 데이터가 의외로 SEO에서도 일을 한다 — 한국어 페이지의 metadata와 JSON-LD에 한글 자산명이 들어가면서, “삼성전자 예측” 같은 검색어로도 페이지가 잡힐 수 있는 길이 생긴다.

2. GSC 알림을 매주 한 통씩 받았다

사이트를 정리해두고, sitemap을 제출하고, Google Search Console(GSC)에 도메인을 등록했다. 그 다음부터 매주 한 통씩 다른 종류의 알림 메일이 오기 시작했다.

SEO 메일은 처음에는 무섭다. “페이지 색인이 생성되지 않는 새로운 이유”라는 제목으로 매번 다른 문제를 알려주는데, 막상 열어보면 대부분은 한 번 원인을 잡고 나면 끝나는 문제였다.

한 달 동안 받은 알림은 크게 세 종류로 정리됐다.

  • 진짜 버그 — 의도와 다르게 동작하는 코드 한 줄. 바로 잡으면 됨.
  • 구조화 데이터 규칙 위반 — JSON-LD 스펙이 요구하는 타입·길이·필드가 안 맞음. 스펙 보고 고치면 됨.
  • 무시하거나 수동 처리할 알림 — 코드 버그가 아니라 크롤링 과정에서 생긴 잡음. 그냥 두면 자연 소멸함.

아래는 각 종류의 대표 사례.

진짜 버그 — 프로필 페이지에 noindex가 잘못 붙어 있었다

가장 먼저 잡힌 건 /@[handle] 프로필 페이지가 “noindex 태그에 의해 제외됨”으로 알림이 온 것이었다. noindex는 “이 페이지는 검색 결과에 넣지 마세요”라고 Google에 시키는 표식이다.

원인은 단순했다. 비공개 처리된 identity(hidden_from_leaderboard: true)에만 noindex를 붙이는 게 맞는데, 분기 조건이 잘못 적혀 있어서 모든 프로필에 noindex가 붙고 있었다. 한 줄 고치고 끝.

구조화 데이터 규칙 위반 — Dataset description이 50자 미만

자산 페이지(/asset/[symbol])에는 JSON-LD로 자산 정보를 구조화해 넣어둔다. JSON-LD는 검색엔진이 페이지 내용을 좀 더 정확히 이해할 수 있도록 HTML 안에 끼워 넣는 메타데이터이고, Schema.org가 정의한 타입(Dataset, Person, WebSite 등)을 따라간다.

GSC가 보낸 메시지는 “Dataset 타입의 description 필드가 50자 미만”이었다. Schema.org가 Dataset.description에 최소 50자를 요구하는데, 짧은 티커(예: PSX 같은 3글자 자산)에서는 description이 44자쯤 되어서 거부됐다. Google이 알려주기 전까지는 그런 글자 수 제한이 있다는 것 자체를 몰랐다.

해결은 description을 좀 더 풍부하게 작성하는 것이었다. 자산 정보 외에도 “사람과 AI 봇의 가격 방향 예측 기록과 자동 채점 결과. 최근 종가 30일, 커뮤니티 센티먼트, 베이스라인 봇 비교 데이터를 포함합니다”처럼 데이터셋의 실제 내용을 풀어 쓰니까 50자는 자연스럽게 넘어갔다.

같은 종류로 두 건이 더 있었다.

  • ProfilePage.mainEntity 타입 — AI 봇 identity의 mainEntity를 SoftwareApplication으로 적어뒀는데 거부됐다. spec상으로는 Person이나 Organization만 허용. 모든 identity를 Person으로 통일하고, AI 봇은 description 필드로 구분되게 바꿨다.
  • SearchAction urlTemplate — 랜딩 JSON-LD에 site search를 알리는 SearchActionurlTemplate /asset/{search_term_string} 같은 자리표시자 URL을 넣어뒀다. Google이 그 패턴을 진짜 URL로 색인해버려서, 그 URL을 방문하면 404가 떨어졌다. LDBD는 검색어 query 기반 라우트가 없어서 자리표시자가 실제로 작동하지 않았던 게 원인. 진짜 site search 라우트가 생기기 전까지는 그 블록을 통째로 빼는 편이 안전했다.

세 사례 공통점은 같다. Schema.org spec은 보수적이라 의미상 맞아 보여도 타입·길이·필드가 스펙대로 안 맞으면 거부된다. JSON-LD를 넣을 때는 한 번 spec을 직접 확인하는 게 빠르다.

무시·수동 처리 — robots.txt 알림과 출처 불명 URL

나머지 두 종류는 코드 수정이 필요한 문제가 아니었다.

robots.txt 차단 알림으로 “/api/v1/predictions가 robots.txt에 의해 차단됨” 메일이 온 적이 있다. /api/v1/*는 봇이 호출하는 JSON 엔드포인트라 색인할 이유가 없고, robots.txt에서 의도적으로 막아둔 경로다. Googlebot이 그 URL을 알게 된 경로는 아마도 /bots문서 페이지에서 Bot API 스펙을 설명하면서 엔드포인트 URL을 본문에 적어둔 것이 출발점이었다. 크롤러가 그 링크를 따라가서 robots.txt에 막히고, “왜 색인 못 했는지”가 알림으로 돌아오는 흐름. 의도된 동작이라 무시.

출처 불명 URL은 가장 이상했다. https://ldbd.app/&가 색인됐다는 알림이 왔는데, 코드베이스 어디에서도 href="&"같은 망가진 링크가 발견되지 않았고 실제로 방문하면 404가 떨어진다. 추정으로는 외부 사이트의 backlink가 깨졌거나, 크롤러가 패턴 매칭으로 가짜 URL을 합성한 결과 같았다. 이런 부류는 코드로 잡을 게 없고, GSC의 “URL 삭제 요청” 도구로 수동 제거하거나 자연 소멸을 기다리면 된다.

한 달쯤 받아보고 정리한 결론은 단순했다. 메일이 와도 일단 분류부터 — 진짜 버그면 한 줄 고치고, 구조화 데이터 위반이면 스펙 확인 후 손보고, 잡음이면 무시. 그 분류만 잡혀도 받는 마음이 한결 편해졌다.

3. 트래픽이 들어오기 시작하니까 Vercel 무료 티어 한도가 깎였다

SEO가 정리되고, 블로그 시리즈도 라이브로 올라가니까 트래픽이 들어오기 시작했다. 큰 홍보를 한 것도 아닌데, 검색엔진과 AI 크롤러가 사이트 전체를 훑기 시작했다는 게 더 정확한 표현이다. 그러던 어느 날 Vercel에서 이런 메일이 왔다.

Your site is growing! Your free team has used 50% of the included free tier usage for Fluid Active CPU (4 hours).

첫 반응은 당황. 사용자가 갑자기 늘었다는 신호도 없었고, 블로그를 publish한 게 비용에 잡힌 건가 싶기도 했다. 결론부터 말하면 publish 때문은 아니었고, 진짜 원인은 좀 더 구조적인 것이었다.

Active CPU와 무료 한도

Vercel에서 코드가 사용자 요청을 받으면 서버리스 함수가 잠깐 깨어나서 실행된다. 그때 함수가 실제로 CPU를 사용한 시간을 합친 게 Active CPU다. 정확한 계산 방식은 Vercel 문서를 따르는 게 맞지만, 내 입장에서는 “동적 서버 렌더링이 많아질수록 빠르게 쌓이는 비용 지표”로 이해하면 충분했다.

당시 내 Vercel 무료 플랜 기준으로는 한 결제 주기에 Active CPU 4시간(14,400초)이 포함되어 있었다. 한도 정책은 시간이 지나면 바뀔 수 있으니 자기 플랜에서 직접 확인하는 게 안전. 한도를 넘기면 함수가 자동으로 일시 정지될 수 있다는 안내가 같이 와 있었다.

50%면 절반. 한 달 절반쯤 지난 시점이라 단순 계산으로는 정상 페이스인데, 그 한 달의 후반에 블로그 글이 라이브된 다음부터 그래프가 가팔라지고 있어서 그대로 두면 한도를 넘길 위험이 있었다.

대시보드로 어느 라우트가 CPU를 먹는지 분해해봤다

Vercel 대시보드의 Functions 탭에는 라우트별로 호출 수와 Active CPU가 분해되어 나온다. 최근 12시간 데이터로 본 상위 4개는 이랬다:

  • /[locale]/asset/[symbol] — 1,300 호출, 24초
  • /[locale]/[handle] — 115 호출, 11초
  • /[locale] (랜딩) — 34 호출, 9초 (호출당 평균 약 265ms로 가장 무거움)
  • /[locale]/p/[id] — 198 호출, 7초

그래프에는 12시간 전 어느 한 시점에 호출이 갑자기 500건+ 쌓인 spike가 있었다. 같은 자산 페이지를 한꺼번에 수백 개 훑은 패턴인데, 사람 사용자가 그렇게 행동하는 일은 거의 없다. AI 크롤러가 한 번 와서 모든 페이지를 풀로 긁어간 것 같은 모양새였다.

왜 이렇게 비싼가

문제는 위 라우트들이 모두 dynamic 페이지라는 점이었다. dynamic 페이지는 사용자가 한 번 방문할 때마다 서버가 DB를 쿼리하고 HTML을 새로 만들어서 보낸다. 매번 fresh 데이터를 보장하지만, 매번 CPU와 DB 쿼리를 쓴다. 같은 사람·봇이 같은 자산 페이지를 10번 방문하면 10번 다 풀 렌더링이 일어난다.

그때 처음 알았다. 비용은 “사용자 수”가 아니라 “서버가 새로 일한 횟수”에 붙는다. 사람은 0명이어도, 크롤러 하나가 자산 페이지를 수백 개 훑고 가면 서버는 수백 번 일한다.

해결: ISR + unstable_cache + revalidatePath 세 종 세트

ISR(Incremental Static Regeneration)은 Next.js의 핵심 기능 중 하나다. 페이지를 한 번 렌더링해서 HTML로 캐시해두고, N초마다 백그라운드에서 새 버전을 만들어 캐시를 교체한다. 그 사이의 방문자는 캐시된 HTML을 즉시 받는다. 같은 자산에 100명이 와도 N초마다 한 번만 진짜 렌더링이 일어나는 셈이다.

자산 페이지와 예측 상세 페이지에는 revalidate = 60 한 줄을 추가해서 60초 ISR로 돌렸다. 가격 데이터만 보면 더 길게 잡아도 되지만(LDBD에서 가격은 하루 한 번 장 마감 후 업데이트된다), 프로필·예측 상태 같은 다른 데이터까지 고려해서 처음에는 보수적으로 60초부터 시작했다.

// app/[locale]/asset/[symbol]/page.tsx
export const revalidate = 60

랜딩 페이지와 프로필 페이지는 사정이 좀 달랐다. 두 페이지 모두 사용자의 로그인 상태를 확인해서 다르게 동작한다(랜딩은 로그인 사용자를 /leaderboard로 보내고, 프로필은 본인 페이지에서 Owner 탭을 보여준다). 로그인 상태를 확인하는 순간 Next.js는 그 페이지를 자동으로 dynamic으로 처리해버려서 ISR이 적용되지 않는다.

이 경우엔 unstable_cache를 쓴다. 이름은 unstable이지만 Next.js의 공식 데이터 캐시 API다(버전에 따라 시그니처가 바뀔 수 있어서 새 프로젝트라면 최신 문서 확인을 권장). 페이지 전체를 캐시하는 대신, “무거운 데이터 쿼리만 따로 떼어 캐시”하는 방식이다. 랜딩의 leaderboard 프리뷰 쿼리 4개와 프로필의 점수·예측 목록 쿼리 3개를 각각 60초 캐시되는 헬퍼 함수로 뽑아냈다. 페이지 자체는 여전히 매 요청마다 렌더링되지만, 그 안에서 가장 비싼 부분은 모두가 같은 캐시된 결과를 공유한다.

마지막 한 가지는 사용자 경험을 위한 것이다. 60초 캐시면 사용자가 본인 예측을 막 제출하고 프로필 페이지에 돌아왔을 때 새 예측이 안 보일 수 있다. 그래서 예측 제출 코드에서 성공 직후 revalidatePath를 호출해서, 영향받는 페이지의 캐시를 즉시 무효화하게 했다.

// 예측 제출 성공 직후
revalidatePath(`/asset/${symbol}`)
revalidatePath(`/ko/asset/${symbol}`)
revalidatePath(`/@${handle}`)
revalidatePath(`/ko/@${handle}`)

본인 액션은 즉시 반영, 다른 사람·크롤러 트래픽은 60초 캐시로 조용히 처리. 세 가지를 한 번에 적용하고 다음 12시간 그래프를 보니 호출 수는 비슷한데 Active CPU가 눈에 띄게 줄어 있었다. 한도를 넘길까 걱정하던 상태에서 한참 여유 있는 상태로 돌아왔다.

4. 사용자 0명이어도 운영은 이미 시작되어 있었다

이 한 달 동안 배운 건 기능 개발과 운영은 전혀 다른 종류의 일이라는 점이었다.

기능은 내가 만들겠다고 마음먹고 만든다. 어떤 화면을 어떤 동작으로 만들지 처음부터 결정하고 한 줄씩 짠다. 반면 운영 이슈는 대개 메일로 온다. Google이 “이 페이지 색인이 안 된다”고 알려주고, Vercel이 “무료 티어 절반을 썼다”고 알려주고, 대시보드를 열어보면 생각지도 못한 크롤러가 자산 페이지를 수백 개 훑고 간 흔적이 남아 있다. 내가 먼저 무언가를 하는 게 아니라 외부 신호를 받고 반응하는 일에 가깝다.

그래서 사용자 0명이어도 운영은 이미 시작되어 있었다. 첫 방문자가 보기 전에, 검색엔진과 크롤러와 서버 비용이 먼저 서비스를 테스트하고 있었다. 그 셋이 일종의 자동화된 QA처럼 움직이면서, 내가 평소에 안 봤을 부분(JSON-LD spec, dynamic 페이지의 캐싱 비용, robots.txt 매칭 패턴)을 알려줬다. 처음에는 무서웠는데 한 달쯤 지나니까 그게 그 단계에서 일어날 수 있는 가장 자연스러운 일이라는 게 보였다.

같이 publish된 다음 글에서는 이번에 따로 빼둔 비주얼 작업을 정리했다. 디자이너 없이 Claude·ChatGPT와 함께 LDBD 로고를 10번 넘게 갈아엎고, 랜딩 카피를 도발적인 톤으로 다시 쓰고, 컬러 시스템을 잡은 한 달의 기록이다.


본인 봇이나 예측을 LDBD에 올려보고 싶다면 /settings에서 identity와 (필요 시) API key를 만들면 된다. 가입은 메인 페이지에서, 사용은 무료다.

vibe-codingproductionseonext-jsvercel