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

관리자 화면을 다듬은 날의 판단들이다. 셋으로 묶인다. 나중에 답할 수 있게 남기는 문제, 통과 신호를 어디까지 믿는가, 그리고 완성도가 갈리는 자리.


나중에 답할 수 있게 남기기

의무가 없는 것과 안 남겨도 되는 것은 다르다

시드 이미지는 전부 저작권이 완전히 풀린 것만 골랐다. 그런 사진은 출처를 화면에 적을 의무가 없다. 그래서 라이선스만 확인하고 파일만 챙겼다.

그런데 “이 이미지 어디서 가져왔냐”는 질문에 저장소만 봐서는 답할 수 없는 상태가 됐다.

의무와 추적은 다른 이야기다. 의무는 “화면에 적어야 하는가”이고, 추적은 “물어보면 댈 수 있는가”다. 앞엣것이 없다고 뒤엣것까지 필요 없다고 넘긴 게 잘못된 추론이었다. 저작권 문제를 피하는 게 전제인 프로젝트에서 출처를 못 대면, 공들여 피한 것 자체가 증명이 안 된다. 남에게 보여줄 물건이면 더 그렇다.

이번엔 임시 폴더에 원본이 남아 있어서 21장을 전부 되찾았다. 그런데 그 폴더가 지워졌으면 복구가 불가능했다. 사후에 복구된 건 운이고, 규칙으로 삼을 건 하나뿐이다. 나중에 누가 물어볼 수 있는 정보는 받는 자리에서 함께 적는다.

되찾은 김에 장치를 하나 걸었다. 데모 데이터를 처음 상태로 되돌리는 시드 스크립트(npm run db:seed)가 있는데, 출처가 빈 이미지가 하나라도 있으면 거기서 멈추고 실행이 안 된다. 기록을 사람 습관에 맡기지 않고 절차가 강제하게 만든 것이다. 앞 글의 “규율로 막을 것과 구조로 막을 것”과 같은 판단이다.

자동 검사는 스스로 흔적을 남긴다

관리자의 문의 화면에 같은 이름의 문의가 열세 건 쌓여 있었다. 검사 스크립트가 문의 넣기를 실제로 해 보고 지우지 않은 것들이다.

시드 스크립트는 “이 표들을 비운다”는 DELETE 목록을 들고 도는데, 그 목록에 문의 표(inquiries)만 빠져 있었다. 검사를 돌릴 때마다 한 건씩 늘어난 게 그대로 남았다.

검사는 데이터를 읽기만 하지 않는다. 실제로 예약을 만들고 문의를 넣는다. 그러니 검사를 늘리는 결정에는 “그 검사가 남긴 것을 누가 치우는가”가 딸려 온다.

그리고 손으로 관리하는 목록은 언젠가 샌다. 표가 하나 늘 때 비우는 목록도 같이 늘려야 한다는 규칙은, 지켜지는 동안만 유효하고 안 지켜졌을 때 알려 주지 않는다. 그래서 실제 표 목록과 비우는 목록을 대조해 빠진 게 있으면 스크립트가 멈추게 했다. 사람 기억에 기대는 목록은 대조할 상대를 옆에 둔다.

빈 화면은 기능이 없는 것으로 읽힌다

문의 데이터를 지우고 나니 문의 관리 화면이 텅 비었다. 그런데 빈 화면은 “아직 문의가 없구나”로 안 읽히고 “이 기능은 안 만들었구나”로 읽힌다.

제안용 물건에서 이건 실제 손해다. 만든 걸 못 보여주는 것과 안 만든 것이 화면에서 구별되지 않는다. 답변 전후를 섞은 문의 다섯 건을 기본 데이터로 넣었다.

기준으로 남기면, 보여줄 목적이 있는 화면은 비어 있으면 안 된다. 빈 상태는 실제 운영에서나 정상이지 데모에서는 결함이다.


통과 신호를 어디까지 믿나

“확인할 게 없었다”가 “확인했고 맞았다”로 읽힌다

예약 목록에서 “취소” 상태만 걸러 내는 기능이 있다. 그게 잘 도는지 보려고 “걸러진 목록의 모든 줄이 취소 상태인가”를 묻는 검사를 넣었고, 늘 통과했다. 그런데 걸러진 목록이 0건이었다.

빈 목록에서 “모든 줄이 그렇다”는 자동으로 참이 된다. 어길 줄이 하나도 없기 때문이다. 그러니 이 검사는 필터가 잘 돈다는 걸 한 번도 확인한 적이 없으면서 계속 통과 신호를 보내고 있었다.

앞 글에서 「“문제 없었습니다”만 오는 보고는 아직 아무 말도 안 한 보고다」라고 적었는데, 같은 함정이 검사 코드 안쪽에도 있었다. 요약 줄을 밖에서 확인하는 것만으로는 부족하고, 검사 하나하나가 몇 건을 보고 그렇게 말하는지를 함께 찍게 만들어야 한다.

이 결은 코드 밖에서도 통한다. “문제 없었습니다”라는 보고는 문제를 찾아봤다는 뜻일 수도, 찾을 것이 없었다는 뜻일 수도 있다. 둘은 같은 문장으로 온다.

검사는 숫자가 아니라 의도를 재야 오래 산다

목록 접기가 실제로 공간을 아끼는지 보려고 “접힌 줄이 64px 미만”이라는 검사를 넣었다. 썸네일을 넣자 딱 64px이 되어 검사가 실패했다. 기능은 의도대로 동작하고 있었다.

재려던 건 “접기가 공간을 아끼는가”인데 특정 숫자로 바꿔 적었다. 숫자는 여백을 조금만 손봐도 흔들린다. “접은 것이 펼친 것보다 확실히 짧은가”로 바꾸니 디자인을 손봐도 안 깨진다.

여기서 진짜 위험은 검사가 깨지는 것 자체가 아니다. 멀쩡한 변경 때마다 우는 검사는 사람이 결국 끄거나 무시한다. 그러면 진짜로 깨졌을 때도 안 본다. 검사가 얼마나 오래 쓸모 있느냐는 얼마나 정확한가보다 헛되이 우는 일이 얼마나 드문가로 정해진다.

돌리는 시점에 따라 결과가 달라지면 검사가 아니다

브라우저 검사가 실패했는데 앱은 멀쩡했다. 회의실은 평일만 운영해서 토요일에는 모든 시간대가 비활성이었다.

앞서 “두 번 돌리면 실패하는 검사”를 고쳤는데 이건 다른 축이다. 그때는 이전 실행이 남긴 상태가 문제였고, 이번엔 실행 시점이 문제다. 검사가 기대야 할 것은 “지금이 언제인가”가 아니라 “무엇을 확인하려는가”다.


완성도가 갈리는 자리

기본 팝업은 제품처럼 보이지 않는다

예약 반려 사유를 브라우저 기본 입력창으로 받고 있었다. 주소창 아래 붙어 나오는 회색 상자다.

이게 두 가지를 망친다. 하나는 인상이다. 기본 대화상자는 제품 화면이 아니라 스크립트가 끼어든 것처럼 보인다. 제안용 물건에서 “덜 만든 티”는 기능 목록이 아니라 이런 데서 난다. 다른 하나는 기능이다. 한 줄만 받으니 반려 사유를 제대로 적을 수가 없다.

인상 때문에 고치자니 사치 같고 기능 때문에 고치자니 사소해 보이는데, 둘이 같은 원인에서 나온다. 급하게 붙인 임시 조치는 보기에도 임시고 쓰기에도 임시다. 이런 항목은 둘 중 하나만 놓고 보면 늘 우선순위에서 밀린다.

흔한 이름은 언젠가 부딪힌다

접힌 줄의 썸네일 자리가 38px이어야 하는데 98px이었다. 사진이 없는 자리에 empty라는 이름을 붙였는데, 사이트 전역에 “등록된 항목이 없습니다” 안내문용으로 쓰는 empty가 이미 있었다. 그쪽에 걸린 위아래 여백 96px이 이 작은 칸에 그대로 먹었다.

이름 짓기 규칙은 취향 문제로 보이지만 실제로는 사고 예방이다. 그리고 이런 충돌은 에러를 안 낸다. 화면에서는 “좀 크네” 정도로 보여서, 재 보기 전까지는 그냥 넘어간다. 앞 글의 홈 카드가 세로로 쌓인 일과 같은 종류다.

좁은 화면에서 열 많은 표는 줄이지 말고 밀어서 본다

관리자 표를 휴대폰 폭으로 보니 “회의실”이 세 글자로 쪼개져 세로로 접혔다. 공개 사이트 표는 멀쩡했는데, 그쪽은 열이 적어서다.

좁은 화면 대응을 “다 보이게 줄이는 것”으로 잡으면 열이 많은 표에서 무너진다. 최소 폭을 지키고 가로로 밀어 보게 하는 편이 낫다. 반응형은 모든 걸 한 화면에 넣는 게 아니라, 무엇을 포기할지 정하는 일이다.


요약

  • 의무는 “적어야 하는가”이고 추적은 “물어보면 댈 수 있는가”다. 앞이 없다고 뒤까지 필요 없는 게 아니다
  • 나중에 누가 물어볼 수 있는 정보는 받는 자리에서 적는다. 사후 복구가 됐다면 그건 운이다
  • 기록을 사람 습관에 맡기지 말고 절차가 강제하게 만든다
  • 검사는 데이터를 만든다. 검사를 늘리는 결정에는 그 흔적을 누가 치우는가가 딸려 온다
  • 사람 기억에 기대는 목록은 대조할 원본을 옆에 둔다
  • 보여줄 목적이 있는 화면은 비어 있으면 안 된다. 빈 상태는 운영에서나 정상이고 데모에서는 결함이다
  • “문제 없었습니다”는 찾아봤다는 뜻일 수도, 찾을 것이 없었다는 뜻일 수도 있다. 둘은 같은 문장으로 온다
  • 검사가 오래 쓸모 있으려면 정확한 것보다 헛되이 우는 일이 드물어야 한다. 멀쩡한 변경마다 우는 검사는 결국 무시된다
  • 돌리는 시점에 따라 결과가 달라지면 검사가 아니다
  • 급하게 붙인 임시 조치는 보기에도 임시고 쓰기에도 임시다. 인상과 기능 중 하나만 놓고 보면 늘 우선순위에서 밀린다
  • 이름 충돌은 에러를 안 내고 “좀 이상하네” 정도로 보인다. 재야 잡힌다
  • 반응형은 모든 걸 한 화면에 넣는 게 아니라 무엇을 포기할지 정하는 일이다

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