본문으로 건너뛰기

© 2026 Molayo

Dev.to헤드라인2026. 05. 22. 03:44

14글자의 정규 표현식(Regex)이 어떻게 내 AI 콘텐츠 파이프라인의 모든 비교 표를 망가뜨렸나

요약

AI 콘텐츠 파이프라인의 '어조 QA' 단계에서 사용된 정규 표현식이 GFM 마크다운 표의 구분선을 파괴한 사례를 다룹니다. 이중 하이픈(--)을 쉼표로 바꾸는 규칙이 마크다운 표의 하이픈 연속체를 잘못 매칭하여 발생한 버그와 그 원인을 분석합니다.

핵심 포인트

  • 정규 표현식의 탐욕적 매칭이 마크다운 표 형식을 파괴함
  • 이중 하이픈(--)을 쉼표로 치환하는 규칙이 버그의 원인
  • LLM 파이프라인 내 결정론적 스크러버의 잠재적 위험성
  • 자동화된 콘텐츠 생성 시 정규식 설계의 중요성

저는 한 달에 30개의 SEO 기사를 발행하는 AI 콘텐츠 파이프라인을 운영하고 있습니다. 이 파이프라인은 개요 작성(outline), 작성(write), 사실 확인(fact-check), 어조 QA(tone-QA), 내부 링크 재구성(internal-link rewire), 검증 및 복구(validate-and-repair)의 6단계로 구성됩니다. 발행되는 모든 기사에는 스키마(schema), FAQ, 인용(citations) 등이 포함됩니다. 지난주에 저는 14개의 기사를 한 번에 배포했습니다. 그중 두 개에는 GFM 마크다운 표(| 도구 | 가격 | 장단점 | 형식)가 포함되어 있었습니다. 기사가 실제 사이트에 게시되었을 때, 표는 시각적 잔해처럼 렌더링되었습니다: | Perplexity | 실시간 조사 | 무료 + Pro | |, - |, - |, - | | ChatGPT | 장문 합성 | 무료 + Plus | 두 번째 행을 보십시오. 원래는 GFM 구분선인 | --- | --- | --- | 이어져야 합니다. 하지만 대신 |, - |, - |, - | 로 나타납니다. 이 버그는 파이프라인이 가동된 이후 모든 비교 표 기사를 조용히 망가뜨리고 있었습니다. 대부분의 기사에는 표가 포함되지 않았기 때문에 저는 미처 알아차리지 못했습니다. 해결책은 단 한 줄입니다. 하지만 교훈은 더 큽니다.

실제로 일어난 일
제 파이프라인에는 "어조 QA (tone-QA)"라고 불리는 단계가 있습니다. 이 단계는 LLM 재작성(rewrite) 단계 이전에 초안에 대해 결정론적 정규 표현식 스크러버(deterministic regex scrubber)를 실행합니다. 이 스크러버는 전형적인 AI 말투를 제거합니다: 쉼표 대신 사용된 em-dash(—), "in today's landscape(오늘날의 환경에서)", "leverage(활용하다)", "robust(강력한)", "delve into(심층적으로 탐구하다)" 등입니다. em-dash 규칙은 다음과 같습니다: { pattern : / [ \t] * [ –— ][ \t] */g , replacement : ' , ' , flag : ' em/en-dash → comma ' }. 이 규칙은 문제가 없습니다. 실제 em-dash( — , U+2014) 또는 en-dash( – , U+2013)만 일치시키기 때문입니다. 버그는 다음 규칙에 있습니다: { pattern : / [ \t] *-- [ \t] /g , replacement : ' , ' , flag : ' double-hyphen → comma ' }. 이중 하이픈(--)은 ASCII에서 em-dash를 대신해 흔히 사용됩니다. 이 정규 표현식은 양옆에 선택적 공백이 있는 --를 찾아 , 로 교체합니다. 이제 GFM 표의 구분선 행을 생각해 보십시오: | --- | --- | --- | 여기서 각 ---는 세 개의 하이픈입니다. 정규 표현식은 -- 뒤에 또 다른 -가 오는지 확인하지 않습니다. 그저 탐욕적(greedily)으로 처음 두 개를 일치시킵니다. | --- | 에서 이를 추적해 보겠습니다: 위치 0: | — 일치하지 않음 (정규 표현식은 어딘가에 --가 필요함). 위치 1: 공백 — [ \t] 가 이를 소비함 (1자). 그 다음 -- 가 위치 2-3을 일치시킴.

그다음 [ \t]* 가 위치 4를 시도합니다 — 그곳은 공백이 아니라 - 이므로, 0개의 문자와 일치합니다. 전체 일치 범위(match span): 위치 1-3, 즉 " --" 입니다. 이를 ", " 로 교체합니다. 결과: |, - | . --- 에서 뒤에 남은 대시 하나가 유지됩니다. 앞의 대시 두 개는 먹혀버립니다. 한 행에 걸쳐 5개의 구분자 셀이 있다고 가정하면, 표 구분자는 다음과 같이 변합니다: |, - |, - |, - |, - |, - | Markdown 파서들은 이를 구분자로 인식하지 못합니다. 표 전체가 깨진 문단 텍스트로 렌더링됩니다.

내가 (결국) 이를 찾아낸 방법
이전 세션에서 파이프라인에 추가해 두었던 수동 LLM 테스트 모드를 통해 이를 발견했습니다. 아이디어는 이렇습니다: 실제로 LLM API를 호출하지 않고, 대신 각 단계의 프롬프트(prompt)를 JSON 파일에 작성한 뒤, 응답을 직접 작성하여 캐시(cache)에 넣고 다시 실행하는 것입니다. 이를 통해 토큰을 낭비하지 않고 전체 파이프라인을 디버깅할 수 있습니다. 기사 하나를 이 수동 모드로 실행했을 때, 톤-QA (tone-QA) 단계의 INPUT에는 깨끗한 표 구분자( | --- | --- | )가 있었지만, OUTPUT에는 손상된 구분자( |, - |, - | )가 있는 것을 확인했습니다. 입력과 출력 사이에는 단 한 가지 요소만 존재했습니다: 결정론적인 정규 표현식 스크러버 (regex scrubber). LLM은 그 이후에 호출되지만, 손상은 이미 발생한 상태였습니다. 캐시 디렉토리 내의 두 캐시 파일 간의 차이(diff)를 확인했습니다. 단 5초 만에 해당 라인을 찾아냈습니다.

해결책
부정 후방 탐색 (Negative lookbehind) + 부정 전방 탐색 (lookahead). --- 의 일부인 경우 -- 를 일치시키지 마십시오.

// 기존 { pattern : / [ \t] *-- [ \t] */g , replacement : ' , ' , flag : ' double-hyphen → comma ' }
// 수정 후 { pattern : / (?<!-)[ \t] *-- (?!-)[ \t] */g , replacement : ' , ' , flag : ' double-hyphen → comma ' }

검증 케이스:

입력기존 출력새로운 출력
\ --- \ --- \before---afterbefore, -after (X)
\ ----\ (4개의 대시)word--word (실제 em-dash 사용)word, word (X)
word -- wordword, wordword, word (✓)

부정 후방 탐색은 "이전 문자가 대시라면 일치시키지 마라"고 말합니다. 부정 전방 탐색은 "다음 문자가 대시라면 일치시키지 마라"고 말합니다. 따라서 -- 는 단독으로 존재할 때(또는 공백으로 둘러싸여 있을 때)만 작동하며, 더 긴 하이픈 연속체 내부에서는 절대 작동하지 않습니다. 이제 표가 안전하게 유지됩니다.

실제 em-dash (—) 대용품들은 여전히 제거됩니다. 하지만 삼중 하이픈 구분자(그리고 사중 하이픈, 그리고 모든 --- 계열)는 이제 안전합니다. 제가 앞으로 가져갈 세 가지 교훈은 다음과 같습니다.

  1. Markdown은 단순한 문자열이 아닙니다.
    Markdown은 산문처럼 보이는 구조적 구분자 (structural delimiters)를 포함하고 있습니다. 저의 스크러버 (scrubber)는 ---를 실제로는 표 구분자임에도 불구하고 ASCII 문장 부호로 취급했습니다. 해결책은 단순히 저의 정규 표현식 (regex) 하나를 고치는 것이 아니라, 범주 자체를 다루는 것입니다. 파이프라인이 Markdown을 정규 표현식으로 변형 (regex-mutates)하는 곳이라면 어디든, 저는 이제 속으로 다음과 같이 확인합니다: '이것이 구분자 토큰 (delimiter token)과 일치할 수 있는가?' 이 버그 이후 제가 테스트 케이스를 추가한 유사한 지뢰들은 다음과 같습니다:
  • bold (강조로 사용되는 별표)
  • code (백틱)
  • link (URL에 제 스크러버가 제거하는 문자가 포함될 수 있음)
  • blockquote (앞에 붙는 > )

  • heading (앞에 붙는 # )

원칙: Markdown에 정규 표현식을 사용하는 것은 산문이 아니라 파서 입력 (parser input)에 정규 표현식을 사용하는 것입니다.

  1. 다단계 파이프라인에는 단계별 차이 (step-level diffs)가 필요합니다.
    저는 수개월 동안 이 파이프라인을 사용해 왔습니다. 버그도 수개월 동안 존재했습니다. 하지만 각 단계의 입력과 출력을 별도의 파일로 검사할 수 있게 되자

이제 제 파이프라인에는 구조적 존재 여부 확인 (structural-presence check) 단계가 포함되어 있습니다. 만약 브리프(brief)의 tableSpec이 설정되어 있다면, 최종 마크다운 (markdown)에서 GFM 테이블 (| ... | 라인 뒤에 | --- | 구분자가 오는 형태)이 있는지 확인합니다. 검증기 (validator)는 테이블이 없을 경우 missingRequiredTable: true를 노출합니다. 3개월 동안 테이블을 작성했지만, 게시된 HTML에는 하나도 포함되지 않았고, 경고 (alert)도 단 한 번 없었습니다. 시각적 출력물에는 시각적 확인이 필요합니다. 결론은 이렇습니다. 버그는 단 한 줄의 코드였습니다. 교훈은 앞으로 제가 건드리는 모든 정규 표현식 (regex)은 그것이 실수로 일치할 수 있는 구분자 (delimiter)를 위한 테스트 케이스 (test case)와 함께 존재해야 한다는 것입니다. 어떤 종류든 AI 콘텐츠 파이프라인을 운영하고 있다면, 코드베이스에서 --, em-dash, , &mdash;를 검색해 보세요. 어떤 스크러버 (scrubber)도 마크다운 테이블 구분자 (markdown table separator) 안으로 걸어 들어가지 않도록 확인하십시오. 반드시 하나는 발견될 것입니다. 약속합니다. 저는 제품화된 AI SEO 플랫폼인 SeoHive를 운영하고 있습니다. 버그는 실재했으며, 기사들은 수정 사항이 적용된 채로 라이브 상태입니다. 그리고 이제 파이프라인은 자신의 테이블을 잡아먹지 않고 한 달에 30개의 기사를 게시합니다.

AI 자동 생성 콘텐츠

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

원문 바로가기
0

댓글

0