Fullstack 74

heo4·2026년 7월 30일

Fullstack

목록 보기
29/71

풀스택

능력단위 평가 제출

AWS 라이트 세일로 운영중인 포트폴리오 제출





FastAPI에서 시작해서 결국 Hono로 — EasyPrompt 백엔드를 하루 만에 갈아엎은 이야기

오늘 하루 EasyPrompt 프로젝트에서 있었던 일을 정리해본다. 아침엔 그냥 Cloudflare D1
데이터베이스 하나 만드는 걸로 시작했는데, 저녁이 되니 백엔드 프레임워크 자체가 통째로
바뀌어 있었다. 그 과정을 순서대로 남겨둔다.

1. 시작은 D1 데이터베이스

기존 백엔드는 FastAPI + SQLAlchemy 조합으로 만들어서 Render에 배포해뒀었다. 여기에
Cloudflare D1을 붙이기로 하고, wrangler d1 create로 데이터베이스를 만들고 기존
SQLAlchemy 모델을 기준으로 schema.sql을 뽑아 원격 D1에 적용했다. 여기까진 순조로웠다.

2. "Python도 Cloudflare Workers에서 돌아간다던데?"

FastAPI를 그대로 살리면서 Cloudflare Workers 위에 올릴 수 있다길래 시도해봤다.
workers-py라는 패키지가 pyproject.toml 기반으로 의존성을 vendoring해주고,
WorkerEntrypoint + asgi.fetch로 FastAPI 앱을 감싸는 구조였다.

이 과정에서 삽질을 꽤 했다.

  • PyPI의 "pywrangler"라는 이름의 패키지는 Cloudflare와 전혀 무관한 데이터 분석 툴이었다.
    진짜는 workers-py 패키지가 제공하는 pywrangler CLI였다.
  • WorkerEntrypoint의 fetch 메서드는 fetch가 아니라 on_fetch여야 했다.
  • Pyodide 기반 샌드박스는 멀티스레딩이 안 돼서, FastAPI 라우트가 동기(sync) 함수면
    run_in_threadpool이 스레드를 만들려다 그대로 죽어버렸다. 전부 async def로 바꿔야 했다.
  • passlib이 import되는 순간, 내부적으로 stdlib crypt 모듈이 난수(entropy)를 요청하는데
    Workers는 "요청 컨텍스트 밖"에서 엔트로피 접근 자체를 차단한다. import만 해도 죽는 라이브러리였던
    셈이다. 결국 bcrypt 패키지를 직접 쓰는 걸로 우회했다.

이렇게 라우터를 전부 D1 기반으로 옮기고 로컬에서는 멀쩡히 돌았다. 문제는 실배포였다.

3. 배포하자마자 벽에 부딪히다

wrangler deploy를 돌리자마자 이런 에러가 떴다.

Your Worker failed validation because it exceeded startup limits.
Uncaught TypeError: Python Worker startup exceeded CPU limit 2119<=1000 with snapshot baseline

Cloudflare Workers는 "글로벌 스코프"(모듈 로드 시점)에서 실행되는 코드에 고정된 CPU
예산(1초)을 둔다. FastAPI가 라우터 7개, 엔드포인트 약 19개를 등록할 때마다 Pydantic
검증 모델을 미리 컴파일하는데, 이 비용이 Pyodide 위에서는 예산의 두 배가 넘게 나왔다.

앱 생성을 첫 요청 시점으로 미루는 지연 초기화로 배포 자체는 통과시켰다(Startup 758ms).
그런데 이번엔 똑같은 비용이 "요청 처리 중" CPU 제한에 걸려서, 모든 요청이 503
(Cloudflare error 1102)으로 죽었다. 문제를 옮겼을 뿐 해결한 게 아니었다.

결론: Pyodide 기반 Python Workers + FastAPI(Pydantic v2) 조합은 라우트가 많은 앱에는
실질적으로 안 맞는다. 여기서 백엔드를 통째로 Hono(TypeScript)로 새로 만들기로 했다.

4. Hono로 갈아엎기

npm create hono@latest로 새 프로젝트(backend-hono)를 만들고, 기존 D1 데이터베이스를
그대로 재사용하면서 라우터를 하나씩 이식했다.

  • 인증: hono/jwt + bcryptjs. JWT는 jsonwebtoken 대신 hono 내장 모듈을 썼다 —
    Node crypto 없이 Web Crypto API 기반이라 Workers 런타임에 훨씬 잘 맞는다.
    (참고로 hono/jwt의 verify()는 sign()과 달리 알고리즘 기본값이 없어서, 생략하면
    유효한 토큰도 무조건 401이 나는 버그를 하나 잡았다.)
  • 요청 검증: zod + @hono/zod-validator.
  • DB 접근: c.env.DB.prepare(sql).bind(...).first() / .all() / .run() 패턴으로 통일.
  • categories, prompts, prompt_forms, favorites, generate, admin/users까지 전부 이식.

다 옮긴 뒤 wrangler deploy를 돌렸더니:

Worker Startup Time: 16 ms

Python 버전이 2119ms에 배포가 거부됐던 것과 비교하면 압도적인 차이다. 무거운
프레임워크 하나 잘못 고르면 플랫폼 자체가 안 맞을 수 있다는 걸 몸으로 배웠다.

5. 배포하고 나서 겪은 자잘한 사고들

Cloudflare Pages 대시보드가 .env.local을 씹어먹는 문제
프론트엔드 .env.local의 VITE_API_BASE_URL을 새 백엔드 주소로 바꿔서 커밋·배포했는데,
빌드된 JS 번들을 열어보니 여전히 예전 Render 주소가 박혀 있었다. 원인은 Cloudflare
Pages 프로젝트 설정에 대시보드로 직접 지정한 환경변수가 따로 있었던 것 — Vite는 이미
존재하는 process.env 값을 .env 파일보다 우선시하기 때문에, 저장소를 아무리 고쳐도
소용이 없었다. wrangler CLI엔 이 값을 조회하는 명령이 없어서, wrangler가 쓰는 OAuth
토큰으로 Cloudflare API를 직접 호출해 원인을 찾고 고쳤다.

로그인 실패 메시지가 계정 존재 여부를 흘리던 문제
"이메일 또는 비밀번호가 올바르지 않습니다"로 메시지는 통일해뒀는데, 계정이 없으면
bcrypt.compare 자체를 건너뛰어서 응답 속도로 계정 존재 여부가 새는 타이밍
사이드채널이 있었다. 계정이 없을 때도 더미 해시로 비교를 한 번 수행하도록 고쳐서
응답 시간을 맞췄다.

프론트엔드 에러 메시지가 화면에 아예 안 뜨던 문제
Login.jsx, Signup.jsx 둘 다 err.response?.data?.detail을 읽고 있었는데, 새
백엔드는 { error: "..." } 형태로 응답한다(옛 FastAPI 시절 관례가 그대로 남아있었음).
백엔드가 친절한 에러 메시지를 내려줘도 프론트가 못 읽고 있었던 셈. .error로 고치고,
회원가입 쪽은 zod 필드별 에러까지 화면에 나눠서 보여주도록 개선했다.

"프롬프트 만들기" 버튼이 한 번도 성공한 적이 없었던 문제
프론트 전체를 훑으며 버튼/링크가 실제로 연결돼 있는지 점검하다가 발견했다. 프론트는
form_values를 { 라벨: 값 } 객체로 보내는데, Hono로 옮기면서 백엔드는 순서 배열을
기대하도록 바뀌어 있었다. 입력을 아무리 잘 채워도 매번 400이 나던 버그였다 — curl로
프론트가 실제로 보내는 payload를 재현해서 확인한 다음, forms.map((f) => formValues[f.label])로 순서 배열로 변환하도록 고쳤다.

6. 콘텐츠도 늘리고, 겉모습도 다듬고

D1에 있던 프롬프트가 카테고리당 딱 2개뿐이라 카테고리당 8개씩, 총 32개를 더 채웠다.
"이미 표준 양식이 있는 문서"가 아니라 "회사마다 제각각이라 매번 처음부터 만드는
상황"(신규 입사자 온보딩, 예산 초과 승인 요청, 협력사 첫 미팅 사전 협의, 프로젝트
회고 등)에 초점을 맞췄다. 제목 중복이나 입력 폼 개수 불일치 같은 실수를 막으려고,
SQL을 손으로 짜지 않고 파이썬 스크립트로 검증(assert)까지 거쳐서 생성했다.

마지막으로 파비콘(SVG → ico/png 여러 사이즈)과 메타 태그(title, description)를
채워 넣고, 가이드 페이지에 "역할을 지정하면 왜 도움이 될까요?" 섹션을 하나 추가하며
하루를 마무리했다.

오늘 배운 것

  • 플랫폼의 실행 모델을 먼저 이해해야 한다. Cloudflare Workers의 "글로벌 스코프
    CPU 예산"처럼 프레임워크 문서만 봐서는 안 보이는 제약이 있다. 가벼운 프레임워크가
    괜히 있는 게 아니었다.
  • 에러가 나면 일단 재현해라. 프론트-백엔드 계약 불일치, Pages 환경변수 문제 모두
    "될 것 같은데 왜 안 되지"에서 멈추지 않고, 실제 요청/응답을 그대로 재현해보니
    원인이 바로 보였다.
  • 보안은 "그럴듯한 메시지"만으로 끝나지 않는다. 에러 메시지를 통일해도 응답
    시간이라는 다른 채널로 정보가 샐 수 있다는 걸 이번에 다시 확인했다.
  • 마이그레이션 중간에 죽은 코드가 남기 쉽다. 프론트가 옛 API 계약(detail 키,
    객체 형태 form_values)을 그대로 들고 있던 것처럼, 한쪽만 바꾸고 반대쪽을
    놓치면 조용히 깨진 채로 방치되기 쉽다. 끝나고 나서 꼭 전체를 한 번 훑어봐야 한다.

전체 변경 이력은 프로젝트의 WORKLOG.md에 시간순으로 더 자세히 남아있다.

0개의 댓글