chatmu · .scratch/commit-button · 티켓 01

한 번 누르면 한 번 일어난다

한글을 치는 중에 버튼을 누르면 아무 일도 안 일어난다. 그것을 막는 규율이 일곱 자리에 손으로 적혀 있었고, 여섯이 반쪽이었다. 낱말은 CONTEXT.md 의 것을 쓴다.

0 · 쓰는 낱말

Room
Message 가 시간순으로 쌓이는 곳. 화면에서는 이름이 있고 DM 이 아닌 것을 채널이라 부른다.
Actor
발화할 수 있는 주체. 사람으로 한정되지 않는다.
User
사람 계정. 로그인의 주체다. 가입 화면이 만드는 것이 User 다(Invite 와 다른 축이다).
Message
Actor 가 Room 에 발화한 것. 작성자 본인은 나중에 고치거나 거둘 수 있다 (ADR 0008).

1 · 무엇이 문제였나

한글을 치는 사람이 버튼을 누른다. IME 가 조합 중이다. 아무 일도 안 일어난다.

이 순서로 사라진다:

  1. 조합 중에 버튼을 누른다.
  2. mousedown 을 IME 가 먹는다. 버튼에 도달하지 않는다.
  3. 브라우저는 mousedown 이 있어야 click 을 만든다 — 안 만든다.
  4. 폼은 click 이 있어야 submit 을 만든다 — 안 만든다.
  5. 핸들러가 아예 안 불린다. 누른 사람에게는 「안 먹는 버튼」이다.

고치는 법은 이미 알고 있었다 — pointerup·mouseup 에도 건다.

그런데 그 고침이 두 번째 문제를 만든다. 평범한 마우스 누름은 pointerupmouseupclick 을 차례로 보낸다. 그래서 한 번 누르면 핸들러가 두세 번 불린다.

그러면 그중 하나만 세어 남겨야 한다. 빠져 있던 것이 이 뒷반쪽이다.

2 · 세어 보니

앞반쪽(세 이벤트에 건다)은 여러 자리가 손으로 적고 있었다. 뒷반쪽(세어서 하나만)은 한 자리에만 있었다.

스펙은 여섯 자리라고 적었다. 세어 보니 일곱이었다.

한 번 누르면 일어나는 일두 번 일어나면무엇이 막고 있었나
1 굵게·기울임·목록을 건다composer/Toolbar.tsx 걸었다 풀었다 다시 건다 버튼
2 Room 에 Message 를 만든다composer/PlateComposer.tsx 같은 Message 가 두 줄 간다 핸들러
3 이모지 패널을 연다composer/EmojiButton.tsx 아무 일도 아니다 — 두 번 열어도 열린 것이다 동작 자체
4 User 를 만든다app/signup/page.tsx 가입 요청이 세 건 나간다 핸들러
5 User 의 비밀번호를 바꾼다app/account/page.tsx 같은 변경이 두 번 나간다 핸들러
6 Room 을 만든다app/rooms/RoomSidebar.tsx 같은 이름의 Room 이 둘 생긴다 핸들러
7 Message 를 고친다app/rooms/[roomId]/MessageEditSession.tsx 같은 변경이 두 번 나간다 핸들러
마지막 칸만 보면 된다. 방어가 버튼에 있는 것은 1번 하나다. 2·4·5·6·7 은 전부 핸들러에 있다 — 버튼을 읽는 사람은 그 방어를 볼 수 없다. 멱등하지 않은 핸들러를 하나 붙이는 날 조용히 두 번 실행된다.

일곱째가 이 티켓의 발견이다

스펙 표에 없던 자리다. 그리고 여섯 중 어느 것보다도 반쪽이었다.

mouseup 하나만 걸려 있었다 — pointerup아예 없었다.

그런데도 아무 증상이 없었다. 핸들러의 깃발이 뒤에서 두 번째 호출을 막고 있었기 때문이다.

이 티켓이 없애려는 모양이 정확히 그것이다.

일부러 안 거는 자리도 하나 있고, 그게 맞다. Room 을 나가는 버튼은 click 하나뿐이다. 삼켜진 첫 클릭이 잃는 것은 확인 단계를 펴는 것뿐이고, 다시 누르면 열린다. 되돌릴 수 없는 동작을 세 이벤트에 걸지 않는다.

3 · 그물은 무엇을 보고 있었나

브라우저 검사가 개수를 센다. 이미 세고 있던 자리는 셋이다 — 발신 · 가입 · 방 만들기.

자리검사재는 대상
Message 발신 세고 있다Room 에 확정된 Message 수 = 1핸들러
User 만들기 세고 있다가입 요청 수 = 1핸들러
Room 만들기 세고 있다그 이름의 Room 수 = 1핸들러
서식(굵게)누른 뒤 굵게가 걸려 있다버튼 · 다만 한 길만
이모지패널이 열린다개수를 안 잰다
비밀번호검사가 없다
Message 고치기개수를 안 잰다
세고 있던 셋(발신·가입·방 만들기)도 버튼이 옳다는 것을 증명하지 않는다. 셋 다 핸들러가 두 번째 호출을 막는다 — 발신은 값을 읽자마자 입력창을 비우고, 가입은 동기 깃발을 세우고, 방 만들기는 이름을 읽자마자 칸을 비운다. 그래서 버튼이 세 번 불려도 결과는 하나이고 검사는 통과한다. 버튼을 반쪽으로 되돌려도 그 셋은 그대로 통과한다. 즉 반쪽인 것을 볼 수 있는 검사는 하나도 없었다 — 빠뜨린 게 아니라 밖에서 보는 검사의 한계다.

그리고 그것이 이 변경의 근거이기도 하다 — 규율을 사람이 읽을 수 있는 자리, 즉 버튼에 둔다.

검사 하나를 더했다

평범한 마우스로 「굵게」를 한 번 누르면 굵게가 정확히 한 번 뒤집혀야 한다.

서식은 토글이라 횟수가 결과에 남는 유일한 자리다.

기존 검사는 mouseup 하나만 오는 길만 봤다 — 세 이벤트가 다 오는 길은 아무도 안 지나갔다.

이 검사도 홀수는 못 본다. 세 번 뒤집히면 결과가 한 번과 같다. 잡히는 것은 두 번이 되는 경우 — 두 규칙 중 하나가 지워졌을 때다.

4 · 무엇을 했나

버튼 하나를 냈다 — frontend/lib/pressOnce.tsx

부르는 쪽은 핸들러 하나만 준다. 네 갈래(pointerup·mouseup·click·폼 제출)는 버튼이 소유한다.

lib/ 인가: 일곱 중 넷이 화면이다. 입력창 모듈 안에 두면 가입 화면이 입력창 모듈을 import 한다 — 방향이 거꾸로다. 화면 쪽에 두면 이번엔 모듈이 화면을 import 한다. 같은 벽을 반대로 넘는다.

「한 번만」을 기본으로 하고, 예외 스위치를 안 뒀다

일곱 중 여섯이 「한 번만」이 필요하고 하나는 상관없다. 지금까지는 안전한 쪽이 예외였다.

스위치를 두면 그 판단이 다시 핸들러로 간다 — 지금 없애려는 모양이 그것이다.

핸들러는 한 줄도 안 바꿨다

깃발도, 「값을 읽자마자 입력창을 비운다」도 그대로다. 막는 것이 다르기 때문이다 — 느린 네트워크에 정말로 두 번 누르는 사람이다. 조작이 둘이면 버튼은 원리상 못 막는다.

바꾼 것은 주석뿐이다.

폼 버튼 넷의 type="submit" 은 유지했다

제출 버튼이 없으면 입력칸이 둘 이상인 폼은 Enter 로 제출되지 않는다. 가입 폼은 칸이 넷이다.

대신 버튼이 click 의 기본 동작을 막는다. 그래서 폼과 겹쳐도 일이 두 번 일어나지 않는다.

5 · 검증 상태

관문을 하나도 안 돌렸다. 사용자가 돌려야 한다.

  • 워크트리에 frontend/node_modules 가 없다. tsc·eslint·npm test 가 애초에 안 돈다.
  • 이 코드는 타입 검사조차 안 거쳤다.
  • 새로 더한 브라우저 검사도 한 번도 안 돌려 봤다.
  • 부러뜨려 빨강을 확인하지 못했다. 티켓에 적은 「어디가 빨개지는가」는 추론이지 실측이 아니다.

남긴 것

커밋