이 글은 Today I Learn 시리즈의 84번째 기록입니다. (총 125개)

첫 출시를 배선하며, 그리고 낸 뒤 터진 것들을 고치며 나온 것들.

오프라인에서 완전할 것

설치해서 쓰는 앱은 인터넷 없이도 그대로여야 한다

웹폰트를 외부 CDN(파일을 여러 지역에 나눠 두고 가까운 곳에서 내려주는 서버망)에서 받아 오게 하면 편하다. 웹에서는 흔한 방식이다. 그런데 설치해서 쓰는 앱이 그러면, 인터넷이 없는 자리에서 글자 모양이 달라지거나 화면이 늦게 뜬다. 공연장에서 쓰는 앱이라 그 자리가 하필 제일 중요한 순간이다.

그래서 폰트 파일을 앱 안에 같이 번들했다(자체 호스팅). 용량이 조금 늘지만 밖에 아무것도 안 물어본다.

설치형 앱을 만들 때는 “인터넷이 끊긴 상태에서 이게 되나”를 기능마다 한 번씩 물어야 한다. 웹에서 옮겨 온 습관 중에 밖을 부르는 게 생각보다 많이 섞여 있다.

곁가지로 알게 된 것 — 영문과 한글에 서로 다른 글꼴을 쓰려고 언어를 판별할 필요는 없다. font-family에 글꼴을 순서대로 적어 두면(폰트 스택) 브라우저가 글자마다 앞에서부터 찾고, 없으면 다음으로 넘어간다. 영문 글꼴에 한글이 없으니 자동으로 갈린다.


돈을 받은 뒤

결제 직후 화면은 전부 다시 그려야 한다

유료를 활성화했더니 워터마크는 바로 사라졌는데, 내보내기 쪽 유료 옵션은 잠긴 채로 남았다. 화면에는 계속 자물쇠가 붙어 있었다.

이유는 화면마다 상태를 보는 시점이 달라서였다. 어떤 것은 매 순간 다시 확인하고, 어떤 것은 앱을 켤 때 한 번 정해 놓고 안 바꾼다. 후자는 결제가 끝나도 스스로 안 바뀐다.

여기서 무서운 건 버그 자체가 아니라 사용자에게 보이는 그림이다. 방금 돈을 냈는데 절반만 풀리면, 사용자는 “결제가 잘못됐나” 아니면 “속았나”를 의심한다. 다른 버그는 불편이지만 이건 신뢰 문제다.

그래서 결제·해제처럼 권한이 바뀌는 순간에는 관련 화면을 하나하나 되짚지 말고 통째로 다시 그리는 쪽이 안전하다. 성능을 아낄 자리가 아니다.

유료 경계는 기술이 이미 그어 준 선에 맞춘다

유료 기능을 어디까지 열지 정하다가, 유료를 앱 버전 전용으로 두기로 했다. 처음엔 웹에서도 일부 유료를 풀 생각이었는데 정리하면서 바꿨다.

근거는 이렇다. 유료의 핵심인 투명 배경 추출과 저지연 라이브 연주는 웹 기술로는 원래 안 된다. 돈 내는 사람은 그 기능 때문에 어차피 앱을 쓴다. 그러면 경계를 “앱이냐 웹이냐”에 맞추는 게 설명하기도 쉽고 예외도 안 생긴다.

유료 경계를 그을 때 임의로 선을 긋는 것보다, 기술이 이미 갈라 놓은 곳에 선을 맞추면 사용자에게 설명할 말이 저절로 생긴다. “앱에서만 되는 기능”은 납득되지만 “웹에서는 일부러 막아 둔 기능”은 반발을 산다.

함께 알아 둔 것 — 라이선스 하나로 몇 대까지 쓸 수 있는지는 앱 코드가 아니라 결제사(Creem) 상품 설정의 활성화 한도에 있다. 한도를 바꾸는 데 앱을 다시 내보낼 필요가 없다. 정책 숫자가 코드 밖에 있으면 이런 자유가 생긴다.


출시하고 나서 안 것

서명했다고 설치 경고가 사라지지 않는다

자동 업데이트를 위해 서명 키를 만들고 서명해서 배포했다. 그런데도 설치할 때 윈도우가 “알 수 없는 게시자”라고 경고했다.

서명이 두 종류인 걸 그때 알았다. 하나는 업데이트 파일이 중간에 바뀌지 않았는지 우리 앱이 검증하는 minisign 서명이고, 다른 하나는 운영체제가 “이 프로그램을 믿을지” 판단하는 코드 서명(Authenticode)이다. 설치할 때 경고를 띄우는 윈도우 기능(SmartScreen)이 보는 건 뒤엣것이다. 앞쪽은 무료로 만들 수 있고 뒤쪽은 인증서를 사야 한다.

경고를 없애려면 돈이 든다. 초기에는 안내 문구로 넘기고, 매출이 붙으면 인증서를 사는 순서로 정했다. 여기서 배운 건 출시 준비를 할 때 “무료로 되는 것”과 “돈을 내야 되는 것”을 미리 갈라 두는 것이다. 둘을 뭉뚱그리면 다 해 놨는데 마지막에 막힌다.

업데이트 경로의 버그는 스스로 못 고친다

릴리스를 냈는데 기존 사용자에게 업데이트 안내창이 안 떴다. 업데이트를 찾긴 찾았는데 “지금 설치할까요”를 묻는 창이 앱 환경에서 안 뜨고 바로 취소로 처리돼서, 업데이트가 조용히 넘어가고 있었다. 앞선 TIL 22의 확인창이 앱에서 안 뜨던 것과 같은 문제였다.

문제는 이 버그가 이미 사용자 컴퓨터에 깔린 옛 버전 안에 박혀 있다는 것이다. 고친 코드는 새 버전에 있는데, 새 버전을 받으려면 옛 버전의 업데이트 기능이 동작해야 한다. 그 사용자는 한 번은 손으로 설치해야 한다.

업데이트·설치·결제처럼 “자기 자신을 갱신하는 통로”에 있는 버그는 다른 버그와 급이 다르다. 원격으로 고칠 수 없고 사용자에게 수동 조치를 부탁해야 한다. 그래서 이 통로는 출시 전에 제일 많이 밟아 봐야 하고, 여기서만큼은 화려한 방식보다 확실히 동작하는 방식을 고르는 게 맞다.

바깥에 내보내는 주소는 안 변하는 이름으로 만든다

판매 페이지의 다운로드 버튼이 설치 파일을 바로 받게 걸어 뒀는데, 파일 이름에 버전 번호가 들어 있었다. 다음 버전을 내면 그 링크가 깨진다.

그래서 설치 파일을 버전 없는 고정된 이름으로도 함께 올리고, 링크는 늘 그 이름을 가리키게 했다. 바깥에 나간 주소는 우리가 회수할 수 없으니, 안 변하는 것으로 만들어 놓고 그 뒤에서 내용을 바꾸는 게 원칙이다.

같은 원칙이 파일 이름뿐 아니라 글 주소·공유 링크에도 그대로 적용된다. 밖으로 나가는 이름에 버전이나 날짜나 순번을 넣는 순간, 나중에 정리할 자유를 잃는다.


요약

  • 설치형 앱은 인터넷이 끊겨도 그대로여야 한다 — 밖을 부르는 습관이 웹에서 따라온다
  • 권한이 바뀌는 순간에는 화면을 통째로 다시 그린다. 절반만 풀리면 사용자는 결제를 의심한다
  • 유료 경계는 기술이 이미 갈라 놓은 곳에 맞춘다 — 설명할 말이 저절로 생긴다
  • 서명에도 종류가 있다. 출시 전에 무료로 되는 것과 돈 내야 되는 것을 갈라 둔다
  • 자기 자신을 갱신하는 통로의 버그는 원격으로 못 고친다 — 여기만큼은 확실한 방식으로
  • 바깥에 나간 주소는 회수할 수 없다. 안 변하는 이름을 먼저 만든다

Series: Today I Learn

1 C++ 자료형(Data Type) 2 MD5 vs pHash 3 C++에서 함수의 선언과 정의 4 Tkinter padx, pady 5 메모리와 포인터 변수 6 Call by Value, Call by Reference, Call by Pointer 비교 7 const 8 Gemfile — Jekyll 프로젝트의 의존성 파일 9 kramdown-parser-gfm — Jekyll의 GFM 파서 10 파서(Parser) 11 AHU vs OHU 12 I might try it vs I'll try it 뉘앙스 차이 13 SESSION_EXPIRE_AT_BROWSER_CLOSE=True 14 configuration key 15 Git stash vs discard 16 subprocess.Popen으로 Windows 탐색기에 명령어를 전달 17 Post 잔디 분석하기 18 Google Sheets Sync 최적화 19 DSL (Domain Specific Language)과 GPL (General Purpose Language) 20 마크다운 표 그리는 방법 21 쿼리 파라미터(Query Parameter). 기존 QR코드 재활용 22 Django 보안 취약점 점검 및 수정 23 OOP Object-Oriented Programming 객체 지향 프로그래밍 24 Fernet 대칭 암호화 25 Jekyll 코드블록 안의 Liquid 태그 26 insertOnConflictUpdate vs DoUpdate(target) 27 세션 필터 28 아코디언(Accordiaon) UI를 펼친상태로 만들기 29 input의 step 30 Word Cloud 31 Google Sheets를 데이터 버스로(with AppSheet) 32 Django 모델 텍스트 필드 자동 수집 패턴 33 localStorage로 섹션 토글 상태 유지 34 순차 ID 생성(`select_for_update()` + `max()` 조합) 35 역참조 검색과 distinct() 36 xlsx 다운로드와 로딩 오버레이 충돌 37 Android 파일 공유 MIME 타입 38 AssetManifest — Flutter 빌드 타임 asset 목록 런타임 조회 39 UTF-8 BOM과 PowerShell 파일 쓰기 40 소리꽃 KeyBloom TIL 1 41 소리꽃 KeyBloom TIL 2 42 소리꽃 KeyBloom TIL 3 43 메트로놈 Simple Metronome TIL 1 44 소리꽃 KeyBloom TIL 4 45 메트로놈 Simple Metronome TIL 2 46 소리꽃 KeyBloom TIL 5 47 메트로놈 Simple Metronome TIL 3 48 정적 블로그 SEO 정비와 Pagefind 검색 도입 49 메트로놈 Simple Metronome TIL 4 50 안드로이드 AudioTrack 연속 재생, 실측 피커 정렬, 카메라 토치 플래시 51 메트로놈 Simple Metronome TIL 5 52 모래게임 Sandrop TIL 1 53 메트로놈 Simple Metronome TIL 6 54 모래게임 Sandrop TIL 2 55 모래게임 Sandrop TIL 3 56 메트로놈 Simple Metronome TIL 7 57 모래게임 Sandrop TIL 4 58 모래게임 Sandrop TIL 5 59 모래게임 Sandrop TIL 6 60 소리꽃 KeyBloom TIL 6 61 모래게임 Sandrop TIL 7 62 모래게임 Sandrop TIL 8 63 소리꽃 KeyBloom TIL 7 64 모래게임 Sandrop TIL 9 65 소리꽃 KeyBloom TIL 8 66 모래게임 Sandrop TIL 10 67 소리꽃 KeyBloom TIL 9 68 소리꽃 KeyBloom TIL 10 69 소리꽃 KeyBloom TIL 11 70 소리꽃 KeyBloom TIL 12 71 소리꽃 KeyBloom TIL 13 72 온실 GreenHouse TIL 1 73 소리꽃 KeyBloom TIL 14 74 온실 GreenHouse TIL 2 75 소리꽃 KeyBloom TIL 15 76 소리꽃 KeyBloom TIL 16 77 소리꽃 KeyBloom TIL 17 78 소리꽃 KeyBloom TIL 18 79 소리꽃 KeyBloom TIL 19 80 소리꽃 KeyBloom TIL 20 81 로그스톤 상표 셀프 출원 TIL 1 82 소리꽃 KeyBloom TIL 21 83 소리꽃 KeyBloom TIL 22 84 소리꽃 KeyBloom TIL 23 읽는 중 85 소리꽃 KeyBloom TIL 24 86 MiniMacro TIL 1 87 Astro가 무엇인지, 왜 옮기는지 88 MiniMacro TIL 2 89 메트로놈 Simple Metronome TIL 8 90 메트로놈 Simple Metronome TIL 9 91 블로그 Astro 이관 TIL 92 오픈데이 Openday TIL 1 93 오픈데이 Openday TIL 2 94 Unreal Engine MCP TIL 1 95 콘티온 Conti On TIL 11 96 오픈데이 Openday TIL 3 97 오픈데이 Openday TIL 4 98 오픈데이 Openday TIL 5 99 ScorePlayer TIL 1 100 BuildingHub TIL 1 101 ScorePlayer TIL 2 102 BuildingHub TIL 2 103 ScorePlayer TIL 3 104 Unreal Engine MCP TIL 2 105 궁 미로 PalaceMaze TIL 1 106 메트로놈 Simple Metronome TIL 10 107 메트로놈 Simple Metronome TIL 11 108 오픈데이 Openday TIL 6 109 BuildingHub TIL 3 110 Obsidian vault에서 코드만 빼기 — directory junction 111 ScorePlayer TIL 4 112 오픈데이 Openday TIL 7 113 URL은 바뀔 수 있는 것에 묶지 않는다 114 도구는 기능이 아니라 내 일에 맞는지로 고른다 115 돌이키기 힘든 변경은 작게 먼저 확인한다 116 궁 미로 PalaceMaze TIL 3 117 궁 미로 PalaceMaze TIL 2 118 소리꽃 KeyBloom TIL 29 119 오픈데이 Openday TIL 8 120 BuildingHub TIL 4 121 ScorePlayer TIL 5 122 BuildingHub TIL 5 123 MiniMacro TIL 3 124 소리꽃 KeyBloom TIL 30 125 궁 미로 PalaceMaze TIL 4