들어가며
결론부터 말하면, 정적 페이지에서 SEO와 GEO를 따로 할 필요는 없습니다. 콘텐츠 데이터를 한 곳에 두고, 그 한 벌에서 화면(HTML)·구조화 데이터(JSON-LD)·크롤러용 파일(robots.txt, sitemap.xml, llms.txt)을 함께 뽑아내면 둘이 동시에 해결됩니다.
요즘 고객은 검색창보다 ChatGPT, Perplexity, Claude, Gemini에 먼저 묻습니다. "파주에 CI4 잘하는 개발자 있어?"라고요. 이때 AI가 답변에 우리 사이트를 인용하게 만드는 작업이 GEO(Generative Engine Optimization, 생성형 엔진 최적화)입니다.
저는 올해 AIvance(aivance.kr) 회사소개서와 메인 페이지 리뉴얼을 하면서 이 구조를 CodeIgniter 4로 정리했습니다. 이 글은 그 기록입니다.
SEO와 GEO, 무엇이 다른가
대상과 목표는 다르지만, 기술적 기반은 상당 부분 겹칩니다. 겹치는 부분이 바로 '데이터 한 벌' 전략이 통하는 이유입니다.
|
구분 |
SEO (검색엔진 최적화) |
GEO (생성형 엔진 최적화) |
|---|---|---|
|
대상 |
Google, Naver 등 검색엔진 |
ChatGPT, Perplexity, Claude, Gemini 등 AI 답변엔진 |
|
목표 |
검색 결과 상위 노출, 클릭 |
AI 답변에 인용·언급 |
|
핵심 요소 |
키워드, 메타태그, 백링크, 페이지 속도 |
구조화 데이터, 명확한 사실 진술, 발췌하기 쉬운 문단, llms.txt |
|
색인 통로 |
sitemap.xml, Search Console, IndexNow |
robots.txt의 AI 크롤러 허용, llms.txt |
|
공통 기반 |
시맨틱 HTML, JSON-LD, 빠른 서버 렌더링, 신뢰도(E-E-A-T) |
동일 |
백링크는 순위에, IndexNow는 색인 속도에 영향을 줍니다. 둘은 대체 관계가 아니라 보완 관계입니다.
핵심 아이디어: 데이터는 한 곳, 출력은 여러 곳
정적 페이지의 문제는 같은 정보를 여러 번 쓴다는 점입니다. 화면의 FAQ, JSON-LD의 FAQPage, llms.txt 요약을 각각 손으로 관리하면 언젠가 셋이 서로 다른 말을 합니다. AI는 이런 불일치를 신뢰도 하락으로 받아들일 수 있습니다.
그래서 콘텐츠는 한 곳에만 두고, 출력은 전부 그 한 곳에서 생성합니다. 내용을 고칠 때는 Config 파일 하나만 수정하면 화면·스키마·크롤러 파일이 함께 바뀝니다.
CodeIgniter 4로 구현하기
DB 없이 app/Config에 콘텐츠를 두고, 컨트롤러가 그 값을 뷰와 JSON-LD partial에 동시에 넘기는 구조입니다. 메인 페이지 정도는 이것으로 충분합니다.
1. 콘텐츠를 Config에 모은다
<?php
declare(strict_types=1);
namespace Config;
use CodeIgniter\Config\BaseConfig;
final class Faq extends BaseConfig
{
/** @var list<array{q: string, a: string}> */
public array $items = [
[
'q' => 'AI 개발환경 구축 컨설팅은 무엇을 해주나요?',
'a' => 'Claude Code 기반 개발환경, CLAUDE.md 작성 규칙, CI/CD 파이프라인을 팀 상황에 맞게 세팅합니다.',
],
[
'q' => '정부 바우처로 이용할 수 있나요?',
'a' => 'AI바우처·혁신바우처 공급기업 등록을 준비 중입니다.',
],
];
}
Site.php(회사 정보·연락처), Products.php(제품 목록)도 같은 방식으로 둡니다. 화면에 보이는 FAQ와 JSON-LD의 FAQPage가 이 배열 하나를 함께 씁니다.
2. 컨트롤러는 데이터만 넘긴다
<?php
declare(strict_types=1);
namespace App\Controllers;
final class Home extends BaseController
{
public function index(): string
{
return view('home/index', [
'site' => config('Site'),
'products' => config('Products')->items,
'faqs' => config('Faq')->items,
]);
}
}
3. JSON-LD는 partial 하나로
app/Views/partials/jsonld.php에서 같은 데이터로 스키마를 만듭니다. 제가 메인 페이지에 넣은 타입은 Organization, Person, ItemList + SoftwareApplication, FAQPage 네 가지입니다.
<?php
$graph = [
[
'@type' => 'Organization',
'@id' => $site->url . '#org',
'name' => $site->name,
'url' => $site->url,
'logo' => $site->url . '/assets/logo.png',
'founder' => ['@id' => $site->url . '#founder'],
],
[
'@type' => 'Person',
'@id' => $site->url . '#founder',
'name' => '변종원',
'alternateName' => '웅파',
'jobTitle' => 'Advisor',
],
[
'@type' => 'FAQPage',
'mainEntity' => array_map(static fn (array $f): array => [
'@type' => 'Question',
'name' => $f['q'],
'acceptedAnswer' => ['@type' => 'Answer', 'text' => $f['a']],
], $faqs),
],
];
?>
<script type="application/ld+json">
<?= json_encode(
['@context' => 'https://schema.org', '@graph' => $graph],
JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_HEX_TAG
) ?>
</script>
포인트는 세 가지입니다.
@graph와@id로 회사와 대표자를 연결합니다. AI가 "이 회사 대표가 누구냐"를 구조로 이해합니다.JSON_HEX_TAG로</script>깨짐을 막습니다. 뷰의 일반 출력은 반드시esc()를 씁니다.- 제품 목록(ItemList + SoftwareApplication)에는 공개된 제품만 넣습니다. 화면에 없는 정보를 스키마에만 넣으면 오히려 신뢰를 잃습니다.
크롤러에게 길 열어주기: robots.txt, sitemap, llms.txt, IndexNow
화면과 스키마가 준비됐으면, 이제 검색엔진과 AI 크롤러가 실제로 들어오게 해야 합니다.
robots.txt: 학습용과 검색용을 구분한다
AI 회사들은 크롤러를 용도별로 나눠 운영합니다. 학습용(GPTBot, ClaudeBot)을 막아도 검색·인용용(OAI-SearchBot, Claude-SearchBot, PerplexityBot)은 별도입니다. 학습은 막고 싶더라도, 검색용 봇까지 막으면 AI 답변에서 우리 사이트가 사라집니다.
# 검색·인용용 AI 크롤러: 허용
User-agent: OAI-SearchBot
Allow: /
User-agent: Claude-SearchBot
Allow: /
User-agent: PerplexityBot
Allow: /
# 학습용 크롤러: 정책에 따라 선택 (예: 차단)
User-agent: GPTBot
Disallow: /
User-agent: ClaudeBot
Disallow: /
User-agent: *
Allow: /
Disallow: /admin/
Sitemap: https://aivance.kr/sitemap.xml
사용자가 직접 요청할 때 페이지를 가져오는 봇(ChatGPT-User, Claude-User, Perplexity-User)은 사람이 읽는 것과 같으니 막을 이유가 없습니다. 봇 이름은 바뀌므로 각 회사 문서를 주기적으로 확인합니다.
sitemap.xml: 라우트로 생성한다
// app/Config/Routes.php
$routes->get('sitemap.xml', 'Sitemap::index');
$routes->get('llms.txt', 'Llms::index');
<?php
declare(strict_types=1);
namespace App\Controllers;
final class Sitemap extends BaseController
{
public function index(): \CodeIgniter\HTTP\ResponseInterface
{
$base = rtrim(config('Site')->url, '/');
$paths = ['/', '/consulting', '/contact'];
$xml = '<?xml version="1.0" encoding="UTF-8"?>'
. '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">';
foreach ($paths as $p) {
$xml .= '<url><loc>' . esc($base . $p) . '</loc></url>';
}
$xml .= '</urlset>';
return $this->response->setContentType('application/xml')->setBody($xml);
}
}
llms.txt: AI를 위한 요약 안내판
llms.txt는 사이트 루트에 두는 마크다운 파일로, "이 사이트는 무엇이고 핵심 페이지는 어디인지"를 LLM에게 짧게 알려주는 제안 규격입니다. 아직 모든 AI 서비스가 공식 지원하는 표준은 아니므로, robots.txt와 JSON-LD를 대신하는 것이 아니라 보조 수단으로 생각하는 게 맞습니다.
좋은 점은 같은 Config 데이터에서 자동 생성할 수 있다는 것입니다. 손으로 관리하면 언젠가 화면과 내용이 어긋납니다.
<?php
declare(strict_types=1);
namespace App\Controllers;
final class Llms extends BaseController
{
public function index(): \CodeIgniter\HTTP\ResponseInterface
{
$site = config('Site');
$out = "# {$site->name}\n\n> {$site->summary}\n\n## 제품\n";
foreach (config('Products')->items as $p) {
$out .= "- [{$p['name']}]({$p['url']}): {$p['oneLiner']}\n";
}
$out .= "\n## 자주 묻는 질문\n";
foreach (config('Faq')->items as $f) {
$out .= "- {$f['q']} {$f['a']}\n";
}
return $this->response
->setContentType('text/plain; charset=UTF-8')
->setBody($out);
}
}
IndexNow: 바뀌면 바로 알린다
IndexNow는 크롤러가 찾아오기를 기다리지 않고, URL이 생기거나 바뀌었다고 검색엔진에 직접 알리는 프로토콜입니다. Bing, Naver 등이 지원하고 Google은 지원하지 않으므로, Google은 Search Console에 sitemap을 따로 제출합니다. 블로그라면 글 발행·수정 이벤트(CI4 Events)에 IndexNow 호출을 걸어두면 손이 가지 않습니다.
마크업·성능·신뢰도 체크리스트
AI는 페이지를 통째로 읽기보다 문단 단위로 발췌합니다. 그래서 구조가 분명하고, 한 문단이 하나의 사실을 말하는 페이지가 유리합니다. 이건 SEO에도 그대로 좋습니다.
<h1>은 페이지당 하나(Hero 헤드라인), 섹션마다<section id="...">+<h2>계층 유지- FAQ는 질문-답변 형태로, 답변 첫 문장에 결론을 둔다
- 숫자·연도·이름처럼 인용 가능한 사실을 문장으로 명확히 쓴다 ("26년 경력", "2013년 한빛미디어 출간")
<title>, meta description, canonical, Open Graph, Twitter Card 태그- 이미지는 WebP +
loading="lazy"+ width/height 명시 (레이아웃 흔들림 방지) - 서버 렌더링으로 HTML에 본문이 들어 있을 것 (JS로만 그리는 콘텐츠는 크롤러가 놓치기 쉽다)
- 저자·회사 정보를 실명과 경력으로 밝힌다 (E-E-A-T: 경험·전문성·권위·신뢰)
- Lighthouse SEO·성능 90점 이상
검증은 테스트 코드로
저는 PHPUnit Feature 테스트에 SEO/GEO 요건을 넣어 두었습니다. 누군가(대개 미래의 나 또는 Claude Code)가 뷰를 고치다 JSON-LD를 날려먹는 일을 CI에서 잡아줍니다.
public function testHomeHasSingleH1AndJsonLd(): void
{
$result = $this->get('/');
$result->assertOK();
$this->assertSame(1, substr_count($result->getBody(), '<h1'));
$result->assertSee('application/ld+json', 'script');
}
public function testLlmsTxtIsServed(): void
{
$this->get('/llms.txt')->assertOK();
}
배포 후에는 Google 리치 결과 테스트와 Schema Markup Validator로 JSON-LD를 확인하고, 서버 로그에서 OAI-SearchBot·Claude-SearchBot 같은 봇이 실제로 들어오는지 봅니다.
정리: 실제로 이렇게 쓰고 있습니다
제가 이 방식을 적용한 곳은 다음과 같습니다.
|
대상 |
적용 내용 |
|---|---|
|
aivance.kr 메인 |
Config 기반 콘텐츠 + JSON-LD 4종(Organization, Person, ItemList/SoftwareApplication, FAQPage), sitemap 라우트, 테스트로 검증 |
|
AICreo (기업 홈페이지 CMS) |
OG·메타·GA·네이버 웹마스터 자동 삽입에 더해 FAQ 스키마와 llms.txt 자동 생성 |
|
AICura (성형외과 광고 솔루션) |
병원·시술 정보를 구조화 데이터로 제공하는 GEO 전략 문서 별도 운영 |
|
AICopia (쇼핑몰) |
상품 정보를 Product 스키마로 자동 생성 |
한 줄로 줄이면 이렇습니다. 사람이 보는 화면과 기계가 읽는 데이터를 같은 출처에서 뽑으면, SEO와 GEO는 한 번에 끝나고 서로 어긋나지도 않습니다. 새 기술을 배우는 일이라기보다, 26년 동안 해온 "데이터는 한 곳에"라는 원칙을 검색에 적용한 것에 가깝습니다.
댓글 0개