검색 색인 차단과 OG 미리보기 유지 정리
URL을 카카오톡이나 Slack에 공유했을 때 제목과 이미지는 보여야 하고, Google이나 네이버 검색 결과에는 나오지 않아야 한다는 요구사항을 만났다.
처음에는 robots.txt로 모든 봇을 차단하면 될 것처럼 보였다. 그런데 그렇게 하면 검색봇뿐 아니라 링크 미리보기 봇까지 페이지에 접근하지 못할 수 있다. 원하는 결과를 얻으려면 크롤링, 색인, Open Graph를 서로 다른 개념으로 봐야 했다.
크롤링과 색인은 같은 단계가 아니었다
검색엔진은 대체로 이런 과정을 거친다.
| 개념 | 의미 |
|---|---|
| 크롤링(Crawling) | 봇이 URL에 접속해 HTML과 응답 헤더를 읽는 것 |
| 색인(Indexing) | 읽은 페이지를 검색엔진 데이터베이스에 저장하는 것 |
| 검색 노출(Serving) | 사용자의 검색어에 맞춰 색인된 페이지를 결과에 보여주는 것 |
| OG 추출 | 소셜 서비스의 봇이 HTML의 Open Graph 태그를 읽는 것 |
크롤링을 허용하면서 색인만 금지할 수 있다. 그러면 검색봇은 페이지에 접근해 noindex를 확인하고 검색 결과에서는 제외한다. 동시에 카카오톡, Slack 등의 봇도 접근해 OG 데이터를 가져갈 수 있다.
robots.txt는 크롤링 가능한 경로만 알려준다
robots.txt는 사이트 루트의 /robots.txt에서 제공하는 크롤링 규칙이다. 모든 봇이 사이트를 크롤링할 수 있다는 뜻의 설정은 이렇다.
User-agent: *
Allow: /반대로 이 설정은 전체 경로의 크롤링을 막는다.
User-agent: *
Disallow: /Disallow는 검색 결과 제거 명령이 아니다. 봇이 페이지를 읽지 못하게 할 뿐이다. 이미 색인된 페이지에 이 설정부터 적용하면 검색봇이 HTML의 noindex를 확인하지 못해 기존 검색 결과가 예상보다 오래 남을 수 있다. 소셜 미리보기 봇까지 같은 규칙을 따르면 OG 데이터 추출도 실패할 수 있다.
noindex는 색인에 넣지 말라는 지시다
HTML 문서에 넣을 수 있는 메타 태그는 이렇게 생겼다.
<meta name="robots" content="noindex, nofollow" />noindex는 페이지를 검색 색인에 포함하지 말라는 지시고, nofollow는 페이지에 있는 링크를 이 문서를 통해 따라가지 말라는 지시다. nofollow는 링크 대상의 크롤링을 완전히 차단하는 보안 기능이 아니다. 검색엔진은 다른 페이지나 sitemap 등에서 같은 URL을 발견할 수 있다.
같은 지시를 HTTP 응답 헤더로도 전달할 수 있다.
X-Robots-Tag: noindex, nofollowX-Robots-Tag는 HTML이 아닌 PDF·이미지·동영상 같은 리소스에도 색인 정책을 걸 수 있고, 서버 설정 한 곳으로 사이트 전체에 적용할 수 있다. HTML 메타 태그와 함께 쓰면 HTML 문서와 그 밖의 리소스 양쪽을 덮는다.
noindex와 OG 태그는 부딪히지 않는다
Open Graph는 링크를 공유할 때 표시할 정보를 정의한다.
<meta property="og:type" content="website" />
<meta property="og:title" content="서비스 이름" />
<meta property="og:description" content="서비스 설명" />
<meta property="og:image" content="https://example.com/og-image.png" />
<meta property="og:url" content="https://example.com/" />검색엔진의 noindex와 OG 태그는 충돌하지 않는다. 하나의 HTML에 두 설정이 함께 있어도 정상이다.
<meta name="robots" content="noindex, nofollow" />
<meta property="og:title" content="서비스 이름" />
<meta property="og:image" content="https://example.com/og-image.png" />이때 검색엔진은 페이지에 접근하지만 색인하지 않고, 카카오톡·Slack 등의 봇은 페이지에 접근해 OG 정보를 읽는다. 일반 사용자는 URL로 직접 접속할 수 있다.
크롤링은 열고 색인만 닫는 구성으로 정리했다
검색 노출은 막고 링크 미리보기는 유지하려고 다섯 가지를 맞췄다. robots.txt에서 크롤링을 허용하고, HTML에 noindex 메타 태그를 넣고, HTTP 응답에 X-Robots-Tag 헤더를 넣었다. 이 헤더는 Google이 문서로 지원을 밝힌 방식이라 HTML 메타 태그를 함께 넣어 검색엔진별 지원 차이를 줄였다. 검색엔진에 URL 목록을 적극 제출하지 않도록 sitemap 노출을 제거하고, OG 메타 태그와 OG 이미지는 그대로 뒀다.
| 동작 | 정책 |
|---|---|
| 크롤러 접근 | 허용 |
| 검색 색인 | 금지 |
| OG 데이터 추출 | 허용 |
| 직접 URL 접속 | 허용 |
sitemap 제거만으로 색인이 차단되지는 않는다. 외부 링크나 과거 방문 기록을 통해 검색엔진이 URL을 발견할 수 있어서, 실제 색인 방지 정책의 중심은 noindex여야 한다.
Next.js App Router에서는 metadata 한 곳에 모았다
Next.js App Router에서는 루트 레이아웃의 metadata에 색인 정책과 OG 데이터를 함께 선언할 수 있다.
import type { Metadata } from 'next'
export const metadata: Metadata = {
metadataBase: new URL('https://example.com'),
robots: {
index: false,
follow: false,
},
title: '서비스 이름',
description: '서비스 설명',
openGraph: {
type: 'website',
title: '서비스 이름',
description: '서비스 설명',
images: [
{
url: '/og-image.png',
width: 1200,
height: 630,
},
],
},
}openGraph.images에 상대 경로를 쓰려면 metadataBase를 함께 선언해야 한다. metadataBase 없이 상대 경로를 쓰면 Next.js가 절대 URL을 만들지 못한다. 절대 URL을 직접 적어도 된다.
이 선언으로 Next.js가 HTML에 만들어주는 태그는 이렇다.
<meta name="robots" content="noindex, nofollow" />응답 헤더는 next.config.js 또는 next.config.mjs에 추가했다.
const nextConfig = {
async headers() {
return [
{
source: '/(.*)',
headers: [
{
key: 'X-Robots-Tag',
value: 'noindex, nofollow',
},
],
},
]
},
}
module.exports = nextConfignext.config.mjs를 쓴다면 마지막 줄은 export default nextConfig가 된다.
robots.ts를 쓰는 경우에도 OG 봇이 접근할 수 있도록 크롤링을 허용해뒀다.
import type { MetadataRoute } from 'next'
export default function robots(): MetadataRoute.Robots {
return {
rules: {
userAgent: '*',
allow: '/',
},
}
}Vite SPA는 index.html에, 헤더는 응답을 만드는 계층에
Vite SPA는 index.html에 메타 태그를 직접 넣었다.
<meta name="robots" content="noindex, nofollow" />
<meta property="og:title" content="서비스 이름" />
<meta property="og:description" content="서비스 설명" />
<meta property="og:image" content="https://example.com/og-image.png" />프로덕션에서 HTML을 FastAPI, Nginx, CDN 등이 제공한다면 X-Robots-Tag는 실제 응답을 만드는 계층에서 붙어야 한다. FastAPI에서는 ASGI 미들웨어로 응답 시작 시점에 헤더를 끼워 넣었다.
class RobotsHeadersMiddleware:
def __init__(self, app):
self.app = app
async def __call__(self, scope, receive, send):
async def send_with_header(message):
if message.get("type") == "http.response.start":
headers = list(message.get("headers") or [])
headers.append((b"x-robots-tag", b"noindex, nofollow"))
message = {**message, "headers": headers}
await send(message)
await self.app(scope, receive, send_with_header)소스 코드에 태그가 있는지가 아니라 운영 URL의 최종 HTML과 HTTP 응답에 설정이 실제로 포함되는지가 기준이다.
로그인 리다이렉트가 OG 봇을 먼저 삼킨다
사내 앱은 보통 미인증 사용자를 로그인 페이지로 리다이렉트한다. OG 봇은 로그인을 수행하지 못하므로 이런 흐름이 만들어진다.
이 경우 미들웨어에서 OG 크롤러 요청만 인증 없는 OG 전용 응답으로 rewrite할 수 있다. 요청 주체에 따라 분기가 갈린다.
OG 전용 응답에는 캐시 정책도 중요하다.
Cache-Control: no-store크롤러용 빈 HTML이 브라우저 요청에 재사용되면 사용자가 하얀 화면을 볼 수 있다. CDN을 사용한다면 User-Agent나 크롤러 판별에 따라 응답이 달라지는 구조가 캐시 키와 맞는지도 봐야 한다.
OG 크롤러 판별 로직에는 오탐 가능성도 있다. 일반 브라우저 요청이 OG 전용 페이지로 rewrite되지 않는지는 실제 브라우저와 운영 CDN 환경에서 확인해야 알 수 있다.
배포하고 나서 운영 URL을 직접 두드려봤다
응답 헤더는 curl -I로 확인했다.
curl -I https://example.com/여기에 아래 헤더가 보여야 한다.
X-Robots-Tag: noindex, nofollowHTML 메타는 응답 본문을 받아 robots와 og:만 걸러 봤다.
curl -s https://example.com/ | grep -iE 'robots|og:'이때 확인할 항목은 색인 정책 한 줄과 OG 세 줄이다.
<meta name="robots" content="noindex, nofollow" />
<meta property="og:title" ... />
<meta property="og:description" ... />
<meta property="og:image" ... />robots.txt도 그대로 받아봤다.
curl -s https://example.com/robots.txt이 구성에서는 전체 경로에 Disallow: /가 없어야 한다.
og:image는 확인할 게 더 많다. 절대 URL을 쓰거나 프레임워크가 올바른 절대 URL로 바꿔줘야 하고, 인증 없이 접근 가능해야 하며, HTTP 200을 반환해야 한다. 이미지 Content-Type도 맞아야 한다. 크기는 Meta가 권장하는 최소 크기인 1200×630 이상이면 되고, 비율은 1.91:1에 가까울수록 잘리지 않는다.
소셜 서비스는 OG 결과를 캐시한다. 메타데이터를 바꿨는데 이전 이미지가 계속 보이면 각 서비스의 디버거나 캐시 갱신 기능을 써야 한다.
이미 색인된 페이지는 며칠에서 수 주가 걸린다
이미 색인된 페이지도 검색봇이 다시 방문해 noindex를 확인하면 검색 결과에서 제거된다. 다만 즉시 제거되지는 않는다. 사이트와 검색엔진의 재크롤링 주기에 따라 며칠에서 수 주가 걸릴 수 있다.
빠른 처리가 필요하면 Google Search Console의 URL 검사 및 삭제 요청, 네이버 서치어드바이저의 URL 삭제·수집 관련 도구를 병행할 수 있다. Google Search Console의 삭제 도구는 하루 안에 검색 결과에서 페이지를 내려주지만, 요청 효력이 약 6개월간 유지되는 임시 조치다. 장기적인 제거 상태를 유지하는 것은 페이지가 반환하는 noindex다.
여기서 착각하면 설정을 다 해도 결과가 안 바뀐다
robots.txt만 막으면 검색 결과에서도 사라진다고 보기 쉽다. Disallow는 크롤링 제어이고 noindex는 색인 제어다. 이미 색인된 URL 제거가 목적이라면 검색봇이 noindex를 읽을 수 있어야 한다.
sitemap만 삭제하는 것도 마찬가지다. sitemap은 URL 발견 수단 중 하나일 뿐이라, sitemap이 없어도 외부 링크 등을 통해 URL이 발견될 수 있다.
OG 태그가 React 렌더링 후에만 생기는 구조도 문제가 된다. 많은 OG 봇은 JavaScript를 실행하지 않는다. 최초 HTML 응답에 OG 태그가 들어 있어야 한다.
OG 이미지에 인증을 요구하는 경우도 그렇다. HTML을 읽어도 이미지 URL이 로그인이나 쿠키를 요구하면 미리보기 이미지가 나타나지 않는다.
noindex를 보안 기능으로 보는 것도 오해다. noindex는 검색엔진에 대한 지시일 뿐 접근 제어가 아니다. URL을 아는 사람은 페이지에 직접 접속할 수 있다. 민감한 데이터는 로그인, 권한 검사, 사내망, IP 제한 같은 보안 수단으로 막아야 한다.
개발 코드만 보고 운영 응답을 안 보는 것도 흔한 함정이다. 리버스 프록시, CDN, 별도 배포 설정이 헤더를 제거하거나 HTML을 바꿀 수 있다. 최종 운영 도메인에서 확인해야 한다.
배포 전에 이 항목들을 훑었다
- 모든 HTML에
robots: noindex, nofollow가 있는지 - 최종 HTTP 응답에
X-Robots-Tag: noindex, nofollow가 있는지 robots.txt가 OG 봇의 접근을 차단하지 않는지- 불필요한 sitemap이 노출되지 않는지
- OG 태그가 최초 HTML 응답에 포함되는지
- OG 이미지가 인증 없이 HTTP 200으로 열리는지
- 일반 브라우저가 OG 전용 응답으로 잘못 rewrite되지 않는지
- 로그인과 앱 기능이 기존처럼 동작하는지
- Google과 네이버의 기존 검색 결과가 제거되고 있는지
앱마다 같은 정책을 넣었는데 저장소는 따로였다
저장소의 앱들에 같은 정책을 적용했다.
| 앱 | HTML noindex | 응답 헤더 | OG |
|---|---|---|---|
apps/platform | 적용 | 공통 Next 설정 | 유지 |
apps/lineup | 적용 | 공통 Next 설정 | 유지 |
apps/admin | 적용 | 공통 Next 설정 | 유지 |
_external/factbook | 적용 | 개별 Next 설정 | 유지 |
_external/seo | 적용 | 개별 Next 설정 | 유지 |
_external/copygen | Vite HTML에 적용 | FastAPI 미들웨어 | 유지 |
apps/platform의 sitemap은 제거했고, robots.txt는 OG 봇과 검색봇이 noindex를 읽을 수 있도록 접근을 허용해뒀다.
_external 아래 앱들은 각각 별도 Git 저장소다. 루트 저장소 변경만 커밋하거나 배포해서는 외부 앱에 반영되지 않아서, 각 저장소에서 따로 커밋하고 배포했다.