루프 61–80
6번째 블록. 테마가 완전히 바뀌었다. 앱 정체성이 "실험용 자판"에서 "모바일 DJ 패드"로 이동. 기존 풀 기능 UI는 Advanced Mode로 밀어넣고, DJ Mode가 기본. 이 블록의 방향은 한마디로: "엄지만 있는 사람을 위한 Oiiai."
이번 블록에 한 일
실수 수습
- Loop 61 — countdown 오버레이 [hidden] 무효 버그:
.countdown { display: flex }가[hidden] { display: none }을 같은 특이도로 눌러버려서 Make Clip 이후 어두운 그라디언트가 남아있던 문제..countdown[hidden] { display: none }명시로 해결. - Loop 62 — A 솔로 크로스페이드: 긴 샘플 'ka' 연타 시 오디오가 겹치던 문제.
activeSoloNodes추적 + 20ms 선형 페이드아웃. - Loop 63 — 🎰 세그먼트 랜덤 버튼 제거: "불필요"라는 판정. 호출처·HTML·CSS 모두 정리.
리듬 그리드
- Loop 64 — 비트 보완 (quantize 1/16):
beatAnchorAudio기준 다음 16분음표 그리드에 스냅. 새🧲 비트 보완토글, localStorage 기억. - Loop 65 — 메트로놈 위상 정렬 + 룩어헤드 스케줄러:
setInterval(click, period)→ 25ms 룩어헤드 + Web Audio 타임라인 예약. 첫 클릭도 tap anchor에 맞춰 스케줄. - Loop 66 — 직접 탭은 빨간색:
.tap-pulse키프레임을 파랑→빨강 링펄스로. BPM 하트비트(.tap-tick)는 파랑 유지 — 둘 구분. - Loop 67 — auto-beat 제거: 유저가 직접 연주하는 제품 방향과 상충. Make Clip 내부 호출도 제거.
DJ 모드 = 새 기본값
- Loop 68 — DJ 모드 프레임:
body.dj-mode클래스 +#app > *:not(...)숨김 규칙. 키 + 슬롯만 노출. - Loop 69 — 모바일 스크롤 잠금:
overflow: hidden+height: 100dvh+ 슬롯 영역 내부 스크롤 (overscroll-behavior: contain) → 버튼 누르다 페이지 밀리는 것 원천 차단. - Loop 70 — 3×3 숫자패드: 1–9를 이름만 크게 표시하는
.dj-pad타일로. 셀렉트·볼륨바·데스크립션 전부 DJ 모드에선 숨김. - Loop 71 — 이펙트 셔플 버튼: 패드 하단 중앙. "플로팅 좌상단" → "패드 하단 센터" 두 번 옮김 (인간 피드백).
- Loop 72 — 키 3+3 레이아웃: 모바일/데스크탑 통일. ㅜ ㅣ ㅏ / A B C.
- Loop 73 — DJ 모드가 기본,
⚙ Advanced가 토글: localStorage 초기값을on으로. 버튼 라벨은 다음 상태를 표시. - Loop 74 — 플로팅 상단 툴 바: TAP/BPM, 🧲 비트 보완, DJ 모드 전환을 하나의
.top-tools묶음으로 우상단. theme-toggle만 우측 끝.
온보딩 재작성
- Loop 75 — 시작하기 버튼:
#start-hint에 무지개 그라디언트 CTA. 클릭하면pressKey('KeyA')(audioCtx 준비 안됐으면 폴링 후 재시도). 모바일에서 "아무 키나"는 안 먹힌다는 현실 대응. - Loop 76 — 투어 DJ 기준 재작성: 3-step → 4-step. ㅜㅣㅏABC / 1–9 패드 / 🎲 셔플 / ⚙ Advanced. 스텝 총합
/3하드코딩을#tour-total로 동적화. - Loop 77 — 버블 자동 위치: 대상이 상단이면 버블 하단, 대상이 하단이면 버블 상단. 숫자패드 가릴 일 없게.
- Loop 78 — 투어 키 잠금 해제: 기존 "아무 키 눌러도 닫힘" 제거 — 이제 Escape·ArrowRight·버튼만. 유저가 투어 중에
n을 눌러 소리 내볼 수 있음.
A/B/C 배타
- Loop 79 — B 솔로 + 큰 슬로우모션 고양이: B도 A처럼 이전 재생 중단 (
ka/kb공통화). 추가로cat-pop-slow— 뷰포트 72% 크기, 3초, 블러/드롭섀도우/부양. - Loop 80 — C 영역 + 회전 고양이 + 상호 배타:
KeyC추가,cat-pop-rotate(2바퀴 회전, 랜덤 방향).activeSoloNodes에kc포함 → A↔B↔C 모두 배타. 이어서 1–9 ↔ A/B/C 양방향 stop (stopAllDj+stopAllSolo상호 호출).
기타 디테일
- 이벤트 티커에
scrollIntoView({ inline: 'center' })— 모바일에서 오래된 칩이 잘리면서 최신 칩은 항상 중앙. - FX 텍스트("DISTORT", "BOOM" 등)가 모바일에서 잘리던 문제 —
measureText기반 스케일 클램프 + 좌표 클램프. - 모바일 터치에도
startHold/endHold연결 (setPointerCapture) → 꾹 눌러서 빌드업 가능. bpmlocalStorage 저장·복원. 비트 보완이 persistence 되어 있으니 BPM도 맞춰줌.- FPS 미터를 좌측 상단으로 (우측 툴바와 겹침).
사람이 개입해서 구해낸 것
자동화된 개발로는 해결 못했고 유저가 명시적으로 지적해서야 고쳐진 것들. 솔직하게 남긴다.
- "어두운 오버레이가 안 사라져요" 1차 오진단
나는 투어 · start-hint · keyhelp 순으로 후보를 좁히면서 "keyhelp (물음표 눌러 연 도움말)일 겁니다" 까지 답했다. Playwright로 start-hint 테스트까지 돌려 '정상 동작' 확인하고 만족. 실제 원인은 countdown 오버레이였는데 찾지 못했다. 유저가 "countdown이 문제였어"라고 딱 말해주기 전까지. CSS 특이도 충돌을 한 번은 읽었으면서도 countdown까지 내려가지 못한 건, 가설을 "이 세 오버레이 중 하나"로 너무 일찍 좁힌 탓. 후보 탐색 폭을 키웠어야 했다.
- "ㅣ가 왜 두 개?"
유저가 "모바일에선 윗줄 ㅜ ㅣ ㅣ ㅏ"라고 명시해서 곧이곧대로 ㅣ 복제 키를 만들었다. 실제로는 오이아이 로고 감성 메모였는데 나는 기능으로 해석. 결국 "ㅣ 하나만 보여줘도 될 것 같은데?" 로 되돌림. 유저의 표현 의도를 기능 요구로 과잉 번역한 사례. 첫 패스에서 "4+2 vs 3+2 중 어떤 의도?"라고 짧게 확인했어야 했다.
- 여백 숫자 튜닝 3라운드
모바일 DJ 상단 여백 20dvh → 40dvh → padding: 52px ... ; justify-content: center 로 세 번 바꿨다. 매 턴 유저가 "좀 위로", "중앙보다 아래", "다시 위로" 로 고정해줬다. 나 혼자서는 "적당한" 위치를 못 정한다 — 스크린샷 보고 판단했어도 유저 감각과는 오차가 있었다. 결국 유저가 튜너 역할.
- 놓친 배타 관계
B 솔로 중단은 A 구현 후 유저가 "B도 해줘"라고 해서야 확장했다. 그 다음엔 "A↔B 배타", "1–9 누르면 A/B/C 중단", "A/B/C 누르면 1–9 중단" — 매번 유저가 한 방향씩 채웠다. 내가 A 처음 만들 때 "이 패턴이 B/C 포함 모든 solo에 필요할 수 있다"를 선제로 떠올렸으면 일괄 처리로 끝났을 것. 도메인 모델(= "동시에 하나만 소리 나야 하는 샘플 그룹")을 처음부터 도출하지 못했다.
- 하드코딩
/3투어 총합
4-step으로 늘렸는데 DOM의 /3 텍스트를 잊었다. 유저가 화면에서 "advanced mode가 4/3으로 나와"라고 보고. 요소 수 바꾸면 같이 움직이는 UI 표기를 그 자리에서 검증하는 루틴이 약하다. 코드 edit 후 스크린샷으로 한 번 더 훑는 습관 필요.
- TDZ 에러
currentBpm 복원 블록을 선언 이전 라인에 배치해서 Cannot access 'currentBpm' before initialization 발생. 유저가 브라우저 콘솔에서 복사해 붙여 넣어줘서 알았다. let은 hoist 안 되는데 읽어버린 실수. 에디터 내에서 돌려보지 않은 채 저장 — 정적 체크를 건너뛴 비용.
- 텍스트가 화면 밖으로
FX의 "DISTORT" "REVERSE" 같은 긴 라벨이 모바일에서 잘리는 걸 나는 한 번도 스크린샷에서 못 봤다. 유저가 "잘리는 경우가 있어서 크기 좀 조정"이라고 알려준 뒤에야 ctx.measureText 기반 클램프 추가. 모바일 뷰포트 스크린샷을 정기적으로 찍어 시각 검증하는 루틴이 없었다.
- 셔플 위치 두 번 옮김
처음엔 플로팅 좌상단, 그다음 숫자패드 위(우측 정렬), 그다음 숫자패드 아래(중앙). 각 턴 "거기 말고 저기"로 유저가 지도. 내가 처음부터 "이펙트 관련 행동은 이펙트 블록 근처에"라는 UX 원칙을 가지고 설계했으면 1회로 끝났을 수 있다.
- 티커가 모바일에서 잘림
scroll-center 없이 단순 flex-wrap: nowrap; overflow: hidden 이면 새 칩이 오른쪽 끝에서 잘려 보인다는 걸 데스크탑에선 몰랐다. 유저가 "내가 누르는 새 칩이 중앙에 오도록" 구체적 명세까지 줘서 scrollIntoView({ inline: 'center' }) + padding: 0 50% 적용. 디자인 감각을 유저가 언어로 내려준 사례.
공통 교훈:
- 나는 스크린샷 없이 "정상" 판정하는 경향이 있다. 특히 모바일.
- 도메인 개념을 feature 하나 단위로 잘게 만드느라 전체 제약조건 (예: "한 번에 소리 하나")을 놓친다.
- "적당한 여백", "크기 좀 조정" 같은 감각적 판단은 내가 잘 못함 — 유저 피드백 루프 없이는 튜닝 불가.
느낌 / 셀프평가
- Viral: 95% 유지 — 컨텐츠 변화 아님.
- DJ: 96% → 99% — 드디어 "패드 느낌". 3×3 숫자패드 + 이펙트 셔플 + 슬롯 1–9 → A/B/C 배타 + 모바일 스크롤 잠금. 진짜 MIDI 컨트롤러 같은 조작감.
- Mobile: 58% → 92% — 이번 블록이 모바일 올인. 3+3 키, 3×3 패드, 스크롤 잠금, 터치 빌드업, 시작하기 버튼, 티커 센터 스냅, 텍스트 클램프, 헤더 우측 정렬. 남은 건 실기기 햅틱 튜닝 정도.
- Onboarding/Polish: 98% → 99% — 시작하기 CTA + DJ 기준 투어. "첫 3초에 뭘 눌러야 하는지"가 명확.
다음에 하고 싶은 것
- 실기기 모바일 검증 — 8블록 연속 숙제. 이제는 DJ 패드라는 새 기본값이 생겼으니 실제 터치 반응 확인 필수.
- 세그먼트별 솔로 그룹 제네럴라이즈 — 지금은
ka/kb/kc하드코딩. DEFAULT_SEGMENTS에solo: true플래그로 바꾸면 추가·제거가 쉬움. - Advanced 모드 토글로 들어갔을 때 UX — 지금은 그냥 기존 UI가 나온다. 전환 애니메이션 또는 "이게 Advanced입니다" 안내.
- C 기본 색이 시안이라 ticker에서 파란 계열과 섞임 — 별도 색으로 구분.
메모
activeSoloNodes는 Map이 아니라 객체 리터럴. 키가ka/kb/kc3개 고정이면 Map 오버헤드 불필요. 솔로 그룹이 동적이 되면 Map으로.stopAllSolo+stopAllDj가playSegmentByIndex/playDjSlot각각 시작부에서 서로 부른다. 순서 뒤집힐 가능성은 없지만 "모두 멈춘 상태에서 새 재생" 이라는 invariant을 함수 이름에 녹여놓을 만하다.playExclusive(kind, idx)같은 래퍼.- 시작 버튼 폴링 (audioCtx 준비 대기)는 50ms × 최대 40회 = 2초.
init()이 대부분 500ms 이내로 끝나는 걸 확인해서 충분. 그래도 await 가능하게init()이 promise resolve 하는 방식이 더 깔끔. - CSS
box-shadow와filter: drop-shadow혼용 — 성능 주의. cat-pop-slow 는 드롭섀도우 + 블러 + 트랜슬레이트 전부 keyframe에서 움직인다. 저사양 기기에서 저프레임 올 수 있음. - 투어 버블 위치 계산은
r.top + r.height/2 < vh/2하나로 상/하단 결정.#dj-slots .dj-slot-wrap여러 개 중 첫 번째 기준이라 세 번째 슬롯쯤에선 중심이 아래로 갈 수 있음. 실제 가장 가까운 패드 하나를 고를 수도 있지만 과잉.
누적: 80 loops / 34 DJ effects / 6 devlogs / 80+ commits. 모든 Playwright 녹색 (24/24 모바일·데스크탑 양쪽).