~/web-app-dev.md
WEB_APP_DEV

에이전트 친화적 페이지는 SEO가 아니라 표현 계약의 문제다

10분 읽기·2026.06.18·출처 5·00
오늘의 질문

내 웹앱의 문서·블로그·상품 페이지는 브라우저와 AI 에이전트에게 같은 HTML만 던져도 괜찮을까?

web-app-dev.md
SURVIVE.exe

에이전트 친화적 페이지는 SEO가 아니라 표현 계약의 문제다

1. 왜 지금 봐야 하나

웹앱 개발자는 오랫동안 “사람이 보는 HTML”과 “검색엔진이 읽는 HTML”을 동시에 만족시키는 일을 해왔다. 그런데 2026년의 변화는 조금 다르다. AI 에이전트가 문서, 블로그, changelog, API 레퍼런스, 상품 설명 페이지를 직접 읽고 작업의 근거로 삼기 시작했다. 문제는 에이전트가 보통 브라우저가 받는 것과 같은 HTML을 받는다는 점이다. 그 안에는 내비게이션, CSS, JS 번들, 추적 스크립트, footer, 관련 글 카드처럼 사람에게는 유용하지만 에이전트에게는 노이즈가 되는 정보가 많다.

Vercel은 2026년 2월 블로그와 changelog에 Accept: text/markdown 기반 content negotiation을 적용했다고 공개했다. 같은 URL에 브라우저가 접근하면 HTML을 주고, Markdown을 선호한다고 밝힌 클라이언트에게는 Markdown 표현을 주는 방식이다. Vercel 예시에서는 한 HTML 페이지가 약 500KB인데 Markdown 표현은 약 3KB로 줄었다고 설명한다. 숫자 자체보다 중요한 것은 “AI가 읽기 쉬운 별도 URL을 하나 더 만들자”가 아니라 “같은 리소스의 표현을 클라이언트 목적에 맞게 선택하자”는 HTTP의 오래된 원리가 다시 실무 문제가 됐다는 점이다.

웹앱 팀 입장에서는 이것이 SEO 팁이 아니다. 문서형 페이지, 기술 블로그, API docs, 사내 어드민 설명, 상품 상세 페이지를 AI 도구가 읽을 때 어떤 표현을 신뢰할지 정하는 제품 품질 계약이다.

2. 핵심 개념

핵심 개념은 “리소스와 표현을 분리한다”이다. RFC 9110과 MDN의 설명에서 HTTP 리소스는 URL로 식별되는 대상이고, 표현(representation)은 그 리소스의 상태를 전달하는 구체적인 바이트다. 같은 /docs/auth라는 리소스라도 HTML, Markdown, JSON, 다른 언어, 다른 압축 방식으로 표현될 수 있다.

Accept 헤더는 클라이언트가 “나는 이런 media type을 이해할 수 있다”고 서버에 알려주는 요청 헤더다.

zsh — 생존확인.sh
Accept: text/markdown, text/html;q=0.8, */*;q=0.1

이 요청은 “Markdown이 가장 좋고, HTML도 가능하며, 그 외 형식은 낮은 우선순위로 가능하다”는 뜻이다. 서버는 자신이 만들 수 있는 표현 중 적절한 것을 선택하고, 응답에는 실제 형식을 Content-Type으로 알려야 한다.

zsh — 생존확인.sh
Content-Type: text/markdown; charset=utf-8
Vary: Accept

여기서 Vary: Accept가 중요하다. CDN이나 브라우저 캐시가 같은 URL의 HTML 응답을 Markdown 요청에 재사용하거나, 반대로 Markdown을 사람 브라우저에 내보내면 안 되기 때문이다. content negotiation을 쓰는 순간 캐시 키가 단순 URL 하나가 아니라 “URL + 선택에 사용한 요청 헤더”가 된다.

에이전트 친화적 페이지의 원리는 그래서 단순하다.

  1. canonical URL은 유지한다.
  2. 브라우저에는 기존 HTML을 준다.
  3. Accept가 Markdown을 선호하면 같은 리소스의 Markdown 표현을 준다.
  4. 캐시가 이 차이를 알도록 Vary를 명시한다.
  5. HTML과 Markdown의 내용이 서로 어긋나지 않게 생성 경로를 공유한다.

3. 최신 이슈와 연결

Vercel의 구현은 Next.js rewrite와 route handler 조합이다. next.config.ts에서 요청 헤더에 text/markdown이 들어오면 /blog/md/.../changelog/md/... 같은 내부 경로로 rewrite하고, route handler가 Markdown을 반환한다. CMS가 rich text를 저장한다면 요청 시 Markdown으로 변환하고, 애초에 Markdown으로 글을 관리한다면 파일을 그대로 제공할 수 있다.

이 흐름은 /llms.txt 제안과도 연결된다. llms.txt는 사이트 루트에 Markdown 파일을 두고, LLM이 읽기 좋은 요약과 중요한 문서 링크를 제공하자는 가벼운 규약이다. 즉 llms.txt는 “무엇을 읽어야 하는가”를 알려주는 지도에 가깝고, Accept: text/markdown은 “그 페이지를 어떤 표현으로 받을 것인가”를 정하는 협상에 가깝다.

둘 중 하나만 고를 문제가 아니다.

  • llms.txt: 에이전트가 사이트의 핵심 문서와 우선순위를 발견하게 한다.
  • Markdown sitemap 또는 Markdown index: 많은 페이지를 계층적으로 훑게 한다.
  • content negotiation: 이미 알고 있는 canonical URL에서 토큰 효율적인 표현을 받게 한다.
  • link rel="alternate" type="text/markdown": 헤더 협상을 하지 않는 클라이언트에게 대체 경로를 알려준다.

여기서 조심할 점은 “AI 전용 문서”가 기존 페이지와 다른 사실을 말하기 시작하는 순간이다. Markdown 표현이 HTML의 축약본이 아니라 별도의 truth source가 되면, 사용자에게 보이는 내용과 에이전트가 근거로 삼는 내용이 갈라진다. 그때부터는 SEO 문제가 아니라 제품 신뢰성 문제가 된다.

4. 개발자 관점 해석

웹앱 개발자가 이 주제를 볼 때 가장 먼저 물어야 할 질문은 “에이전트에게 무엇을 더 보여줄까?”가 아니라 “같은 리소스를 몇 개의 표현으로 운영할 수 있을까?”이다.

첫 번째 트레이드오프는 구현 단순성과 협상 정확성이다. Vercel 예시처럼 accept 헤더에 text/markdown 문자열이 포함되는지 보고 rewrite하는 방식은 이해하기 쉽다. 하지만 실전에서는 q 값, wildcard, 대소문자, 여러 media type 우선순위가 섞인다. Accept: text/html, text/markdown;q=0.1이라면 HTML을 더 선호한다는 뜻이다. 단순 substring 매칭은 “Markdown을 받을 수 있다”와 “Markdown을 가장 원한다”를 구분하지 못한다. 작은 팀의 첫 구현은 단순해도 되지만, 공개 API나 문서 플랫폼이라면 Accept parser를 쓰는 편이 안전하다.

두 번째 트레이드오프는 캐시 효율이다. 같은 URL에 여러 표현이 생기면 CDN 캐시가 나뉜다. Vary: Accept를 넣지 않으면 잘못된 표현이 섞일 수 있고, 너무 많은 헤더를 Vary에 넣으면 캐시 hit rate가 떨어진다. Markdown 협상은 가능하면 Accept 하나만 선택 기준으로 삼고, 언어·인증·개인화까지 한꺼번에 섞지 않는 편이 좋다.

세 번째 트레이드오프는 보안과 권한이다. public 문서나 블로그는 문제가 작지만, 로그인 후 볼 수 있는 어드민 문서, 고객별 리포트, 내부 데이터 페이지를 Markdown으로 제공할 때는 HTML 경로와 같은 auth/authz 검사를 반드시 통과해야 한다. “HTML route는 보호되어 있는데 Markdown route는 별도 route handler라서 빠졌다”는 전형적인 실패 모드다.

네 번째 트레이드오프는 콘텐츠 동기화다. HTML은 CMS에서 렌더링하고 Markdown은 별도 파일로 사람이 관리하면 시간이 지나며 어긋난다. 가능하면 하나의 원본에서 HTML과 Markdown을 같이 생성하라. 불가능하다면 최소한 updated_at, checksum, 빌드 테스트로 두 표현의 제목·요약·주요 heading이 맞는지 검사해야 한다.

5. 내 프로젝트에 적용할 체크포인트

  • 이 페이지는 에이전트가 읽을 가치가 있는가? 마케팅 landing보다 docs, changelog, API reference, pricing, policy, product guide부터 시작하라.
  • canonical URL을 유지할 것인가? 가능하면 /docs/foo는 그대로 두고, Accept로 Markdown을 돌려준다.
  • 응답 헤더가 맞는가? Markdown이면 Content-Type: text/markdown; charset=utf-8, 협상에 Accept를 썼다면 Vary: Accept를 확인한다.
  • HTML과 Markdown이 같은 원본에서 나오는가? CMS rich text, MDX, Markdown file, database body 중 truth source를 하나로 정한다.
  • Markdown 변환이 구조를 보존하는가? heading hierarchy, code fence 언어, 링크, 표, 경고 박스, API parameter가 깨지면 에이전트 품질이 떨어진다.
  • 권한 검사가 공유되는가? HTML route와 Markdown route가 같은 loader, 같은 session check, 같은 RLS/API 권한을 통과해야 한다.
  • 캐시 정책이 분리되어 있는가? HTML과 Markdown의 ETag, Cache-Control, revalidation 시점이 충돌하지 않는지 확인한다.
  • 발견 경로가 있는가? /llms.txt, Markdown sitemap, HTML의 link rel="alternate" 중 하나 이상을 제공하면 에이전트가 Markdown 표현을 찾기 쉽다.

6. 오늘 10분 액션

오늘은 전체 사이트를 바꾸지 말고 문서형 페이지 하나만 골라서 확인하자.

  1. 가장 자주 AI에게 붙여 넣는 URL 하나를 고른다. 예: /docs/getting-started, /pricing, /changelog.
  2. 브라우저 HTML 크기와 텍스트 노이즈를 확인한다.
zsh — 생존확인.sh
curl -L https://example.com/docs/getting-started -o page.html
wc -c page.html
  1. 같은 내용을 Markdown으로 만들 수 있는지 확인한다. 원본이 MDX/Markdown이면 파일을 그대로 쓰고, CMS라면 변환 함수를 하나 둔다.
  2. 로컬 route handler에서 Markdown 응답을 붙인다.
zsh — 생존확인.sh
export async function GET() {
  return new Response(markdown, {
    headers: {
      "Content-Type": "text/markdown; charset=utf-8",
      "Vary": "Accept",
    },
  });
}
  1. 다음 두 요청이 서로 다른 표현을 돌려주는지 테스트한다.
zsh — 생존확인.sh
curl -I https://example.com/docs/getting-started
curl -I -H 'Accept: text/markdown' https://example.com/docs/getting-started
  1. 마지막으로 “Markdown route도 기존 auth를 타는가?”를 체크한다. 공개 페이지가 아니라면 이 항목이 10분 액션의 핵심이다.

7. 더 볼 자료

  • Vercel 글은 Next.js에서 rewrite와 route handler로 content negotiation을 붙이는 구체적인 예시를 보여준다.
  • MDN의 content negotiation 문서는 Vary와 캐시 문제를 이해하는 데 좋다.
  • MDN의 Accept 헤더 문서는 media type, wildcard, q 가중치의 기본 문법을 확인할 때 유용하다.
  • RFC 9110은 리소스, 표현, 선택된 표현(selected representation), content negotiation의 표준적 의미를 확인하는 기준점이다.
  • llms.txt 제안은 에이전트가 사이트의 핵심 문서를 발견하게 하는 가벼운 Markdown 지도라는 관점에서 함께 읽을 만하다.

중복 회피 메모

최근 web_app_dev 글은 React Compiler의 렌더링 계약, Next.js Cache Components의 데이터 신선도 계약, Supabase AI 에이전트 도구 권한표, Supabase Auth/RLS/Realtime, Hono 보안 릴리스, Node TypeScript 실행 계약을 다뤘다. 이번 글은 프레임워크 내부 최적화나 DB 권한이 아니라, AI 에이전트가 웹앱의 공개 콘텐츠를 읽을 때 같은 URL에서 HTML과 Markdown 표현을 어떻게 협상·캐시·권한 검사할지에 집중한다. 즉 “AI 코딩 도구를 어떻게 쓰나”가 아니라 “AI 도구가 읽는 웹 표면을 제품 계약으로 어떻게 설계하나”가 새 관점이다.

댓글 0

최신순 ▾
한 줄 남기기

혹시 이 글을 읽는 동료 개발자가 있다면, GitHub으로 로그인하고 한 줄 흔적을 남겨줘요. (스팸 방지용 로그인이에요)