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

기기 차이를 우리가 분기하면 모르는 기기에서 틀린다

태블릿에서 화면 아래 광고가 시스템 버튼 막대에 가려진다는 걸 발견했다. 휴대폰에서는 멀쩡했다.

원인은 예외로 넣어 둔 규칙이었다. 가로 화면이면 아래쪽 여백을 0으로 두게 해 뒀는데, 휴대폰은 가로로 돌리면 시스템 버튼 막대가 옆으로 가니까 그게 맞았다. 태블릿은 가로에서도 그 막대가 아래에 그대로 있다. 손에 있는 기기 하나를 기준으로 규칙을 만든 것이다.

안드로이드는 시스템 UI가 가리는 가장자리 여백을 앱에 알려 준다. 이걸 인셋이라고 한다. 예외를 지우고 이 값을 그대로 쓰자 휴대폰과 태블릿이 둘 다 맞았다. 휴대폰 가로에서는 그 값이 0으로 오고, 태블릿 가로에서는 막대 높이만큼 온다. 우리가 나눌 필요가 없었다.

배운 건 이거다. 플랫폼이 이미 계산해서 주는 값이 있으면 그걸 믿는다. 우리가 조건을 나누는 순간, 그 조건을 세울 때 손에 없던 기기에서는 반드시 틀린다. 그리고 그런 기기가 대부분이다.


다국어는 코드를 짜는 일이 아니라 자원을 나누고 누락을 찾는 일이다

한국어만 있던 앱에 영어를 넣었다. 시작 전엔 언어 전환 기능을 만들어야 하는 줄 알았는데, 실제로 짠 코드는 거의 없었다.

안드로이드는 문구를 자원 파일에 모아 두면 기기 언어에 맞는 것을 알아서 고른다. 기본 자원을 영어로 두고 한국어 자원을 따로 두는 것으로 끝났다. 여기에 이 앱이 지원하는 언어 목록을 선언해 두면, 사용자가 기기 전체 언어를 바꾸지 않고 이 앱만 다른 언어로 볼 수도 있다.

그래서 실제 작업의 대부분은 번역이 아니라 누락 찾기였다. 코드나 화면 파일에 직접 박아 둔 문구가 하나라도 남아 있으면 그것만 한국어로 남는다. 버튼 글자, 대화상자의 확인 버튼처럼 눈에 잘 안 띄는 자리에 있었다.

판단이 하나 더 있었다. 템포 이름은 번역하지 않기로 했다. 음악에서 쓰는 이탈리아어 용어라 그대로 두는 게 맞다. 다국어는 전부 번역하는 게 아니라 무엇을 번역하지 않을지도 정하는 일이다.


크기는 고정값이 아니라 관계로 맞춘다

재생 버튼이 옆에 있는 박자표 상자보다 눈에 띄게 짧아 보였다. 높이를 꽉 채우게 설정해 뒀는데도 그랬다.

원인은 우리 설정이 아니라 그 버튼 부품의 기본값이었다. 이 버튼은 기본적으로 위아래에 여백을 두고 그려진다. 자리는 다 차지하는데 실제로 색이 칠해지는 부분이 안쪽으로 들어가 있으니 짧아 보인 것이다. 그 여백을 0으로 없애니 맞았다.

여기서 하나 더 있다. 버튼 높이를 숫자로 박지 않고 “줄 전체를 채우라”로 두면, 그 줄의 높이는 옆에 있는 형제 중 가장 큰 것이 정한다. 그러면 박자표 상자 크기가 나중에 바뀌어도 버튼이 알아서 따라온다. 숫자로 맞춰 뒀다면 글꼴 크기 설정이 다른 기기에서 다시 어긋났을 것이다.

두 가지를 얻었다. 크기가 안 맞을 때 우리 설정을 의심하기 전에 그 부품의 기본값을 먼저 본다. 그리고 크기는 가능하면 고정값이 아니라 다른 요소와의 관계로 정한다.


같은 내용을 두 곳에서 보여줘야 하면 하나로 만들어 재사용한다

첫 실행 때 자동으로 뜨는 사용 안내와, 설정 안의 사용 방법 버튼이 있다. 내용이 같다.

이걸 각각 만들어 두면 나중에 안내 문구를 고칠 때 한쪽만 고치게 된다. 그래서 안내를 띄우는 것 하나만 만들고 두 자리에서 같이 부르게 했다. 첫 실행 여부는 기록 하나로 판단한다.

사소해 보이지만 이런 중복이 쌓이면 “고쳤는데 어떤 화면에서는 옛날 문구가 나온다”는 버그가 된다. 같은 내용이 두 자리에 필요하면 만드는 시점에 합쳐 두는 게 싸다.


도구가 만드는 파일에도 공유할 것과 개인 것이 섞여 있다

개발 도구가 실행할 때마다 고쳐 놓는 설정 파일이 커밋에 자꾸 딸려 들어왔다. 내용은 개발 기기에서 어느 단말로 실행했는지 같은 것이라 공유할 이유가 없다.

무시 목록에 넣어 해결했는데, 그 폴더를 통째로 무시하지는 않았다. 같은 폴더 안에 코드 서식 규칙처럼 팀이 공유해야 하는 설정도 들어 있기 때문이다.

도구가 만든 파일이라고 전부 개인 것은 아니다. 폴더 단위로 잘라내면 편하지만 공유해야 할 것까지 같이 나간다. 시끄러운 파일만 골라내는 쪽이 맞다.


요약

  • 기기 차이를 우리가 조건으로 나누면 그 조건을 세울 때 손에 없던 기기에서 틀린다. 플랫폼이 주는 값을 그대로 믿는다.
  • 다국어의 본체는 번역이 아니라 누락 찾기다. 코드에 박아 둔 문구가 하나라도 남으면 그것만 안 바뀐다.
  • 무엇을 번역하지 않을지도 정해야 한다. 그 분야에서 원어로 쓰는 용어는 그대로 둔다.
  • 크기가 안 맞으면 우리 설정보다 그 부품의 기본값을 먼저 의심한다.
  • 크기는 고정값이 아니라 다른 요소와의 관계로 정한다. 그래야 나중에 옆이 바뀌어도 따라온다.
  • 같은 내용이 두 자리에 필요하면 만들 때 합친다. 나중에 한쪽만 고치는 버그가 안 생긴다.
  • 도구가 만든 파일에도 공유할 것과 개인 것이 섞여 있다. 폴더째 잘라내지 않는다.

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