Loading the catalog…
Loading the catalog…
유전자 페이지를 회사 사이트로 옮기자 검색 유입이 늘었다. 그런데 화면에 잘 보이던 링크와 본문이 서버 HTML에는 빠져 있었다. 유전자 이름을 검색하면 우리 사이트가 나왔으면 했다. 이미 유전자 약 1만 9천 개의 상세 페이지가 있었지만, 별도 서브도메인에 있어서인지 검색 유입은 거의 없었다. 이 페이지들을 회사의 메인 도메인으로 옮겼다. 영문과 국문을 합쳐 약 4만 페이지. 옮긴 뒤에는 검색으로 들어오는 사람이 늘었다. 그런데 안 보이던 문제도 하나씩 드러났다. 목록에는 분명 링크가 있는데 크롤러는 찾지 못했고, 정작 중요한 질병 정보는 버튼을 눌러야 나타났다. 4월에는 Search Console에 soft 404가 1,414건 잡혔다. 5월 Vercel 청구서는 2월의 2.3배였다. 옮기기만 하면 될 줄 알았는데, 그 뒤에 할 일이 더 많았다. 이 글에는 검색과 색인 문제를 고친 과정을 정리했다. 비용 이야기는 2부 「Vercel ISR 쓰기 비용을 83% 줄이기까지」에서 이어진다. 처음에는 단순한 정적 사이트였다 내가 다니는 회사는 AI로 유전자 데이터를 분석한다. Gene Browser는 유전자 정보를 찾아보는 서비스로, gene-browser.3billion.io 에서 따로 운영하고 있었다. Next.js 13.4에 output: 'export' 를 설정하고, 유전자 19,312개의 데이터를 JSON 파일로 저장소에 넣어뒀다. 빌드할 때 HTML을 전부 만든 뒤 S3에 올렸다. 영문만 지원했고, 헤더와 푸터는 회사 사이트에서 복사해 왔다. 회사 사이트의 메뉴가 바뀌면 여기도 따로 고쳐야 했다. 데이터가 거의 바뀌지 않았으니 이 정도면 충분했다. 아쉬운 건 검색 유입이었다. Search Console 기록이 남아 있는 2025년 4월부터 12월까지 유입은 미미했고, 평균 게재순위는 24위에서 37위 사이였다. 외부에서 걸어준 링크는 메인 도메인 쪽이 훨씬 많았다. 유전자 페이지도 이쪽으로 옮기면 좀 더 잘 노출되지 않을까 싶었다. 유전자 이름을 검색해서 들어온 사람이 다른 글이나 서비스까지 둘러봐 주면 더 좋겠다고 생각했다. 2025년 12월 29일, Gene Browser를 회사 사이트 저장소의 /gene-browser 경로로 옮겨 배포했다. 회사 사이트가 영문과 국문을 모두 지원해 페이지 수는 약 두 배가 됐다. 옮기고 얼마 지나지 않아 문제가 생겼다. 상세 페이지를 전부 정적으로 생성하려 했는데 빌드 산출물이 Vercel의 크기 한도를 넘었다. 우선 페이지를 요청받을 때 생성하도록 바꿔서 배포했다. 몇 달 뒤에는 이때 바꾼 방식을 다시 들여다보게 됐다. 링크가 있는데 왜 못 찾을까 2월에 Ahrefs가 유전자 상세 페이지들을 orphan page로 보고했다. 사이트 안에서 이 페이지로 연결되는 링크를 찾지 못했다는 뜻이다. 이상했다. 목록 페이지에 유전자 이름이 있고, 누르면 상세 페이지로 잘 넘어갔다. 하지만 서버 응답을 확인해 보니 HTML에 그 링크가 없었다. 목록을 필터링하고 링크를 그리는 부분을 서버 컴포넌트로 옮겼다. 사용자 입력을 처리하는 검색창만 클라이언트 컴포넌트로 남겼다. 아래는 바꾼 구조를 간단히 옮긴 코드다. 이름과 경로는 예시로 바꿨다. // 서버 컴포넌트 export default async function CatalogPage() { const items = await getItems(); return ( <> <SearchBar /> <ol> {items.map((item) => ( <li key={item.id}> <a href={`/catalog/item/${item.id}`}> {item.name} </a> </li> ))} </ol> </> ); } 화면은 똑같았다. 이제는 curl 로 받은 HTML에서도 <a href> 를 확인할 수 있었다. 다만 이걸 'use client' 때문이라고만 설명하면 정확하지 않다. Next.js는 첫 페이지를 보낼 때 클라이언트 컴포넌트도 서버에서 HTML로 렌더링할 수 있다. Next.js 문서 에도 나오는 동작이다. 우리 코드에서는 링크가 빠져 있었고, 서버 컴포넌트로 분리한 뒤에는 포함됐다. 컴포넌트 종류만 보고 판단할 게 아니라 실제 응답을 확인해야 했다. 목록은 26개인데 대표 URL은 하나였다 알파벳별 목록에도 문제가 있었다. 당시에는 ?prefix=A 처럼 쿼리 파라미터로 목록을 구분했다. 그런데 26개 목록의 canonical이 모두 /gene-browser 를 가리키고 있었다. canonical은 검색엔진에 대표 URL을 알려주는 설정이다. 우리는 A 목록과 B 목록을 각각 검색에 노출하고 싶으면서, 둘의 대표 URL은 같다고 적어두고 있었다. /gene-browser/prefix/A , /gene-browser/gene/BRCA1 처럼 경로를 정리하고, 기존 URL에는 301 리디렉션을 걸었다. 알파벳 목록 26개는 빌드할 때 정적으로 만들었다. 쿼리 파라미터를 쓴 것 자체가 잘못은 아니다. Google도 페이지를 구분할 때 파라미터를 사용할 수 있다고 안내한다. 우리에게 문제였던 건 서로 다른 목록이 같은 canonical을 가리키는 설정이었다. Google의 페이지네이션 안내 에서도 각 페이지의 URL과 canonical을 어떻게 잡아야 하는지 설명한다. Search Console의 ‘리디렉션이 포함된 페이지’는 약 1만 6천 건으로 늘었다. 기존 URL을 새 URL로 보내고 있었으니 예상한 결과였다. 가장 중요한 내용을 버튼 뒤에 숨겨뒀다 4월에는 Search Console에 soft 404가 1,414건 잡혔다. 서버는 정상 응답인 HTTP 200을 보내는데, Google은 오류 페이지나 내용이 없는 페이지처럼 보고 있다는 뜻이었다. 페이지를 다시 살펴보니 고칠 곳이 두 군데 보였다. 먼저 질병 연관 정보 테이블이었다. 유전자 페이지에서 가장 중요한 내용인데, ‘더 보기’를 눌러야 렌더링되도록 만들어뒀다. 'use client'; import { useState } from 'react'; export default function DiseaseSection({ rows }) { const [expanded, setExpanded] = useState(false); return ( <> <button onClick={() => setExpanded(!expanded)}> {expanded ? '접기' : '더 보기'} </button> {expanded && <DiseaseTable rows={rows} />} </> ); } 초깃값이 false 라서 클릭 전에는 테이블 자체가 없었다. Google이 JavaScript를 실행할 수 있다고 해도, 보통 이런 버튼까지 눌러서 내용을 불러오지는 않는다. Google도 사용자 동작이 필요한 콘텐츠는 주의하라고 안내한다 . 접고 펼치는 동작을 <details> 와 <summary> 로 바꿨다. // 서버 컴포넌트 export default function DiseaseSection({ rows }) { return ( <details> <summary>질병 연관 정보</summary> <DiseaseTable rows={rows} /> </details> ); } 이제 테이블은 접혀 있어도 HTML 안에 들어 있다. 접고 펼치는 일은 브라우저가 한다. React로 상태를 관리할 필요가 없어졌고, 기본적인 키보드 조작도 됐다. 펼침 추적은 toggle 이벤트를 듣도록 바꿨다. 이것만으로 색인이 보장되는 건 아니다. 그래도 클릭하기 전에는 아예 없던 내용을 처음부터 읽을 수 있게 했다. 그 뒤로는 핵심 정보가 버튼이나 탭 안에 있을 때, 숨겨져만 있는지 아니면 누르기 전까지 생성조차 안 되는지부터 확인한다. 사이트맵에 넣기엔 내용이 너무 적었다 나머지 문제는 페이지 내용이었다. 유전자 19,312개 중 질병 연관 정보가 있는 건 4,998개였다. 나머지 약 1만 4천 개에는 유전자명, 좌표, 동의어 정도만 있었다. 이 페이지들까지 전부 사이트맵에 들어가 있었다. 질병 정보가 있는 유전자만 남겼다. 사이트맵에 넣는 대상이 약 74% 줄었다. 페이지를 삭제하지는 않았다. 사이트 안에서 링크를 따라 들어가면 그대로 열렸다. 사이트맵에는 검색 결과에 보여주고 싶은 URL을 넣는다. 다만 넣는다고 반드시 색인되는 것도, 뺀다고 색인에서 없어지는 것도 아니다. Google 사이트맵 문서 에서도 사이트맵 제출을 힌트로 설명한다. 우선 내용이 있는 페이지부터 알리기로 한 것이다. 나머지 페이지의 내용을 보강하는 일까지 끝난 건 아니었다. HTML에 본문을 넣어둔 이유 브라우저에서 JSON을 받아 화면을 그리는 CSR로 만들 수도 있었다. 작은 정적 셸만 보내면 되니 생각해 볼 만한 방법이었다. Google은 JavaScript를 렌더링한다. CSR이라는 이유만으로 검색에 안 잡히는 것은 아니다. 우리가 겪은 soft 404도 모든 CSR 페이지에 생기는 문제라고 볼 수는 없다. 다만 이 페이지는 검색으로 찾아와 정보를 읽는 용도로 만들었다. 처음 받는 HTML에 본문이 들어 있으면, 브라우저나 크롤러가 JavaScript를 실행하고 데이터를 가져올 때까지 기다리지 않아도 된다. Google의 JavaScript SEO 가이드 도 모든 봇이 JavaScript를 실행하지는 않는다며 서버 렌더링이나 프리렌더링을 고려하라고 권한다. 우리 로그에는 ClaudeBot도 보였다. Vercel과 MERJ의 2024년 12월 연구 에서는 테스트한 여러 AI 크롤러가 JavaScript를 실행하지 않았다. 지금도 모두 같다고 단정할 수는 없지만, 적어도 본문을 읽는 데 JavaScript가 꼭 필요하게 만들고 싶지는 않았다. HTML을 제공한다고 검색 노출이나 AI의 인용까지 보장할 수는 없다. 우리가 할 수 있는 일은 우선 콘텐츠를 읽을 수 있게 해두는 것이었다. 그래서 검색 유입은 얼마나 늘었나 이전 후 유전자 페이지의 검색 유입은 확실히 늘었다. 아래는 클릭과 노출을 지수로 바꾼 값이다. 클릭은 2026년 4월을 100, 노출은 3월을 100으로 잡았다. 기준이 다르니 두 지수는 각각의 월별 흐름만 보면 된다. 월 클릭 지수 노출 지수 검색 결과에 나온 페이지 1월 16 47 약 1만 1천 개 3월 35 100 약 1만 1천 개 4월 100 63 약 6,600개 5월 71 59 약 3,600개 1월부터 5월까지 받은 클릭을 합치면, 서브도메인의 2025년 아홉 달 누적 클릭보다 200배 이상 많았다. 다섯 달과 아홉 달의 총합을 비교한 값이므로 월간 성장률과는 다르다. 4월 한 달에는 유전자 페이지가 사이트 전체 검색 클릭의 20%를 차지했다. 검색어는 KYNU , STRIP2 , PARN 같은 유전자 심볼이었다. 모든 지표가 계속 오른 건 아니다. 클릭은 4월보다 5월에 줄었고, 검색 결과에 나온 페이지 수도 감소했다. 도메인만 바꿔서 얻은 결과라고 말하기도 어렵다. 옮기는 동안 호스팅과 지원 언어가 바뀌었고, 이후에는 내부 링크, canonical, 콘텐츠 노출 방식도 고쳤다. 검색 수요 자체도 달라졌다. 이전과 후속 작업을 거치며 검색 성과가 좋아진 건 확인했다. 다만 그중 얼마가 도메인 덕분인지는 따로 알 수 없다. KYNU에 관심이 몰렸을 때 4월 유전자 페이지 클릭의 절반 이상은 KYNU 한 페이지에서 나왔다. 2026년 3월 20일, KYNU와 항종양 면역을 다룬 논문이 Cell Reports에 온라인으로 공개됐다. PubMed 논문 기록 에서 날짜를 확인할 수 있다. 약 2주 뒤부터 Search Console의 kynu 검색 노출이 열 배 넘게 늘었다. 우리 페이지는 이 검색어에서 평균 3위 안팎이었다. 논문이 나온 시기와 노출이 늘어난 시기가 겹쳤다. 이것만으로 논문 때문에 검색이 늘었다고 확정할 수는 없지만, 관심이 커졌을 때 우리 페이지가 검색에 나오고 있었다는 점은 확인할 수 있었다. 1월부터 5월까지를 합쳐도 KYNU가 전체 유전자 페이지 클릭의 58%를 차지했다. 전체 성과를 볼 때 이 한 페이지의 영향을 빼놓을 수 없다. 어떤 유전자가 언제 주목받을지는 미리 알 수 없었다. 그래도 페이지를 준비해 둔 덕분에 KYNU를 찾는 사람들에게 보여줄 내용은 있었다. 2025년 서브도메인 기록에는 없던 검색 노출이었다. 그 자리에 그대로 뒀다면 어땠을지는 알 수 없지만, 이번에는 실제 방문으로 이어졌다. 이제는 들어온 사람이 읽을 내용을 채워야 한다 처음에는 유전자 페이지로 들어온 사람들이 다른 콘텐츠도 읽어주길 기대했다. 이 부분은 아직 확인하지 못했다. 정작 유전자 페이지에는 관련 블로그 글이나 질환 페이지로 이어지는 링크가 부족했다. 검색으로 들어올 곳은 만들었지만, 다음에 무엇을 읽으면 좋을지 충분히 연결해 두지 않은 것이다. 사이트 전체에도 도움이 됐다고 말할 근거는 아직 없었다. 옮긴 지 약 9개월이 지난 지금은 이 일을 해야 한다. 유전자 기능과 질병 정보를 더 자세히 쓰고, 관련 데이터베이스와 우리 사이트의 다른 글을 연결하려고 한다. 다시 많은 페이지를 한꺼번에 공개한다면 네 가지부터 확인할 것 같다. 중요한 내부 링크가 응답 HTML에 들어 있는지 본다. Search Console의 URL 검사로 렌더링 결과도 확인한다. 버튼을 누르거나 탭을 열기 전에도 핵심 내용을 읽을 수 있는지 본다. 리디렉션, 내부 링크, 사이트맵, canonical에 적힌 URL이 서로 맞는지 확인한다. 검색해서 들어온 사람이 궁금한 내용을 찾을 수 있는 페이지인지 읽어본다. 페이지를 옮기는 데서 끝날 줄 알았는데, 링크와 URL 설정부터 본문을 보여주는 방식까지 다시 살펴보게 됐다. 그렇게 유입은 늘었고, 이제는 들어온 사람이 읽을 내용을 더 채워야 한다. 그 전에 해결해야 했던 비용 문제는 2부 「Vercel ISR 쓰기 비용을 83% 줄이기까지」에 정리했다.
What RADAR observed and classified to build this opportunity. It is what the source published, not a verification that the offer is still active.
4만 페이지를 옮기며 겪은 Next.js SEO 문제들. 유전자 페이지를 회사 사이트로 옮기자 검색 유입이 늘었다. 그런데 화면에 잘 보이던 링크와 본문이 서버 HTML에는 빠져 있었다. 유전자 이름을 검색하면 우리 사이트가 나왔으면 했다. 이미 유전자 약 1만 9천 개의 상세 페이지가 있었지만, 별도 서브도메인에 있어서인지 검색 유입은 거의 없었다. 이 페이지들을 회사의 메인 도메인으로 옮겼다. 영문과 국문을 합쳐 약 4만 페이지. 옮긴 뒤에는 검색으로 들어오는 사람이 늘었다. 그런데 안 보이던 문제도 하나씩 드러났다. 목록에는 분명 링크가 있는데 크롤러는 찾지 못했고, 정작 중요한 질병 정보는 버튼을 눌러야 나타났다. 4월에는 Search Console에 soft 404가 1,414건 잡혔다. 5월…
Open source