매일공부

사내용 이메일 캠페인 관리 툴을 Flask로 직접 만든 이유와 구조 본문

IT/작업기록

사내용 이메일 캠페인 관리 툴을 Flask로 직접 만든 이유와 구조

aram 2026. 8. 31. 12:37

이 도구를 왜 독립 프로젝트로 만들기로 했는지는 이전 글에서 다뤘다. 결론만 요약하면 — 1회성 임시 도구이고, 정식 시스템과의 통합/분리 여부는 나중에 논의할 문제라, 지금은 최소한의 안전장치만 갖추고 그 이상은 확장하지 않는 걸 기준으로 삼았다.

배경

그린텍는 친환경 사업(eco_contacts)과 SW 사업(salesdb.customer)이라는 서로 다른 두 고객 데이터가 있었다. 두 쪽 모두에 이메일 캠페인(뉴스레터/공지)을 보내야 하는데, 기존 사내 프로그램에는 "발송 대상을 그룹으로 묶어서 예약 발송하고, 수신거부까지 자동으로 처리하는" 기능이 없었다. 정식 프로그램에 이 기능을 넣기 전, 급하게 필요한 캠페인 발송을 처리하기 위해 1회성으로 급하게 만든 Flask 관리자 웹앱이 unsub_app이다.

"임시로 빨리 만든다"는 전제였지만, 실제 운영 메일을 수백 건씩 내보내는 도구라 안전장치(테스트 승인, 발송 이력, 실패 원인 추적)는 처음부터 필요했다. 이 시리즈는 그 과정에서 마주친 문제와 해결 방식을 정리한 것이다.

무엇을 하는 앱인가

  • 로그인: admins 테이블 기반 관리자 인증 (bcrypt)
  • 캠페인 관리: 작성(제목/본문 HTML 에디터/예약 발송 시각/대상 그룹/첨부파일) → 테스트 발송 → 승인 → 예약 발송 → 발송 이력 확인
  • 구독자(고객) 관리: 친환경/SW 두 데이터 소스를 하나의 구독자 목록으로 통합, 검색·필터·CRUD
  • 그룹(회사 유형) 관리: 발송 대상 그룹 CRUD
  • SMTP 설정: 발신 서버/계정/공통 서명 관리 (비밀번호는 Fernet 암호화 저장)
  • 수신거부 페이지: 토큰 기반 공개 페이지 (로그인 불필요)

기술 스택

  • Python 3.12 / Flask 3, 별도 프론트엔드 프레임워크 없이 Jinja2 템플릿
  • MySQL(PyMySQL) — 앱 자체 DB(unsub_db) + 외부 참조 스키마(salesdb, 읽기 전용)
  • bcrypt(관리자 비밀번호), cryptography/Fernet(SMTP 비밀번호 암호화)
  • gunicorn + Docker, Synology NAS 배포

디렉터리 구조

app.py                          Flask 라우트 (관리자 페이지 전체)
models.py                       메일 발송/암호화/HTML 인라인 스타일 공용 로직
setup_admin.py                  관리자 계정 생성 CLI
scripts/send_scheduled_campaigns.py   예약 발송 배치 스크립트 (Task Scheduler/cron용)
templates/                      Jinja2 템플릿
migrations/                     수동 적용 SQL 마이그레이션
docs/db.md                      DB 스키마 명세서
docs/deploy.md                  Synology NAS + Docker 배포 가이드

핵심 설계는 "앱의 테스트 발송 버튼예약 발송 배치 스크립트가 완전히 같은 발송 함수(models.py)를 쓰게 만드는 것"이었다. 처음엔 발송 로직이 scripts/send_scheduled_campaigns.py 안에만 있었는데, 이러면 "테스트로 확인한 내용"과 "실제 예약 발송되는 내용"이 코드 경로가 달라서 미묘하게 어긋날 위험이 있다. models.py로 공용화한 뒤로는 두 경로가 함수 단위로 100% 동일해졌다.

이 시리즈의 다른 글

  1. 독립시킬지 통합할지 판단한 과정
  2. (이 글) 프로젝트 개요와 구조
  3. 이메일 캠페인 DB 설계 노트
  4. Flask 첫 프로젝트에서 마주친 삽질들
  5. 캠페인 상태 머신: 테스트 없이 실제 메일이 나가지 않도록 만들기
  6. HTML 이메일 렌더링 트러블슈팅: Quill 에디터, 표 스타일, RFC 5322 버그
  7. 서로 다른 두 고객 데이터소스를 하나의 구독자 목록으로 통합하기
  8. 발송 이력과 장애 격리: "부가 기능의 실패가 핵심 기능을 끌고 내려가지 않게"
  9. Docker + Synology NAS 배포, 그리고 9시간 어긋난 타임존 버그
Comments