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

저장 묶음의 경계는 데이터가 아니라 사용자의 기대가 정한다

연습 설정을 이름 붙여 저장하는 프리셋을 넣었다. 무엇까지 담을지가 첫 결정이었다.

앱이 가진 설정은 여러 가지다. 템포, 박자표, 소리 종류 같은 연주 설정이 있고 테마 색, 화면 깜빡임 같은 전역 설정도 있다. 기술적으로는 전부 담을 수 있었다.

연주 설정만 담기로 했다. 사용자가 프리셋을 부르는 상황은 “아까 그 리듬으로 돌아가자”이지 “아까 그 화면으로 돌아가자”가 아니다. 리듬 하나를 불렀는데 앱 색이 바뀌면 고장으로 느껴진다.

저장 기능을 만들 때 담을 항목은 “저장 가능한가”가 아니라 “불러올 때 같이 바뀌어도 사용자가 놀라지 않는가”로 고른다. 이 기준이 있으면 항목이 늘어날 때마다 다시 판단할 수 있다.


개수가 변하는 목록은 칸을 늘리지 말고 한 덩이로 넣는다

프리셋은 몇 개가 될지 모른다. 처음 떠오른 방법은 저장소에 프리셋 1, 프리셋 2처럼 칸을 늘려 가는 것이었는데, 이러면 중간을 지우거나 순서를 바꿀 때마다 번호를 다시 매겨야 한다.

대신 목록 전체를 문자열 하나로 바꿔서 한 칸에 저장했다. 이렇게 구조를 문자열로 펴는 것을 직렬화라고 하고, 여기서는 JSON 형식을 썼다. 이름과 값이 짝지어진 구조를 글자로 적어 두는 방식이다. 읽을 때는 반대로 되돌린다.

칸이 하나뿐이니 추가·삭제·순서 변경이 전부 목록을 다루는 일이 되고, 저장은 매번 통째로 덮어쓰면 끝난다. 항목 수가 수십 개 수준이면 이 단순함이 성능보다 이득이다.

읽는 쪽에는 한 가지를 더 뒀다. 저장된 글자가 깨져 있으면 빈 목록으로 시작하게 했다. 사용자 기기에서 무슨 일이 있었는지는 알 수 없고, 그때 앱이 죽는 것보다 프리셋이 비는 편이 낫다.

새로 저장과 덮어쓰기를 사용자에게 고르게 하지 않는다

저장할 때 이름이 이미 있으면 그 자리를 갱신하고, 없으면 새로 추가한다. 사용자는 이름만 정하면 되고 “새로 저장”인지 “덮어쓰기”인지 고르지 않는다.

대신 이미 있는 이름이면 덮어쓸지 한 번 묻는다. 프리셋 삭제도 마찬가지로 묻는다. 되돌리기 기능이 없는 앱에서는 이 확인 한 번이 되돌리기를 대신한다. 확인을 넣을 자리는 “실수하면 복구할 방법이 있는가”로 정하면 된다.


기능을 지우는 일은 화면을 지우는 게 아니라 흔적을 찾는 일이다

프리셋이 생기니 예전에 만든 “마지막 템포 기억하기” 스위치가 할 일이 없어졌다. 프리셋이 그 역할을 더 잘한다.

지우는 게 만드는 것보다 쉬울 줄 알았는데 아니었다. 화면에서 스위치를 빼는 것은 시작일 뿐이고, 그 기능이 남긴 자리가 여기저기 흩어져 있었다. 값을 들고 있던 자리, 저장소 이름표, 화면이 뜰 때 연결하던 부분, 값이 바뀔 때 부르던 부분, 그 기능만 쓰던 함수까지 있었다.

그래서 순서를 이렇게 잡았다. 지우기 전에 관련된 이름들로 전부 검색해 목록을 먼저 만들고, 목록대로 지운 다음, 같은 검색을 다시 돌려 하나도 안 남았는지 확인했다.

여기서 알게 된 게 하나 더 있다. 코드에서 안 쓰는 함수를 남겨 두면 대개 경고라도 뜨는데, 그림이나 스타일 같은 자원은 안 써도 아무 말이 없다. 도구가 안 잡아 주는 종류의 쓰레기가 따로 있어서, 기능을 걷어낼 때는 그 기능이 데려온 자원 목록도 직접 세어 봐야 한다.

겉을 바꿔도 이름을 유지하면 연결이 안 끊긴다

저장 버튼을 화면 전체 폭을 쓰는 큰 버튼에서 헤더 오른쪽의 작은 아이콘으로 바꿨다. 생김새도 부품 종류도 달라졌는데 코드는 한 줄도 안 고쳤다. 그 부품에 붙여 둔 이름표를 그대로 뒀기 때문이다.

디자인을 바꾸는 작업이 비싸질 때와 싸질 때가 여기서 갈린다. 이름표를 유지하면 껍데기 교체는 거의 공짜고, 이름표까지 바꾸면 그 이름을 쓰던 자리를 전부 따라다녀야 한다. 겉모양을 자주 손볼 것 같은 자리일수록 이름을 먼저 안정시켜 두는 게 좋다.


효과 없음을 확인하고 되돌리는 것도 결론이다

첫 박이 작게 들리는 문제를 다시 잡아 보려고, 소리를 더 빠른 경로로 내보내는 설정을 켜 봤다. 체감 차이가 없었고 그대로 되돌렸다.

되돌렸지만 버린 시간은 아니다. 소리 데이터 자체는 크기가 같다는 것을 이미 확인했고, 여기서 재생 경로를 바꿔도 안 변한다는 것까지 확인했으니, 원인이 앱 바깥에 있을 가능성이 그만큼 올라갔다. 남은 후보가 줄어든 것이 결과다.

안 되는 시도를 기록해 두면 다음에 같은 길을 다시 파지 않는다. 기록하지 않으면 몇 달 뒤에 똑같은 설정을 또 켜 보게 된다.


요약

  • 저장 묶음에 넣을 항목은 “저장 가능한가”가 아니라 “불러올 때 같이 바뀌어도 안 놀라는가”로 고른다.
  • 개수가 변하는 목록은 저장 칸을 늘리지 말고 통째로 한 덩이로 만들어 한 칸에 넣는다. 추가·삭제·순서 변경이 전부 목록 다루는 일로 바뀐다.
  • 되돌리기가 없는 앱에서는 확인 한 번이 되돌리기를 대신한다. 확인을 넣을 자리는 복구 가능성으로 정한다.
  • 지우는 일은 만드는 일보다 쉽지 않다. 지우기 전에 검색으로 목록을 만들고, 지운 뒤 같은 검색으로 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