본문으로 건너뛰기

© 2026 Molayo

Zenn헤드라인2026. 06. 17. 09:13

WordPress 자동 포스팅 파이프라인에서 이미지가 3장씩 표시된 원인 조사

요약

WordPress 자동 포스팅 파이프라인에서 리뷰용으로 삽입한 이미지 버리에이션이 그대로 게시되는 버그를 조사하고 해결한 과정을 다룹니다. Markdown을 HTML로 변환하는 과정에서 특정 조건의 이미지를 필터링하도록 수정했습니다.

핵심 포인트

  • 리뷰를 위해 삽입한 이미지 버리에이션이 포스팅에 포함되는 문제 발생
  • Markdown을 HTML로 변환하는 스크립트의 정규식 처리 로직 확인
  • alt 텍스트를 기준으로 불필요한 버리에이션 이미지를 스킵하도록 수정

AI로 기사를 자동 생성하여 WordPress에 게시하는 파이프라인을 운용하고 있다. 현직 엔지니어링 매니저(Engineering Manager)로 일하면서, 부업으로 미디어 운영을 하며 이 파이프라인을 개인적으로 돌리고 있다. 어느 날, WordPress의 임시 저장(Draft) 목록을 확인했더니, 섹션마다 이미지가 3장씩 나열되어 있는 기사가 몇 개 있었다. "보기 흉하다"라는 감상밖에 나오지 않는 모습이었다.

이 기사는 그 원인을 조사하고 수정하기까지의 과정을 기록한 것이다. 에러는 전혀 발생하지 않는데 겉모습만 망가지는, 이른바 "실패가 보이지 않는 실패"의 일종으로, 200 응답이 오지만 제대로 동작하지 않는 디버깅 기록과 본질은 같다.

파이프라인 구성

먼저 전제로, 기사 생성 파이프라인의 구성을 정리해 둔다.

/write-article 키워드
↓
Step 1: 경쟁사 리서치
...

Python 스크립트 2개(이미지 생성과 포스팅)가 연동되어 있다. 이미지 생성의 내용(무료 범위 내에서의 이미지 생성)은 ComfyUI와 Imagen 4로 이미지를 무료 생성하는 기사에, 초안(Draft) 생성의 병렬화는 LLM으로 섹션을 병렬 생성하는 기사에 나누어 작성했다.

증상 확인

WordPress의 임시 저장을 보면 다음과 같은 상태였다.

<!-- 섹션 헤딩 직후 -->
<figure class="wp-block-image size-large">
<img src="...variation1.jpg" alt="...(버리에이션 1)" />
...

1개 섹션에 3장의 이미지가 세로로 나열되어 있다. 아이캐치(Eyecatch) 이미지도 기사 서두에 3장이 나열되어 있다. 전체적으로 18~24장의 이미지가 표시되는 상태였다.

원인 추적

call_comfyui.py 읽기

이미지 생성 스크립트의 insert_images_into_markdown 함수를 읽었다.

def insert_images_into_markdown(md_content: str, images: list[dict]) -> str:
"""frontmatter 직후에 아이캐치 전 버리에이션을,
H2 직후에 섹션 이미지 전 버리에이션을 삽입한다."""
...

docstring에 "전 버리에이션을 삽입한다"라고 적혀 있다. 이는 의도적인 설계였다.

왜 전 버리에이션을 넣는가

리뷰 플로우(Review flow)를 위해 전 버리에이션을 Markdown에 심어두고 있다. Step 6(generate_review.py)에서 브라우저 확인용 HTML을 생성할 때, 3장을 나열하여 "어느 것이 가장 좋은가"를 사람이 판단할 수 있도록 하려는 의도가 있었다.

합리적인 설계다. 하지만 문제는 다음 단계에 있었다.

wp_post.py 읽기

WordPress 포스팅 스크립트의 Markdown → HTML 변환 부분을 읽어보니, 이미지 행을 그대로 전부 <figure>로 변환하고 있었다.

# 이미지
elif re.match(r"^!\\[.*?\\\]\(.*?\)", line.strip()):
    flush_list()
...

alt에 "버리에이션 2", "버리에이션 3"이 포함되어 있는지 확인하지 않고 전부 변환하고 있었다. 리뷰용으로 심어둔 전 버리에이션이 그대로 WordPress에 포스팅되고 있었던 것이다.

수정

수정은 한 곳이면 충분했다. 이미지 변환 직전에 버리에이션 2·3을 스킵하는 조건을 추가했다.

# 이미지
elif re.match(r"^!\\[.*?\\\]\(.*?\)", line.strip()):
    flush_list()
...

alt에 "버리에이션 2" 이상이 포함된 행을 continue로 스킵한다. 이로써 WordPress로의 포스팅은 항상 1섹션 1장이 된다.

또 다른 문제: HTML이 코드 블록에 싸임

같은 조사 과정에서 다른 문제도 발견되었다. 일부 기사에서 섹션 본문 전체가 <pre><code> 태그로 싸여, HTML 소스 코드가 그대로 표시되는 상태가 되어 있었다. SVG 도표나 H2 헤딩, 단락 텍스트가 HTML 엔티티(<svg> 등)인 채로 화면에 나타나고 있었다.

원인 추적

프롬프트(prompt-draft.md

)에 「인라인 SVG 도해 규칙」이 있으며, 섹션에 SVG로 시각적인 도해를 넣도록 지시하고 있다. Claude가 SVG+HTML을 생성할 때, Markdown 관습에 따라 **html 코드 펜스 (html code fence)**로 전체 섹션 내용을 하나로 묶어 출력하는 경우가 있다.

wp_post.py는 코드 펜스를 <pre><code>로 변환하도록 설계되어 있었다. 코드로 HTML을 소개하는 기사에서는 올바른 동작이지만, 섹션 본문으로 작성된 HTML이 실수로 코드 블록이 되어버린 것이다.

어디서 수정할 것인가: 코드 측면 vs 프롬프트 측면

수정 방법은 두 가지를 생각할 수 있었다.

첫 번째는 wp_post.py 측에서
```html 펜스를 생(raw) HTML로 전개하는
안이다. html 펜스를 감지하면 <pre><code>로 만들지 않고, 내용물을 그대로 출력한다.

# wp_post.py 측에서 수정한다면 (이번에는 채택하지 않음)
if lang == "html":
    in_html_block = True # 코드로 만들지 않고 생 HTML로 전개한다
else:
    parts.append(f'<pre class="wp-block-code"><code class="language-{lang}">')
    in_code = True

하지만 이것은 채택하지 않았다. 이유는 단순하다. HTML 소스 코드를 '코드로 보여주고 싶은' 기사——바로 이 기사와 같이

````를 멋대로 전개하면 오히려 곤란하기 때문이다. 코드 측에서 자동으로 전개하면 HTML을 해설하는 기사들이 줄줄이 망가진다. "펜스 안의 내용이 샘플 코드인지, 렌더링되어야 할 본문인지"는 펜스의 언어 지정만으로는 구분할 수 없다.

즉, 이것은 변환기의 버그가 아니라, **입력(Markdown) 측에서 `<svg>`를 
` ```html `로 감싸버린 것이 원인**이다. 따라서 수정해야 할 것은 입력을 만드는 LLM의 프롬프트였다.

### 근본 대책: 프롬프트에 명시적 지시 추가

...
```markdown
## SVG·HTML 취급
- SVG나 HTML을 본문 중에 직접 삽입하는 경우, 
` ```html ` 코드 블록으로 감싸지 않는다.
- 인라인 SVG는 Markdown 본문에 그대로 (생 `<svg>...</svg>` 형태로) 작성한다.
- `<svg>` 태그를 
` ```html ` 로 감싸면 WordPress에서 소스 코드로 표시되므로 절대로 하지 않는다.
```

## 기존의 문제 기사를 수정하기

이미 게시된 초안에도 동일한 문제가 있었다. WordPress REST API를 사용하여 일괄 수정하는 스크립트를 작성했다.

```typescript
// 변형 2·3의 <figure>를 삭제하는 함수
function removeVariantImages(html: string): string {
  return html.replace(
    ...
```html 코드 블록을 전개하는 함수
```typescript
function expandHtmlCodeBlocks(raw: string): string {
  return raw.replace(
    /<pre><code class="language-html">([\s\S]*?)<\/code><\/pre>\n?/g,
    (_, inner: string) => {
      return inner
        .replace(/</g, "<")
        .replace(/>/g, ">")
        .replace(/&/g, "&")
        // ...기타 엔티티 디코딩
        .trim() + "\n\n";
    }
  );
}
```

WordPress REST API의 `POST /wp/v2/posts/:id` 
...
````html
````로 감싸면서 의도와 반대로 동작하게 되었다.

파이프라인이 여러 스크립트와 LLM을 조합할수록, "여기는 이런 의도이다"라는 문맥이 전달되지 않는 부분이 생긴다. 각각의 스크립트가 상대방의 출력 포맷을 알고 있어야 한다.

## 요약

| 문제 | 원인 | 수정 사항 |
|---|---|---|
| 이미지가 3장씩 표시됨 | wp_post.py 가 변형 (variation)을 구분하지 않고 전부 변환함 | alt 텍스트에서 변형 2·3을 스킵 |
| HTML 이 코드 블록으로 표시됨 | Claude 가 SVG 를 ```html 로 감쌈 | 프롬프트 측에서 SVG 를 ```html 로 감싸지 않도록 명시 (코드 측의 자동 전개 방식은 채택하지 않음) |

콘텐츠 자동화 파이프라인은 "작동하고 있는" 상태에서 "깔끔하게 작동하고 있는" 상태로 만들기까지의 디버깅이 의외로 심오하다. 각 스크립트가 서로의 출력을 올바르게 해석하고 있는지를 정기적으로 체크하는 메커니즘을 넣어두는 것이 장기 운용의 요령이다.

### Discussion

![](https://static.zenn.studio/images/drawing/discussion.png)

AI 자동 생성 콘텐츠

본 콘텐츠는 Zenn AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.

원문 바로가기
0

댓글

0