FeedLog를 만든 방법: 레포 3개, 하나의 제품
I’m happy to translate the article for you, but I’ll need the text you’d like translated. Could you please paste the content of the article (or the specific sections you want translated) here? I’ll keep the source line and all formatting exactly as you requested.
저장소 레이아웃
| 저장소 | 가시성 | 목적 |
|---|---|---|
| feedlog-api | 비공개 | Node 백엔드: 웹훅, AI 처리, 공개 API |
| feedlog-app | 비공개 | 웹 대시보드: OAuth, 설정, 변경 로그 관리 |
| feedlog-toolkit | 공개 (MIT) | 고객이 사이트에 삽입하여 변경 로그 위젯을 렌더링할 수 있는 임베드형 SDK |
세 개의 저장소로 나눈 것은 의도된 설계입니다: 툴킷은 고객이 직접 통합하는 유일한 부분이므로 공개 상태를 유지하고, Changesets를 통해 독립적으로 버전 관리되며, 내부 코드를 건드리지 않고도 배포할 수 있습니다. API와 앱도 각각 독립적으로 배포됩니다—프론트엔드 배포가 API 재시작을 강제하지 않으며, 그 반대도 마찬가지입니다.
툴킷
- 기술 스택: Stencil monorepo → 진정한 웹 컴포넌트 + 자동 생성된 React 및 Vue 래퍼.
- 출력: 하나의 컴포넌트 소스 → 세 개의 프레임워크 타깃.
API
- 런타임: Node with Fastify.
- 왜 Fastify인가? 플러그인 시스템과 내장 스키마 검증이 작은 팀에 적합합니다.
- 타이핑:
fastify-type-provider-zod→ 모든 라우트가 Zod 스키마에서 핸들러까지 엔드‑투‑엔드로 타입이 지정됩니다 (동기화할 별도의 OpenAPI 스펙이 없습니다).
데이터베이스
- ORM: Neon Postgres 위에 Drizzle ORM.
- Neon 장점: 브랜칭이 가능한 서버리스 Postgres (마이그레이션 미리보기 시 유용).
- 마이그레이션:
// scripts/migrate.ts
// run with tsx
Drizzle Kit은 TypeScript 스키마에서 SQL 마이그레이션을 생성합니다; 마이그레이션은 별도 스크립트로 실행되며, 시작 시에 실행되지 않습니다.
비동기 작업
| 구성 요소 | 역할 |
|---|---|
| BullMQ + Redis | GitHub 웹훅 이벤트와 AI 초안 생성을 큐에 넣습니다. 웹훅 엔드포인트는 즉시 응답하고, 워커가 작업을 처리합니다. |
| Croner (in‑process) | - 웹훅 복구 작업 (실패한 페이로드를 15 분마다 재전송) - 모든 세 프로세스(API, 이벤트 워커, 외부 워커)에 대한 Sentry 크론 모니터 하트비트 |
| opossum | Postgres 풀을 서킷 브레이커로 감싸 DB 장애 시에도 점진적으로 서비스가 감소하도록 합니다. |
| @fastify/rate-limit | Redis에 저장되는 API 키당 속도 제한 (재시작 후에도 유지되며 인스턴스 간에 작동). |
대시보드
- 프레임워크: TanStack Start (React SSR)와 TanStack Router 및 TanStack Query.
- UI: Tailwind CSS v4 + Radix UI 프리미티브 (shadcn 패턴).
- 배포: Wrangler를 통한 Cloudflare Workers → 콜드 스타트 비용 없이 엣지에 배포된 SSR.
기본 키 – UUID v7
모든 테이블은 UUIDv7을 사용합니다 (Postgres 확장 uuidv7()가 컬럼 기본값으로 생성).
장점
- 시간 순서대로 정렬되고 단조 증가 → 삽입이 항상 B‑tree의 끝에 위치 (페이지 분할·단편화 없음).
- 생성 타임스탬프를 인코딩 → 별도의
created_at컬럼이 필요 없음.
단점
Neon 콘솔 및 Drizzle Studio에서는 UUID를 불투명한 값으로 표시해 읽을 수 있는 타임스탬프가 보이지 않습니다.
해결 방법
-- Helper to extract timestamp from a UUIDv7
SELECT extractCreatedAtFromUuid7(uuid_column) AS created_at FROM table;
Public IDs
내부 기본 키는 내부에 그대로 유지됩니다. 외부에 노출되는 모든 테이블에는 public_id 컬럼이 추가됩니다: 의미 있는 접두사가 붙은 짧고 URL‑안전한 문자열입니다.
usr_a3b7kx9m2p1z ← user
ins_q8tnrfw4j6yd ← installation
rep_c2mh5vp0xk3a ← repository
iss_e9rz1db7yt4n ← issue
pk_lw6gc8nu0fqj ← API key
Generation
import { customAlphabet } from 'nanoid';
const nanoid = customAlphabet('0123456789abcdefghijklmnopqrstuvwxyz', 12);
const publicId = `${prefix}_${nanoid()}`;
왜? 접두사는 로그, 티켓, 혹은 URL에서 즉시 타입 힌트를 제공해 줍니다 — Stripe가 대중화한 접근 방식입니다.
Soft Deletes
All tables have a deleted_at timestamp column. Every delete (even trivial ones) becomes a soft delete (deleted_at set).
Benefits
| ✅ | Benefit |
|---|---|
| 우발적인 복구 | 간단한 UPDATE 로 복원합니다. |
| 감사 추적 | 무엇이 존재했는지, 언제 제거됐는지 항상 알 수 있습니다. |
| 실행 취소 흐름 | 예시: 업보트 토글: deleted_at 를 NULL ↔ 로 바꿉니다. |
| 보다 안전한 디버깅 | 문제 파악을 위해 소프트 삭제된 행을 실시간 행과 함께 조회합니다. |
Trade‑off & Mitigation
- 누적: 시간이 지남에 따라 소프트 삭제된 행이 쌓입니다.
- 해결책: 각 테이블별 정리 크론을 두어
deleted_at이 설정 가능한 임계값보다 오래된 행을 하드 삭제합니다. - 이미 프로세스 내에서
croner를 실행하고 있으므로, 정리 작업을 추가하는 것은 간단합니다—각 테이블은 자체 보존 기간을 설정할 수 있습니다.
미해결 질문 / 향후 고려사항
- Cron 위치: 현재
croner는 API 서버 내에서 프로세스 내에서 실행됩니다.- Pros: 시작이 더 간단합니다.
- Cons: 각 API 인스턴스가 동일한 크론을 실행하려고 경쟁하므로 분산 락이 필요합니다.
- Potential direction: 장시간 실행되거나 무거운 정리 작업을 전용 스케줄러 프로세스로 옮깁니다.
TL;DR
- Three repos (API, app, toolkit)은 공개 인터페이스를 최소화하고 독립적으로 유지합니다.
- Fastify + Zod는 별도의 스펙 없이 타입‑안전한 라우트를 제공합니다.
- Neon + Drizzle는 TypeScript 기반 마이그레이션을 지원하는 서버리스 Postgres를 제공합니다.
- BullMQ + Redis는 비동기 작업을 처리하고, opossum은 데이터베이스를 보호합니다.
- UUIDv7 + public_id 스키마는 내부 효율성과 외부 가독성 사이의 균형을 맞춥니다.
- Soft deletes는 복구, 감사, 토글을 간소화하며, 주기적인 하드‑삭제 정리와 함께 사용됩니다.
모든 결정은 지금까지 잘 유지되어 왔으며, 아키텍처는 향후 확장을 위해 유연성을 유지하고 있습니다. 현재 작업들은 중복 실행이 무해할 정도로 멱등성을 갖추고 있지만, 시스템이 규모가 커짐에 따라 다시 검토할 필요가 있습니다.