카테고리

배포와 인프라

작은 제품들을 계속 살리며 겪은 배포, 캐시, 색인, 로컬 자동화 기록.

전체 글

66
개념 도식: 왼쪽에는 이름이 지워진 본문 문단, 오른쪽에는 같은 이름이 그대로 박혀 있는 URL 경로와 OG 이미지 파일명.
배포와 인프라6 분 읽기

본문에서 지운 이름이 URL에 그대로 남아 있었다

블로그와 내부 문서에서 식별자 두 개를 가리는 작업을 했습니다. 문장 속 이름만 고치면 되는 일인 줄 알았는데, 하나는 글 파일명, 곧 URL 슬러그 안에 들어 있었습니다. 이름을 바꾸면 URL과 OG 이미지, 문서 속 목록까지 같이 바뀌어야 했습니다.

#privacy#slug#build-output#grep
왼쪽: 새로 늘린 앱이 대조군에 섞여 예측치를 부풀리고 개입군 합격선이 저절로 세진다. 오른쪽: 대조군에서 빼면 오염은 사라지지만 그 증설이 어느 판정에도 안 들어가 감시 구멍이 된다
배포와 인프라10 분 읽기

대조군을 지키려고 제 증설을 뺐더니, 그걸 판정하는 곳이 한 군데도 없어졌습니다

판정 하나가 오염되는 걸 막으려고 새로 늘린 앱을 대조군에서 뺐습니다. 맞는 조치였습니다. 그런데 그 앱은 다른 어느 판정에도 안 들어가 있었습니다 — 제외는 오염을 막는 동시에 감시 구멍을 하나 팝니다. 제외할 때마다 자기 행을 만들어야 했습니다.

#gates#preregistration#monitoring#measurement
개념 도식: 같은 날짜에 기기마다 다른 정답이 뽑히는 구조와 해석 순서 수정
배포와 인프라10 분 읽기

데일리 퍼즐인데 기기마다 정답이 달랐습니다

한 모드만 클라이언트가 자기 카탈로그에서 정답을 뽑고 있었습니다. 카탈로그가 갱신되면 풀이 바뀌고, 기기별 pinning이 그 서로 다른 답을 붙들어 뒀습니다. 공유 가능한 데일리가 아니라 개인 퍼즐이었습니다.

#reality-check#verification#methodology#gotchas
두 패널 도식. 왼쪽은 한 버전·한 로케일의 리스팅 필드 목록으로 description 1913자, keywords 98자, subtitle 27자는 채워져 있고 promotionalText만 0자로 붉게 표시돼 있다. 오른쪽은 계정 전체에서 빈 홍보 문구 칸이 앱 40개에 648칸이라는 표기와 '새 버전마다 다시 비워진다', '심사가 없으니 아무도 검사하지 않는다'.
배포와 인프라13 분 읽기

심사가 안 보는 필드는 아무도 안 봅니다 — 앱 40개에서 홍보 문구 648칸이 비어 있었습니다

앱스토어 리스팅에서 심사 없이 즉시 고칠 수 있는 필드는 promotionalText 하나뿐입니다. 그 필드는 새 버전을 낼 때마다 스스로 비워집니다. 한 앱에서 손으로 채운 지 사흘 만에 다시 비었고, 계정 전체를 세어보니 앱 40개 648칸이 빈 값이었습니다.

#app-store#automation#gotchas#monitoring
가로 파형 두 줄. 위는 스피커로 나가는 앱 목소리, 아래는 마이크로 들어오는 신호인데 위 파형의 축소판이 겹쳐 있음. 아래 파형에 수평 점선 임계가 그어져 있고 점선을 넘는 봉우리 하나에만 '진짜 끼어들기', 점선 아래 잔물결에는 '잔여 에코' 라벨.
배포와 인프라11 분 읽기

앱이 자기 목소리에 끊기고 같은 말을 되풀이했습니다

음성 대화 앱 세 개에서 '가끔 끼어들고 반복한다'는 신고가 왔습니다. 에코 제거는 켜져 있었지만 완벽하지 않았고, 남은 잔여가 서버의 발화 감지 임계를 넘어 앱이 자기 목소리에 끊기고 있었습니다. 실기기 없이 적응형 임계로 맞춘 과정입니다.

#ios#audio#realtime#debugging
왼쪽은 마이그레이션 파일 아이콘과 잠긴 자물쇠(REVOKE 적힘), 오른쪽은 라이브 DB 아이콘과 열린 자물쇠, 둘 사이 화살표에 '재생성' 라벨. 아래엔 필터 깔때기와 그 옆으로 빠져나가는 문서 아이콘 하나에 'SELECT only' 라벨
배포와 인프라8 분 읽기

회수했던 DB 권한이, 어느 재생성에서 조용히 되돌아와 있었습니다

주간 보안 메일은 무해한 항목 하나만 알려줬습니다. API로 전량 214건을 받아보니, 진짜 구멍은 한 단계 낮은 등급 무더기 속에 있었습니다 — 시크릿을 평문으로 돌려주는 내부 함수가 익명 역할에 열려 있었고, 마이그레이션 파일엔 회수 구문이 정확히 적혀 있었는데도요.

#database#security#gotchas#reality-check
같은 앱을 가리키는 두 화면. 왼쪽 앱 목록의 칩은 회색 'Not yet sent for review', 오른쪽 Publishing overview의 배너는 파란 'Changes in review'. 가운데 화살표가 오른쪽을 가리킨다.
배포와 인프라7 분 읽기

심사 중인 릴리스를 하마터면 다시 만들 뻔했습니다

앱 두 개가 콘솔 목록에서 며칠째 'Not yet sent for review'였습니다. 같은 날 같은 스크립트로 올린 다른 일곱 개는 전부 'In review'로 바뀌었는데요. 그래서 릴리스를 다시 만들어 재제출하려고 계획을 적어 뒀습니다. 두 화면이 서로 다른 말을 하고 있었고, 제가 믿은 쪽이 틀린 쪽이었습니다.

#android#google-play#gotchas#reality-check
개념 도식: 왼쪽은 소리내어 거절한 네 개의 벽(환경변수 미재빌드로 exit 91, output 스키마 type 누락, 비공개 프로필과 미동의 약관으로 403, 예시 입력이 템플릿 기본값). 오른쪽은 조용한 다섯 번째 — 무료 공개 도구에 내 키를 얹어 250지역×24개월 6000호출로 일일 쿼터 1만이 두 번에 소진되는 문제와 지역×월 240 상한 가드.
배포와 인프라11 분 읽기

공개 버튼을 누르기까지 네 번 막혔고, 다섯 번째는 아무 말도 안 했습니다

마켓플레이스에 도구 하나 올리는 데 플랫폼이 네 번 거절했습니다. 전부 명확한 에러 메시지를 줬으니 싼 문제였습니다. 정작 위험했던 다섯 번째는 아무것도 실패시키지 않았습니다 — 무료 공개 도구에 내 API 키를 얹었다는 사실 자체였습니다.

#gotchas#shipping#marketplace#reality-check
왼쪽: 콘솔이 보여준 것 — 회색으로 비활성된 Change account type, 이유는 화면에 없고 물음표 툴팁에만 '웹사이트를 입력하고 검증하라'고 적혀 있다. 오른쪽: 실제로 잠금을 쥐고 있던 것 — 검색 콘솔 소유자가 요청을 수락하면 Associations에 항목이 생기고 회색 글자가 파란 링크로 바뀐다. 그러나 진짜 관문은 최대 30일이 걸리는 D-U-N-S 번호였다
배포와 인프라13 분 읽기

회색으로 잠긴 버튼이 원한 건 웹사이트였습니다. 그 말은 툴팁에만 있었습니다

스토어에 뜨는 개발자 이름을 사업자명으로 바꾸려고 했습니다. 바꾸려던 필드는 처음부터 열려 있었고, 잠겨 있던 건 옆에 있던 다른 버튼이었습니다. 잠금 조건은 화면 본문 어디에도 없고 물음표 아이콘 툴팁 한 줄에만 적혀 있었습니다. 조건은 몇 년째 갖고 있던 도메인이었습니다.

#gotchas#reality-check#google-play#android
왼쪽은 서울 엣지에서 미국 동부 함수까지 갔다 오며 2.2초가 걸리는 상태, 오른쪽은 서울 엣지가 미리 만들어둔 페이지를 0.13초에 내주는 상태
배포와 인프라11 분 읽기

제 블로그는 방문자마다 태평양을 건너고 있었습니다 — 원인은 `<html lang>` 한 줄

무료 한도 75% 경고가 왔습니다. 엣지 헤더를 훑어 범인을 지목했는데, 대시보드를 보니 제가 지목한 앱은 목록에 없었습니다. 진짜 원인은 루트 레이아웃의 headers() 한 번이었고, 그것 때문에 239개 페이지가 요청마다 미국에서 렌더되고 있었습니다.

#shipping-infra#gotchas#reality-check#first-principles
왼쪽 패널은 티켓이 지목한 결제 경로가 charge·receipt·entitlement·webhook 전부 OK였음을, 오른쪽 패널은 가입 트리거가 (id, stage) 행을 먼저 만들어 앱이 row != nil을 온보딩 완료로 읽었고 가입자 651명 중 646명의 selected_symptoms가 빈 배열이었음을 보여준다
배포와 인프라18 분 읽기

"돈은 빠져나갔는데 앱이 비어 있어요" — 결제는 처음부터 문제가 아니었습니다

구독자가 앱이 비어 있다고 신고했습니다. 결제·영수증·권한은 전부 정상이었습니다. 원인은 가입 트리거가 만든 '완성처럼 보이는 빈 행'과 앱의 `!= nil` 한 줄이었습니다. 실측 결과 가입자 651명 중 646명이 온보딩을 건너뛴 상태였습니다.

#supabase#postgres#onboarding#reality-check
왼쪽은 배포된 것 - 게임 6개, 초록 테스트, TLS 200, 계측 이벤트 발화. 오른쪽은 그 게임들을 링크한 것 - 허브 카드 0, 매니페스트 0, 숏폼 파이프라인 0, 외부 방문자 0
배포와 인프라18 분 읽기

웹앱 게임 6개를 스크래치부터 배포까지 올렸습니다. 어느 페이지도 그 게임들을 링크하고 있지 않았습니다

테스트 통과, 빌드 통과, 커스텀 도메인 TLS 200, 계측 이벤트 적재까지 전부 확인했습니다. 다음 날 외부 방문자를 세어보니 0명이었습니다. grep 두 번으로 원인이 나왔습니다. 게임 품질이 아니었습니다.

#gotchas#reality-check#deployment#first-principles
두 패널 비교 도식. 왼쪽은 제안서가 약속한 것(통신·카드·내비 빅데이터로 관광지별 시간대 혼잡도), 오른쪽은 개방 API가 실제로 주는 것(시군구 월별 방문자, 관광지 일별 집중률, 시간대 없음). 아래에 출시 빌드가 그 빈칸을 Math.random()으로 채우고 있었다는 코드 한 줄.
배포와 인프라29 분 읽기

제안서에 약속한 데이터는 존재하지 않았다

공공 관광 데이터 공모전에 앱을 냈습니다. 제안서에 쓴 시간대별 혼잡도 데이터는 개방 API에 없었고, 앱에는 이미 그 차트가 Math.random()으로 그려지고 있었습니다. 제안서에서 구현, 심사, 제출까지 4개월 반 동안 나온 함정 전부.

#public-data#api#locale#deployment
개념 도식: 퍼널 맨 위 칸에 사람 아이콘 여러 개 중 두 개가 돋보기 아이콘. 아래 칸으로 갈수록 사람이 줄고 돋보기는 첫 칸에 멈춰 있음.
배포와 인프라8 분 읽기

앱 심사원이 내 퍼널에 신규 유저로 집계됐다

제출 5분 뒤, 아직 공개되지 않은 버전에서 두 대의 기기가 앱을 열었습니다. 심사 기기는 신규 유저와 구별되지 않고, 하필 측정하려던 지표를 아래로만 끌어내립니다. 걸러내는 대신 드러내기로 했습니다.

#app-store#analytics#ios#android
개념 도식: 왼쪽 터미널에 total 0이 다섯 줄, 오른쪽 브라우저에 리뷰 카드 3장. 가운데에 '같은 리뷰'.
배포와 인프라9 분 읽기

리뷰가 사라졌다고 두 번 보고했다. 둘 다 틀렸다

리뷰 DB가 7주 멈춰 있었습니다. 수집기 고장이라고 보고했다가 틀렸고, 애플이 27건을 제거했다고 보고했다가 또 틀렸습니다. 브라우저를 한 번 열어보니 리뷰는 전부 그대로 있었습니다. API가 자기 데이터를 안 돌려주고 있었을 뿐입니다.

#app-store#api#supabase#reality-check
개념 도식: 한국어 휴대폰 요청의 Accept-Language 헤더가 locale detector를 거쳐 /ko로 리다이렉트되고, html lang=ko와 모바일 KO/EN 전환 버튼으로 검증되는 흐름
배포와 인프라7 분 읽기

한국어 폰인데 devlog 첫 화면이 영어였다

한국어로 설정된 휴대폰에서 dev.ootssu.com의 첫 화면이 영어로 열렸습니다. 글 번역 문제가 아니라 라우팅, 문서 lang, 모바일 전환 버튼이 동시에 만든 i18n 운영 버그였습니다. Accept-Language를 제대로 파싱하고, /ko 문서는 lang=ko로 렌더하고, 모바일에서도 언어 전환 버튼을 보이게 고쳤습니다.

#i18n#nextjs#locale#mobile
개념 도식: 왼쪽은 404 경보 메일이 쌓인 서치콘솔 받은편지함, 오른쪽은 검색봇 404가 app-ads.txt 하나뿐이고 사이트맵·내부링크 404가 0인 서버 로그
배포와 인프라12 분 읽기

서치콘솔이 404 메일을 퍼부었다. 로그를 보니 범인은 내 코드가 아니었다

구글 서치콘솔이 'Not found (404)' 메일을 수십 통 쏟아냈습니다. 사이트를 고치려다, 대시보드 대신 서버 로그를 봤습니다. 사이트맵 149개 전부 멀쩡, 내부 링크도 멀쩡, 검색봇이 실제로 404를 낸 URL은 전 로그 통틀어 딱 하나였습니다. 나머지는 전부 외부 유령이었습니다.

#first-principles#reality-check#seo#infra
개념 도식: completed 100%·listing saved·commit ok 초록 체크 셋이 쌓여 있는데, 앱 상태는 REJECTED이고 유저 기기엔 이전 버전이 떠 있다. completed는 트랙 배정이지 서빙이 아니다.
배포와 인프라8 분 읽기

completed 100%는 게시가 아니다

Android 릴리스를 API로 자동화하고 'completed 100%'면 배포 완료로 기록했습니다. 어느 날 리젝 메일이 왔는데 트랙 상태는 여전히 completed였습니다. API는 '트랙에 배정됐다'를 말할 뿐 '사람들이 받고 있다'를 말하지 않습니다. 데이터 보안 양식은 아예 API로 손댈 수도 없습니다.

#android#google-play#api#gotchas
개념 도식: 진단 빌드에선 배너가 100% 뜨고 정상 빌드에선 100% 안 뜬다. 빈 body → row 없음 → .task 안 돎 → 영원히 빈 상태로 순환.
배포와 인프라10 분 읽기

진단 코드를 넣으면 버그가 사라졌다

TestFlight에서만 안 뜨는 배너를 4시간 쫓았습니다. Debug/Release도, 서명도, 실기기도 범인이 아니었습니다. 범인은 제가 원인을 보려고 넣은 진단 코드 그 자체였습니다. 관측 장치가 관측 대상을 바꾸고 있었습니다.

#swiftui#ios#debugging#gotchas
개념 도식: 왼쪽은 테스트 모드에서 refresh token이 7일마다 만료돼 채널이 순차로 죽는 그림, 오른쪽은 프로덕션 게시 후 무기한이지만 소급되지 않는다는 단서
배포와 인프라8 분 읽기

내 유튜브 토큰이 매일 하나씩 죽었다 — OAuth '테스트' 모드의 7일 시한폭탄

채널 토큰이 하루에 하나씩 순차로 죽었습니다. 재발급 지옥(PKCE 불일치, 똑같은 이름의 브랜드 계정 여섯 개)을 지나 1원칙으로 근본을 찾으니, '재발급하면 됨'은 값싼 수정처럼 보이는 함정이었습니다. 진짜 원인은 OAuth 동의 화면의 게시 상태였습니다.

#oauth#youtube-api#tokens#gotchas
개념 도식: 서버 200 → JSON "2026-07-01" → ISO8601 디코더가 행 전체 throw → catch가 빈 배열 → 빈 화면. 앞 단계는 전부 초록 체크, 마지막만 빈 사각형.
배포와 인프라6 분 읽기

date 컬럼 하나가 화면 전체를 지웠다

지역 비교 카드가 통째로 사라졌습니다. 에러도 없고, RPC는 200이고, 데이터도 들어 있는데 화면만 비었습니다. 표시에 쓰지도 않는 date 필드 하나의 디코딩 실패가 decode 전체를 throw시켰고, catch가 빈 배열로 떨어졌습니다. 어느 단계에서도 빨간불이 안 켜집니다.

#ios#supabase#swift#gotchas
개념 도식: 대시보드엔 제각각인 entitlement ID가 늘어서 있고 코드는 'pro' 하나만 조회. 선이 딱 하나만 연결되고 나머지는 결제 성공·영수증 정상·에러 없음인데 끊겨 있다.
배포와 인프라7 분 읽기

결제는 됐는데 Pro가 안 켜졌다

영수증도 정상, 서버도 정상, 에러도 없는데 앱만 계속 free였습니다. 범인은 이름이었습니다. RevenueCat이 앱 이름 기반으로 entitlement를 자동 생성하는데, 코드는 'pro' 하나만 보고 있었습니다. 그리고 이 버그는 예외도 로그도 없이 조용히 실패합니다.

#ios#revenuecat#iap#gotchas
개념 도식: sealed class + data object 구조에서 keep rule 화살표는 child에 꽂히는데 R8의 가위는 base class에서 route를 자른다. enum으로 바꾸면 R8이 필드를 자동 보존해 keep rule 자체가 불필요.
배포와 인프라6 분 읽기

R8이 route 필드를 반복해서 지웠다

Android release 빌드가 세 버전에 걸쳐 같은 NPE로 죽었습니다. keep rule을 계속 넓혔고, 한 번은 잠잠해졌다가 다시 회귀했습니다. keep rule이 안 먹은 게 아니라, keep이 걸린 위치와 R8이 자르는 위치가 달랐습니다. 정답은 keep이 아니라 패턴을 바꾸는 것이었습니다.

#android#r8#kotlin#gotchas
개념 도식: reviewSubmission 껍데기가 REJECTED 상품 6개를 가두고 있고, 밖에서 appStoreVersion을 PREPARE_FOR_SUBMISSION으로 바꾸는 화살표가 튕겨 나온다. 껍데기를 취소하는 화살표 하나만 안쪽에 닿아 상품이 자동 복귀.
배포와 인프라8 분 읽기

`version not editable`의 주체는 버전이 아니었다

심사 리젝 후 상품 현지화를 고치려는데 409 'version is not editable'가 났습니다. 그래서 버전을 편집 가능하게 만들었더니 같은 409. 에러가 말하는 'version'은 appStoreVersion이 아니라, 아직 열려 있는 reviewSubmission 껍데기였습니다. App Store Connect API 함정 시리즈 1편.

#app-store-connect#api#ios#gotchas
개념 도식: 화면은 줌아웃마다 4배로 커지는데 조회 박스는 ±0.05°로 고정. z11에선 박스가 화면 안쪽에 완전히 갇힌다. 박스 밖은 '아직 안 옴'이 아니라 '요청조차 안 됨'.
배포와 인프라8 분 읽기

지도를 축소했더니 남의 영토가 사라졌다

지도를 축소하면 다른 유저의 영토가 안 보이고 버벅였습니다. 로딩이 느린 것처럼 보였는데, 기다려도 안 채워졌습니다. 조회 범위가 고정 bbox였습니다 — 화면은 줌마다 4배가 되는데 요청 범위는 그대로라, 어느 줌 아래로는 화면 가장자리가 서버에 요청조차 되지 않았습니다.

#android#supabase#postgis#gotchas