AmputatorBot → Devvit 마이그레이션 계획 v7
상태: 작업 중인 문서 — M1–M5 제공 완료, M6(클라우드 배포 + 출시) 진행 중.
작성일: 2026-05-25
예산 한도: 월 €15
작업 스타일 — 먼저 읽으세요
Dev는 코드가 작성되기 전에 아키텍처 결정에 대해 협의받기를 선호합니다. 다음에 해당하는 모든 선택:
- 사용할 라이브러리, 프레임워크 또는 패턴에 관한 것
- 데이터 모델 설계 또는 스키마 결정에 관한 것
- 새 모듈 또는 계층 구조 구성 방법에 관한 것
- 비용 대비 복잡성 트레이드오프에 관한 것
- 이 계획에서 중요하지 않은 방식으로 이탈하는 것
…에 대해서는 진행 전에 중단하고 질문하십시오. 옵션을 제시하고, 트레이드오프를 정직하게 설명(동의하지 않는 경우 반대 의견 포함)하고, 결정을 기다리십시오. 기계적 구현 작업은 협의가 필요하지 않지만, 아키텍처 형태는 필요합니다.
Dev가 수동으로 수행하는 외부 작업(Claude 불가):
- 프로덕션 MySQL/Postgres 데이터베이스에 대한 쿼리 실행(Claude가 SQL을 작성하고 Dev가 실행)
- 서비스 가입(Scaleway, Reddit 개발자 계정, App Directory 제출, 현상금 프로그램 등록)
- Dev의 신원을 사용하는 외부 계정 관련 작업(OAuth 로그인,
devvit login,scw init, 청구 변경) - Cloudflare의 DNS 변경
- 프로덕션 PythonAnywhere 봇 시작/중지
- 프로덕션에 영향을 미치는 원격 저장소에
git push - Devvit 앱 게시(
devvit upload --publish)
Claude는 정확한 명령을 준비하고 Dev에게 실행 시점을 알려줍니다. 코드 읽기, 코드 작성, 로컬 테스트 실행, 로컬 Docker 빌드, 로컬 cargo/npm — 이러한 작업은 Claude가 수행합니다.
프로젝트 컨텍스트
AmputatorBot이란: 댓글/게시물/PM에서 AMP URL을 감지하고 표준(비AMP) 링크로 응답하는 Reddit 봇입니다. Dev(u/Killed_Mufasa)가 유지 관리합니다. https://github.com/jvdburgh/AmputatorBot 에서 운영 중입니다. 약 181개의 GitHub 별을 보유하고 있으며, 대규모로 실행됩니다. Reddit의 Developer Platform 마이그레이션 프로그램은 2026년 12월 31일까지 Data API에서 Devvit으로 마이그레이션하는 봇에 $1,000를 제공합니다.
마이그레이션 대상:
- Reddit 봇(Python + PRAW) → Devvit 앱(TypeScript,
@devvit/web) - HTTP API(Flask) → Rust(Axum + Mozilla-Readability-포트 + sqlx)
- 웹사이트(Flask + Bootstrap) → Astro 5 + Tailwind 4 + shadcn/ui
- 데이터베이스(PythonAnywhere의 MySQL) → Scaleway Managed Database의 Postgres 17
- 호스트(PythonAnywhere) → Scaleway(파리, 프랑스 — EU 기원 관리형 클라우드)
아키텍처 모델 변경: Devvit는 전역이 아닌 서브레딧별 옵트인입니다. 모드가 서브레딧별로 앱을 설치하며, 설치되지 않은 곳에서는 콘텐츠를 볼 수 없습니다. 응답은 u/AmputatorBot이 아닌 설치별 앱 ID로 게시됩니다. 수용됨. 예상 응답 볼륨 감소: 30–60%.
목표(사용자 명시):
- 현대적인 스택, 과정에서 학습(Rust, Devvit, Astro/Tailwind4/shadcn)
- API 계약은 하위 호환성 유지
- 재작성 기간(M1–M5) 동안 이전 봇은 PythonAnywhere에서 계속 실행되어 새 쪽이 사용자에게 영향을 주지 않고 자유롭게 중단 가능. 이전 봇은 M6 전환 시 폐기 — 영구 병렬 실행 없음.
- 재작성을 신속하게 수행, 이전 코드는
praw-python-archive/에 보존
확정된 결정
| 경로 | 서브레딧별 옵트인. M6에서 직접 전환(이전 봇은 M1–M5 개발 중에만 유지) |
| Devvit SDK | @devvit/web(현대 서버 모델) |
| Devvit 구성 | devvit.json(공식 형식, $schema 포함) |
| Devvit 스캐폴드 | reddit/devvit-template-bare, 게시물/메뉴 UI 제거 |
| 백엔드 언어 | Rust |
| 백엔드 프레임워크 | Axum 0.8(Tokio + Hyper) |
| 문서 추출 | dom_smoothie 크레이트(≈0.17, Rust Readability 포트, Mozilla의 readability.js를 밀접하게 추적하며 2026년 3월 기준 적극 유지 관리) |
| HTML 파싱 | scraper 크레이트(html5ever 기반) |
| HTTP 클라이언트 | reqwest |
| Postgres 드라이버 | sqlx(컴파일 타임 검사 쿼리) |
| 데이터베이스 | Scaleway Managed Database의 Postgres 17(DB DEV-S 또는 그 다음 티어, 약 2GB + 성장 여유분 크기). 170만 행 확인됨. |
| 웹사이트 | Astro 5 + Tailwind 4 + shadcn/ui |
| 호스팅 | Scaleway Serverless Containers(파리/AMS, EU 기원 프랑스 클라우드) |
| 단일 서비스 | 하나의 Rust 바이너리가 tower-http::ServeDir을 통해 /api/* 및 Astro 정적 파일 제공 |
| 관측 가능성 | 초기에는 Scaleway Cockpit(내장 로그 + 메트릭)만 사용 |
| 코드 호스팅 | GitHub(기존 모노레포) |
| DNS | Cloudflare(기존) — 레이트 제한도 처리 |
| API 인증 | 모든 경로에 없음 — 완전히 공개. Cloudflare가 남용 처리. |
| 레이트 제한 | Cloudflare만 사용, Rust 미들웨어 없음 |
| 도메인 | 단일 — 모든 것을 www.amputatorbot.com에 통합 |
| 테스트 서브레딧 | r/test |
| AI 도구 | Claude Code + Devvit MCP + IntelliJ의 JetBrains MCP |
| 사용자별 옵트아웃 | 삭제됨. 사용자가 봇 계정을 차단하면 설치별 앱 ID로 인해 설치 수준에서 블록 기반 옵트아웃이 작동함. |
| 서브레딧별 옵트아웃 | 삭제됨(모드가 설치 설정에서 autoReply를 끄거나 앱 제거) |
| Modmail / DM | 없음. 인바운드 modmail 핸들러, 환영 modmail, DM 없음 — Reddit의 "App" 배지가 봇 공개를 처리하고 Devvit App Directory 목록이 온보딩을 담당. |
| 카르마 임계값 | 없음 |
| App Directory 제출자 | u/Killed_Mufasa |
이러한 선택의 이유
위 결정에 대한 근거. 각각은 신뢰할 수 있는 대안보다 의도적으로 선택되었습니다.
-
Node/TS나 Python 대신 Rust + Axum. 단일 정적 바이너리, 런타임 불필요, virtualenv 불필요,
node_modules불필요. 이러한 서비스(HTTP fetch + regex + HTML 쿼리 + DB 쓰기)에서는 Rust의 어려운 부분(수명, unsafe, 고급 제네릭)이 거의 나타나지 않으므로 학습 곡선은 주로Result/?, async/Tokio 및 sqlx 매크로 — 모두 알아둘 가치가 있습니다. Axum 0.8은 2026년의 컨센서스 프레임워크(Tokio + Hyper + Tower 미들웨어)입니다. sqlx는 컴파일 타임 검사 쿼리를 제공합니다. -
rs-trafilatura및 기타 Readability 포트 대신dom_smoothie. 기존 봇은newspaper3k+difflib.SequenceMatcher를 사용하여 추측 및 확인 신뢰도 점수(helpers/article_comparer.py)를 수행합니다. Rust 포트에서는 Mozilla-Readability 정렬 추출기를 원합니다. 2026년 5월 기준 후보:dom_smoothie0.17(마지막 릴리스 2026-03, 약 40k 최근 다운로드, 저장소niklak/dom_smoothie) —readability.js를 밀접하게 따르며, 두 가지 모드(Mozilla 모방 + 사용자 정의), JSON-LD + OG 메타데이터 파싱, 적극 유지 관리.readability0.3 by kumabook(마지막 릴리스 2023-12, 정체) — arc90의 원본 Readability 포트(Mozilla의 포크 이전). 더 높은 과거 다운로드 수지만 유지 관리되지 않음.readable-readability0.4(마지막 릴리스 2022-12, 정체).llm_readability0.0.17(활성 but 0.0.x — 프로덕션에 너무 초기 단계).
dom_smoothie는 적극 유지 관리 + 명시적 Mozillareadability.js계보로 승리합니다. 유사성 점수에 표준(Mozilla 모방) 모드를 사용합니다. 스파이크 불필요. M2에서dom_smoothie = "0.17"로 고정. -
어디에도 인증 없음. 프로덕션은 현재
/convert에서authorization_required=False(AmputatorBotCom/main.py:162). Bearer 인증에 의존하는 사람이 없으며, 옵트아웃이 제거되면서 백엔드에 권한 있는 경로가 전혀 없습니다. Cloudflare가 남용을 처리합니다. -
Cloudflare 측 레이트 제한. Cloudflare가 이미 도메인을 앞서고 있습니다. IP별 규칙은 Rust 코드가 전혀 없으며 재배포 없이 구성 가능합니다.
-
옵트아웃 메커니즘, modmail, DM 없음. 봇을 원하지 않는 사용자와 모드는 각각 설치별 앱 ID를 차단하거나 앱을 제거할 수 있습니다. 그것으로 충분합니다 — 그 위에 별도의 옵트아웃 레지스트리를 구축하는 것은 필요 없는 메커니즘입니다. modmail 트리거, 환영 modmail, bearer-auth 권한 있는 경로,
optouts테이블, Redis 옵트아웃 캐시, 그리고 "DM을 통해 소환" 레거시 경로를 한 번에 제거합니다. -
마이그레이션 중 DB 스키마 현대화. 마이그레이션 도구는 한 번 실행됩니다.
VARCHAR(4000)→TEXT정리,models/table.py에 없는 인덱스 추가, 죽은 열 제거는 한 번만 하면 다시 할 필요가 없습니다. -
초기에는 로그 전용 관측 가능성. Scaleway Cockpit(내장 로그 + 메트릭 스트림)은 문제 발생 시 grep하기에 충분합니다. 로그-grep이 불편해질 때만 Sentry/Bugsink를 추가하십시오.
-
기존 AMP 감지 + 표준 방법 포팅, 단 false positives를 제자리에서 수정. 14개의 서브스트링 패턴(
static/static.py)은 잡음이 많습니다(&HTML 엔티티,lamp_post,streaming_amplifier). 10개의 표준 방법과helpers/canonical_methods.py의 순서는 잘 조정되어 있으며 유지됩니다. 개선은 AMP 감지 레이어(단어 경계, 추출 후 URL 구성 요소로 범위 지정)와 고신호 마커에 대한 고속 경로 허용 목록에서 이루어집니다. -
봇 응답 마크다운 새로 고침. 새 템플릿에 대한 일회성 디자인 패스를 마이그레이션 시 적용합니다. 이전 봇과 동일해 보일 필요는 없습니다.
-
git mv로 모든 이전 항목을 한 번에praw-python-archive/로 이동. 신속하게 처리. 이전 PRAW 봇은 M1–M5 동안 PythonAnywhere에서 계속 실행되어 새 쪽이 자유롭게 중단될 수 있습니다. M6 전환 시 중지됩니다. -
6개의 마일스톤, 일정 없음. 로컬 우선: M1–M5는 모두 로컬 개발(스캐폴딩, 표준 엔진, API 패리티, Astro, Devvit 플레이테스트)입니다. M6는 클라우드 + 공개 출시(Scaleway 프로비저닝, 프로덕션 데이터 마이그레이션, DNS 전환, App Directory, 이전 봇 폐기)입니다. 외부 마감일 없음(현상금은 2026년 12월). 이전 봇이 전환까지 프로덕션을 담당합니다. Rust + Devvit + Astro 학습이 목표의 일부이므로 페이싱은 일정이 아닌 이해도와 일치해야 합니다.
아키텍처
┌─────────────────────────────────────────────┐
│ 서브레딧 X (모드가 amputatorbot 설치) │
│ 댓글 / 게시물 │
└──────────────┬──────────────────────────────┘
│ Devvit 트리거(내부 경로로 HTTP POST)
▼
┌─────────────────────────────────────────────┐
│ Devvit 앱(Node, @devvit/web) │
│ Reddit 인프라에서 실행 │
│ │
│ 내부 HTTP 경로: │
│ /internal/triggers/comment-submit │
│ /internal/triggers/post-submit │
│ │
│ Devvit Redis(내장): │
│ handled:{commentId} TTL 1h │
│ │
│ 설치별 설정: │
│ autoReply, customFooter, killSwitch │
│ │
│ 허용 목록(devvit.json): │
│ http.domains: ["www.amputatorbot.com"] │
└──────────────┬──────────────────────────────┘
│ HTTPS(인증 없음):
│ GET /api/v1/convert?q=...&gac=true&md=3
▼
┌─────────────────────────────────────────────┐
│ Cloudflare(DNS + 레이트 제한 + WAF) │
└──────────────┬──────────────────────────────┘
▼
┌─────────────────────────────────────────────┐
│ Scaleway(파리/AMS, EU) │
│ www.amputatorbot.com │
│ │
│ Rust 백엔드(Axum 0.8) — 단일 바이너리 │
│ /api/v1/convert (기존 계약) │
│ /api/v2/convert (camelCase JSON) │
│ /api/v2/stats (캐시된 카운트) │
│ /api/v2/health │
│ fallback → Astro 정적(ServeDir) │
│ │
│ 표준 찾기(10개 방법, Python 구현과 │
│ 동일한 순서): │
│ REL, CANURL, OG_URL, │
│ GOOGLE_MANUAL_REDIRECT, GOOGLE_JS_…, │
│ BING_ORIGINAL_URL, SCHEMA_MAINENTITY, │
│ TCO_PAGETITLE, META_REDIRECT, │
│ GUESS_AND_CHECK(문서 유사성 │
│ 신뢰도 via Mozilla Readability), │
│ DATABASE(캐시 조회) │
│ │
│ Postgres 17(Scaleway Managed Database): │
│ links │
└─────────────────────────────────────────────┘
API + 정적 사이트를 제공하는 단일 Rust 서비스의 이유: Devvit의 허용 목록은 도메인 수준입니다. 하나의 Scaleway Serverless Container는 배포 스토리를 단순하게 유지합니다. 폴백으로 tower-http::ServeDir는 약 5줄입니다.
API 계약
두 개의 엔드포인트가 함께 제공됩니다:
GET /api/v1/convert— 레거시 계약 유지(snake_case 쿼리 문자열, snake_case JSON 응답). 레거시 봇이 항상 쿼리 문자열 기반이었으므로 GET 전용. POST는 v2 전용입니다. 새 호출자에게는 더 이상 사용되지 않지만 외부 서비스가 중단되지 않도록 무기한 지원됩니다.POST /api/v2/convert— 현대 계약: JSON 본문은 camelCase, JSON 응답은 camelCase.entryType은 헤더 대신 본문 필드입니다. 엄격한 검증(알 수 없는 필드 → 422). Devvit(M5) + 웹사이트(M4) + 새 외부 호출자에게 권장됩니다.
links 캐시에 대한 모든 쓰기는 api_version(1 또는 2 — 사전 v7 레거시 봇의 CSV 가져온 행은 NULL)을 기록하여 채택률을 쿼리할 수 있도록 합니다: SELECT api_version, COUNT(*) FROM links GROUP BY 1.
v1 — GET /api/v1/convert (AmputatorBotCom/main.py:161+에서)
쿼리 매개변수:
q(필수) — URL 또는 URL을 포함하는 텍스트gac— 추측 및 확인, 기본값truemd— 최대 깊이, 기본값static.MAX_DEPTH(값 포팅)gc— 응답에 회신 마크다운 생성, 기본값falser— 리디렉션 모드(JSON 반환 대신 표준으로 303), 기본값false
URL 인코딩 동작 — 둘 다 작동해야 함
API는 두 가지 호출자를 지원합니다: URL을 퍼센트 인코딩하는 잘 동작하는 클라이언트와 브라우저 주소 표시줄에 원시 URL을 붙여넣는 인간. v7 계약:
- 인코딩된 URL은 항상 작동, 매개변수 목록에서
q의 위치에 관계없이.?q=https%3A%2F%2F...&gac=true및?gac=true&q=https%3A%2F%2F...모두 유효합니다. - 인코딩되지 않은 URL은
q가 마지막 매개변수일 때 작동.?gac=true&q=https://example.eu/article?id=1은 문제 없음 — URL의 꼬리 매개변수(?id=1)가 보존됩니다. 다른 알려진 매개변수 사이에q가 있으면 …
(참고: 원본이 잘린 부분은 그대로 유지되었습니다. 전체 내용은 원본 문서 참조)