앱 43개를 다시 빌드하는 대신 좌표 하나를 옮겼습니다 — '이런 앱도 있어요'에 허브를 올리는 법
제 앱들은 화면 아래 '이런 앱도 있어요' 목록으로 서로를 소개합니다. 허브를 맨 위에 올리는 규칙을 코드로 만들었지만, 이미 토스에 나가 있는 개별 앱 43개는 그 전 코드라 다시 빌드해야 실립니다. 대신 앱마다 붙은 1차원 유사도 좌표에서 허브의 자리만 수록 앱 무리 가운데로 옮겼습니다. 시뮬레이션으로 자기 허브가 가까운 다섯 칸에 걸리는 앱이 38개 중 13개에서 19개로 늘었고, 좌표 하나로는 넓게 퍼진 무리를 다 덮을 수 없다는 한계도 보였습니다
Read in English I Moved One Coordinate Instead of Rebuilding 43 Apps — Getting the Hubs into "You Might Also Like"
제 앱들은 화면 아래에 “이런 앱도 있어요” 목록을 띄워 서로를 소개합니다. 서버 없이 사이트에 올린 앱 목록 파일 하나를 모든 앱이 실행할 때마다 받아 가는 구조입니다. 허브로 전환한 뒤에는 이 목록 맨 위에 허브가 나오는 게 맞습니다. 허브 하나에 비슷한 게임이 열 개씩 들어 있기 때문입니다. 이 글은 그 목표를 앱을 다시 빌드하지 않고 어디까지 이룰 수 있었는지에 대한 기록입니다.
목록은 어떻게 고르나
앱마다 0~999 사이의 숫자 좌표가 하나 붙어 있습니다. 태그가 얼마나 겹치는지로 앱 사이의 유사도를 계산한 뒤, 비슷한 앱끼리 가까운 숫자를 받도록 한 줄 위에 배치한 값입니다. 목록은 열 칸입니다.
- 다섯 칸은 내 좌표와 가까운 앱 다섯 개입니다.
- 다섯 칸은 나머지에서 하루 단위로 고정된 랜덤입니다.
허브를 맨 위에 올리는 새 규칙은 10월 26일 새벽 코드에 넣었습니다. 허브가 있으면 전부 위로 올리고 그 아래를 지금처럼 채웁니다. 다만 이 규칙은 앱 안의 코드라, 그 뒤에 빌드한 앱에만 실립니다. 토스에 나가 있는 개별 앱 43개는 모두 그 전에 빌드됐습니다.
다시 빌드하지 않은 이유
43개를 다시 빌드하면 검수를 43건 받아야 합니다. 그보다 큰 문제는 작업 트리에 아직 검증하지 않은 다른 변경이 섞여 있다는 점입니다. 목록 규칙 하나를 위해 빌드하면 그 변경까지 같이 나갑니다. 그래서 각 앱이 다음에 업데이트될 때 자연스럽게 실리게 두기로 했습니다.
그 사이에 할 수 있는 일을 찾다가, 사용자가 질문을 하나 던졌습니다. “앱을 다 빌드할 게 아니라 허브 좌표를 비슷한 앱 가까이 옮기면 되지 않나?”
허브 좌표만 옮긴다
옛 규칙에서도 허브는 다른 앱과 똑같이 좌표 거리로 골라집니다. 허브의 좌표가 수록 앱 무리 한가운데 있으면, 그 앱들의 “가까운 다섯 칸”에 허브가 들어갑니다. 바뀌는 것은 허브 아홉 개의 숫자뿐이고, 개별 앱끼리의 거리는 그대로라 유사도가 꼬이지 않습니다.
오히려 바로잡는 쪽에 가까웠습니다. 허브 좌표는 처음에 손으로 정한 값이었는데, 하루상식은 790이었지만 수록 앱인 매일 한국사는 215였습니다. 상식 퀴즈 앱들과 한참 떨어진 자리에 허브가 서 있었던 셈입니다.
고치기 전 (레지스트리 apps.csv, 필요한 칸만)
id,…,pos
hub-edu,…,790
daily-history,…,215
고친 후
id,…,pos
hub-edu,…,215
daily-history,…,215
새 좌표는 토스에 단독으로 나가 있는 수록 앱 좌표의 중앙값으로 정했습니다. 허브가 보여야 할 곳이 바로 그 앱들의 목록이기 때문입니다. 레지스트리에 있는 수록 앱 전부로 계산하면 Play 전용 앱까지 섞여 중심이 밀렸고, 시뮬레이션에서도 효과가 더 작았습니다.
| 허브 | 이전 | 이후 |
|---|---|---|
| 퍼즐카페 | 727 | 666 |
| 뇌나이 | 478 | 377 |
| 오락실 | 518 | 337 |
| 한판승부 | 654 | 707 |
| 올림픽 | 391 | 307 |
| 행운연구소 | 940 | 946 |
| 하루상식 | 790 | 215 |
| 복불복 | 148 | 153 |
시뮬레이션
토스 개별 앱 하나하나에 대해 옛 규칙의 “가까운 다섯 칸”을 계산해 봤습니다(아동 앱 두 개는 목록 규칙이 달라 뺐습니다).
| 다섯 칸에 허브가 하나라도 | 자기가 든 허브가 | |
|---|---|---|
| 손으로 정한 좌표 | 41개 중 23개 | 38개 중 13개 |
| 전체 수록 앱 중앙값 | — | 38개 중 15개 |
| 토스 수록 앱 중앙값 | 41개 중 29개 | 38개 중 19개 |
나머지 다섯 칸은 랜덤이라, 걸리지 않은 앱에서도 허브가 날마다 섞여 나올 수 있습니다.
한계
좌표가 한 줄짜리라 무리가 넓게 퍼진 허브는 한 점으로 다 덮을 수 없습니다. 퍼즐카페 수록 앱은 530부터 733까지, 한판승부의 보드게임 열여섯 개는 629부터 716까지 퍼져 있습니다. 가운데에 허브를 두면 양 끝의 앱에서는 다른 비슷한 앱들이 더 가까워 다섯 칸을 먼저 채웁니다. 반응속도, 탭 버드, 2048, 장기 같은 열아홉 개 앱이 여기에 해당하고, 이 앱들은 결국 새 규칙이 실린 빌드가 나가야 허브가 맨 위에 올라옵니다.
다시 계산해도 남게
좌표는 태그가 바뀌거나 앱이 늘 때마다 스크립트로 전부 다시 매깁니다. 허브 좌표만 손으로 고쳐 두면 다음 재계산 때 사라집니다. 그래서 “허브 좌표는 토스 수록 앱 좌표의 중앙값”이라는 규칙을 작은 스크립트로 만들고, 재계산 스크립트가 끝에서 그것을 부르게 했습니다. 앞으로 허브가 늘어도 손으로 정할 일이 없습니다.
고친 후 (registry/hub-pos.py 핵심)
1
2
3
4
5
6
7
8
9
def apply(rows, header):
col = {k: i for i, k in enumerate(header)}
pos = {r[col["id"]]: r[col["pos"]] for r in rows}
toss = {r[col["id"]] for r in rows if r[col["toss"]].strip() == "1"}
for hub, games in hub_members().items(): # hubs/<id>/hub.json 의 수록 게임
live = [g for g in games if g in toss] # 토스에 단독으로 나가 있는 수록 앱
ms = [int(pos[g]) for g in (live or games) if pos.get(g, "").isdigit()]
if hub in pos and ms:
set_pos(rows, hub, int(statistics.median(ms))) # 허브 좌표 = 중앙값
1
2
# compute-pos.py 끝 — 태그로 전부 다시 매긴 뒤 허브만 덮는다
subprocess.run([sys.executable, os.path.join(HERE, "hub-pos.py")], check=True)
고치기 전에는 이 단계가 없었습니다. 재계산 스크립트가 허브도 태그만 보고 자리를 매겼고, 손으로 바꾼 값은 다음 실행에서 덮였습니다.
목록 밖의 전용 카드 하나
같이 고친 게 하나 있습니다. 로또 앱 네 개는 ‘내 운’ 결과 아래에 로또 대신 저축으로 잇는 카드를 따로 띄웁니다. 그런데 토스에서 로또 대신 저축은 이제 행운연구소 허브 안에 있어서, 단독 앱으로는 목록에서 빠졌습니다. 카드는 “그 앱이 토스에 있을 때만” 뜨게 돼 있어서 통째로 사라졌습니다. 이제 그 자리에 행운연구소를 띄우고 허브로 연결합니다.
고치기 전 (promo 패키지 findSavingsApp)
1
2
3
const app = csv ? parseAppsCsv(csv).find((a) => a.id === SAVINGS_APP_ID) : undefined;
if (!app || !(platform === 'toss' ? app.toss : app.play) || !supportsLang(app, lang)) return null;
return app;
고친 후
ts const all = csv ? parseAppsCsv(csv) : []; const ok = (a: PromoApp | undefined): a is PromoApp => !!a && (platform === 'toss' ? a.toss : a.play) && supportsLang(a, lang); const app = all.find((a) => a.id === SAVINGS_APP_ID); // 'lotto-saving' if (ok(app)) return app; const hub = all.find((a) => a.id === SAVINGS_HUB_ID); // 'hub-fortune' return ok(hub) ? hub : null; 이건 앱 코드라 로또 앱들의 다음 빌드부터 실립니다.
정리
- 허브를 맨 위에 올리는 규칙은 코드에 있지만, 이미 나간 앱 43개에는 다음 빌드부터 실린다.
- 그 사이에는 데이터로 할 수 있는 만큼 했다. 허브 좌표를 수록 앱 무리 가운데로 옮겨, 자기 허브가 목록 위쪽에 걸리는 앱이 13개에서 19개로 늘었다.
- 좌표가 1차원이라 넓게 퍼진 무리는 덮지 못한다. 그 부분은 결국 빌드로 해결한다.
- 손으로 맞춘 값은 재계산에 지워지므로, 규칙을 스크립트에 넣어 두었다.
만든 앱들은 GAMES와 APPS에 있고, 일부는 브라우저에서 바로 해볼 수 있습니다. 소식은 이 블로그와 인스타그램(@fadongkwon.soft) (새 창에서 열림)에서 전해드립니다.
숫자는 2026년 10월 26일 레지스트리 기준 시뮬레이션 결과이고, 실제 목록은 하루 단위 랜덤 칸 때문에 날마다 조금씩 다릅니다.
