일기 slecs

앱 세 개가 동시에 심사 큐로 들어간 날

목차

오늘 저녁 git log를 처음 펼쳤을 때, 커밋이 30개를 훌쩍 넘겨 있었다. 세 개 프로젝트에 걸쳐서. 각자 성격도 다르고, 각자가 요구하는 집중도도 달랐다. 하루를 어떻게 정리하면 좋을지 잠깐 멍하다가, 그냥 흐름대로 쓰기로 했다.


topikbox라는 이름을 땅에 묻기까지

슬랭딕트 얘기부터 해야겠다. 프로젝트 이름이 topikbox였다. TOPIK은 한국어 자격시험이고, 우리가 실제로 만드는 건 영어권 부모가 십대 자녀의 슬랭을 해독하는 앱이다. 처음부터 포지셔닝이 완전히 어긋나 있었는데, 스캐폴드 이름을 그냥 끌고 왔던 것. 글로벌판 포지셔닝을 정정하는 커밋도 오늘 있었다 - "K-팬 아님, 영어권 부모 teen slang 해독". 이게 단순 문서 수정이 아니라, 방향이 틀어진 걸 인정하고 바로잡는 작업이었다.

rename 커밋 하나에 앱 ID, 패키지명, 클래스 경로, MainActivity.kt 위치까지 죄다 slangdict로 갈아엎었다. 안드로이드 rename이 항상 그렇듯이 파일 시스템 경로와 패키지명이 얽혀 있어서 하나 놓치면 런타임에 가서야 터진다. 오늘은 그냥 통으로 날렸다. com.hedvion.slangdict로 일원화.

그 다음에 백엔드를 통째로 새로 썼다. 기존 API가 topik 구조 위에 있어서 슬랭 사전 도메인과 안 맞는 부분이 여럿이었다. server.js 뒤엎고, 크롤/프로세스/리뷰 파이프라인 새로 구축. systemd 유닛 경로도 /opt/slangdict/api로 잡아줬고, 도메인은 slang.hedvion.com 확정. api2로 SSOT(hedvion-CLAUDE.md)에 등재하기까지 했다. 이제 이 도메인이 어디에서 뭘 하는지 어디서든 확인할 수 있다.

era 컬럼이 64자로는 짧았다

ingest 파이프라인 돌리다가 크래시가 났다. era 필드 길이 초과. 시대 분류를 문자열로 넣는 구조인데 컬럼을 64자로 잡아놓은 게 실제 데이터 앞에서 뚫린 것. db/schema.sql 수정하고, process_candidates.py에 삽입 전 절단 가드를 심었다.

이런 버그가 짜증나는 이유가 있다. 로컬 테스트에서는 안 터진다. 실제 슬랭 용어를 집어넣기 전까지는 길이 초과가 발생하지 않으니까. 오늘 시드 158개 - 늙크크, 영크크, 6-7 같은 2025-26 최신어들 - 를 새로 넣으면서 발견됐다. 그냥 지나쳤으면 cron이 조용히 죽고 있었을 거다.

오늘의단어 선정 로직도 손댔다. era 2024-2026 범위 슬랭에 가중치를 주는 방식으로 daily_pick.py를 수정했다. 앱 특성상 "요즘 애들이 쓰는 말"이 먼저 나와야 하는데, 과거 시드들이 섞이면서 체감상 오래된 표현이 튀어나오는 문제가 있었다. 코드 한 줄 바꾼 거지만 앱 첫인상에서 차이가 난다.

cron 환경 이슈도 하나. run_pipeline.sh가 로컬에서는 잘 돌다가 cron에서 python3를 못 찾고 .env도 안 읽혔다. 셸 인터프리터 명시하고 dotenv 로드 순서 정리해서 잡았다. 한 번만 겪으면 다음부터는 셸 스크립트 첫 줄부터 cron 환경을 고려하게 된다.

스샷 160장의 현장

슬랭딕트의 진짜 하이라이트는 스크린샷 파이프라인이었다. 아니, 하이라이트라기보다는 제일 오래 걸린 부분.

2 SKU (글로벌판, 한국판) × 8 로케일 × iPhone + iPad. 세트 수로만 32, 실제 장수는 훨씬 많다. build_cap_screenshots.sh로 전체 파이프라인을 자동화하고, arrange_and_sheet.py로 콘택트시트 구성, finish_shots.sh로 마무리. 한국판은 captions_kr.json, 글로벌판은 captions_global.json으로 캡션 언어를 분기. ASC 업로드는 asc_ss_upload.py, Play는 play_api.py가 따로 맡는다.

중간에 kr SKU 리스팅 오류가 있었다. 한국어 슬랭 사전인데 "한국어 번역기" 느낌으로 잡혀 있었던 것. 사전이지 번역기가 아니니까 listing.md 고치고 verify_counts.py로 다시 검증했다.

iOS 플레이버 분리도 이날 끝냈다. 글로벌판과 한국판 앱아이콘을 AppIcon-Global로 분리하고, Pods xcconfig를 config별(Debug/Profile/Release)로 include 처리. 파일이 글로벌용, 한국판용 각각 3개씩 생겼다. 이걸 안 하면 두 플레이버 빌드할 때 아이콘이 교차 오염되는 문제가 생긴다. 초기에 글로벌 빌드에 한국판 아이콘이 붙어나오는 걸 보고 정신이 번쩍 들었던 기억이 있다.

서버쪽에서는 크로스팩 잠금 본문 미전송 버그도 잡았다. pack_rank > 30인 팩은 스텁만 내려서 UI 블러 우회를 차단하는 로직. 클라이언트에서 블러 씌워놓는 것만으로는 충분하지 않다. 서버가 실제로 본문을 안 줘야 한다.

BendWise: fork에서 ASC 제출까지 하루

솔직히 오늘 가장 놀란 건 BendWise다. camberpro에서 fork해서 conduit bending calculator로 탈바꿈시키고, 하루 안에 submitted for review까지 완주했다.

fork 직후 할 일 목록이 만만치 않았다. 아이콘부터 - hi-viz conduit 디자인으로 새로 만들어서 Android mipmap 전 해상도(hdpi, mdpi, xhdpi, xxhdpi, xxxhdpi)에 교체하고, iOS 1024×1024도 갈았다. style 커밋에서 코드 주석 전체의 em-dash를 하이픈으로 바꿨는데, 사소해 보여도 코드 일관성 차원에서 한 번에 정리해 두는 게 맞다.

기능적으로 가장 중요했던 건 parseLength 수정이었다. 음수나 0 이하 길이값을 reject하도록 고치고 bend_math_test.dart에 케이스 추가. 물리적으로 의미 없는 값이 계산에 들어가면 이상한 결과가 나오는데, 이게 사용자 신뢰를 깎는다. "이 앱 계산이 맞나?"라는 의심이 드는 순간 설치 유지율이 떨어진다. review panel에서 "calculation trust" 이슈가 제기됐을 때 같이 처리했다.

review panel에서 나온 field UX 문제들도 이날 같이 잡았다. integration_test/full_sweep_test.dart로 전체 인터랙션을 훑는 테스트를 작성하고, QA 스크린샷 5장(01_home_nonpro.png ~ 05_reference_locked.png)으로 눈검수까지 했다. 단, bottom_sheet_bg_test.dart는 드롭했다. google_fonts 로드 이슈로 테스트 존에서 크래시가 나는데, 시뮬레이터 렌더링으로 이미 검증된 부분이라 flaky test를 안고 가는 것보다 제거가 낫다고 판단했다.

Pro 게이팅은 saddle reference rows와 IMC/Rigid 프리셋에 적용했다. l10n 파일 6개 언어 전부 - app_de.arb, app_es.arb, app_fr.arb, app_it.arb, app_ja.arb, app_en.arb - 업데이트. paywall 쪽도 손봤다. 가격 로딩 실패 시 빈 화면, 구매 후 restore feedback 누락, purchase_service 경계 강화. 유저가 결제하는 순간에 UX가 어설프면 안 된다.

스크린샷 하네스는 DEMO 플래그로 가드된 입력값 프리필 방식으로 구현했다. offset_screen.dart, saddle3_screen.dart, stub_screen.dart에 각각 prefill 로직을 넣어서, 시뮬레이터 실행 시 데이터가 자동으로 채워진다. 덕분에 iap_review_shot.py로 contact_sheet 생성과 ASC 업로드가 자동화됐다.

그리고 오늘 BendWise v1.0 + IAP가 submitted for review로 상태가 바뀌었다.

5.1.2, 순서를 틀린 대가

슬랭딕트와 BendWise가 앞으로 나아가는 동안, 다른 앱 하나는 반려 수습 중이었다.

1.2.0이 5.1.2로 걸렸다. App Privacy 불일치 반려다. 경위가 좀 멋쩍다. 광고를 일시 휴면 시키면서 ATT 흔적을 전부 지웠다. 코드에서 ATT 권한 요청을 뺐는데, 프라이버시 라벨에서 트래킹 선언을 같이 수정하지 않았다. 심사관 입장에서는 라벨에 트래킹 선언이 있는데 ATT를 안 띄우는 앱이 되어버린 거다. 순서를 틀린 것.

오늘 1.2.0+11 빌드로 ATT 권한 요청을 복원했다. lib/main.dart에 권한 요청 흐름을 다시 넣고, pubspec.yaml에 패키지 추가. 프라이버시 라벨과 코드의 선언을 일치시키는 방향으로.

ASC 재제출할 때 함정도 하나 더 발견했다. reviewSubmissionItems가 resolved 처리되기 전에 새 버전을 올리면 이전 리뷰 아이템이 꼬인다. 광클하다가 실측에서 발견한 것들을 docs/mobile-app-launch.md에 추가해뒀다. 다음 사람이 같은 삽질을 안 하도록.

404 대시보드: 가용액 공식이 틀렸었다

백엔드 얘기도 빼놓으면 안 된다. 404 프로젝트에서 파트너 예치금 관리를 오늘 상당 부분 완성했다.

예치금이라는 개념이 얼핏 단순해 보이지만 구현하다 보면 갈래가 많다. 가용액, 유보금, 법인회수, 위임 가능 금액. 각각 의미가 다르고 공식이 다르고 권한도 다르다. BalanceChangeType.java를 신규로 만들고, ExcelConfigRegistry.java도 건드렸다. 관리자 화면에서 예치금 총액을 대시보드 카드에 표기하는 배선을 total-summary.jsp에 붙이고, deposit-list.jsp에서 파트너별 예치금 조회가 되도록 Controller 두 개를 이었다.

가용액 공식이 정정됐다고 커밋에 나와 있다. 운영 들어가기 전에 잡은 게 다행이다. 이런 계산 오류는 실제 정산이 시작되면 복구가 복잡해진다.

파트너 법인명 마스킹도 이날 넣었다. QR 코드나 파트너 코드로 회원가입하면 화면에 법인명이 뜨는데, "주식회사 사*" 형태로 마스킹해야 하는 요건이 있었다. member/weblink/web Controller에서 처리하고, 유틸 클래스에 마스킹 로직 추가.

applovin_max가 compileSdk를 건드리는 문제

build.gradle.kts에 applovin_max compileSdk 오버라이드 커밋이 있다. AAR 메타데이터가 compileSdk 버전을 강제하려 드는 구조인데, 오버라이드를 안 넣으면 Gradle 빌드가 불일치로 깨진다. 오류 메시지가 직관적이지 않아서 처음 만나면 당황스럽다. docs/README.md에 함정과 스니펫을 남겨뒀다. 나중에 또 만나거나 팀원이 만날 때 삽질 덜 하도록.

psy는 알아서 돌았다

마지막에 psy 자동 테스트 커밋 하나 - concert-ticketing-type, meal-decision-with-friend, new-room-diary-horror. 커버 이미지와 JSON이 자동 생성되어 들어온 것. 직접 손댄 게 아니라 cron이 돌린 거지만, 파이프라인이 오늘도 문제없이 돌았다는 확인이다.


오늘은 커밋 수로 치면 올해 들어 손에 꼽히는 날이다. slangdict 전체 파이프라인 구축과 스토어 제출, BendWise fork부터 ASC 심사 제출까지, 반려 앱 수습, 404 대시보드 기능 추가. 그 사이사이 스크린샷 자동화, l10n, 테스트, 문서화. 세 개 프로젝트를 동시에 들고 돌아다닌 하루.

지금 피곤한 건 사실인데, 이상하게 찝찝하지는 않다. 오늘 흘러간 것들이 죄다 어딘가로 실제로 향하고 있다는 느낌 때문인 것 같다.

댓글 0

첫 댓글 달아줘.