모임 앱을 마흔 번 고쳤다 — 기능마다 어떻게 만들었나
club313을 탭 순서대로 뜯어봤습니다. 비슷한 걸 만들려는 분이 가져갈 방법과, 거기서 틀린 것을 같이 적었습니다.
Summary
- 과제
- 기능이 쌓이는 동안 기록이 날짜별로 흩어져서, 어떤 기능을 어떻게 만들었는지 한 번에 볼 데가 없었다.
- 수단
- 버전 기록과 커밋을 기능 단위로 다시 묶어, 탭 순서대로 방법과 시행착오를 한 편에 정리했다.
DETAILED DESCRIPTION
club313은 친구 모임 운영 앱입니다. 5월 말에 첫 버전을 내고 9월까지 마흔 번 남짓 고쳤습니다. 그동안 기능 이야기를 날짜별로 따로 써왔는데, 비슷한 기능을 만들어 보려는 분에게는 날짜보다 기능 순서가 찾기 쉽겠다 싶어 한 편으로 합쳤습니다.
각 절은 대체로 세 가지로 되어 있습니다. 무엇을 하는가 → 어떻게 만들었나 → 어디서 틀렸나.
괄호 안의 v1.6.0 같은 것은 앱 버전 번호입니다. 날짜는 앱 변경 기록 기준입니다.
목차
- 처음 며칠
- 홈 — 상황별 인사말 · 날씨 카드
- 자산 — 거래 한 줄로 다시 계산하기 · 손익 식을 두 번 고친 일 · 통장 사이 이체 · 시세 · 마켓맵 · 그래프 스와이프 · 문자 붙여넣기
- 회비 — 앱을 안 열고 처리하기 · 이름이 다를 때 · 독촉과 송금
- 갤러리 — 사진 창고 옮기기 · 유튜브는 용량 0 · 공유 버튼에서 올리기 · 여러 장을 한 게시물로
- 캘린더 — 공휴일 · 하루 당겨진 날짜
- 월드컵 — 마감이 있는 기능 · 경우의 수
- 공통 — 관리자가 회원 화면 보기 · 알림 · 버전 붙이기 · 정해진 시각에 돌리기
- 남는 생각
1. 처음 며칠
5월 30일부터 6월 2일까지 커밋이 53개 있습니다. 앱을 처음 만든 기간입니다.
5월 31일에 첫 정식 버전(v1.0.0)을 냈는데, 이때 이미 로그인·홈·자산·회비·갤러리·캘린더·공지·투표·경조사·맛집·알림·홈 화면 설치가 전부 들어 있었습니다. 뼈대를 먼저 한 번에 세운 셈입니다. 그 뒤로는 있는 기능을 다듬으면서, 필요해진 기능을 하나씩 더했습니다. 아래는 그 이야기입니다.
같은 날 좌우로 밀어 탭을 넘기는 동작과, 아이폰에서 홈 화면 앱을 열 때 뜨는 시작 화면(스플래시)도 붙였습니다. 아이폰은 기기 해상도마다 시작 화면 이미지를 따로 줘야 합니다. 하나만 주면 안 뜹니다.
2. 홈
상황별 인사말 (7월 20일, v1.34.0~v1.35.1)
홈 맨 위의 한 줄 인사말입니다. 처음엔 고정 문구였습니다.
만드는 법 — 문장마다 태그를 붙여둡니다. 「봄」「3월」「아침」「밤」 같은 것입니다. 화면을 열 때 지금 계절·월·시간대에 맞는 태그가 붙은 문장만 추리고, 그중 하나를 무작위로 고릅니다. 문장은 100개쯤 됩니다.
여기에 두 가지를 더했습니다.
- 우선순위 — 오늘이 회원 생일이거나 명절·공휴일이면 그 문장을 먼저 씁니다(v1.35.0)
- 가중치 — 계절·시간대가 맞는 문장이 더 자주 나오게 했습니다(v1.35.1)
나중에 날씨 카드가 생기고 나서 날씨에 맞는 인사말도 넣었습니다(v1.38.0).
따라 만드신다면, 문장을 코드에 흩어놓지 말고 목록 하나에 태그와 함께 모아두는 게 편합니다. 문장을 더하는 일과 고르는 규칙을 고치는 일이 따로 놀게 됩니다.
날씨 카드 (7월 21일, v1.36.0~v1.40.0)
현재 날씨, 시간별, 주간 예보를 보여주는 카드입니다. 기상청 데이터를 씁니다.
붙이는 데는 반나절이면 됐습니다. 나머지 하루 반은 안 될 때를 막는 일이었습니다. 버전 아홉 번 중 여덟 번이 그쪽이었습니다.
사라지는 카드. 처음엔 「받아오면 그리고, 못 받아오면 안 그린다」였습니다. 그런데 못 받아오는 일이 생각보다 잦았습니다. 기상청 서버가 흔들리기도 하고, 호출 횟수 제한에 걸리기도 하고, 위치를 빠르게 바꾸면 앞선 응답이 늦게 와서 뒤엉켰습니다. 그때마다 카드가 통째로 사라졌습니다. 보는 사람은 고장인지 원래 그런 건지 알 수가 없습니다.
없어지는 것보다 「지금 안 된다」고 말하는 게 낫습니다. 실패해도 카드는 남기고 「다시 시도」 버튼을 보이게 했습니다.
한 곳만 믿지 않기. 기상청이 아예 안 되면 Open-Meteo(무료 해외 날씨)로 자동으로 넘어가게 했습니다(v1.39.0). 둘의 형식을 맞춰뒀으니 화면은 그대로이고, 어디서 온 값인지만 지역명 옆에 작게 적습니다. 예보가 조금 다를 수 있으니 어디 것을 보고 있는지는 알려주는 게 맞다고 봤습니다.
기다리게 하지 않기. 열면 GPS 응답을 기다리느라 한참 비어 있었습니다(v1.37.1에서 고침).
- 기본 위치를 고정값으로 두어 GPS 없이 일단 그리고
- 마지막으로 본 위치의 날씨를 기기에 저장해 다시 열면 즉시 보이게 하고
- 새 값은 뒤에서 받아 조용히 갈아 끼웁니다
지난 날씨를 잠깐 보여주는 셈인데, 빈 화면보다는 낫습니다.
동네 이름. 좌표만 있으면 「어디」인지 사람이 못 알아봅니다. 좌표를 「○○동」으로 바꿔주는 무료 서비스(BigDataCloud, 키 불필요)를 붙였습니다(v1.40.0). 이렇게 좌표에서 주소를 찾는 걸 역지오코딩이라고 부릅니다.
어이없던 것 하나. 한동안 하늘 상태가 **비만 안 오면 무조건 「맑음」**이었습니다. 비 여부만 보고 판단하고 있었습니다. 흐린 날에도 맑음이라고 떴는데, 창밖을 보면 바로 아는 것이라 아무도 말을 안 해준 게 더 이상했습니다.
| 같은 문제를 만나면 | |
|---|---|
| 증상 | 날씨 카드가 가끔 통째로 안 보이거나, 열자마자 한참 비어 있다 |
| 원인 | 한 곳에서만 받아왔고, 실패하면 카드를 안 그렸다. 위치는 매번 GPS 응답을 기다렸다 |
| 해결 | 실패하면 다른 무료 날씨로 넘어간다. 실패해도 카드는 남기고 「다시 시도」를 보인다. 기본 위치 고정 + 마지막 위치 캐시로 먼저 그린다 |
| 확인 | 네트워크를 끊고 열어도 카드 자리가 남고 「다시 시도」가 보이면 해결. 열자마자 지난 날씨가 보이면 캐시가 도는 것 |
3. 자산
모임이 같이 투자한 돈을 보여주는 탭입니다. 가장 공들인 곳이고, 가장 많이 틀린 곳입니다.
거래 한 줄로 전부 다시 계산하기 (6월 1일, v1.6.0)
처음엔 관리자 화면에 종목별 수량·평균단가를 직접 입력하는 칸이 있었습니다. 사면 수량을 늘리고, 평균단가를 다시 계산해 적고, 예수금도 따로 맞춥니다. 한 번 거래에 여러 군데를 고칩니다.
그래서 방식을 뒤집었습니다. 숫자를 고치지 않고, 거래를 적습니다.
- 관리자는 거래를 한 줄씩 적기만 합니다 — 매수·매도·입금·출금·분배금·이용료
- 앱은 기준점(처음 상태) + 지금까지의 모든 거래를 처음부터 끝까지 다시 돌려서 수량·평균단가·예수금·투자원금을 계산합니다
- 거래를 하나 넣을 때마다 이 계산을 통째로 다시 합니다
은행 통장이 잔액을 따로 적어두는 게 아니라 거래 내역을 더해서 보여주는 것과 같습니다. 회계에서 원장(元帳) 이라고 부르는 방식입니다.
계산 규칙은 이렇습니다.
- 평균단가 = 산 금액의 합 ÷ 산 수량의 합
- 수수료·세금은 평균단가에 넣지 않고 예수금에서만 뺍니다
거래를 넣거나 지울 때마다 날짜순으로 처음부터 다시 계산하므로, 거래를 나중에 끼워 넣어도 순서가 꼬이지 않습니다. 변경 기록에 적어둔 장점이 이것입니다.
손익 식을 두 번 고쳤다 (6월 1일, v1.6.3~v1.6.4)
원장으로 바꾼 날 투자손익 식도 두 번 고쳤습니다. 식이 한 번에 맞지 않았습니다.
처음 식 — 기간 손익 = 끝날의 종목 평가액 − 첫날의 종목 평가액. 그런데 이러면 돈을 넣어서 새로 산 주식까지 손익으로 잡힙니다. 입금이 곧 수익처럼 보여 금액이 부풀었습니다.
두 번째 식(v1.6.3) — 평가액 − 산 원가. 입금 문제는 풀렸는데, 이번엔 주식이 아니라 예수금으로 들어온 수익이 빠졌습니다. 분배금, 예탁금 이용료, 팔아서 확정된 손익이 그렇습니다.
마지막 식(v1.6.4)
투자손익 = 지금 평가액 + 예수금 − 투자원금
「지금 가진 것 전부」에서 「넣은 돈」을 빼는 식입니다. 이렇게 하면 분배금·이용료·실현손익이 따로 챙기지 않아도 저절로 들어갑니다. 돈이 어디로 갔든 결국 평가액이나 예수금 둘 중 한 곳에 있기 때문입니다.
모임이 따로 쓰던 엑셀 장부의 한 달 투자수익과 원 단위까지 맞는 걸 확인하고 멈췄습니다.
식을 바꾸면 과거 그래프도 다시 계산해야 합니다. 추이 그래프는 날마다 저장해둔 기록으로 그리는데, 그 기록은 옛 식으로 계산된 값입니다. 식을 바꿀 때마다 과거 기록 140여 개를 새 식으로 다시 채웠고, 입금하기 전 날짜의 점 몇 개는 지웠습니다. 따라 만드신다면 이미 믿는 장부 하나를 옆에 두고 맞춰보시길 권합니다. 맞출 대상이 없으면 틀린 줄도 모릅니다.
| 같은 문제를 만나면 | |
|---|---|
| 증상 | 입금한 달에 투자손익이 부풀거나, 분배금이 들어와도 손익이 그대로다 |
| 원인 | 평가액 차이로 계산하면 입금으로 산 주식이 손익에 섞이고, 평가액−원가로 계산하면 예수금으로 들어온 수익이 빠진다 |
| 해결 | 투자손익 = 평가액 + 예수금 − 투자원금 으로 바꾸고, 저장해둔 과거 기록도 새 식으로 다시 채웠다 |
| 확인 | 같은 날짜 기준으로 믿을 만한 장부와 원 단위까지 같으면 해결 |
그 사이 v1.6.1에서는 회비통장 예금이 마이너스로 나오는 버그도 고쳤습니다. 투자원금을 두 번 빼고 있었습니다. 그 뒤에도 수익률의 분모(무엇으로 나누나)를 두 번 더 다듬었습니다(v1.7.1, v1.8.2).
틀린 거래를 나중에 고친다 (9월 16일)
세금이나 수수료는 거래한 날 바로 확정되지 않을 때가 있습니다. 그래서 이미 적은 거래를 고치는 기능을 넣었습니다.
원장 방식이라 이게 쉬웠습니다. 한 줄을 고치면 처음부터 다시 계산하면 끝입니다. 숫자를 직접 고치는 방식이었다면 그 거래 뒤의 모든 숫자를 거꾸로 따라가 고쳐야 했습니다. 석 달 전에 방식을 뒤집어둔 게 여기서 돌아왔습니다.
같은 날 전부 팔아 수량이 0이 된 종목은 보유 목록과 마켓맵에서 숨겼습니다. 관리자가 거래를 고치는 화면에는 그대로 남겨뒀습니다 — 거기서까지 숨기면 고칠 수가 없습니다.
통장 사이 이체를 한 번만 적는다 (6월 1일, v1.7.0)
모임에는 회비통장과 투자통장이 따로 있습니다. 회비통장에서 투자통장으로 돈을 옮기면 양쪽 장부에 한 번씩 적어야 맞습니다. 한쪽만 적으면 두 장부가 어긋납니다.
그래서 투자 쪽에 입금이나 출금을 적으면 가계부에 반대 방향 한 줄이 자동으로 생기게 했습니다. 두 줄은 서로를 가리키는 번호로 묶어둡니다. 어느 거래에서 생긴 줄인지 나중에도 알 수 있습니다.
시세는 서버에서만 받는다 (6월 1일, v1.5.4)
처음엔 야후 파이낸스 시세를 썼는데 15분 늦은 값이었습니다. 네이버 증권 실시간 시세로 바꾸고, 네이버가 안 될 때만 야후로 넘어가게 했습니다. 받아온 값은 30초 동안 들고 있다가 다시 씁니다.
두 가지를 같이 했습니다.
- 시세 받기와 손익 계산은 전부 서버에서 합니다. 휴대폰 쪽 코드는 누구나 열어볼 수 있어서, 계산 방식과 요청 주소를 거기 두지 않았습니다
- 화면을 조각으로 나눠 준비되는 대로 보여줍니다. 데이터베이스에서 읽는 총액은 금방 오는데 실시간 시세는 느립니다. 한 화면으로 묶어두면 느린 쪽을 기다리느라 빠른 쪽도 못 봅니다
라이브러리 없이 마켓맵 (6월 16일, v1.22.0)
보유 종목을 사각형 타일로 나눠 보여주는 화면입니다. 주식 앱에서 흔히 보는 그것입니다.
- 타일 크기 = 그 종목의 평가액
- 타일 색 = 오늘 오르내린 비율
그래프 도구(라이브러리)를 가져다 쓰지 않고 직접 그렸습니다.
따라 만드신다면 「사각형을 어떤 순서로 나눌지」가 핵심입니다. 그냥 차례로 자르면 가늘고 긴 타일이 생겨 알아보기 어렵습니다. 「squarified treemap」 으로 찾아보시면 타일을 되도록 정사각형에 가깝게 나누는 널리 알려진 방법이 나옵니다.
보유 목록과 마켓맵은 버튼 하나로 오갑니다.
그래프를 밀면 탭이 넘어갔다 (6월 16일, v1.22.1~v1.22.2)
자산 추이 그래프를 좌우로 밀어 지난 기간을 보는 기능을 넣었습니다. 그런데 앱 전체에 좌우로 밀면 옆 탭으로 넘어가는 동작이 이미 있었습니다. 그래프를 밀었더니 탭이 넘어갔습니다.
같은 손짓을 두 곳이 서로 가지려 한 것입니다. 두 번에 걸쳐 둘이 부딪히지 않게 갈라놨습니다. 손짓 기능을 하나 더 넣을 때는 이미 같은 손짓을 쓰는 곳이 있는지부터 보는 게 좋습니다.
증권사 문자를 붙여넣으면 채워진다 (6월 말, v1.30.0)
체결·배당 문자를 붙여넣으면 거래 종류·종목·수량·금액이 채워집니다. 가족 자산 트래커에 먼저 넣고 이쪽으로 옮긴 기능이라, 만든 이야기는 그쪽 기록에 있습니다. 두 앱에 같은 기능을 하루 차이로 넣게 된 사정은 같은 기능을 하루 차이로 두 앱에 넣었다에 적었습니다.
4. 회비
앱을 안 열고 처리한다 — 아이폰 단축어 (6월 12일, v1.9.0v1.10.0)
회비가 들어오면 앱을 열고, 회비 탭에 가서, 그 사람을 찾아 「납부」를 눌러야 했습니다. 매달 여러 번 하는 일입니다.
그래서 아이폰 단축어(Shortcuts) 로 앱을 거치지 않게 했습니다. 단축어는 아이폰에 기본으로 있는 자동화 앱입니다. 정해진 순서대로 동작을 이어 붙여 버튼 하나로 돌립니다.
- 납부 처리 — 단축어가 앱 서버의 납부 처리 전용 주소로 「누가 냈다」를 보냅니다. 아무나 이 주소를 부르면 안 되니 서버와 단축어만 아는 비밀값을 같이 보내게 했습니다. 비밀값이 안 맞으면 서버가 무시합니다
- 독촉 보내기 — 미납자 옆 「바로전송」을 누르면 그 사람 이름이 붙은 단축어가 돌아 카카오톡으로 안내를 보냅니다(v1.9.0)
따라 만드신다면 요점은 「앱 밖에서 부를 수 있는 입구를 하나 만들고, 그 입구에 자물쇠를 건다」 입니다. 자물쇠(비밀값)는 코드에 적지 말고 서버 설정에 둡니다.
은행 이름과 앱 이름이 다르다 (6월 초, v1.11.0)
은행 입금자명은 본명으로 찍히는데, 앱에는 회원이 영문 이름으로 들어가 있었습니다. 이름으로 사람을 찾으면 안 맞습니다.
회원마다 별칭 목록을 둬서, 입금자명이 별칭 중 하나와 맞으면 그 회원으로 잡게 했습니다. 이름으로 사람을 찾는 기능을 만드신다면 처음부터 이름이 여러 개일 수 있다고 두는 게 편합니다.
독촉과 송금, 그리고 「냈어요」 (v1.4.0, v1.8.0)
- 미납 회원에게 보내는 카톡 안내에 계좌와 토스 송금 링크를 같이 넣습니다(v1.8.0). 로그인하지 않아도 열리는 송금 안내 페이지를 따로 두었습니다
- 반대 방향도 있습니다. 회원이 먼저 「냈어요」 확인 요청을 보내면 관리자에게 알림이 갑니다(v1.4.0)
- 가계부 금액을 1원 단위까지 받게 했습니다(v1.30.1). 통장 이자처럼 몇 원짜리 자투리를 적을 수 없었습니다
5. 갤러리
사진 창고를 옮겼다 (6월 22일, v1.29.0 · v1.29.1 · v1.29.3)
쓰다 보니 사진 저장 공간 1GB가 차 갔습니다. 늘리면 그때부터 요금이 붙습니다.
무엇을 보고 골랐나. 저장 공간은 보통 용량만 보고 고릅니다. 그런데 사진은 올리는 것보다 보는 게 훨씬 많습니다. 한 번 올린 사진을 열 명이 열 번씩 봅니다. 그러면 진짜 비용은 내려받는 트래픽입니다. 용량은 한 번 사면 끝인데 트래픽은 볼 때마다 붙습니다.
Cloudflare R2는 내려받는 데 요금이 없고, 무료로 10GB를 줍니다. 사진첩처럼 올린 걸 두고두고 보는 데는 이만한 게 없었습니다.
어떻게 옮겼나. 기존 사진이 그대로 보여야 했습니다. 옮겼다고 예전 사진이 안 보이면 그건 옮긴 게 아니라 버린 것입니다.
요령은 파일 이름(저장 경로)을 그대로 복사한 것입니다. 데이터베이스에는 사진마다 「어디에 있는지」가 적혀 있는데, 새 창고에 같은 이름으로 넣었으니 데이터베이스는 한 글자도 안 고쳤습니다. 바뀐 건 「어느 창고에서 꺼내나」 한 줄뿐입니다.
창고는 비공개로 두고, 사진을 볼 때만 잠깐 유효한 서명 주소를 발급합니다. 주소가 밖에 새어도 금방 못 쓰게 됩니다.
옮기고 나서 걸린 것 둘. 이런 건 옮기고 나서야 드러납니다.
- 단축어로 올린 큰 사진이 실패했습니다(v1.29.1). 단축어 사진은 앱 서버가 받아서 창고에 넣는데, 이때 사진 전체의 지문(해시)을 계산해 서명에 넣는 방식을 쓰고 있었습니다. 이 방식이 서버에서 몇 MB짜리 사진을 만나면 실패했습니다. 그래서 서버도 화면에서 올릴 때와 똑같이 서명 주소를 받아 평범하게 올리는 방식으로 바꿨습니다
- 「저장」 버튼이 안 됐습니다(v1.29.3). 브라우저가 창고에서 사진을 직접 받아오다가 막혔습니다. 브라우저는 다른 주소(출처)에서 파일을 가져오는 걸 기본적으로 막는데(CORS라고 부릅니다), 여기에 걸린 것으로 봤습니다. 앱 서버가 대신 받아서 같은 주소로 내려주게 바꿨습니다
지금은 올릴 때는 서명 주소, 저장할 때는 앱 서버를 거쳐서 갑니다. 둘은 하는 일이 달라서 둘 다 남아 있습니다.
두 달 뒤. 8월에 업무 도구의 실행파일을 나눠줄 곳이 필요해졌을 때 다시 R2를 골랐습니다. 이번엔 처음 알아보는 게 아니었습니다. 한 번 올려두고 여러 사람이 받아 간다는 점에서 사진과 실행파일이 같았습니다.
유튜브는 용량 0 (6월 1314일, v1.16.0v1.17.0)
영상은 사진보다 훨씬 큽니다. 그래서 영상은 올리지 않고 유튜브 링크를 붙이게 했습니다. 링크를 붙이면 앱 안에서 유튜브 플레이어로 재생됩니다. 창고에는 아무것도 안 쌓입니다.
재생목록 링크를 붙이면 목록 전체를 한 번에 가져옵니다(v1.17.0). 이건 유튜브의 공식 API(YouTube Data API)로 목록을 읽어옵니다. 읽기만 되는 키를 발급받아 서버에만 두었습니다. 휴대폰 쪽 코드에 두면 누구나 꺼내 쓸 수 있습니다.
공유 버튼에서 바로 올린다 (6월 22일, v1.28.0)
사진 앱에서 사진을 고르고 공유 → 단축어를 누르면 갤러리에 올라갑니다.
단축어는 사진을 한 장씩 따로 보냅니다. 앱 서버가 한 번에 받을 수 있는 크기(4.5MB)를 넘지 않게 하려는 것입니다. 그런데 그러면 여러 장을 골랐을 때 게시물이 사진 수만큼 생깁니다. 그래서 20초 안에 들어온 사진은 한 게시물로 묶게 했습니다. 「같이 고른 사진」인지를 들어온 시간으로 판단하는 셈입니다.
여러 장을 한 게시물로 (6월 16일, v1.19.0)
처음엔 사진 한 장이 곧 게시물 하나였고, 좋아요·댓글도 사진마다 달렸습니다. 여러 장을 한 게시물로 묶어 좌우로 넘겨 보게 바꾸면서 좋아요·댓글을 게시물 단위로 옮겼습니다.
이런 변경은 화면보다 데이터 구조를 바꾸는 쪽이 일입니다. 이미 달린 좋아요와 댓글이 있으니까요. 그 변경을 번호 붙인 파일(데이터베이스 변경 19번)로 남겨뒀습니다.
그 뒤 꾹 눌러 끌어서 순서 바꾸기와 글자 크기 조절을 넣었습니다(v1.25.0).
6. 캘린더
공휴일 (v1.18.0, v1.26.0)
모임 일정 옆에 대한민국 공휴일을 같이 띄웁니다. 공휴일은 무료 공개 서비스(date.nager.at)에서 받아옵니다. 서비스가 안 될 때를 대비해 2026년 공휴일은 앱에 직접 적어뒀습니다.
처음엔 대체공휴일이 빠져 있었습니다(v1.26.0에서 넣음). 공휴일 목록을 받아 쓰신다면 대체공휴일이 들어 있는지 꼭 확인하세요.
공지에 일정을 연결해두면 캘린더에서 눌러서 그 공지로 갑니다(v1.12.0).
하루 당겨진 날짜 (v1.8.2)
경조사 날짜가 하루 앞당겨져 보였습니다.
6월 3일로 등록했는데 6월 2일로 보이는 식이었습니다. 원인은 시간대였습니다.
저장된 날짜는 멀쩡했습니다. 처음부터 「한국 시간 0시」로 만들고 있었습니다. 문제는 화면에 날짜를 글자로 찍을 때 어느 시간대 기준인지 지정하지 않은 것이었습니다. 화면은 서버에서 그리는데, 서버는 세계 표준시(UTC) 로 돕니다. 한국 시간 6월 3일 0시는 세계 표준시로 6월 2일 오후 3시라, 서버는 그걸 6월 2일로 찍었습니다.
날짜를 글자로 바꾸는 자리에 「한국 시간 기준」 한 줄을 넣어 고쳤습니다. 같은 원인이 있던 투표의 날짜 표시도 같이 고쳤습니다. 날짜를 다루는 기능에서 가장 흔하게 만나는 문제입니다. 내 휴대폰에서는 멀쩡하고 서버에서만 틀리니 더 늦게 발견됩니다.
| 같은 문제를 만나면 | |
|---|---|
| 증상 | 저장한 날짜가 화면에서 하루 앞(또는 뒤)으로 보인다 |
| 원인 | 서버(세계 표준시)에서 날짜를 글자로 찍으면서 시간대를 지정하지 않았다. 한국 0시가 전날 15시로 읽혔다 |
| 해결 | 날짜를 찍는 자리마다 한국 시간(Asia/Seoul) 기준을 명시했다 (v1.8.2) |
| 확인 | 서버에서 그린 화면(배포된 앱)에서 등록한 날짜가 그대로 보이면 해결. 내 컴퓨터에서 띄운 앱은 한국 시간이라 원래 멀쩡하게 보인다 |
7. 월드컵
마감이 있는 기능 (6월 12일, v1.14.0)
월드컵이 다가오자 친구들끼리 승부예측을 하면 재미있겠다 싶었습니다. 문제는 대회가 시작하면 늦는다는 것이었습니다. 개막하고 나서 예측을 열면 소용이 없습니다. 6월 12일에 우리나라 경기가 있었고, 그 전에 열어야 했습니다.
평소엔 하나를 붙이고 다듬고, 마음에 안 들면 되돌립니다. 이번엔 그럴 시간이 없어서 순서부터 정했습니다.
- 일정·조편성이 없으면 아무것도 안 된다 → 먼저
- 예측은 경기 전에 열려야 한다 → 그다음
- 하이라이트는 경기가 끝나야 쓸모가 있다 → 나중에
무엇을 먼저 할지 정하고 나니 「이게 지금 필요한가」로 대부분 답이 나왔습니다.
만들면서 챙긴 것들
- 경기 정보 — 네이버 스포츠에서 받아옵니다. 공식 API가 아니라 언제든 바뀔 수 있다는 걸 알고 썼고, 90초씩 들고 있습니다
- 예측 마감은 서버 시계로 — 경기 시작 시각이 지나면 예측을 잠급니다. 이걸 휴대폰 시계로 판단하면 폰 시간을 돌려서 늦게 찍을 수 있습니다. 그래서 서버가 판단합니다
- 정산은 가계부로 — 참가비와 상금을 정산하면 가계부에 자동으로 한 줄이 들어갑니다
- 하이라이트 — 직접 만들 수는 없으니 남이 올린 영상을 끌어다 앱 안에서 재생합니다. 처음엔 치지직 영상을 걸었다가 JTBC 유튜브로 바꿨습니다(v1.21.x)
대회 중에도 규칙을 여러 번 고쳤습니다. 승패만 맞히던 걸 스코어 정확히 맞히기 + 총상금 나눠갖기로 바꿨고(v1.15.0), 예측을 언제 공개하고 언제까지 바꿀 수 있는지도 정했습니다(v1.24.x). 대회가 끝나고는 이벤트 탭을 옆 메뉴로 보관했습니다(v1.33.0).
경우의 수 (v1.31.0~v1.32.0)
조별리그가 끝나갈 무렵 경우의 수가 단톡방 이야깃거리가 됐습니다. 서로 다른 계산이 오갔고, 누구 말이 맞는지 아무도 몰랐습니다.
앱에 순위표는 이미 있었습니다. 그런데 순위표는 지금 상태를 보여줄 뿐이고, 사람들이 궁금해한 건 「어느 경기에서 어느 나라가 이겨야 우리가 올라가나」 였습니다.
만드는 법은 단순합니다. 다 돌려봅니다. 남은 경기마다 결과가 승·무·패 셋이니, 경기 수만큼 곱하면 모든 경우가 나옵니다. 그 모든 경우에 순위 규칙(승점, 골득실, 동률일 때 규칙)을 적용해 우리가 올라가는 경우만 골라 보여줍니다.
똑똑한 방법은 아닙니다. 그런데 경우가 몇십 개뿐이면 이게 제일 정확하고 제일 빨리 만들어집니다. 규칙을 사람이 요약해 「이러면 되고 저러면 안 된다」로 적으면 반드시 빠뜨립니다. 빠뜨릴 자리를 아예 안 만드는 쪽을 골랐습니다.
그 뒤로 단톡방에서 계산 이야기가 사라졌습니다. 화면을 열어보면 되니까요. 토너먼트로 넘어가서는 대진표를 고쳤습니다. 앱이 대회를 따라다닌 셈입니다.
8. 공통
관리자가 회원 화면을 본다 (6월 4일, v1.13.0)
관리자는 편집 버튼이 잔뜩 보이는 화면을 씁니다. 그래서 회원에게는 앱이 어떻게 보이는지 알 수가 없었습니다. 따로 회원 계정을 만들어 로그인하기는 번거롭습니다.
그래서 「회원 모드로 보기」 스위치를 넣었습니다. 켜면 편집 버튼이 사라지고 회원 화면이 됩니다.
여기서 중요한 건 이 스위치가 화면만 바꾼다는 점입니다. 스위치 상태는 브라우저에 작은 표시(쿠키)로만 남기고, 데이터베이스의 권한 규칙(RLS)은 건드리지 않았습니다.
- 「무엇을 보여줄까」는 화면이 정합니다
- 「무엇을 허락할까」는 데이터베이스가 정합니다
그래서 회원 화면으로 보고 있는 동안에도 데이터 규칙은 관리자 그대로입니다. 보이는 것을 바꾸는 일과 허락을 바꾸는 일을 같은 스위치에 묶지 않았습니다.
알림 (5월 31일6월 1일, v1.0.1v1.5.1)
앱스토어 앱이 아니어도 웹 푸시로 휴대폰에 알림을 보낼 수 있습니다. 아이폰은 홈 화면에 추가한 뒤에만 알림이 옵니다. 안내할 때 이걸 먼저 말해야 합니다.
회원이 무엇을 받을지 고르게 했습니다 — 회비, 공지, 투표, 일정, 사진. 처음엔 투표 마감 30분 전 알림도 있었는데 정확한 시각에 안 와서 뺐습니다(v1.5.3). 안 맞는 알림은 없는 알림보다 나쁩니다.
버전 붙이기 (v1.27.0)
버전 번호는 세 자리로 붙입니다. 뒤 = 고친 것, 가운데 = 새 기능, 앞 = 큰 변화. 배포할 때마다 세 군데(앱 안의 버전 표시, 패키지 설정, 변경 기록 문서)를 같이 올립니다.
옆 메뉴의 버전 번호를 누르면 무엇이 바뀌었는지 창이 뜹니다(v1.27.0). 회원도 앱 안에서 바뀐 것을 볼 수 있습니다.
틀린 것 하나. 버전 기록의 날짜와 코드 저장 기록(커밋)의 날짜가 일부 다릅니다. 날씨 카드는 7월 21일에 작업했는데 커밋은 8월 29일에 들어가 있습니다. 작업해두고 한참 뒤에 저장한 것입니다. 그래서 이 글의 날짜는 버전 기록 기준으로 적었습니다. 나중에 「언제 했지」를 찾을 일이 있다면 작업한 날 바로 저장해두는 게 좋습니다.
이 앱은 코드를 GitHub에 올리지 않고 제 컴퓨터에만 기록을 쌓습니다. 배포도 GitHub를 거치지 않고 제 컴퓨터에서 Vercel로 바로 올립니다.
정해진 시각에 돌리기
매일 오후 4시에 그날의 자산을 기록하고 알림을 보냅니다. 자산 추이 그래프는 이 기록으로 그립니다.
정해진 시각에 뭔가를 돌리는 걸 크론(cron) 이라고 부릅니다. Vercel에서는 무료로 두 개까지만 걸 수 있어서, 기록과 알림을 하나로 합쳤습니다. 무료 한도가 있는 곳에서는 「하는 일마다 하나씩」이 아니라 「같은 시각에 하는 일끼리 하나로」 묶는 게 요령입니다.
9. 남는 생각
기능을 붙이는 일과 그 기능이 안 될 때를 다루는 일은 크기가 다릅니다. 날씨 카드도, 시세도, 사진 업로드도 붙이는 건 금방이었고 나머지가 길었습니다.
방식을 한 번 뒤집어두면 나중에 돌아옵니다. 거래를 적고 다시 계산하는 쪽으로 바꾼 게 석 달 뒤 「거래 고치기」를 거저로 만들어줬습니다.
필요해진 다음에 만들면 무엇이 필요한지가 이미 정해져 있습니다. 경우의 수는 조별리그가 끝나갈 때 필요했고, 그 전에는 필요 없었습니다.
취미로 만든 것과 일로 만든 것은 같은 데서 막힙니다. 사진을 어디에 둘지 고른 경험이 두 달 뒤 업무 도구 배포에 그대로 쓰였습니다.