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

태블릿에서 게임을 켰더니

세로 고정이라고 정해 둔 것이 안 지켜졌다

태블릿으로 게임을 켰더니 가로로 떴다. 이 게임은 세로 전용이라 세로로 고정해 뒀는데도 그랬다.

알아보니 Android 16부터는 화면이 큰 기기에서 앱의 세로 고정을 무시한다. 기준은 짧은 쪽이 600dp 이상인데, dp는 기기마다 다른 화소 밀도를 걷어낸 길이 단위다. 같은 600dp라도 촘촘한 화면이면 화소는 더 많다. 태블릿·폴더블 안쪽 화면·크롬북이 여기 해당하고, 분할 화면이나 제조사의 회전 강제에서도 같은 일이 생긴다.

폰만 보고 만든 게임이라도 태블릿에서 받아 간다. 내려받는 쪽을 막지 않는 한 “우리는 세로 앱”이라는 전제는 기기가 지켜 주지 않는다.

  • 내가 정한 제약이 OS 버전이 올라가면 무효가 될 수 있다. 화면을 어떻게 쓰겠다는 약속일수록 기기가 커질 때 먼저 깨진다
  • 약속이 깨졌을 때 무엇이 보일지를 정해 두는 게 진짜 대응이다. 우리는 가로가 되면 세로 비율 상자를 가운데 두고 양옆을 여백으로 채우는 레터박스(원래 비율을 지키려고 남는 자리를 여백으로 채우는 방식)를 걸어 뒀다

확인하려고 깔았더니 그 기기의 기록이 지워졌다

수정한 것을 태블릿에서 보려면 개발판을 깔아야 했다. 그런데 그 태블릿에는 스토어에서 받은 게임이 이미 깔려 있었다.

둘은 같이 못 깐다. 앞선 TIL 20에서 업로드 키와 앱 서명 키가 다르다는 걸 정리했는데, 그 결과가 여기서 나온다. 스토어로 나간 앱은 구글이 앱 서명 키로 다시 서명한 것이라 이쪽에서 만든 빌드와 서명이 다르다. 덮어 깔리지 않으니 먼저 지워야 하고, 지우면 그 기기의 진행 기록도 같이 없어진다.

그 태블릿은 테스트용이라 지우기로 했다. 대신 고를 수 있는 길이 둘 더 있었다. 내부 테스트 트랙(스토어에 올리되 지정한 사람에게만 보이는 배포 통로)에 올려 스토어로 받으면 서명이 같아 기록이 남는다. 그리고 앞선 TIL 30에서 넣은 클라우드 저장에 연결해 둔 기기라면, 지우고 다시 깔아도 진행 상황은 불러올 수 있다.

  • 확인 방법을 고를 때 그 기기에 쌓인 것이 얼마짜리인지 먼저 본다. 지우는 쪽은 빠르고, 기록을 지키는 쪽은 한 번 올렸다 받는 수고가 든다
  • 지워도 되는 기기를 하나 정해 두면 이 판단을 매번 안 해도 된다. 클라우드 저장을 붙여 둔 뒤로는 그 기기 범위가 넓어졌다

재현이 안 되는 증상 앞에서

테스트를 조일까, 내가 본 것을 의심할까

태블릿에서 찍은 화면에서 일일 보상 화면이 세로 상자를 다 덮지 않고 있었다. 같은 상황을 위젯 테스트(기기 없이 화면 구조만 메모리에서 조립해 크기를 재는 시험)로 만들어 봤더니 문제가 없었다.

여기서 갈 길이 둘이다. 테스트가 실제 조건을 못 담았다고 보고 조건을 더 얹거나, 내가 본 것을 잘못 읽었다고 보고 관측부터 다시 읽거나. 둘 다 그럴듯해서 먼저 집는 쪽으로 시간이 흐른다.

이번에 정한 기준은 이렇다. 관측이 몇 겹인지를 센다. 스크린샷 한 장에는 화소 위치와 색까지만 들어 있고, 어느 색이 무엇이 칠한 것인지는 내가 얹은 해석이다. 해석이 한 겹 끼어 있으면 그쪽을 먼저 의심한다. 반대로 숫자가 기기에서 직접 나온 것이면 테스트 쪽을 조인다.

  • 테스트를 조이는 작업은 끝이 없고 조일수록 그럴듯해진다. 해석을 다시 읽는 일은 십 분이면 끝난다. 싼 쪽부터 한다
  • 재현 실패는 실패가 아니라 정보다. 내가 본 것과 코드가 하는 일 사이 어딘가가 어긋났다는 뜻이고, 그 어딘가에는 내 눈도 들어간다

반례가 있는 근거는 근거가 아니다

해석을 다시 읽을 때 근거를 둘 잡았다. 하나는 여백 색이 앱의 창 배경색과 같다는 것이고, 다른 하나는 어두운 화면이 화면 한가운데가 아니라 왼쪽에 붙어 있다는 것이었다. 앱이 스스로 가운데 두는 방식이면 한쪽으로 쏠릴 리 없다고 봤다.

두 번째 근거는 무를 수밖에 없다. 앞선 TIL 2에서 같은 함정을 이미 겪었다. 그때도 앱은 가운데 정렬을 하고 있었는데 화면에서는 치우쳐 보였다. 기준이 화면 전체가 아니라 남은 자리였기 때문이다. 가운데로 두라고 해 놓아도 무엇의 가운데인지에 따라 치우친다.

  • 근거를 셀 때 개수보다 반례를 먼저 찾는다. 앞 글에 이미 반례가 있으면 그 근거는 남 앞에서 못 쓴다
  • 자기 프로젝트의 앞 기록이 가장 가까운 반례 창고다. 남이 반박하기 전에 거기부터 뒤진다

재 보니 두 번째 추측도 틀렸다

남은 근거가 색 하나뿐이라 “OS가 준 창일지도 모른다”까지가 한계였다. 태블릿을 연결해 앱이 받은 창 크기를 쟀다. 창은 화면 전체였다. OS는 아무것도 줄이지 않았다.

재 보니 층이 갈렸다. 이 게임은 가로 화면을 받으면 세로 비율 상자를 만들어 그 안에 화면을 그리는데, 상자도 594였고 화면이 받은 크기도 594였다. 그런데 칠해진 배경은 394였다.

원인은 배경이 자기 내용물 크기로 줄어든 것이었다. 화면 틀은 본문에 “최대 이만큼까지 써도 된다”고만 알려 주는데, 배경이 자기 크기를 안 정해 두면 그 말을 “자식만큼만 쓰겠다”로 받는다. 그래서 배경 폭이 카드 세 장 폭과 같아졌다. 폰에서는 카드 세 장이 화면 폭을 거의 채워서 안 보였고, 태블릿에서만 드러났다.

  • 추측은 두 번 틀릴 수 있다. 첫 해석이 틀린 걸 알고 세운 두 번째 해석도 근거가 하나뿐이면 마찬가지다
  • “최대 이만큼”과 “여기까지 채워라”는 다른 말이다. 배경처럼 끝까지 차야 하는 것은 스스로 크기를 정하게 한다
  • 폰에서만 보면 이 차이가 영영 안 보인다. 화면이 넓은 기기가 이런 종류의 버그를 드러낸다

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 | 습관만들기 Pawbit TIL 1 27 | insertOnConflictUpdate vs DoUpdate(target) 28 | 세션 필터 29 | 아코디언(Accordiaon) UI를 펼친상태로 만들기 30 | input의 step 31 | 습관만들기 Pawbit TIL 2 32 | 습관만들기 Pawbit TIL 3 33 | Word Cloud 34 | Google Sheets를 데이터 버스로(with AppSheet) 35 | Django 모델 텍스트 필드 자동 수집 패턴 36 | localStorage로 섹션 토글 상태 유지 37 | 순차 ID 생성(`select_for_update()` + `max()` 조합) 38 | 역참조 검색과 distinct() 39 | xlsx 다운로드와 로딩 오버레이 충돌 40 | Android 파일 공유 MIME 타입 41 | AssetManifest — Flutter 빌드 타임 asset 목록 런타임 조회 42 | 크레센도 Phaser + TypeScript TIL 1 43 | 크레센도 Phaser + TypeScript TIL 2 44 | 크레센도 Phaser + TypeScript TIL 3 45 | 크레센도 Phaser + TypeScript TIL 4 46 | 크레센도 Phaser + TypeScript TIL 5 47 | 여둘까 Office Layout TIL 1 48 | 습관만들기 Pawbit TIL 4 49 | 여둘까 Office Layout TIL 2 50 | 여둘까 Office Layout TIL 3 51 | 크레센도 Phaser + TypeScript TIL 6 52 | UTF-8 BOM과 PowerShell 파일 쓰기 53 | 소리꽃 KeyBloom TIL 1 54 | 습관만들기 Pawbit TIL 5 55 | 여둘까 Office Layout TIL 4 56 | 소리꽃 KeyBloom TIL 2 57 | 소리꽃 KeyBloom TIL 3 58 | 메트로놈 Simple Metronome TIL 1 59 | 소리꽃 KeyBloom TIL 4 60 | 메트로놈 Simple Metronome TIL 2 61 | 소리꽃 KeyBloom TIL 5 62 | 습관만들기 Pawbit TIL 6 63 | 크레센도 Phaser + TypeScript TIL 7 64 | 메트로놈 Simple Metronome TIL 3 65 | 소설 타임라인 관리 도구 Lumior TIL 1 66 | 습관만들기 Pawbit TIL 7 67 | 정적 블로그 SEO 정비와 Pagefind 검색 도입 68 | 크레센도 Phaser + TypeScript TIL 8 69 | 메트로놈 Simple Metronome TIL 4 70 | 습관만들기 Pawbit TIL 8 71 | 안드로이드 AudioTrack 연속 재생, 실측 피커 정렬, 카메라 토치 플래시 72 | 메트로놈 Simple Metronome TIL 5 73 | 모래게임 Sandrop TIL 1 74 | 습관만들기 Pawbit TIL 9 75 | 메트로놈 Simple Metronome TIL 6 76 | 모래게임 Sandrop TIL 2 77 | 모래게임 Sandrop TIL 3 78 | 습관만들기 Pawbit TIL 10 79 | 습관만들기 Pawbit TIL 11 80 | 메트로놈 Simple Metronome TIL 7 81 | 모래게임 Sandrop TIL 4 82 | 모래게임 Sandrop TIL 5 83 | 습관만들기 Pawbit TIL 12 84 | 모래게임 Sandrop TIL 6 85 | 소리꽃 KeyBloom TIL 6 86 | 모래게임 Sandrop TIL 7 87 | 모래게임 Sandrop TIL 8 88 | 소리꽃 KeyBloom TIL 7 89 | 모래게임 Sandrop TIL 9 90 | 소리꽃 KeyBloom TIL 8 91 | 모래게임 Sandrop TIL 10 92 | 소리꽃 KeyBloom TIL 9 93 | 콘티온 ContiOn TIL 1 94 | 소리꽃 KeyBloom TIL 10 95 | 콘티온 ContiOn TIL 2 96 | 습관만들기 Pawbit TIL 13 97 | 콘티온 ContiOn TIL 3 98 | 룸톤 Roomtone TIL 1 99 | 모래게임 Sandrop TIL 11 100 | 소리꽃 KeyBloom TIL 11 101 | 여둘까 Office Layout TIL 5 102 | 모래게임 Sandrop TIL 12 103 | 모래게임 Sandrop TIL 13 104 | 콘티온 ContiOn TIL 4 105 | 모래게임 Sandrop TIL 14 106 | 소리꽃 KeyBloom TIL 12 107 | 습관만들기 Pawbit TIL 14 108 | 모래게임 Sandrop TIL 15 109 | 모래게임 Sandrop TIL 16 110 | 소리꽃 KeyBloom TIL 13 111 | 온실 GreenHouse TIL 1 112 | 콘티온 ContiOn TIL 5 113 | 모래게임 Sandrop TIL 17 114 | 소리꽃 KeyBloom TIL 14 115 | 온실 GreenHouse TIL 2 116 | 콘티온 ContiOn TIL 6 117 | 모래게임 Sandrop TIL 18 118 | 콘티온 ContiOn TIL 7 119 | 모래게임 Sandrop TIL 19 120 | 소리꽃 KeyBloom TIL 15 121 | 룸톤 Roomtone TIL 2 122 | 모래게임 Sandrop TIL 20 123 | 모래게임 Sandrop TIL 21 124 | 소리꽃 KeyBloom TIL 16 125 | 소리꽃 KeyBloom TIL 17 126 | 소리꽃 KeyBloom TIL 18 127 | 여둘까 Office Layout TIL 6 128 | 소리꽃 KeyBloom TIL 19 129 | 소리꽃 KeyBloom TIL 20 130 | 콘티온 ContiOn TIL 8 131 | 콘티온 ContiOn TIL 9 132 | 로그스톤 상표 셀프 출원 TIL 1 133 | 소리꽃 KeyBloom TIL 21 134 | 로그스톤 샵 LogStone Shop TIL 1 135 | 소리꽃 KeyBloom TIL 22 136 | 로그스톤 샵 LogStone Shop TIL 2 137 | 룸톤 Roomtone TIL 3 138 | 소리꽃 KeyBloom TIL 23 139 | 모래게임 Sandrop TIL 22 140 | 모래게임 Sandrop TIL 23 141 | 소리꽃 KeyBloom TIL 24 142 | 로그스톤 샵 LogStone Shop TIL 3 143 | 모래게임 Sandrop TIL 24 144 | 소리꽃 KeyBloom TIL 25 145 | 모래게임 Sandrop TIL 25 146 | MiniMacro TIL 1 147 | 룸톤 Roomtone TIL 4 148 | 모래게임 Sandrop TIL 26 149 | 콘티온 ContiOn TIL 10 150 | Astro가 무엇인지, 왜 옮기는지 151 | 룸톤 Roomtone TIL 5 152 | 룸톤 Roomtone TIL 6 153 | 모래게임 Sandrop TIL 27 154 | 소리꽃 KeyBloom TIL 26 155 | MiniMacro TIL 2 156 | 로그스톤 샵 LogStone Shop TIL 4 157 | 메트로놈 Simple Metronome TIL 8 158 | 모래게임 Sandrop TIL 28 159 | 여둘까 Office Layout TIL 7 160 | 룸톤 Roomtone TIL 7 161 | 메트로놈 Simple Metronome TIL 9 162 | 블로그 Astro 이관 TIL 163 | 여둘까 Office Layout TIL 8 164 | 로그스톤 샵 LogStone Shop TIL 5 165 | 모래게임 Sandrop TIL 29 166 | 오픈데이 Openday TIL 1 167 | 로그스톤 샵 LogStone Shop TIL 6 168 | 모래게임 Sandrop TIL 30 169 | 오픈데이 Openday TIL 2 170 | 모래게임 Sandrop TIL 31 171 | Unreal Engine MCP TIL 1 172 | 모래게임 Sandrop TIL 32 173 | 소리꽃 KeyBloom TIL 27 174 | 콘티온 ContiOn TIL 11 175 | 오픈데이 Openday TIL 3 176 | 로그스톤 샵 LogStone Shop TIL 7 177 | 모래게임 Sandrop TIL 33 178 | 소리꽃 KeyBloom TIL 28 179 | 오픈데이 Openday TIL 4 180 | 여둘까 Office Layout TIL 9 181 | 오픈데이 Openday TIL 5 182 | ScorePlayer TIL 1 183 | 모래게임 Sandrop TIL 34 184 | BuildingHub TIL 1 185 | ScorePlayer TIL 2 186 | 여둘까 Office Layout TIL 10 187 | BuildingHub TIL 2 188 | ScorePlayer TIL 3 189 | Unreal Engine MCP TIL 2 190 | 궁 미로 PalaceMaze TIL 1 191 | 메트로놈 Simple Metronome TIL 11 192 | 메트로놈 Simple Metronome TIL 10 193 | 오픈데이 Openday TIL 6 194 | BuildingHub TIL 3 195 | Obsidian vault에서 코드만 빼기 — directory junction 196 | ScorePlayer TIL 4 197 | 여둘까 Office Layout TIL 11 198 | 오픈데이 Openday TIL 7 199 | URL은 바뀔 수 있는 것에 묶지 않는다 200 | 도구는 기능이 아니라 내 일에 맞는지로 고른다 201 | 돌이키기 힘든 변경은 작게 먼저 확인한다 202 | 궁 미로 PalaceMaze TIL 3 203 | 궁 미로 PalaceMaze TIL 2 204 | 소리꽃 KeyBloom TIL 29 205 | 오픈데이 Openday TIL 8 206 | 룸톤 Roomtone TIL 8 207 | BuildingHub TIL 4 208 | ScorePlayer TIL 5 209 | 로그스톤 샵 LogStone Shop TIL 8 210 | 여둘까 Office Layout TIL 12 211 | 오픈데이 Openday TIL 9 212 | BuildingHub TIL 5 213 | MiniMacro TIL 3 214 | 룸톤 Roomtone TIL 10 215 | 룸톤 Roomtone TIL 9 216 | 소리꽃 KeyBloom TIL 30 217 | 오픈데이 Openday TIL 10 218 | 여둘까 Office Layout TIL 13 219 | 룸톤 Roomtone TIL 11 220 | 소리꽃 KeyBloom TIL 31 221 | 궁 미로 PalaceMaze TIL 4 222 | 룸톤 Roomtone TIL 12 223 | 로그스톤 샵 LogStone Shop TIL 9 224 | 룸톤 Roomtone TIL 13 225 | 로그스톤 샵 LogStone Shop TIL 10 226 | 룸톤 Roomtone TIL 14 227 | 소리꽃 KeyBloom TIL 32 228 | 룸톤 Roomtone TIL 15 229 | 룸톤 Roomtone TIL 16 230 | 로그스톤 샵 LogStone Shop TIL 11 231 | 룸톤 Roomtone TIL 17 232 | 룸톤 Roomtone TIL 18 233 | 메트로놈 Simple Metronome TIL 12 234 | 콘티온 ContiOn TIL 12 235 | 소리꽃 KeyBloom TIL 33 236 | 온실 GreenHouse TIL 3 237 | 로그스톤 샵 LogStone Shop TIL 12 238 | 룸톤 Roomtone TIL 19 239 | 모래게임 Sandrop TIL 35 읽는 중 240 | 여러 세션을 INBOX 하나로 굴리기 241 | 궁 미로 PalaceMaze TIL 5 242 | CS2 Practice Host TIL 1 243 | 룸톤 Roomtone TIL 20 244 | 여둘까 Office Layout TIL 14