주어진 URL의 Open Graph(OG) 메타데이터를 수집해 JSON으로 반환하는 서버이다.
링크 미리보기 카드, 공유 미리보기, 콘텐츠 요약 UI를 만들 때 필요한 title, description, image, favicon 같은 메타데이터를 수집한다.
개발 중이던 서비스들(#1, #2)에서 링크의 메타데이터 수집이 필요해졌는데, 많은 외부 서비스들은 호출 제한과 유료 플랜 의존 문제가 있었다.
(e.g. OpenGraph.io - 100 Requests per month, 1 concurrent request)
외부 서비스의 의존도를 줄이고 추가적인 비용 지출을 막기 위해 기획했고, 주 목표는 메타데이터의 정확도와 데이터 추출 속도(응답 속도)이다.
AI-assisted 방식으로 구현 (요구사항, 서비스 플로우를 직접 정의하고 이를 기반으로 기능 구현에 AI 활용)
Node.js 기반 Express 서버로 구성
- HTTP 레이어:
Express - 정적 HTML 파싱:
Cheerio - 동적 렌더링 대응:
Playwright + Chromium - 배포 대상:
VercelorAWS Lambda(Function URL)orRender
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를 진입점으로 사용
- 테스트 러너:
bun:test - HTTP API 테스트:
supertest의request(app)패턴 사용 - 실행 명령:
bun test
Express 서버 테스트는 src/server.js처럼 실제 포트를 띄우지 않고 src/app.js를 직접 불러 통합 테스트를 작성한다.
외부 네트워크, Playwright 브라우저, 시간 의존성은 mock/stub으로 격리하고, 유틸/파서/캐시 같은 내부 로직은 실제 구현을 검증한다.
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
-
staticfetch()로 HTML을 가져오고 Cheerio로 파싱- 전체 HTML을 무조건 다 받지 않고, 먼저
</head>까지만 읽는 head-only 경로를 시도 - SSR 또는 정적 페이지에서 주로 사용
-
dynamic- Playwright와 Chromium으로 실제 브라우저 환경을 띄워 페이지를 렌더링한 뒤 DOM을 기준으로 메타데이터를 추출합니다.
- JavaScript 렌더링 기반 사이트나 정적 응답만으로 OG를 얻기 어려운 사이트를 위한 보완 경로입니다.
-
auto- static을 먼저 시작하고, 일정 시간 뒤 dynamic을 병행 시작하는 delay race 전략을 사용합니다.
- static 결과가 충분히 좋으면 빠르게 반환하고, 부족하면 dynamic 결과로 전환합니다.
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> 를 정확하게 가져오기 위한 선택이다.
참고
GET /healthGET /metricsGET /api/crawl?url=<target>&mode=<auto|static|dynamic>
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"-
url(required)http,httpsURL만 허용
-
mode(optional)auto,static,dynamic중 선택- 기본값은
auto
-
timings(optional)1,true,yes,on이면 응답에 단계별 타이밍을 포함
{
"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
}
}- 잘못된
url또는 누락된url400 Bad Request
- 잘못된
mode또는timings400 Bad Request
- 대상 사이트 요청 실패
- 대상 서버 상태 코드 기반 에러 반환 가능
- static / dynamic timeout
504
- 존재하지 않는 경로
404
참고
초기 구조에서 파악된 핵심 병목은:
- 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
참고