1편에서 "시스템은 기대보다 잘 돌았다"고 썼다. 거짓말은 아니다. 다만 그 문장 뒤에는 넉 달 동안 615개의 커밋이 숨어 있다. 그중 절반이 글 발행 커밋이고, 나머지 절반이 이 글의 재료다.

자동화는 한 번 만들면 끝나는 게 아니었다. 매일 아침 초안을 열어볼 때마다 뭔가가 하나씩 거슬렸고, 거슬리는 걸 고치면 다른 게 튀어나왔다.

"AI 냄새"와의 전쟁

첫 초안들을 읽고 든 생각은 하나였다. 글이 전부 똑같이 생겼다.

문장은 멀쩡했다. 오히려 너무 멀쩡해서 문제였다. 모든 글이 "안녕하세요 여러분"으로 시작해 "여러분은 어떻게 생각하시나요?"로 끝났다. 중간에는 반드시 "개발자에게 주는 시사점" 단락이 있었다. 시사점이 없는 소식에도 억지로 하나를 지어 붙였다. 언론 기사체와 블로그체가 한 문단 안에서 섞였고, 존댓말과 반말이 문장마다 널을 뛰었다.

그래서 프롬프트를 고치기 시작했다. 그리고 그게 4월 말부터 5월 중순까지 이어졌다.

  • 1차. 인사말과 맺음말 금지. 이모지 금지. 본문은 핵심부터.

  • 2차. 반말로 통일. 여기서 배운 것 하나. "~죠"는 반말이 아니다. 반말처럼 보이는 존대다. LLM은 이걸 반말 목록에 슬쩍 끼워 넣었다. 허용 목록에서 빼고 금지 목록에 올렸다. 같은 날 "~거다"도 금지했다. 글마다 "~거다"가 다섯 번씩 나왔다.

  • 2차 추가. "시사점" 단락 가이드를 아예 삭제했다. 가이드가 있으면 억지 시사점이 나오고, 없으면 안 나온다. 글은 본론으로 끝내라고 못 박았다.

  • 3차. 주제 다양화. 뉴스 소스와 커뮤니티 소스를 분리해서 넣고, 같은 회사의 다른 소식을 억지로 하나로 묶지 말라는 규칙을 추가했다.

  • 4차. 여기서 방향이 뒤집혔다. 금지 목록이 길어질수록 글이 더 딱딱해졌다. "~거다 금지"를 두 군데서 강조하고 대체 어미 사전까지 붙였더니, 모델이 오히려 그 어미에 집착했다. 분홍 코끼리를 생각하지 말라고 하면 분홍 코끼리만 생각하는 것과 같다. 금지 예시를 한 줄로 줄이고 나쁜 예문들을 지웠다. 그러자 나아졌다.

본문 이미지 1

여기까지 하고 나서야 초안을 읽을 때 손이 덜 갔다. 그래도 "위트가 부족하다"는 느낌은 끝까지 남았다. 위트를 넣으라고 하면 억지 비유가 나왔고, 억지 비유를 금지하면 위트가 사라졌다. 이 줄다리기는 결국 이기지 못했다.

아침마다 터지는 API

시스템은 아침 8시에 돈다. 그래서 장애도 아침에 났다. 관리자 페이지를 열면 초안이 0개인 날이 있었고, 원인은 매번 달랐다.

첫 번째는 500과 503이었다. 무료 티어 LLM이 "high demand"를 이유로 요청을 거절했다. 즉시 재시도 2회로는 부족해서 3회로 늘리고 재시도 사이에 0초, 2초, 5초 지연을 넣었다.

두 번째는 400 "User location is not supported"였다. 서버리스 함수가 전 세계 어느 엣지에서 실행될지 모르는데, 그중 일부 지역은 LLM API가 지원하지 않는 곳이었다. 실행 위치를 API 서버 근처로 붙이는 옵션을 켰다. 해결된 줄 알았는데 2주 뒤 다시 났다. 그 옵션은 "가급적"이지 "반드시"가 아니었다. 결국 LLM 호출을 프록시 게이트웨이로 한 번 우회시켜 나가는 IP를 고정했다. 그제야 멈췄다.

세 번째는 429였다. 무료 티어의 하루 호출 한도는 20회다. 한도를 다 쓴 뒤에도 재시도 로직이 성실하게 세 번 더 두드렸다. 한도 초과는 재시도해봐야 낭비라서, 429에 "quota"라는 단어가 있으면 즉시 포기하게 바꿨다.

네 번째는 몇 시간짜리 503이었다. 어느 날은 새 API 키를 발급받아도 같은 503이 났다. 키 문제가 아니라 모델 쪽 용량 문제였다. 같은 회사의 더 작은 모델은 살아 있길래, 큰 모델이 끝까지 실패하면 작은 모델로 넘어가는 폴백 체인을 만들었다. 이후로 아침에 초안 0개인 날은 거의 없어졌다.

정리하면 재시도 강화, 실행 위치 고정, 게이트웨이 우회, 한도 즉시 포기, 모델 폴백. 다섯 겹의 방어를 두르고 나서야 "매일 돈다"는 말을 할 수 있었다. 무료 API를 매일 자동으로 쓴다는 건 그렇게 간단하지 않았다.

같은 글이 또 나온다

배치 버튼을 두 번 누르면 같은 글이 두 번 나왔다. 처음엔 우연인 줄 알았다.

원인은 세 겹이었다. RSS는 몇 시간 단위로만 갱신되니 입력이 같고, LLM은 같은 입력에 거의 같은 출력을 내고, 배치에는 "이 글 이미 썼는지" 확인하는 단계가 없었다. 최근 7일 안에 다룬 출처 URL은 입력에서 빼는 필터를 넣었다. 나중에는 7일이 아니라 영구로 바꿨다. 한 번 발행한 출처는 다시 쓰지 않는다.

이건 작은 버그였지만, 뒤에 나올 색인 문제와 무관하지 않았다.

시간대 이슈

발행 버튼을 새벽 1시에 누르면 발행일이 어제로 찍혔다. 파일명 날짜도 어제였다. 저장된 시각은 UTC였는데 날짜를 앞 열 글자만 잘라 쓰고 있었다.

고쳤다. 그리고 넉 달 뒤, 홈 화면 헤더의 요일이 항상 하루 밀려 있다는 걸 발견했다. 한국 시각으로 만든 날짜 객체에서 UTC 기준 요일을 꺼내고 있었다. 처음 만든 날부터 있던 버그였다.

OG 이미지, 네 번 만들다

  • 1차. 빌드 시점에 글마다 캔버스로 이미지를 그리는 플러그인. 잘 됐다. 다만 호스팅이 빌드 캐시를 지원하지 않아서, 글 하나 발행할 때마다 모든 이미지를 처음부터 다시 그렸다. 글이 적을 땐 티가 안 났다.

  • 2차. 디자인이 마음에 안 들어서 렌더러를 통째로 바꿨다. 크림색 종이 질감에 커밋 해시가 찍힌 터미널 느낌. 글 제목에서 키워드를 자동으로 뽑아 형광펜 강조까지 넣었다. 예뻤다. 빌드는 150초에서 200초가 됐다.

  • 3차. 글이 190개를 넘자 OG 생성이 빌드 시간의 96%를 차지했다. 이 속도면 1년 안에 호스팅의 빌드 시간 한도에 닿는다는 계산이 나왔다. 이미지 생성을 빌드에서 떼어내 별도 워크플로우로 옮기고, 글의 제목과 설명이 바뀐 것만 다시 그리게 했다. 빌드 83초가 3.4초가 됐다.

  • 4차. 두 달 뒤 또 디자인을 바꿨다. 이번엔 다크.

돌아보면 이 사이트에서 가장 많이 다듬은 건 글이 아니라 글의 썸네일이었다.

본문 이미지 2

아침에 고친 문장이 사라진다

이건 어이없게도 세 달 동안 모르고 있었다.

아침 검토 흐름은 이렇다. 초안을 열고, 어색한 문장을 고치고, 발행 버튼을 누른다. 그런데 발행 버튼이 편집 폼을 건너뛰고 발행 API만 부르고 있었다. 발행은 DB에 저장된 초안을 기준으로 이뤄지니, 내가 화면에서 고친 문장은 저장되지 않은 채 원본이 나갔다.

즉 4월부터 7월까지, 내가 아침마다 고친 문장 중 상당수는 사이트에 올라간 적이 없다. 고치고 나서 저장 버튼을 따로 눌렀던 날만 반영됐다.

그 밖의 작은 것들

  • 본문 폰트를 예쁜 한글 폰트로 바꿨다. 772KB였다. 성능 점수가 떨어졌다. 같은 날 되돌렸다.

  • 요약 상자와 본문 첫 단락이 같은 말을 두 번 하고 있었다. 프롬프트에서 TL;DR을 뺐다.

  • 6월에 "이거 저작권 괜찮나" 싶어서 발행된 글 200개를 전수 점검했다. 단일 출처 글 86개 중 10개가 원문 요약에 가까웠다. 분석 비중을 강제하는 가드를 프롬프트에 넣고, 10개는 다시 썼다. 크롤러가 원문 전문이 아니라 RSS 요약 300자만 가져간다는 사실이 그나마 위안이었다. 이 300자가 나중에 다른 의미로 돌아온다.

그래서..

5월 중순쯤 시스템은 안정됐다. 아침 8시에 돌고, 초안이 있고, 고친 문장이 그대로 나가고, 썸네일이 3초 만에 붙었다. 넉 달 치 커밋의 절반이 이 안정화 작업에 들어갔다.

그리고 나는 시스템이 안정됐으니 이제 유입이 쌓이는 일만 남았다고 생각했다.

문제는 전혀 다른 곳에서 터졌다.