왜 Nuxt4 AEO 모듈을 만들게 되었나요?
지금까지의 웹 생태계에서는 SEO(Search Engine Optimization)가 핵심이었습니다.
하지만 AI 시대가 도래하면서, 단순히 검색 엔진에 최적화하는 것만으로는 부족하다는 생각이 들었습니다.
앞으로는 AI가 LLM 학습을 통해 웹사이트를 이해하고, 이를 바탕으로 사용자에게 정보를 제공하는 시대가 될 것이라고 생각했습니다.
이런 생각을 하던 중, Nuxt 생태계에 기여하는 것을 좋아하는 저로서는 자연스럽게 한 가지 아이디어가 떠올랐습니다.
Nuxt4를 통해 웹사이트를 구축하는 개발자들이, AI 모델들이 웹사이트를 크롤링하여 학습할 때
더 정확하고 의미 있는 정보를 제공할 수 있도록 도와주는 모듈을 만들면 어떨까? 하구요!
최근 ChatGPT, Claude, Perplexity, Gemini, DeepSeek 등과 같은 AI 모델들이 웹 크롤링을 통해 정보를 수집하고 학습하는 방식이 주목받고 있습니다.
하지만 AI 모델들이 웹 페이지의 콘텐츠를 이해하는 데에는 여전히 한계가 있습니다.
특히, 단순한 HTML 마크업만으로는 AI 모델이 페이지의 구조와 의미를 정확히 파악하기 어렵습니다.
마치 사람이 책을 읽을 때 단순히 텍스트만 보는 것과, 목차와 구조가 명확한 책을 보는 것의 차이와 같습니다.
이러한 문제를 해결하기 위해 Schema.org JSON-LD 구조화된 데이터를 사용하는 것이 중요합니다.
하지만 기존의 Nuxt 모듈들(nuxt-schema-org, nuxt-jsonld 등)을 살펴보니, 단순히 JSON-LD만 생성할 뿐,
AI 모델의 학습을 최적화하기 위한 추가적인 기능이 부족했습니다.
이것이 바로 nuxt-aeo 모듈을 만들게 된 직접적인 동기였습니다.
AEO란 AI Engine Optimization의 약자로, AI 모델이 웹 페이지를 크롤링하여 학습할 때, 더 정확하고 의미 있는 정보를 제공할 수 있도록 도와주는 모듈입니다.
즉, AI 모델이 웹 페이지를 크롤링하여 학습할 때, 더 정확하고 의미 있는 정보를 제공할 수 있도록 도와주는 모듈입니다.
이러한 AEO 모듈을 통해, Nuxt4를 통해 웹사이트를 구축하는 개발자들이, AI 모델들이 웹사이트를 크롤링하여 학습할 때, 더 정확하고 의미 있는 정보를 제공할 수 있도록 도와줄 수 있습니다.
현재 AI의 LLM 학습에 있어서 각 페이지의 한계점
시멘틱 HTML의 부재
기존 모듈들은 JSON-LD만 생성하고, 실제 HTML에는 시멘틱 태그가 없었습니다.
하지만 LLM 크롤러들은 JSON-LD와 시멘틱 HTML을 함께 사용할 때 더 정확하게 콘텐츠를 이해할 수 있습니다.
예를 들어, FAQ 페이지의 경우:
JSON-LD만 있는 경우: AI 모델이 구조를 이해하지만, 실제 HTML 구조와의 연결이 약함JSON-LD + 시멘틱 HTML: AI 모델이 HTML 구조와 JSON-LD를 함께 분석하여 더 정확한 이해 가능
다른 모듈과의 차별점
기존에도 nuxt-schema-org, nuxt-jsonld 등과 같은 JSON-LD 모듈들이 있었습니다.
하지만, 이러한 모듈들은 단순히 JSON-LD만 생성하고, 실제 HTML에는 시멘틱 태그가 없었습니다.
또한, AI 모델이 웹 페이지를 크롤링하여 학습할 때, 더 정확하고 의미 있는 정보를 제공할 수 있도록 도와주는 기능이 부족했습니다.
이러한 문제를 해결하기 위해 nuxt-aeo 모듈을 만들게 되었습니다.
nuxt-aeo는 기존 모듈들과 달리 AI 엔진 최적화에 특화되어 있습니다:
| 기능 | nuxt-aeo | nuxt-schema-org | nuxt-jsonld |
|---|---|---|---|
| AI Engine Optimization | ✅ | ❌ | ❌ |
시멘틱 HTML 생성 | ✅ | ❌ | ❌ |
전역 스키마 설정 | ✅ | ✅ | ❌ |
초기 설정 없이 사용 가능 | ✅ | ✅ | ❌ |
주요 차별점 정리
- 자동
시멘틱 HTML생성: JSON-LD 스키마 데이터를 기반으로 시멘틱 HTML을 자동 생성합니다. 이 HTML은 사용자에게는 보이지 않지만(visually-hidden), LLM 크롤러와 검색 엔진은 읽을 수 있습니다. AI 엔진 최적화:ChatGPT,Claude,Perplexity,Gemini,DeepSeek같은 AI 모델이 웹 콘텐츠를 크롤링할 때 더 정확한 정보를 제공할 수 있도록 최적화되어 있습니다.타입 안전성: TypeScript로 작성되어 모든 Schema 타입에 대한 타입 체크가 가능합니다.- 간편한 사용법:
context와type을 사용하면 내부적으로@context와@type으로 자동 변환되어, 따옴표 없이 일반 속성처럼 사용할 수 있습니다.
Nuxt AEO 모듈 구현기
이제 nuxt-aeo 모듈이 어떻게 구현되었는지 살펴보겠습니다.
1. 기본 구조 설계
Nuxt 모듈의 기본 구조를 defineNuxtModule로 구현했습니다.
핵심은 runtimeConfig.public에 옵션을 저장하고, 플러그인과 composable을 자동으로 등록하는 것이었습니다.
이렇게 하면 사용자가 nuxt.config.ts에서 설정한 옵션이 런타임에 접근 가능하게 되고,
플러그인과 composable이 자동으로 등록되어 바로 사용할 수 있게 됩니다.
// src/module.ts
export default defineNuxtModule<ModuleOptions>({
meta: { name: 'nuxt-aeo', configKey: 'aeo' },
setup(options, nuxt) {
// 옵션을 runtime config에 저장
nuxt.options.runtimeConfig.public.aeo = options
// 플러그인 등록
addPlugin(resolver.resolve('./runtime/plugin'))
// composable 자동 import
addImportsDir(resolver.resolve('./runtime/composables'))
},
})
2. useSchema() 구현: 범용 Schema Composable
2.1 JSON-LD 생성
가장 먼저 Schema.org JSON-LD를 <head>에 주입하는 기본 기능을 구현했습니다.
이 기능은 사용자가 useSchema() 컴포저블을 통해 쉽게 스키마를 추가할 수 있도록 해줍니다.
export function useSchema(schema: Record<string, unknown>) {
useHead({
script: [{
type: 'application/ld+json',
innerHTML: JSON.stringify(schema, null, 2),
}],
})
}
useHead()를 사용한 이유는 Nuxt의 SSR 환경에서 <head> 태그에 안전하게 주입할 수 있기 때문입니다.
클라이언트와 서버 양쪽에서 동작하며, Nuxt의 내장 기능을 활용하여 안정성을 보장할 수 있습니다.
2.2 키 자동 변환 로직
사용자 편의를 위해 context와 type을 @context와 @type으로 자동 변환하는 기능을 추가했습니다.
이 기능은 사용자가 context와 type을 일반 속성처럼 사용할 수 있도록 해줍니다.
function transformSchemaKeys(obj: Record<string, unknown>): Record<string, unknown> {
const result: Record<string, unknown> = {}
for (const [key, value] of Object.entries(obj)) {
// context → @context, type → @type 변환
const transformedKey = key === 'context' ? '@context' : key === 'type' ? '@type' : key
// 중첩 객체와 배열도 재귀적으로 변환
if (value && typeof value === 'object' && !Array.isArray(value)) {
result[transformedKey] = transformSchemaKeys(value as Record<string, unknown>)
} else if (Array.isArray(value)) {
result[transformedKey] = value.map(item =>
item && typeof item === 'object'
? transformSchemaKeys(item as Record<string, unknown>)
: item
)
} else {
result[transformedKey] = value
}
}
return result
}
왜 이렇게 구현했나요?
JavaScript에서 @로 시작하는 키는 따옴표 없이 사용하기 어렵습니다.
context: 'https://schema.org'처럼 일반 속성처럼 사용할 수 있게 하기 위해 자동 변환 로직을 구현했습니다.
중첩된 객체와 배열도 재귀적으로 변환하여 모든 Schema 구조를 지원합니다.
이렇게 하면 사용자가 쉽게 스키마를 추가할 수 있게 됩니다.
예를 들어, context: 'https://schema.org'처럼 일반 속성처럼 사용할 수 있게 됩니다.
2.3 시멘틱 HTML 자동 생성
AI 모델 학습 최적화를 위해 시멘틱 HTML 자동 생성 기능을 추가했습니다.
이 기능은 사용자가 useSchema() 컴포저블을 통해 쉽게 스키마를 추가할 수 있도록 해줍니다.
// Schema 타입별 HTML 생성
function generateSemanticHTML(schemaType: string, schemaData: Record<string, unknown>): string | null {
switch (schemaType.toLowerCase()) {
case 'organization': return generateOrganizationHTML(schemaData)
case 'person': return generatePersonHTML(schemaData)
case 'itemlist': return generateItemListHTML(schemaData)
default: return generateGenericHTML(schemaType, schemaData)
}
}
// 예: Organization HTML 생성
function generateOrganizationHTML(data: Record<string, unknown>): string {
return `
<div itemscope itemtype="https://schema.org/Organization">
<span itemprop="name">${escapeHtml(String(data.name || ''))}</span>
<span itemprop="description">${escapeHtml(String(data.description || ''))}</span>
<a itemprop="url" href="${escapeHtml(String(data.url || ''))}">${data.url}</a>
</div>
`
}
핵심 포인트:
itemscope,itemtype,itemprop속성을 사용한 마이크로데이터 형식- HTML 이스케이프 처리로 XSS 방지
visually-hiddenCSS로 사용자에게는 보이지 않지만 크롤러는 읽을 수 있도록 처리하여 실제로 보이지 않지만 DOM에는 존재하게 되어 AI 모델이 읽을 수 있도록 해줍니다.
2.4 클라이언트 사이드 HTML 주입
시멘틱 HTML을 클라이언트에서 동적으로 주입합니다.
이 기능은 사용자가 useSchema() 컴포저블을 통해 쉽게 스키마를 추가할 수 있도록 해줍니다.
if (import.meta.client && renderHtml) {
const injectSemanticHTML = () => {
// 중복 주입 방지
const existing = document.querySelector(`.nuxt-aeo-semantic-${schemaType.toLowerCase()}`)
if (existing) existing.remove()
// 시멘틱 HTML 주입
const semanticDiv = document.createElement('div')
semanticDiv.className = `nuxt-aeo-semantic-${schemaType.toLowerCase()} ${visuallyHidden ? 'nuxt-aeo-visually-hidden' : ''}`
semanticDiv.setAttribute('aria-hidden', 'true')
semanticDiv.innerHTML = semanticHtml
document.body.appendChild(semanticDiv)
}
// DOM 준비 상태에 따라 실행
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', injectSemanticHTML)
} else {
injectSemanticHTML()
}
}
왜 클라이언트에서 주입하나요?
SSR에서는 <body>에 직접 주입하기 어렵고, useHead()는 <head>에만 주입 가능합니다.
클라이언트에서 주입하면 DOM 조작이 자유로워 시멘틱 HTML을 원하는 위치에 배치할 수 있습니다.
3. useSchemaPage() 구현: FAQPage 전용 Composable
FAQPage는 가장 많이 사용되는 Schema 타입이었기 때문에, 전용 composable을 만들기로 결정했습니다.
이 기능은 사용자가 FAQPage에 스키마를 추가할 수 있도록 해줍니다.
예를 들어, name: 'What is the Nuxt AEO module?'처럼 일반 속성처럼 사용할 수 있게 됩니다.
해당 컴포저블을 통해, 해당 페이지 또는 기능들에 대해서 어떠한 역할을 하고있는지 AI모델들에게 질문과 답변의 형태로 쉽게 학습할 수 있게 도와주게 됩니다.
export function useSchemaPage(data: {
mainEntity: Array<{
name: string
acceptedAnswer: { text: string }
}>
}) {
// useSchema를 내부적으로 사용
useSchema({
context: 'https://schema.org',
type: 'FAQPage',
mainEntity: data.mainEntity.map(questionInput => ({
type: 'Question',
name: questionInput.name,
acceptedAnswer: {
type: 'Answer',
text: questionInput.acceptedAnswer.text,
},
})),
renderHtml: true, // 기본값: true
visuallyHidden: true, // 기본값: true
})
// FAQPage 전용 시멘틱 HTML 생성
if (renderHtml) {
const semanticHtml = generateFAQPageSemanticHTML(data.mainEntity)
// ... HTML 주입 로직
}
}
4. 전역 스키마 자동 주입
모든 페이지에 전역 스키마를 자동으로 주입하는 플러그인을 구현했습니다.
이 기능은 사용자가 nuxt.config.ts에서 설정한 전역 스키마를 모든 페이지에 자동으로 주입할 수 있게 해줍니다.
// src/runtime/plugin.ts
export default defineNuxtPlugin(() => {
const config = useRuntimeConfig()
const aeoConfig = config.public.aeo as ModuleOptions
if (aeoConfig?.autoInject === false) return
if (aeoConfig?.schemas && aeoConfig.schemas.length > 0) {
// 전역 스키마 주입
for (const schema of aeoConfig.schemas) {
useSchema({
context: 'https://schema.org',
...schemaData,
renderHtml,
visuallyHidden,
})
}
} else {
// 기본 Project Schema 주입
useSchema({
context: 'https://schema.org',
type: 'Project',
})
}
})
5. 배포 및 Nuxt 생태계에 알리기!
이렇게 모듈을 구현하였다면, npm에 배포준비를 진행합니다. Github Actions를 통해, main에 병합시 각종 설정 (lint, test) 를 통과 할 경우에 npm packages에 배포할 수 있도록 설정을 해줍니다. 해당 모듈은 다음의 링크에서 확인 가능합니다! nuxt-aeo
모듈을 생성하였다면, Nuxt Modules 레포지토리에 이슈를 생성해서 리스트 등록요청을 해주면 됩니다!
아주 쉽죠??
6. 플레이그라운드
실제로 어떻게 사용되는지 플레이 그라운드 페이지도 생성은 해두었습니다. 다만.. 아직 조금 더 보완이 필요한 영역이에요! ㅠ
플레이그라운드에서는 다음과 같은 예제를 확인할 수 있습니다:
- Person Schema 예제
- Organization Schema 예제
- ItemList Schema 예제
- Article Schema 예제
- FAQPage Schema 예제
각 예제 페이지에서 실제로 생성된 JSON-LD와 시멘틱 HTML을 확인할 수 있습니다.
7. 개발문서
Nuxt Content와 Nuxt Studio를 통해 만든 개발문서도 함께 배포하였습니다! (아직 조금 고장난 부분도 존재해요) 해당 문서에서는 모듈의 사용법, 옵션, 예제 등을 확인할 수 있습니다!
문서에는 다음 내용이 포함되어 있습니다:
- 설치 가이드
- 모듈 옵션 설정 방법
- 각 Schema 타입별 사용 예제
- 시멘틱 HTML 자동 생성 기능 설명
- API 레퍼런스
설치 및 기본적인 사용방법 소개
설치 및 기본 사용법
설치는 매우 간단합니다:
bun add nuxt-aeo
설치 후 nuxt.config.ts에 모듈을 추가하면 바로 사용할 수 있습니다!
각 스키마를 nuxt.config.ts에 전부 등록해도 되고, 각 페이지 별로 스키마를 추가할 수 있습니다.
// nuxt.config.ts
export default defineNuxtConfig({
modules: ['nuxt-aeo'],
aeo: {
schemas: [
{
type: 'Organization',
name: 'My Company',
url: 'https://www.example.com',
},
],
},
})
페이지별 스키마 추가
FAQ 페이지에 스키마를 추가하는 것은 매우 간단합니다!
useSchemaPage() 컴포저블을 통해, 해당 페이지 또는 기능들에 대해서 어떠한 역할을 하고있는지 AI모델들에게 질문과 답변의 형태로 쉽게 학습할 수 있게 도와주게 됩니다.
<script setup lang="ts">
useSchemaPage({
mainEntity: [
{
name: 'Nuxt AEO 모듈이란?',
acceptedAnswer: {
text: 'Nuxt AEO는 Schema.org JSON-LD를 통해 AI Engine Optimization을 지원하는 Nuxt 모듈입니다.',
},
},
],
})
</script>
useSchemaPage()는 기본적으로 renderHtml: true로 설정되어 있어,
시멘틱 HTML이 자동으로 생성되지만, visually-hidden 클래스가 추가되어있기 때문에, 실제로 보이지 않지만 DOM에는 존재하게 되어 AI 모델이 읽을 수 있도록 해줍니다.
범용 스키마 사용
<script setup lang="ts">
// Person Schema
useSchema({
context: 'https://schema.org',
type: 'Person',
name: 'Yeonju Lee',
jobTitle: 'Software Engineer',
renderHtml: true,
visuallyHidden: true,
})
// Article Schema
useSchema({
context: 'https://schema.org',
type: 'Article',
headline: 'Article Title',
datePublished: '2024-01-15',
author: {
type: 'Person',
name: 'Author Name',
},
})
</script>
context와 type을 사용하면 내부적으로 @context와 @type으로 자동 변환되므로,
따옴표 없이 일반 속성처럼 사용할 수 있습니다.
마무리
그동안 Nuxt4와 Nuxt UI 에 기여만 하다가, 실제로 모듈을 통해 Nuxt 생태계에 기여를 할 수 있어서 너무나 뿌듯한 경험이었어요!
아직 막 등록한 모듈이기 때문에 어떠한 피드백을 받지 못하였지만, 당연하게도 넓은 마음가짐으로 모든 피드백을 수용할 준비가 되어있어요!ㅋ
앞으로 조금씩 발전시킬 수 있는 그러한 Nuxt-AEO로 발전시키고 싶은 마음이 있고,
더 나아가, Nuxt Modules의 first party 모듈로 등록될 수 있으면 좋겠다는 생각까지 하게 되었습니다!
그럼 오늘의 글은 여기까지로 하고! 다음글에서 뵐게요! 모두 25년 마무리 잘하시고, 새해 복 미리 많이 받으세요!

