블로그 글 발행 직전 자동 QC — 6개월간 잘못된 발행 0건의 비밀

3 min read · 749 words
활용 팁 / 블로그 운영 / Python · 자동화
약 2,300자

블로그를 200편 넘게 운영하다 보면 사람이 직접 글을 검수해도 반드시 빠뜨리는 게 생깁니다. 마크다운 잔재(굵게가 그대로 노출), 이모지 화이트리스트 위반, 출처 누락, 빈 표, 박스 스타일 잔재 같은 것들이죠. 그래서 우리는 글이 블로그 API 로 넘어가기 직전에 자동으로 점검하고 고치는 단계를 따로 만들었습니다.

이 글은 그 자동 QC 시스템을 어떤 의도로 만들었고, 어떻게 작동하고, 실제 어떤 효과를 봤고, 어떻게 검증했는지까지 풀어 둡니다. 같은 고민을 하는 운영자가 코드 한 페이지로 따라 만들 수 있도록 핵심만 추렸습니다.

만든 이유

처음 1년 동안 우리는 두 종류의 사고를 자주 봤습니다.

첫째는 모델 출력 잔재. LLM 으로 본문을 뽑으면 굵게, ## 소제목, --- 같은 마크다운 토큰이 종종 HTML 으로 변환 안 된 채 남았습니다. 라이브에서 별표가 그대로 보였습니다.

둘째는 작성 직후엔 멀쩡한데 발행 직전 어딘가의 hook 이 망가뜨리는 케이스. 어떤 함수가 본문에

를 하나 더 열고 닫지 못해서 카드/사이드바 레이아웃이 무너지거나, 가격표 자동 삽입이 빈

작동 원리

검문소는 두 단계로 구성합니다.

1단계 sanitize — 무조건 고치는 작업

HTML 을 받아서 다음을 일률 적용합니다.

  • 위험한 inline 스타일 제거 (width:800px, margin-left:-30px, position:absolute 등)
  • 블로그 글 발행 직전 자동 QC — 6개월간 잘못된 발행 0건의 비밀 태그의 고정 width/height 속성 제거 → 반응형 보존
  • 마크다운 잔재 토큰을 HTML 로 변환 (XX, 임의 ---
    )
  • 이모지 정책 위반 문자 strip (U+2600–27BF, U+1F000–1FAFF 범위)
  • 박스 스타일 (border, box-shadow, padding>20px
    ) 평탄화
  • 본문 컨테이너에 안전 CSS 한 줄 강제 삽입 (max-width:100%, overflow-wrap:anywhere)

이 단계는 사람의 판단이 필요 없는 기계적 작업입니다. 어떤 글이든 같은 결과가 나오게 만들었습니다.

2단계 quality gate — 통과 실패 시 발행 차단

사람이 보면 알아챘을 누락을 자동으로 점검합니다. 통과 못 하면 발행 자체가 거부됩니다.

  • 본문 글자 수 600자 미만 → fail
  • 가 3개 미만 → fail (가이드/비교 글)

  • 이미지 0장 → fail (글 유형 무관)
  • 실제 효과

    도입 후 6개월간의 결과:

    • 마크다운 잔재 노출 사고: 도입 전 평균 월 4건 → 도입 후 0건
    • 좌우 짤림: 평균 월 7건 → 0건
    • 빈 표 / 빈 차트 본문: 평균 월 3건 → 0건
    • 발행 차단된 글: 누적 38편 (모두 작성자가 수정 후 재발행)

    차단된 38편은 글 손실이 아닙니다. 단지 작성자가 미리 인지하고 다듬은 후 다시 시도해서 모두 정상 발행됐습니다. 차단된 사유 분포는 출처 누락 41% / 글자 부족 26% / 이미지 0장 21% / 기타 12% 였습니다.

    검증 방법

    검문소를 만들고 나서 우리가 한 검증입니다.

    골든 셋 회귀 테스트 — 과거에 사고가 났던 글 41편의 원본을 모아 골든 셋으로 만들었습니다. sanitize + quality gate 를 통과시켰을 때 41편 모두 사고 패턴이 사라지는지 자동 비교했습니다. 39/41 통과. 실패한 2편을 보고 정규식을 보강해 41/41 통과까지 끌어올렸습니다.

    라이브 spot-check — 새 sanitize 를 적용한 첫 주에 발행된 18편 중 8편을 무작위로 골라 라이브 페이지를 직접 fetch 했습니다. desktop 1280px, mobile 360px 두 너비에서 가로 스크롤이 발생하는지, 글자가 컨테이너 밖으로 나가는지, 이미지가 깨지는지 점검했습니다. 8/8 정상.

    더블 패스 idempotency — sanitize 를 한 번 통과한 결과물에 sanitize 를 한 번 더 통과시켰을 때 출력이 동일한지 확인했습니다. 이건 publish hook chain 이 두 번 도는 케이스를 막기 위한 검증입니다. 100/100 동일.

    따라 만드는 법

    전체 코드를 베끼는 것보다 핵심 한두 가지만 본인 환경에 맞게 가져가시면 됩니다.

    
    import re
    
    def sanitize_pre_publish(html: str) -> tuple[str, list[str]]:
     fixes = []
     # 위험 inline width 제거
     html, n = re.subn(r'width\s*:\s*(?:[4-9]\d{2}|[1-9]\d{3,})px\s*;?', '', html)
     if n: fixes.append('strip_wide_width')
     # 마크다운 잔재 → HTML
     html, n = re.subn(r'\*\*(.+?)\*\*', r'<strong>\1</strong>', html)
     if n: fixes.append('md_bold')
     # 이모지 strip (필요시)
     html, n = re.subn(r'[\U0001F300-\U0001FAFF]', '', html)
     if n: fixes.append('strip_emoji')
     return html, fixes
    
    def quality_gate(html: str, post_type: str) -> tuple[bool, list[str]]:
     fails = []
     text = re.sub(r'<[^>]+>', '', html)
     if len(text.replace(' ', '')) < 600: fails.append('too_short')
     if html.count('<h2') < 3 and post_type in ('howto', 'compare'): fails.append('few_h2')
     if '<img' not in html: fails.append('no_image')
     if 'TODO' in html or 'REDACTED' in html: fails.append('placeholder')
     return (len(fails) == 0), fails
    

    이 두 함수를 발행 직전 단 한 군데에서 호출하면 됩니다. quality_gate 가 fail 을 반환하면 발행을 막고 사유를 사람에게 돌려주세요. sanitize 는 출력 HTML 을 받아서 그대로 발행 API 에 넘기면 됩니다.

    요약하면, "발행 전 검문소 한 곳에서 모든 사고를 자동으로 막는다" 한 줄입니다. 사람이 매번 검수하는 데 쓰던 시간이 통째로 사라집니다.

    Category Coverage Notice

    This article follows our label-specific editorial criteria. Details:

ToolSignal Pro Editorial

ToolSignal Pro editor. We compare AI tools, CRM, automation, and SaaS for small business buyers — no fluff, just the decision drivers that matter.

이전 글 다음 글