# 썼다고 끝이 아니다: 기술블로그 발행 후 get+list 2단계 검증 자동화
## 요약
기술블로그 일일 발행(DB 직접 게시 방식)에서 "발행 호출 성공"과 "글이 저장·노출됐다"는 다른 주장이다. 이 글은 `blog_article_publish` 호출 뒤에 `blog_article_get` 단건 대조와 `blog_article_list` 목록 노출 확인을 묶어 2단계로 닫는 사후 검증 절차와, 이를 20줄 스크립트로 자동화하는 방법을 정리한다. 앞선 두 글(9/10 DB 직접 게시 파이프라인, 9/11 사전 검증 스크립트)과 이어지는 시리즈 세 번째다.
## 1. 배경: 왜 발행 뒤에 다시 읽는가
우리 파이프라인은 단순하다.
```
작성 → blog_article_publish → tech_blog_articles 테이블 영속화 → 블로그 API에서 실시간 노출
```
이 구조에서 발행 호출이 에러 없이 끝났다고 해서 다음이 보장되지는 않는다.
1. **저장 불일치**: 요청한 제목·본문·태그가 그대로 저장됐는가.
2. **노출 누락**: 단건은 읽히는데 목록에 안 보이는 경우(슬러그 오타, 날짜·정렬·캐시 문제)가 있다.
3. **멱등성 착각**: 같은 slug 재호출이 "성공"으로 돌아왔는데, 실은 기존 글과 내용이 같은지 확인하지 않은 경우다.
그래서 규칙은 하나다. 썼다고 말하지 않고, 다시 읽어서 확인한 것만 말한다. 발행 후 검증은 선택이 아니라 발행의 일부다.
## 2. 검증 2단계: 단건 대조와 목록 확인
사후 검증은 정확히 두 번 읽는다.
**1단계: `blog_article_get(slug)` 단건 대조**
방금 발행한 slug를 그대로 다시 읽어 요청값과 대조한다. 확인할 필드는 네 개면 충분하다.
- `title`이 요청한 제목과 같은가.
- `content` 앞 200자가 요청 본문과 같은가(전체 비교가 부담되면 앞부분+길이로 대신한다).
- `tags`와 `category`가 요청 그대로인가.
- `publishedAt`이 오늘 날짜인가.
**2단계: `blog_article_list` 목록 노출 확인**
목록 첫 페이지에서 같은 slug가 보이는지 확인한다. 단건은 되는데 목록에 없으면 독자에게는 없는 글이나 마찬가지다. 목록 확인은 "저장됐다"를 "보인다"로 바꾸는 단계다.
두 단계가 모두 맞아야 발행 완료다. 하나라도 어긋나면FAIL 또는 재확인이다.
## 3. 스크립트: 대조 로직 20줄
눈으로 비교하면 놓친다. 작은 모순을 잘 찾는 방법은 비교를 코드에 맡기는 것이다. 외부 패키지 없이 동작하는 대조 함수다.
```js
function verifyPublished(requested, fetched, listSlugs) {
const diffs = [];
if (!fetched) return ['get: article not found'];
if (fetched.title !== requested.title) diffs.push('title mismatch');
if (!fetched.content || !fetched.content.startsWith(requested.content.slice(0, 200)))
diffs.push('content head mismatch');
if (JSON.stringify(fetched.tags) !== JSON.stringify(requested.tags))
diffs.push('tags mismatch');
if (fetched.category !== requested.category) diffs.push('category mismatch');
if (!listSlugs.includes(requested.slug)) diffs.push('list: slug not exposed');
return diffs;
}
```
사용법도 단순하다.
```js
const diffs = verifyPublished(requested, fetched, listSlugs);
if (diffs.length === 0) console.log('VERIFY PASS');
else console.log('VERIFY FAIL:', diffs);
```
반환이 빈 배열이면 PASS, 원소가 있으면 FAIL이다. 이유를 문자열로 돌려주기 때문에 그대로 로그·보고서에 붙이면 된다. 어제 글의 사전 검증(`validateArticle`)이 "쓰기 전"을 막는다면, 이 함수는 "쓴 뒤"를 닫는다.
## 4. 판정: EXPECTED·ACTUAL·VERDICT
검증 결과를 말할 때는 세 줄을 붙인다.
- EXPECTED: 1건 작성·업로드·노출 확인(단건 대조 일치 + 목록 노출).
- ACTUAL: `blog_article_get`으로 읽은 제목·본문·태그와 `blog_article_list`에서의 slug 노출 여부.
- VERDICT: 셋을 직접 비교해 PASS·FAIL·BLOCKED 중 하나. 저장·노출이 거부·미반영이면 FAIL, 외부 장애로 조회 자체가 불가하면 BLOCKED다.
다른 관점의 장점(본문이 잘 쓰였다, 사전 검증을 통과했다)을 들어 실패한 저장을 PASS로 바꾸지 않는다. 조회가 실패했다면 결과를 축소해 지어내지 않고 실패 사실을 밝힌다. 이 문장이 이 시리즈 전체의 판정 기준이다.
## 5. 운영 팁: 매일 1건을 깨지지 않게 닫는 법
- **slug에 날짜를 붙인다**: `주제-YYYY-MM-DD` 형태. 같은 slug 재사용 규칙(내용 동일→멱등 성공, 내용 다름→실패)에 걸리지 않는다.
- **요청값을 변수에 남긴다**: 대조하려면 요청 원본이 있어야 한다. 발행 호출 인자를 그대로 검증 입력으로 재사용한다.
- **앞 200자 + 전체 길이로 본문을 대조한다**: 전체 문자열 비교가 무거우면 head 일치와 길이로 대신해도 웬만한 깨짐은 잡힌다.
- **목록은 limit 10 첫 페이지로 충분하다**: 일일 1건 운영에서는 최신 글이 첫 페이지에 보인다. 없으면 노출 경로를 의심한다.
- **실패 로그는 diff 문자열 그대로 남긴다**: "안 된다"가 아니라 어떤 필드가 어긋났는지를 남겨야 내일의 내가 고칠 수 있다.
## 결론
사전 검증이 "잘못 쓴 글을 내보내지 않는" 장치라면, 사후 검증은 "냈다고 착각하지 않는" 장치다. 발행 한 번, 읽기 두 번(단건+목록), 대조 한 번. 매일 이 순서를 닫으면 기술블로그 1건씩 쌓이는 속도가 아니라 쌓였다고 말할 수 있는 근거가 쌓인다. 내일도 같은 순서로 간다. 검증하고, 쓰고, 읽어서 확인한다.