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

날짜를 20270830처럼 이어서 칠 수 있게 만들었다. 원래는 202708-30이 됐는데, 왜 하필 그렇게 어긋나는지를 알고 나니 그다음 판단이 다 따라 나왔다.


이어 치는 게 되는 건 칸이 「몇 자리인가」를 알기 때문이다

브라우저에는 날짜를 받는 칸이 기본으로 들어 있다. 우리가 만든 게 아니라 브라우저가 안까지 다 만들어 통째로 내주는 것이고, 이 글에서 「완제품 칸」이라고 부르는 게 그거다. 파일 고르기 창이나 색 고르기도 같은 종류다.

이 칸은 하나처럼 보이지만 안에 연·월·일 세 칸이 따로 있다. 숫자를 치면 한 칸을 채우고 다음 칸으로 넘어간다. 이 「넘어감」이 이어치기의 전부다.

넘어갈지 판단하려면 그 칸이 찼는지 알아야 하고, 찼는지 알려면 몇 자리짜리인지가 정해져 있어야 한다. 월은 두 자리, 일도 두 자리다. 그래서 08을 치면 바로 넘어간다.

연도만 그렇지 않다. 브라우저가 다루는 연도 상한이 275760년이라 이 칸은 여섯 자리까지 받는다. 네 자리를 쳐도 아직 안 찼으니 안 넘어간다.

20270830 을 이어 치면
  연도 ← 202708   여섯 자리를 다 받는다
  월   ← 30       12를 넘으니 접힌다
  일   ← 빈다

사람은 「연월일 여덟 자리」로 세는데 칸은 「연도 여섯 자리까지」로 센다. 어긋나는 지점은 여기 하나다.

  • 자동으로 넘어가는 칸은 「몇 자리인가」가 정해져 있어야 성립한다. 범위가 열려 있는 값에는 그 수가 없다
  • 그리고 한 칸에서만 어긋나도 그 뒤가 전부 밀린다. 이어치기는 칸 하나하나가 아니라 칸들 사이의 약속 위에 서 있고, 약속은 한 군데만 깨져도 약속이 아니게 된다
  • 카드번호·전화번호·인증번호처럼 자동으로 넘어가는 칸이 있는 곳은 전부 같은 구조다. 자리 수가 고정인 동안에만 매끄럽다

연도 칸만 네 자리에서 넘어가게 할 방법이 없었다

고치려면 「연도가 네 자리 차면 월 칸으로 넘겨라」를 어딘가에 적어야 한다. 그 자리가 없었다.

이 날짜 칸에 우리 코드가 할 수 있는 일은 이게 전부다.

  • 완성된 날짜를 통째로 넣는다 — 2027-08-30
  • 완성된 날짜를 통째로 읽는다
  • 고를 수 있는 날의 최소·최대를 정한다

없는 것은 이쪽이다.

  • 지금 커서가 세 칸 중 어디에 있는지 알기
  • 「이제 다음 칸으로 넘어가라」고 시키기
  • 연도 칸은 네 자리만 받게 정하기
  • 연도 칸에만 2027을 넣기 — 값은 세 칸이 다 찬 상태로만 들어간다

키 입력을 가로채는 길도 막혀 있다. 키를 막는 것까지는 되는데, 막고 나면 대신 넣어 줄 방법이 없다. 넣는 수단이 「완성된 날짜 하나」뿐이라, 사람이 2027까지만 친 도중 상태를 표현할 수가 없다.

  • 완제품이 편한 건 안쪽을 안 봐도 되기 때문인데, 안쪽이 틀렸을 때 못 들어가는 것도 같은 이유다. 편의와 막힘이 같은 성질에서 나온다
  • 그래서 이 결정은 「더 낫게 만들자」가 아니라 「여기서는 고칠 수가 없다」에서 나왔다. 이 둘은 무게가 다르다 — 앞엣것은 미룰 수 있고 뒤엣것은 그 칸을 안 쓰는 것 말고 길이 없다

브라우저가 공짜로 주던 것을 버려도 되는 때

브라우저가 통째로 주는 것은 대개 직접 만든 것보다 낫다. 접근성, 휴대폰 자판, 나라마다 다른 날짜 표기, 손댈 일 없는 유지보수까지 딸려 온다. 그래서 「이건 직접 만들자」는 말이 나오면 일단 의심하는 게 맞다.

이번에는 버렸다. 셋을 따져서다.

  • 못 고치는가 — 고칠 수 있으면 고치는 쪽이 훨씬 싸다
  • 자주 쓰는가 — 어쩌다 걸리는 것이면 참는 쪽이 싸다
  • 잃는 것을 되찾을 수 있는가 — 못 되찾으면 문제를 옮긴 것뿐이다

셋이 다 그렇다면 버려도 되고, 하나라도 아니면 다시 생각해야 한다. 앞 절이 첫째를 확인해 준 셈이고, 날짜는 이 앱에서 반복 일정을 만들 때마다 치는 것이라 둘째도 해당됐다.

못 되찾은 것을 적어 두는 것까지가 그 결정이다

버리기로 하면 딸려 오던 것을 하나씩 떠안는다. 되찾은 것과 못 되찾은 것이 갈렸다.

  • 달력 — 되찾았다. 안 보이는 칸을 하나 두고 그 달력만 빌려 썼다
  • 휴대폰 숫자 자판 — 되찾았다. 한 줄이면 된다
  • 없는 날 거르기 — 되찾았다. 2월 30일 같은 건 짧은 검사로 걸러진다
  • 나라별 표기 — 못 되찾았다. 지금 한국어만 쓴다는 사정에 기대 미뤘다

마지막 줄이 중요하다. 「지금은 상관없다」로 끝내면 나중에 언어를 붙일 때 이유를 모른 채 마주친다. 무엇을 포기했고 무엇에 기대어 포기했는지를 같이 적어 두면, 그 전제가 깨지는 날 여기부터 보게 된다.


넘치는 입력을 조용히 자르지 않는다

자리 수가 여덟으로 정해진 칸에 아홉 번째 숫자가 들어오면 무슨 일이 일어나야 하나. 답이 셋이다.

  • 자른다 — 넘친 만큼 버린다
  • 거절한다 — 아예 안 받는다
  • 덮어쓴다 — 그 자리의 것을 바꾼다

셋 중 무엇인지 고르는 것이 설계다. 그리고 아무 말 없이 잘리게 두는 건 셋 다 아니다. 그건 고르지 않은 것이고, 고르지 않으면 「모양만 맞는 값이 조용히 만들어지는」 쪽으로 기운다.

이 칸은 덮어쓰기가 맞았다. 자리마다 뜻이 정해져 있어서(앞 넷은 연도, 다음 둘은 월) 하나를 끼워 넣는 순간 뒤가 통째로 밀려 뜻이 어긋난다.

조용한 손상은 오류보다 나쁘다

이 버그가 오래 안 보인 이유가 있다. 오류가 안 났다.

앞자리를 고쳤을 뿐인데 뒷자리가 바뀌는데도 화면에 아무 표시가 없다. 게다가 결과가 2202-70-83처럼 날짜 모양을 그대로 유지하고 있어서, 그럴듯한 만큼 더 안 보인다.

오류는 시끄러워서 고쳐진다. 조용한 손상은 사람이 눈으로 잡아내야 하고, 안 잡히면 그대로 저장된다. 이 앱에서는 그게 「반복 일정이 엉뚱한 해에 걸린다」가 된다.


같은 숫자를 다시 쳤더니 친 글자가 칸에 남았다

요즘 화면 코드의 기본 약속은 이렇다 — 값을 물려 주면 화면이 알아서 따라온다. 그래서 물려 준 값과 눈에 보이는 것이 늘 같다고 여기게 된다.

실제로는 물려 준 값이 바뀌었을 때 따라온다. 값이 그대로면 아무도 되돌려 놓지 않는다. 원래와 같은 숫자를 쳐서 결과가 안 바뀐 경우가 정확히 그 자리였고, 방금 친 글자가 칸에 남았다.

  • 편하게 해 주는 장치일수록 조건을 모른 채 쓰게 된다. 잘 도니까 들여다볼 일이 없다
  • 조건을 모르면 조건 밖에서 난 일을 버그로 못 알아본다. 「그럴 리가 없는데」에서 시간이 간다
  • 그런 자리에서는 약속을 잠깐 벗어나야 한다. 규칙을 어기는 게 아니라 그 규칙이 안 닿는 자리를 메우는 것이다

가져온 코드는, 코드가 아니라 그것이 풀던 문제를 본다

달력을 여는 코드가 앱 안에 이미 있어서 그대로 가져왔다. 그런데 반대로 돌았다 — 날짜를 쳐 놓고 달력을 열면 그 날이 아니라 이번 달이 나왔다.

가져온 쪽은 「고르면 무언가 하는 자리」였다. 같은 날을 다시 골라도 동작해야 해서, 열기 전에 칸을 비워 두고 있었다. 새 자리는 「값을 정하는 자리」다. 같은 날을 고르면 아무 일도 안 일어나는 게 맞으니 비울 이유가 없었다.

  • 가져올 때 봐야 하는 건 코드가 무엇을 하는지가 아니라 무엇을 막으려고 그렇게 돼 있는지다. 그다음 그게 여기서도 막을 것인지 묻는다
  • 같은 앱 안에서 옮길 때가 오히려 위험하다. 남의 코드는 의심하고 보는데 우리 코드는 안 그런다. 같은 달력, 몇 줄 안 되는 코드였는데도 정반대였다

값이 싸면 고르게 하지 말고 둘 다 남긴다

칸을 직접 만들면서 달력을 없앨 수도 있었다. 날짜를 이어서 칠 수 있으면 달력이 굳이 필요하냐는 것이다.

그러지 않았다. 날짜를 아는 사람은 치는 게 빠르고, 「다음 주 금요일」처럼 요일로 세는 사람은 달력이 빠르다. 어느 한쪽이 맞는 게 아니라 사람마다 다르고, 같은 사람도 그때그때 다르다.

  • 「둘 중 무엇이 나은가」를 묻기 전에 「둘 다 두는 값이 얼마인가」를 먼저 본다
  • 여기서는 안 보이는 칸 하나와 버튼 하나였다. 그 정도면 고를 일이 아니다
  • 값이 비쌀 때만 고르는 문제가 된다. 싼데도 고르려 드는 건 대개 취향 싸움이다

요약 — 만들어진 것을 쓸 때는 그것이 무엇에 기대는지까지 본다

여섯 가지가 다 한 줄기였다. 완제품 칸이 편했던 이유(안을 안 봐도 된다)가 그대로 못 고치는 이유였고, 버릴지 정하는 기준도 결국 딸려 오던 것을 되찾을 수 있느냐였다. 알아서 맞춰 주는 장치도, 앱 안에서 가져온 달력 코드도 같은 자리에서 걸렸다 — 잘 도는 동안에는 조건이 안 보이고, 조건 밖으로 나가는 날 처음 보인다.

  • 만들어진 것을 쓸 때는 무엇을 해 주는지가 아니라 무엇에 기대어 그렇게 해 주는지를 본다
  • 그 전제가 이 자리에서도 참인지 묻는다. 아니라면 고칠 수 있는 자리인지부터 확인하고, 못 고칠 때에만 직접 만드는 쪽으로 간다
  • 무엇을 포기했고 무엇에 기대어 포기했는지는 적어 둔다. 전제가 깨지는 날 여기부터 보게 된다

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