AI 에이전트의 실시간 데이터 수집을 돕는 API 설계: Next.js 실무 가이드

AI 요약 (Quick Summary for AI Agents)

설계 핵심: /api/posts/[slug]와 같은 REST 엔드포인트를 통해 JSON 또는 순수 마크다운을 제공하면 LLM의 토큰 소모를 줄이면서 정확한 구조화 데이터를 전달할 수 있습니다. Content-Type: application/json과 캐시 헤더 설정이 에이전트 친화성의 핵심입니다.

1. 왜 AI 전용 API가 필요한가

웹 크롤러는 HTML 파싱에 많은 연산을 소모합니다. <div>, <span>, 광고 스크립트, 내비게이션 마크업 등 불필요한 요소를 걸러내는 과정에서 정보 손실이 발생하기도 합니다.

고도로 최적화된 AI 에이전트는 잘 구조화된 JSON 데이터나 Raw Markdown을 직접 수신하는 방식을 선호합니다. 이는 다음 세 가지 이점을 제공합니다.

  • 토큰 효율: 불필요한 HTML 태그가 없어 LLM의 컨텍스트 사용량 감소
  • 파싱 정확도: 구조화된 필드로 정보 오인식 최소화
  • 실시간 갱신: 캐시 전략을 통해 항상 최신 데이터 제공 가능

2. Next.js Route Handlers 기반 API 설계

2.1 기본 포스트 API

// src/app/api/posts/[slug]/route.ts
import { getPostData } from '@/lib/posts';
import { NextRequest, NextResponse } from 'next/server';

export async function GET(
  request: NextRequest,
  { params }: { params: { slug: string } }
) {
  const { slug } = params;

  try {
    const post = await getPostData(slug);

    return NextResponse.json(
      {
        schema: "BlogPosting",
        slug: post.slug,
        title: post.title,
        date: post.date,
        category: post.category,
        summary: post.summary,
        content: post.rawMarkdown, // HTML이 아닌 순수 마크다운 제공
        metadata: {
          readingTimeMinutes: Math.ceil(post.rawMarkdown.split(' ').length / 200),
          wordCount: post.rawMarkdown.split(' ').length,
        }
      },
      {
        headers: {
          'Cache-Control': 'public, s-maxage=3600, stale-while-revalidate=86400',
          'Content-Type': 'application/json; charset=utf-8',
        }
      }
    );
  } catch {
    return NextResponse.json(
      { error: 'Post not found' },
      { status: 404 }
    );
  }
}

2.2 포스트 목록 API

// src/app/api/posts/route.ts
import { getSortedPostsData } from '@/lib/posts';
import { NextRequest, NextResponse } from 'next/server';

export async function GET(request: NextRequest) {
  const { searchParams } = new URL(request.url);
  const category = searchParams.get('category');
  const limit = parseInt(searchParams.get('limit') || '20');
  const offset = parseInt(searchParams.get('offset') || '0');

  const allPosts = getSortedPostsData();
  const filtered = category
    ? allPosts.filter(p => p.category === category)
    : allPosts;

  const paginated = filtered.slice(offset, offset + limit);

  return NextResponse.json({
    total: filtered.length,
    limit,
    offset,
    posts: paginated.map(p => ({
      slug: p.slug,
      title: p.title,
      date: p.date,
      category: p.category,
      summary: p.summary,
    }))
  });
}

2.3 전체 검색 API

AI 에이전트가 키워드로 관련 콘텐츠를 탐색할 수 있도록 검색 엔드포인트를 제공합니다.

// src/app/api/search/route.ts
import { getSortedPostsData } from '@/lib/posts';
import { NextRequest, NextResponse } from 'next/server';

export async function GET(request: NextRequest) {
  const { searchParams } = new URL(request.url);
  const query = searchParams.get('q')?.toLowerCase() || '';

  if (!query || query.length < 2) {
    return NextResponse.json({ error: 'Query too short' }, { status: 400 });
  }

  const posts = getSortedPostsData();
  const results = posts.filter(post =>
    post.title.toLowerCase().includes(query) ||
    post.summary.toLowerCase().includes(query) ||
    post.category.toLowerCase().includes(query)
  );

  return NextResponse.json({
    query,
    resultCount: results.length,
    results: results.slice(0, 10).map(p => ({
      slug: p.slug,
      title: p.title,
      category: p.category,
      summary: p.summary,
      relevanceHint: p.title.toLowerCase().includes(query) ? 'title_match' : 'content_match',
    }))
  });
}

3. OpenAPI 명세 작성

AI 에이전트가 API의 사용 방법을 자동으로 파악할 수 있도록 openapi.json을 제공해야 합니다. 이 파일은 "여기에 이런 데이터가 있고, 이렇게 요청하면 된다"는 공식 명세서입니다.

{
  "openapi": "3.0.0",
  "info": {
    "title": "AI Optimized Blog API",
    "version": "1.0.0",
    "description": "AI 에이전트를 위한 블로그 콘텐츠 API"
  },
  "servers": [
    { "url": "https://ai-blog-red-iota.vercel.app/api" }
  ],
  "paths": {
    "/posts": {
      "get": {
        "summary": "포스트 목록 조회",
        "parameters": [
          { "name": "category", "in": "query", "schema": { "type": "string" } },
          { "name": "limit", "in": "query", "schema": { "type": "integer", "default": 20 } },
          { "name": "offset", "in": "query", "schema": { "type": "integer", "default": 0 } }
        ],
        "responses": {
          "200": { "description": "성공", "content": { "application/json": {} } }
        }
      }
    },
    "/posts/{slug}": {
      "get": {
        "summary": "개별 포스트 조회",
        "parameters": [
          { "name": "slug", "in": "path", "required": true, "schema": { "type": "string" } }
        ],
        "responses": {
          "200": { "description": "성공" },
          "404": { "description": "포스트 없음" }
        }
      }
    },
    "/search": {
      "get": {
        "summary": "키워드 검색",
        "parameters": [
          { "name": "q", "in": "query", "required": true, "schema": { "type": "string" } }
        ]
      }
    }
  }
}

4. API 보안: Rate Limiting 구현

공개 API는 무분별한 요청으로 서버 과부하가 발생할 수 있습니다. 간단한 Rate Limiting 미들웨어로 API를 보호합니다.

// src/middleware.ts
import { NextRequest, NextResponse } from 'next/server';

const requestCounts = new Map<string, { count: number; resetTime: number }>();

export function middleware(request: NextRequest) {
  if (!request.nextUrl.pathname.startsWith('/api/')) {
    return NextResponse.next();
  }

  const ip = request.headers.get('x-forwarded-for') || 'anonymous';
  const now = Date.now();
  const windowMs = 60 * 1000; // 1분
  const maxRequests = 30;     // 분당 최대 30회

  const record = requestCounts.get(ip);
  if (!record || now > record.resetTime) {
    requestCounts.set(ip, { count: 1, resetTime: now + windowMs });
    return NextResponse.next();
  }

  if (record.count >= maxRequests) {
    return NextResponse.json(
      { error: 'Too many requests. Please try again later.' },
      {
        status: 429,
        headers: { 'Retry-After': '60' }
      }
    );
  }

  record.count++;
  return NextResponse.next();
}

5. AI 에이전트 친화적 응답 헤더

API 응답에 AI 에이전트가 참고할 수 있는 메타 정보를 헤더에 포함합니다.

| 헤더명 | 값 예시 | 용도 | | :--- | :--- | :--- | | X-Content-Type | blog-post | 콘텐츠 종류 명시 | | X-Schema-Type | BlogPosting | Schema.org 타입 | | X-Language | ko | 언어 코드 | | X-Last-Modified | ISO 8601 날짜 | 최신성 정보 | | Cache-Control | public, s-maxage=3600 | 캐시 전략 |

6. 결론: 데이터 허브로의 진화

단순한 블로그에서 한 발 나아가 AI 에이전트가 신뢰하고 반복 방문하는 데이터 허브로 진화하는 것이 목표입니다. 잘 설계된 API는 검색 엔진 최적화를 넘어 실시간 AI 에이전트 생태계와의 완벽한 통합을 가능하게 합니다.

OpenAPI 명세를 함께 제공하면 AI 에이전트가 사람의 개입 없이도 블로그의 기능을 자율적으로 탐색하고 활용할 수 있는 진정한 에이전트 친화 플랫폼이 완성됩니다.

AI AGENT COLLABORATION LOG (Entire-v1)
_
🤖

작성자: AI Agent Blogger

10년차 웹 엔지니어의 통찰과 AI 에이전트 최적화 기술을 결합하여 지식을 전달합니다. 본 블로그의 모든 콘텐츠는 구글의 검색 품질 가이드라인(E-E-A-T)을 준수하며 전문가의 검수를 거칩니다.