숨은 탭 판 표식 누수 전수 가드 — 그룹 작성창을 열어 둔 채 탭을 옮기면 바닥 여백이 사라지던 것에서 CSS 게이트 56건 한 문법·정적 게이트·일 요약 크롬 채널까지 (2026-09-03)
- 기간: 2026-09-03 (세션 3개 — 감사
11f17c78, 구현 Phase 1~51d5419b5, 마무리60667bd1; 오너 지시 원문 #1184 D3 "13건에 그치지 말고 비슷한 케이스까지 전체적으로 봐 달라", go "1195 진행해줘") - 랜딩: PR #1213 (Phase 1~5 + 후속 1건,
a9b3e974) — 마이그레이션·엣지 함수 없음, Vercel 앱 배포(릴리스 레인) - 설계서: 없음 — 감사 결과·Phase 계획·예상 효과 = 이슈 #1195 본문, 전체 감사 표(규칙 20건 + 다른 숨김 방식 + JS 측) = 이슈 첫 댓글
- 정본:
docs/contracts/shell-frames.md§2 "숨은 판 가드" · §2-1 채널 열거값none·core.css판 기구 주석 - 도구: 로그인 없는 CSS 하네스(워크트리 루트
scratch-1195.html/ts, 커밋 제외 — 실제 CSS를 앱 순서로 import하고.app > .screen > [layer] + .tab-keep > .tab-pane골격을 표식만으로 재현해 computed 값 읽기) · 정확 치환 스크립트(스크래치패드) - 게이트:
tests/react/hiddenPaneGateGuards.test.mjs(신설, 허용 목록 없음) ·dockGeometryContract.test.mjslayerOpen·none앵커 ·appScrollPosition.test.mjs(신설) ·tabSwitchLatencyProbe.test.mjs5건 추가 · e2edock-geometry.spec.mjs"drawer → records-tool layer over home" 스텝(full 레인) - 버그리포트:
bug-report/bug-071-20260903.md(그룹 보드 작성창을 열어 둔 채 탭 이동 시 바닥 여백 소실) - 계약:
shell-frames.md§2 숨은 판 가드 문법 1종(예외 없음) · §2-1data-bottom-chrome값에none추가, 잔존:has()예외 목록에서 일 요약 제거
Phase 현황
| Phase | 내용 | 상태 |
|---|---|---|
| Phase 0 | 전수 감사 — 판 게이트 20건·다른 숨김 방식·JS 6곳 판정(수리 3·권장 1·확인 4·무해 다수) | ✅ 이슈 #1195 본문·첫 댓글 |
| Phase 1 | 실사고 수리: core.css workout 여백·workout.css 편집 행 240px/시트 도크·홈 flex·시트 스크림 | ✅ PR #1213 |
| Phase 2 | .app/.screen 레벨 :has() 게이트 전수(56건) :not(.tab-pane.off *):not(.tab-keep.off *) 문법 통일 | ✅ PR #1213 |
| Phase 3 | 재발 방지 정적 게이트 hiddenPaneGateGuards.test.mjs | ✅ PR #1213 |
| Phase 4 | 일 요약 레이어 = 크롬 채널 none, 드로어 도구 열 때 세션 상세·일 요약 정리, e2e 드로어→레이어 스텝, 계약 문서 | ✅ PR #1213 |
| Phase 5 | 탭 전환 프로브·앱 스크롤 위치 읽기에서 숨은 판 제외 + 단위 10건 | ✅ PR #1213 |
| Phase 6 | 릴리스 레인 Production 배포 → [반영완료] | ⬜ 릴리스 PR 대기 |
1. 배경
모바일 앱은 한 번 방문한 탭 화면(홈·일지·피드·그룹·리포트)을 지우지 않고 뒤에 숨겨 둔다. 탭 전환이 빠른 이유다. 드로어에서 여는 도구 화면(나의 주요 종목·내 1RM 기록 관리·커스텀 종목 관리)과 일 요약은 그 위에 얹는 "레이어"라, 그 순간 활성 탭 화면도 산 채로 가려진다.
화면 바닥 여백·탭바 표시·레이어 지면 같은 규칙은 "화면 안에 어떤 표식(작성창·시트·상세 화면)이 있으면"으로 켜지는 CSS :has() 규칙이다. 이 규칙이 숨어 있는 화면 안의 표식까지 세어 버리면, 지금 보이는 탭이나 레이어의 모양이 뒤에 숨은 화면에 따라 달라진다. 디자인 배송 v86(#1184)에서 "워드마크 행 흔들림" 13건이 이 부류로 잡혔고, 오너가 D3로 "비슷한 케이스 전체"를 보라고 했다.
2. 문제 제기
2-1. 그룹 보드 작성창을 열어 둔 채 다른 탭으로 가면 그 탭의 바닥이 탭바 밑으로 들어간다
유저 A가 그룹 탭에서 보드 작성창을 열고 종목·세트를 적다가 홈 탭을 누른다. 작성창은 닫히지 않고 그룹 판 안에 숨는다. 그런데 작성창은 운동 기록 화면 부품을 재사용해 workout.record 표식을 달고 있고, core.css의 "운동 중이면 바닥 여백 16px" 규칙에는 숨은 판 가드가 아예 없었다. 결과: 홈 화면 바닥 여백 118px → 16px, 콘텐츠 끝이 탭바 밑으로. 세트를 편집하던 중이면 workout.css의 "편집 행 있으면 240px" 규칙이 반대로 새어 홈 바닥이 240px로 늘어난다. 탭바 숨김 규칙은 8월에 채널 방식으로 옮겨 안전했는데, 여백 규칙 3건이 남아 있었다.
2-2. 홈 탭 위에 레이어를 열면 레이어 지면이 뒤 탭에 따라 달라진다
홈 화면 규칙(display:flex)은 .tab-pane.off 가드는 있었지만 .tab-keep.off(레이어 열림 때 전체 판 숨김) 가드가 없었다. 홈에서 드로어 → 나의 주요 종목을 열면 숨은 홈 판이 세어져 레이어 지면이 flex 컨테이너가 된다. 리포트 탭에서 열면 block이다. "뒤에 어떤 탭이 있느냐로 레이어 레이아웃이 갈리는" 바로 그 부류.
2-3. 가드 문법이 세 가지로 갈려 있었고, 섞이면 우선순위가 뒤바뀐다
56건 중 :not(.tab-pane.off *)만 있는 것, 둘 다 있는 것, 없는 것이 섞여 있었다. :not()은 인자의 특정도를 더하므로 가드가 붙은 규칙은 클래스 4개분 더 강하다. 같은 속성을 다루는 규칙 두 개 중 한쪽만 가드가 있으면, 선언 순서가 아니라 특정도로 승부가 나서 뒤에 선언한 규칙이 져 버린다(실례: core.css의 workout 여백 16px 규칙에 가드를 붙이자 바로 다음 줄 .flow-endbar 74px 규칙이 밀렸다).
2-4. 일 요약 레이어에서 하단 크롬 채널이 실제와 어긋났다
일 요약을 열면 탭바는 nav.css의 :has() 규칙이 숨기는데, 컨테이너가 게시하는 채널값은 "tabbar"(또는 시작 바 포함 "tabbar-start")라 여백 유도식 --bottom-chrome-h가 50/104px로 남았다. 실제 크롬은 0px. e2e dock-geometry는 "채널 = 실제 크롬"을 원칙으로 두지만 레이어를 밟지 않아 못 잡았다.
2-5. 계측 두 곳이 숨은 판을 읽었다
탭 전환 프로브는 .tab-keep.off를 안 봐 레이어 열림 중 탭 전환을 "페인트 완료"로 계측했고, 앱 스크롤 위치 읽기는 숨은 판·친구 스코프에 가려진 화면의 scrollTop까지 최댓값에 넣었다. 기능 영향은 없고 계측값만 오염.
3. 해결 방안
원칙 (오너 결정, 2026-09-03)
- D3(#1184) "13건에 그치지 말고 비슷한 케이스까지 전체적으로 봐 달라" → 전수 감사 뒤 Phase 계획, go.
- 대안 C 채택: 즉시 봉합(가드) + 재발 방지 게이트 + 채널 승격은 일 요약 한 건만.
접근
| 대안 | 판단 |
|---|---|
| A. 규칙마다 가드를 손으로 추가 | 부분 채택 — 새 규칙이 또 빠질 수 있어 게이트 동반 |
| B. 표식 규칙 전부를 채널 방식(컨테이너가 상태 게시)으로 승격 | 부분 채택 — 컨테이너가 아는 상태(일 요약)만. 화면 로컬 신호(댓글 시트·시트 스왑 상세)는 채널이 모른다 |
| C. A + 정적 게이트 + 채널 승격 1건 | 채택 |
| D. 증상 보고 때마다 개별 수리 | 기각 — 원인이 같은 규칙 묶음 |
| 게이트에 허용 목록(레이어·플로우 전용 표식은 가드 면제) | 기각(계획 대비 변경) — 2-3의 특정도 문제로 면제가 있으면 우선순위가 갈린다. 전수 같은 문법 |
4. 적용한 내용
Phase 1 — 실사고 수리 (PR #1213)
core.css workout 여백·.flow-endbar 여백, workout.css 편집 행 240px·.setbox-add 시트 도크, 홈 flex, overlays.css 시트 스크림(FAB·시작/복귀 바 숨김)에 :not(.tab-pane.off *):not(.tab-keep.off *).
Phase 2 — 전수 통일 (PR #1213)
.app/.app .screen을 주어로 하는 :has() 인자 56건 전부 같은 가드. nav.css 세션 상세 탭바 숨김은 두 다리(상세 표식·시트 스왑 예외)에 동시에 — 한쪽만 붙이면 레이어 위에서 탭바가 사라진다. feedCommentsTabbarGate.test 정규식 동반 갱신. 친구 스코프 바닥 여백 4건(홈·피드·리포트·일지) 통일.
Phase 3 — 정적 게이트 (PR #1213)
hiddenPaneGateGuards.test.mjs: 모바일 CSS 전체를 읽어 .app/.screen 레벨 :has() 인자가 가드로 끝나지 않으면 실패. 허용 목록 없음. 가드 하나를 지우면 빨간불이 나는 것을 확인.
Phase 4 — 일 요약 채널 승격 (PR #1213)
MobileTabsRegion에daySummaryOpen입력,layerOpen = recordsToolOpen || daySummaryOpen으로 시작/복귀 바 게이트, 채널값none신설(nav.css사상 0px, 탭바 숨김도 채널 규칙). 구 일 요약:has()규칙 삭제 — 계약 테스트가 그 문자열 부재를 단언하므로 주석에도 옛 문자열을 적지 못한다.openRecordsToolFromDrawer가 종목 상세만 닫던 것을 세션 상세·일 요약까지 닫는다 — 레이어를 닫을 때 옛 상세가 되살아나던 것도 함께 사라진다.- e2e dock-geometry에 "드로어 → 기록 도구 레이어 over 홈" 스텝(레이어 위 여백·탭바 실측). 일 요약 e2e는 운동한 날 데이터(리포트 잔디 셀)가 필요해 미포함.
Phase 5 — 계측 (PR #1213)
paneBecameVisible(.tab-keep.off 안이면 보이지 않음으로), lgVisibleScrollRoots(숨은 판·덮인 .screen 제외; navigationStore 테스트의 최소 document에는 querySelector가 없어 존재 확인 뒤 호출). 단위 10건. tabSwitchLatencyProbe.test 정규식을 CRLF 허용으로 바꿔 Windows 로컬 실패 해소.
Phase 4 후속 (PR #1213 b2b131aa)
전체 npm run check에서 workoutFlowDraftSafety·mobileGamificationDesignWiring 테스트가 옛 식 !recordsToolOpen을 앵커로 잡고 있어 실패 → !layerOpen으로 재조준.
주요 결정과 그 근거
- 허용 목록 대신 전수 가드:
:not()가드는 특정도를 올리므로 가드 유무가 섞이면 규칙 간 우선순위가 선언 순서 대신 특정도로 갈린다. 예외를 두면 그 예외와 같은 속성을 다루는 이웃 규칙이 조용히 진다. - 채널 승격은 일 요약만: 컨테이너가 이미 아는 상태라 채널이 정본이 될 수 있다. 댓글 시트·시트 스왑 상세는 화면 로컬 신호라
:has()잔존(가드 붙인 채).
작업 중 드러난 것
- 이슈 본문의 "가드 추가는 특정도를 올리지 않는다"는 판단이 틀렸다 —
:not()은 인자 최대 특정도를 더한다. Phase 1 하네스에서.flow-endbar규칙이 밀리는 것으로 발견, Phase 2·3 설계가 바뀌었다. - 워크트리 파일은 CRLF(autocrlf), 인덱스는 LF — 다중행 치환은 CRLF로.
fsb탭바 숨김 줄은friendScope.test정규식 때문에 그룹 마지막 자리를 유지해야 한다.- 로그인 없이 CSS 규칙을 검증하려면 실제 CSS를 앱 순서로 import한 골격 페이지에서 computed 값을 읽으면 된다(하네스, 커밋 제외).
- 범위 밖 관측: 커스텀 종목 관리 레이어(
.pf-cust)는 바닥 숨 14px vs 다른 레이어 12px 불일치,openHomeDay호출부 0,--sheet-dock기본 0이라.setbox-add규칙은 실질 무동작.
5. 적용 결과
| 항목 | 전 | 후 |
|---|---|---|
| 그룹 작성창 열어 둔 채 홈 이동 시 홈 바닥 여백(하네스) | 16px / 편집 중 240px | 118px(유도식 값) |
| 홈 위 레이어 지면 display | flex(홈 뒤) / block(리포트 뒤) | block(뒤 탭 무관) |
| 시트 스크림·댓글 시트·풀페이지 상세·복귀 바가 레이어 위 탭바를 지움 | 지움(none) | 표시 |
일 요약 레이어 --bottom-chrome-h | 104px(채널 tabbar-start) | 0px(채널 none) |
.app/.screen 레벨 :has() 가드 문법 | 3종 혼재(없음·1중·2중) | 1종 56건, 게이트 잠금 |
| 단위 스위트 | — | 2,482 pass · 0 fail (npm run check 전부) |
| e2e dock-geometry | 탭 4종 | + 드로어→레이어 스텝(CI full 레인) |
미검증: 일 요약 레이어 e2e(데이터 의존, 단위·계약 테스트로 대신), 시각적 품질은 하네스 computed 값과 자동 테스트만.
6. 이번 개선으로 향상된 것
- 그룹 보드를 쓰다 다른 탭으로 옮겨도 그 탭의 바닥이 탭바 밑으로 들어가지 않는다.
- 드로어 도구 화면·일 요약의 레이아웃이 뒤에 숨은 탭과 무관해졌다.
- 새
:has()규칙이 가드를 빠뜨리면 로컬 게이트·CI가 바로 잡는다. - 하단 크롬 채널이 일 요약까지 실제 크롬과 같은 값을 말한다.
남은 것
- Phase 6: 릴리스 PR(main → production) 머지 시 Production 배포 → 이슈 [반영완료].
- 범위 밖 관측 3건(
.pf-cust바닥 숨 불일치,openHomeDay미사용,.setbox-add무동작)은 이 트랙에서 손대지 않았다.