개발 slecs

하루 저녁에 두 앱 파이프라인 완성하고 SEO 자동화 인프라를 전면 개편한

목차

저녁 6시부터 자정까지 커밋 로그를 훑으면 Flutter 앱 코드, Python 스크립트, 감사 리포트, 콘솔 메모가 뒤섞여 있다. 겉으로는 산만해 보이지만 실제 흐름은 단순했다. 빌드 대기와 콘솔 응답 시간을 기다리는 사이에 SEO 인프라를 개편했고, NoiseProof의 스토어 파이프라인을 마무리하면서 Vidpress 초기 스캐폴드도 끊어뒀다. 7월 23일을 출시 게이트로 잡아둔 이상, 이날 저녁이 그 준비의 핵심 구간이었다.

NoiseProof: 스캐폴드에서 TestFlight 제출까지

NoiseProof는 소음 증거 수집 앱이다. 측정값 실시간 표시, 임계값 초과 시 자동 녹음, 예약 녹음, 기록 관리, PDF 보고서 생성이 MVP 범위였고, 핵심 기능 구현은 이미 이전 세션에서 끝났다. 오늘 저녁 목표는 "스토어에 실제로 올릴 수 있는 상태" - 앱 ID, 광고 ID, 서명 키스토어, 법적 문서, 스토어 리스팅, TestFlight 업로드까지 파이프라인 전체를 닫는 것이었다.

첫 번째 걸림돌은 iOS 실기기 검증에서 터졌다. 시뮬레이터에서는 멀쩡했던 코드 세 곳이 실기기에서 크래시나 미동작으로 나왔다. 가장 골치 아팠던 건 PHPhotoLibrary 권한 관련 코드였다. VideoCompressorPlugin.swift에서 iOS 14부터 추가된 limitedLibrary API를 가용성 체크 없이 직접 호출하고 있었다. iOS 13 기기에서는 그대로 죽어버린다. #available(iOS 14, *) 분기를 추가해서 해결했는데, 이런 류의 버그는 "시뮬레이터는 최신 SDK 위에서 돈다"는 사실을 잊는 순간 생긴다. 실기기 검증을 별도 단계로 박아두는 이유가 있다는 걸 이번에도 확인했다.

크래시를 잡은 뒤 다국어 레이어를 붙였다. 8개국어 l10n이었는데, 한국어 외에 아랍어와 힌디(데바나가리 계열), 일본어가 포함되면서 폰트 이슈가 따라왔다. NotoSansArabic.ttf, NotoSansDevanagari.ttf, NotoSansJP.ttf를 assets에 추가했다. 앱 UI의 l10n은 l10n.yaml 기반으로 처리됐지만, PDF 보고서 생성 코드는 별도로 폰트 경로를 명시해야 했다. Flutter의 PDF 렌더링 레이어는 앱 폰트 시스템을 그대로 쓰지 않는다. 두 군데를 따로 관리해야 한다는 점이 처음엔 의외였다. 다국어 PDF 지원이 필요한 앱에서는 이 레이어 분리를 처음부터 염두에 두고 설계하는 게 낫다.

AdMob 실 ID 반영은 비교적 기계적인 작업이었다. 콘솔에서 앱 ID와 광고 유닛 6개를 생성하고 AndroidManifest.xml, Info.plist, lib/services/ad_config.dart 세 파일에 각각 반영했다. versionCode도 이 시점에 2로 올렸다.

출시 준비물을 정리하면 이렇다:

항목 처리 내용
법적 문서 4종 store/seed-legal-cms.sql 시드 적용 완료
스토어 리스팅 8개국어 store/listing.md 작성 완료
Android 서명 키스토어 생성 완료
AdMob 실 ID (앱·유닛 6개) 콘솔 생성 + 코드 반영 완료
Play 콘솔 완료
iOS TestFlight ASC 앱·IAP·프로파일 후 업로드 완료

중간에 삽질이 하나 있었다. iOS 상품 ID가 콘솔과 코드 사이에서 불일치한다고 메모를 남겼는데, 재확인해보니 실제로는 일치하고 있었다. 다른 직원이 같은 콘솔을 동시에 작업 중이어서 내가 본 스냅샷이 stale이었던 것이다. "타 직원 작업 중, 재확인 후 진행"이라고 CLAUDE.md에 주의를 남기고 정정 커밋을 따로 올렸다. 콘솔 상태는 실시간으로 변한다. 문서에 적힌 스냅샷을 단일 진실로 취급하면 이런 오보가 생긴다는 걸 이날 몸으로 체감했다.

로컬 알림도 이 세션에서 추가했다. 자동 녹음 시작·저장 시 알림을 띄우는 기능인데, FCM 없이 로컬에서만 처리했다. Android 쪽은 core desugaring을 활성화해야 했다. build.gradle.ktsisCoreLibraryDesugaringEnabled = true 한 줄 추가와 dependency 추가로 해결됐다.

NoiseProof 파이프라인이 닫힌 뒤, 같은 저녁에 Vidpress - 영상 압축기 앱의 초기 스캐폴드를 lumen 템플릿 기반으로 끊어뒀다. 기능 구현까지 이 세션에서 하기엔 시간이 없었고, 뼈대만이라도 커밋 위에 올려두는 게 낫다고 판단했다. 다음 세션의 진입점이 생긴 셈이다.

SEO 인프라 - 사이트맵 다이어트와 골든존 멀티프로퍼티화

앱 빌드가 돌아가는 사이를 SEO 인프라 개편에 썼다. 두 방향이었다: 사이트맵 슬리밍과 골든존 파이프라인의 멀티 프로퍼티 대응.

사이트맵 다이어트는 여러 사이트에서 동시에 진행했다. 공통 문제 의식은 하나였다 - 색인 가치가 낮은 URL이 사이트맵을 부풀려서 Googlebot 크롤 버짓을 낭비하고 있다.

  • 게임 정보 사이트: Metacritic 또는 Steam 500+ 리뷰 조건을 충족하지 못하는 게임 페이지에 noindex를 달고 사이트맵에서 제외했다. 41,000개였던 사이트맵 항목이 1,300개로 줄었다. 31배 슬림.
  • kpop 피규어 사이트: 이미지와 가격/풀레이트 데이터가 모두 있는 항목만 rich figure 섹션에 포함되도록 게이트를 걸었다. 데이터 없는 항목을 사이트맵에 올려봤자 크롤러가 빈 페이지를 만나는 것과 같다.
  • 스포츠 정보 사이트: 팀·리그·F1 프로필 같은 집합 페이지를 en-only로만 사이트맵에 포함하고 다국어 버전은 제외했다.

골든존 파이프라인은 GSC 데이터를 끌어와 클릭이 붙은 상위 쿼리를 추출하고, 연결된 페이지의 메타 타이틀과 디스크립션을 자동 최적화하는 구조다. 문제는 gsc_keywords.pygsc_golden_loader.py가 단일 GSC 프로퍼티 기준으로만 돌았다는 것이다. 사이트마다 별도로 실행해야 했고, 새 사이트가 추가될 때마다 스크립트를 복사해서 쓰는 방식으로 운영되고 있었다. 이번에 멀티프로퍼티 루프로 묶었다.

# 기존 방식 - 프로퍼티 하드코딩
PROPERTY = "sc-domain:example.com"
run_loader(PROPERTY)

# 개편 후 - 설정에서 로드해 루프 처리
PROPERTIES = load_properties_from_config()
for prop in PROPERTIES:
    run_golden_loader(prop)
    run_meta_optimize(prop)

이 과정에서 버그도 두 개 잡았다. 첫째, gsc_golden_loader.py가 스냅샷을 저장할 때 피규어 dex 사이트 URL이 압도적으로 많아서 cms_post 계열 URL들이 일정 비율 이상 잘리는 편향이 있었다. 총 URL 수 기준으로 저장 한도를 두는 방식이었는데, 사이트 간 URL 수 편차가 크면 이런 쏠림이 생긴다. 전량 저장으로 바꿔 해소했다.

둘째, golden_meta_optimize.py에서 두 가지:

  1. 메타 타이틀 60자 하드컷이 단어 중간을 잘라버리는 문제 - 역방향으로 단어 경계를 찾아 끊도록 수정
  2. opsvoro 생성 글 URL이 post?id=123 쿼리스트링 형태라서 cms_post.id 기준 매칭이 안 되던 문제 - 쿼리스트링 파싱 후 id 추출 로직 추가

두 fix 모두 에러 없이 실행은 되는데 의도대로 작동하지 않는 조용한 류의 버그였다. 출력을 꼼꼼히 찍어보지 않으면 그냥 지나치기 쉽다. 자동화 스크립트는 "실행 성공 = 정상 동작"이 아니라는 걸 주기적으로 검증해야 한다는 생각을 이번에도 했다.

opsvoro 콘텐츠 품질: retention hook과 E-E-A-T 일괄 소급

opsvoro는 CMS 글 자동 생성 파이프라인이다. 이날 두 가지 작업을 했다.

generate.py 프롬프트에 retention hook 구조를 추가했다. 핵심 답변을 글 앞에 front-load하는 건 이미 되어 있었는데, 독자가 그 뒤로도 계속 읽게 만드는 장치가 없었다. "이 주제에서 흔히 저지르는 실수" 또는 "처음엔 안 보이는 함정" 같은 pitfall line 한 줄을 핵심 답변 직후에 배치하도록 프롬프트 구조를 바꿨다. 체류시간 지표에 얼마나 반영될지는 데이터를 봐야 알겠지만, 구조 자체는 맞다.

두 번째는 이미 발행된 글 248개에 대한 일괄 E-E-A-T 보강이었다. enrich_eeat.py가 배치로 돌면서 dwell-time 최적화와 E-E-A-T 기준으로 기존 글을 재작성한다. 새 글에는 처음부터 좋은 구조가 들어가지만, 과거 글들을 묵혀두면 그 트래픽이 낭비된다. 248개면 작지 않은 소급 범위인데 배치로 한 번에 처리했다. 수작업으로는 엄두도 못 낼 분량이다.


마지막으로 11개 앱에 대한 정적 감사 리포트를 커밋했다. 코드는 건드리지 않고 리뷰 노트만 남긴 것인데 - 출시차단 1건, 중간 5건, 경미 6건. 이 번호들이 다음 세션 우선순위 인풋이 된다. 이날 저녁 작업 전반을 관통하는 맥락은 결국 "7월 23일 게이트" 하나였다. 그 데드라인이 없었다면 이 작업들이 이렇게 한 저녁에 집중되지 않았을 것이다. 빌드 대기 중 SEO 스크립트를 고치고, 콘솔 응답을 기다리며 프롬프트 구조를 다듬는 방식으로 시간 공백을 거의 남기지 않았다. 총괄 포지션에서 여러 프로덕트를 동시에 끌고 가는 방식이 자연스럽게 이런 패턴으로 수렴된다.


🛒 이 글과 어울리는 추천 상품

*위 링크는 쿠팡파트너스 활동의 일환이며, 일정액의 수수료를 제공받을 수 있습니다.

댓글 0

첫 댓글 달아줘.