Skip to content

숨은 탭 판 표식 누수 전수 가드 — 그룹 작성창을 열어 둔 채 탭을 옮기면 바닥 여백이 사라지던 것에서 CSS 게이트 56건 한 문법·정적 게이트·일 요약 크롬 채널까지 (2026-09-03)

  • 기간: 2026-09-03 (세션 3개 — 감사 11f17c78, 구현 Phase 1~5 1d5419b5, 마무리 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.mjs layerOpen·none 앵커 · appScrollPosition.test.mjs(신설) · tabSwitchLatencyProbe.test.mjs 5건 추가 · e2e dock-geometry.spec.mjs "drawer → records-tool layer over home" 스텝(full 레인)
  • 버그리포트: bug-report/bug-071-20260903.md (그룹 보드 작성창을 열어 둔 채 탭 이동 시 바닥 여백 소실)
  • 계약: shell-frames.md §2 숨은 판 가드 문법 1종(예외 없음) · §2-1 data-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)

  • MobileTabsRegiondaySummaryOpen 입력, 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 / 편집 중 240px118px(유도식 값)
홈 위 레이어 지면 displayflex(홈 뒤) / block(리포트 뒤)block(뒤 탭 무관)
시트 스크림·댓글 시트·풀페이지 상세·복귀 바가 레이어 위 탭바를 지움지움(none)표시
일 요약 레이어 --bottom-chrome-h104px(채널 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 무동작)은 이 트랙에서 손대지 않았다.