Skip to content

justhumanb2ing/og-crawler-server

Repository files navigation

OpenGraph Crawler Server

English | 프로젝트 문서

Overview

주어진 URL의 Open Graph(OG) 메타데이터를 수집해 JSON으로 반환하는 서버이다. 링크 미리보기 카드, 공유 미리보기, 콘텐츠 요약 UI를 만들 때 필요한 title, description, image, favicon 같은 메타데이터를 수집한다.

Background

개발 중이던 서비스들(#1, #2)에서 링크의 메타데이터 수집이 필요해졌는데, 많은 외부 서비스들은 호출 제한과 유료 플랜 의존 문제가 있었다. (e.g. OpenGraph.io - 100 Requests per month, 1 concurrent request)
외부 서비스의 의존도를 줄이고 추가적인 비용 지출을 막기 위해 기획했고, 주 목표는 메타데이터의 정확도와 데이터 추출 속도(응답 속도)이다.

With AI

AI-assisted 방식으로 구현 (요구사항, 서비스 플로우를 직접 정의하고 이를 기반으로 기능 구현에 AI 활용)

Architecture

Node.js 기반 Express 서버로 구성

  • HTTP 레이어: Express
  • 정적 HTML 파싱: Cheerio
  • 동적 렌더링 대응: Playwright + Chromium
  • 배포 대상: Vercel or AWS Lambda(Function URL) or Render
Client
  |
  v
Vercel / AWS Lambda
  |
  v
api/index.js or src/lambda.js
  |
  v
src/app.js (Express)
  |
  v
src/controllers/crawlController.js
  |
  v
src/services/crawlService.js
  |
  +--> src/services/staticCrawler.js
  |       |
  |       +--> src/utils/http.js
  |       +--> src/services/ogExtractor.js
  |
  +--> src/services/dynamicCrawler.js
          |
          +--> Playwright / Chromium
          +--> src/services/ogExtractor.js

로컬 개발 환경에서는 src/server.js로 서버를 실행하고, Vercel에서는 api/index.js, AWS Lambda에서는 src/lambda.js를 진입점으로 사용

Testing

  • 테스트 러너: bun:test
  • HTTP API 테스트: supertestrequest(app) 패턴 사용
  • 실행 명령: bun test

Express 서버 테스트는 src/server.js처럼 실제 포트를 띄우지 않고 src/app.js를 직접 불러 통합 테스트를 작성한다. 외부 네트워크, Playwright 브라우저, 시간 의존성은 mock/stub으로 격리하고, 유틸/파서/캐시 같은 내부 로직은 실제 구현을 검증한다.

Reuqest Sequence Diagram

Client
  |
  |  GET /api/crawl?url=<target>&mode=auto
  v
Express App (src/app.js)
  |
  v
Route (src/routes/crawl.js)
  |
  v
Controller (src/controllers/crawlController.js)
  |
  |  validate(url, mode, timings)
  v
Crawl Service (src/services/crawlService.js)
  |
  +--> Static Crawler (src/services/staticCrawler.js)
  |       |
  |       +--> HTTP fetch / head-only read
  |       +--> OG Extractor
  |
  +--> Dynamic Crawler (src/services/dynamicCrawler.js)
  |       |
  |       +--> Playwright + Chromium
  |       +--> OG Extractor
  |
  v
JSON Response

About mode in crawling

static, dynamic, auto의 역할

  • static

    • fetch()로 HTML을 가져오고 Cheerio로 파싱
    • 전체 HTML을 무조건 다 받지 않고, 먼저 </head>까지만 읽는 head-only 경로를 시도
    • SSR 또는 정적 페이지에서 주로 사용
  • dynamic

    • Playwright와 Chromium으로 실제 브라우저 환경을 띄워 페이지를 렌더링한 뒤 DOM을 기준으로 메타데이터를 추출합니다.
    • JavaScript 렌더링 기반 사이트나 정적 응답만으로 OG를 얻기 어려운 사이트를 위한 보완 경로입니다.
  • auto

    • static을 먼저 시작하고, 일정 시간 뒤 dynamic을 병행 시작하는 delay race 전략을 사용합니다.
    • static 결과가 충분히 좋으면 빠르게 반환하고, 부족하면 dynamic 결과로 전환합니다.

fallback의 동작 원리

auto 모드는 순차 fallback이 아니라 지연 병행 전략이다.

1) static 시작
2) AUTO_DYNAMIC_DELAY_MS 동안 기다림
3) static이 충분하지 않거나 아직 끝나지 않았으면 dynamic 시작
4) 먼저 유효한 결과를 만든 경로를 선택
5) 불필요해진 경로는 abort

static 내부에서도 한 번 더 fallback이 존재한다.

  • 먼저 head-only 수집 시도
  • </head>를 찾지 못하거나 바이트 제한에 걸리면 full body fetch로 fallback

설계 배경

이 설계의 핵심은 정확도와 비용의 균형이다.

static은 SSR, 정적 페이지에서 사용가능하다. (최초 응답 HTML에 <meta> 가 이미 들어있기 때문) 즉, 브라우저 실행없이 HTML만 받아서 데이터를 읽을 수 있다. 반면, dynamic은 SPA 또는 JS로 <meta>를 직접 주입하는 경우에 사용한다. Playwright를 사용하여 페이지 콘텐츠를 읽어서 파싱한다.

이 둘을 분리한 이유는, static은 파싱 속도가 빠르지만 일부 SPA 구조 웹 어플리케이션에서 데이터를 받아오지 못한다. dynamic은 정확도는 높아지지만 브라우저 실행 비용이 크고 데이터를 받기까지 시간이 오래 걸린다.

auto 전략은 auto 전략은 대부분의 요청은 static으로 빠르게 처리하고, 필요한 경우에만 dynamic 비용을 추가로 지불하게 해 <meta> 를 정확하게 가져오기 위한 선택이다.

참고

6. API

Endpoint

  • GET /health
  • GET /metrics
  • GET /api/crawl?url=<target>&mode=<auto|static|dynamic>

Sample Request

curl "http://localhost:3000/api/crawl?url=https://example.com&mode=auto"

단계별 타이밍과 캐시 메타까지 확인하고 싶다면:

curl "http://localhost:3000/api/crawl?url=https://example.com&mode=auto&timings=1"

optional params

  • url (required)

    • http, https URL만 허용
  • mode (optional)

    • auto, static, dynamic 중 선택
    • 기본값은 auto
  • timings (optional)

    • 1, true, yes, on이면 응답에 단계별 타이밍을 포함

Sample Response

{
  "ok": true,
  "mode": "dynamic",
  "fallback": true,
  "durationMs": 1234,
  "data": {
    "title": "Example",
    "description": "...",
    "url": "https://example.com",
    "site_name": "example.com",
    "image": "https://example.com/og.png",
    "favicon": "https://example.com/favicon.ico"
  }
}

timings=1일 때는 아래와 같은 필드가 추가될 수 있다.

{
  "timings": {
    "static": {
      "fetchMs": 120,
      "extractMs": 5,
      "totalMs": 126
    },
    "dynamic": {
      "launchMs": 540,
      "navigationMs": 820,
      "extractMs": 7,
      "totalMs": 1375
    }
  },
  "meta": {
    "static": {
      "head_only": true,
      "head_complete": true,
      "head_bytes": 8123,
      "head_truncated": false,
      "head_fallback": false
    }
  },
  "cache": {
    "hit": false,
    "ttlMs": 300000
  }
}

Case of Error

  • 잘못된 url 또는 누락된 url
    • 400 Bad Request
  • 잘못된 mode 또는 timings
    • 400 Bad Request
  • 대상 사이트 요청 실패
    • 대상 서버 상태 코드 기반 에러 반환 가능
  • static / dynamic timeout
    • 504
  • 존재하지 않는 경로
    • 404

참고

7. from Before to After

초기 구조에서 파악된 핵심 병목은:

  • static 경로가 전체 HTML을 끝까지 다운로드해 불필요한 비용이 컸음
  • auto 모드가 사실상 순차 fallback처럼 동작하면 전체 응답이 느려질 수 있었음
  • dynamic 경로에서 브라우저 런치 비용이 컸음
  • 반복 요청에도 동일한 작업을 다시 수행했음

현재는:

  • static head-only 우선 수집 + full body fallback
  • auto delay race 전략
  • dynamic browser reuse 및 networkidle 정책 분리
  • URL 기반 in-memory TTL cache
  • stage-level timing log 및 optional response timings

참고

About

Express server that crawls Open Graph metadata and returns normalized JSON for a given URL

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors