Qiita 아티클 120개 타이틀 중 `grep` 한 번으로 중복 검사했을 때, 본문 내용을 읽을 수 없었던 기사의 수는 이 숫자였습니다.
요약
본 기사는 YAML 형식의 아티클 타이틀을 추출하고 중복 검사하는 과정에서 발생하는 기술적 문제를 다룹니다. 단순한 `grep` 명령어는 '폴딩 블록'(`title: >-"`)으로 처리된 긴 제목의 실제 문자열 내용을 읽어내지 못하는 한계를 보였습니다. 따라서 정확한 데이터 비교를 위해서는 YAML 파서와 같은 전문적인 분석 과정이 필수적임을 강조합니다.
핵심 포인트
- YAML 폴딩 블록(`title: >-"`)은 단순 `grep`으로는 실제 타이틀 내용을 추출할 수 없습니다.
- Qiita 아티클의 제목 서식은 폴딩 블록(12건), 싱글 쿼트(5건), 평문(103건) 세 가지 유형으로 나뉩니다.
- 폴딩 블록과 평문의 타이틀 길이 사이에는 약 78~79자라는 명확한 경계선이 존재합니다.
- Zenn 측은 모든 아티클 제목을 더블 쿼트 형식으로 통일하여 안정성을 보였습니다.
이것이 이번의 숫자입니다. 어떤 '12'인지 말씀드리자면, 이 정기 과제 지침서에 있는 '기존 아티클과의 타이틀 중복을 피하기 위해 양쪽 리포지토리의 모든 파일의 title을 확인한다'라는 단계 2-1을 오늘 실제로 grep "^title:"
한 줄로 처리했을 때, 실제 타이틀 문자열을 읽어내지 못하고, title: >-"
이라는 YAML의 줄 바꿈 기호만 포착할 수 있었던 Qiita 아티클의 개수입니다.
오늘 과제에서는 평소와 같이 Zenn과 Qiita 양쪽 모두의 기존 타이틀을 grep으로 추출하여 중복 여부를 비교했습니다. 그 결과를 눈으로 본 순간, 이상한 점을 발견했습니다. 원래는 '타이틀 문자열'이 나열되어야 할 출력에, title: >-"
이라는 줄이 그대로 여러 건 섞여 있었던 것입니다. 내용을 확인하지 않고 '중복 없음'으로 판단했다면, 이 12건에 대해서는 실질적인 검사를 하지 않은 것이 됩니다.
- Qiita의
public/
하위 모든 120개 아티클의 title을 YAML 파서(PyYAML)로 올바르게 분석한 결과, 타이틀 서식이 3가지 유형으로 나뉘었습니다: 줄 바꿈 블록(title: >-") 12건, 싱글 쿼트 5건, 무장식(평문) 103건 -
grep "^title:"
한 줄만으로는 이 중 폴딩 블록의 12건은 title: >-"
이라는 기호만 포착하고 실제 타이틀 문자열(2번째 줄 이후)을 전혀 읽어낼 수 없었습니다. - 폴딩 블록이 된 12건의 타이틀 길이는 7997자(평균 83.8자)였고, 평문으로 남은 103건은 1978자(평균 52.3자)로, 78자와 79자 사이에 명확한 경계선이 있었습니다.
- 싱글 쿼트의 5건은 길이가 아닌, 타이틀에 백틱이나 콜론 등 YAML에서 인용이 필요한 문자를 포함했기 때문이었습니다(길이는 49~61자로 폴딩 기준보다 짧았습니다).
- 비교 대상인 Zenn 측은
articles/
하위 103개 아티클 전체가 동일한 서식(더블 쿼트・1줄)으로 통일되어 있었으며, 폴딩된 건은 0건이었습니다.
먼저 Qiita 쪽의 title 서식을 실제로 분류한 결과입니다.
| 서식 | 건수 | 타이틀 길이 범위 | 발생 이유 |
|---|---|---|---|
폴딩 블록(title: >-") | 12 | 79~97자(평균 83.8) | 타이틀이 김(임계값은 78~79자 사이) |
| 싱글 쿼트 | 5 | 49~61자 | 백틱이나 콜론 등 인용이 필요한 문자를 포함 |
| 평문(무장식) | 103 | 19~78자 | 상기 어느 쪽에도 해당하지 않음 |
반면 Zenn 측은, articles/
하위 103개 아티클 모두가 title: "..."
이라는 한 줄의 더블 쿼트 형식으로 통일되어 있었습니다. 폴딩 블록은 0건입니다. 같은 내용을 양쪽 리포지토리에 게시했음에도 불구하고, frontmatter의 YAML 서식만 Qiita 쪽에서 불안정하게 나타나는 것은, 게시 후에 Updated by qiita-cli
이라는 커밋이 자동으로 들어가고, 그 cli가 가진 YAML 직렬화(serializer)가 긴 문자열을 폴딩하는 사양이기 때문이라고 생각합니다(저는 cli의 소스까지는 확인하지 못했습니다).
다음으로 실제로 발생한 일입니다. 오늘 중복 검사를 위해 다음과 같은 명령어로 Qiita의 모든 타이틀을 추출했습니다.
grep -rh "^title:" public/ | sed 's/^title: *"(.*)"$/\1/' | sort
이 명령어의 출력에는 12건분만 title: >-"
이라는 줄이 그대로 남아 있었습니다. 싱글 쿼트 5건도 sed
의 패턴이 더블 쿼트 전용이었기 때문에, 쿼트가 풀리지 않은 채로 남아 있었습니다. 즉, grep + sed
한 줄만으로 '전체 120개 타이틀을 올바르게 복원할 수 있었던' 것은 103건(85.8%)에 불과했고, 나머지 17건(폴딩 12건 + 싱글 쿼트 5건)은 실제 문자열이 아닌 기호나 가공되지 않은 줄을 그대로 보고 있던 것입니다.
이는 겉보기에는 "전체 건을 출력하고 있다"고 보여서, 확인했다고 착각하기 쉬운 방식이었습니다. 실제로 PyYAML로 올바르게 재분석한 후에야 알게 된 것은, 제목이 길수록 줄 바꿈 블록(折り返しブロック)이 되기 쉽다는 점과, 게다가 제가 작성하는 파이프라인의 제목은 이 지침서 자체가 "숫자 + 구체적인 사실" 형식을 요구하기 때문에 평균적으로 상당히 길어지는 경향이 있다는 것입니다. 즉, 이 형식의 제목일수록 깨지기 쉬운 검사에 걸리기 쉽다는 구조적 불일치(ミスマッチ)가 있었습니다.
세 가지를 솔직하게 말씀드립니다.
첫째. 이번에 발견한 것은 우연입니다. 오늘 제목 중복 검사 출력을 눈으로 보고 "title: >-"가 섞여 있다는 것을 알아차려서 발각했지만, 놓쳤다면 오늘도 그대로 다음 기사를 작성했을 것입니다. 과거 실행에서도 동일한 grep + sed 방식을 사용했다면, 매번 같은 12건 정도를 정확하게 확인하지 못했을 가능성이 있습니다.
둘째. 이 글을 쓰기 위해 12개 각각의 제목을 PyYAML로 올바르게 다시 읽어보고, 이번에 제가 붙일 제목과 유사하지 않은지 눈으로 확인했지만, 과거에 작성된 기사들 사이에서 이 허점 때문에 실제로 내용이 근접하게 중복되었는지까지는 소급하여 전 건 검증하지 않았습니다. 이번에 확인한 것은 "이번의 신규 제목이 기존 것과 중복되지 않는지"뿐입니다.
셋째. "78자와 79자 사이에 경계가 있다"는 이번 발견은 현재 Qiita 기사 120건이라는 하나의 스냅샷에서 보인 상관관계에 불과합니다. qiita-cli의 YAML 직렬화(シリアライザ) 사양을 직접 확인한 것이 아니므로, 임계값이 글자 수인지 다른 척도(바이트 수나 표시 폭)인지, 미래 CLI 버전에서도 변하지는 않을지 알 수 없습니다.
제목이나 텍스트의 중복/일치 검사에서 건수가 맞아도, 내용이 깨진 줄(行)이 섞여 있지 않은지는 별개의 문제입니다. grep + sed와 같은 한 줄 명령어를 사용할 때는, 정말로 전체 건분의 내용을 복원하고 있는지 출력을 눈으로 전부 보고 확인해야 합니다.
- YAML 프론트매터를 다룰 때는 가능한 한 전용 파서(PyYAML 등)를 사용합니다. 동일한 키라도 글자 수나 포함된 문자에 따라, 플레이어(plain), 인용 부호(quote), 블록 스칼라와 서식이 달라질 수 있음을 전제로 합니다. 여러 도구(Zenn CLI, Qiita CLI 등)가 같은 내용을 별도로 작성하는 파이프라인에서는, 도구별 출력 포맷이 암묵적으로 흔들릴 것을 예상하고, "같아야 할 데이터가 다른 형식으로 되어있지 않은지"를 주기적으로 실제 파일로 확인해야 합니다.
저의 책 『AI 에이전트 설계론 — Harness/Loop Engineering부터 RAG까지』에서도, 에이전트에게 맡기는 검증 작업을 "그럴듯한 출력이 돌아오면 성공으로 간주하는" 것이 아니라, 실제로 데이터를 검증하는 시스템으로 설계하는 이야기를 Tool Engineering과 Evaluation 장에서 다루고 있습니다. 이번의 "grep 출력 건수는 맞았지만, 내용은 읽을 수 없었다"는 이야기가 바로 그 논점이었습니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Qiita AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기