Skip to content

좌우 슬라이드 화면 이동 규칙 정리 — 뒤로가기가 경로마다 다르던 것에서 화면 스택 하나로 (2026-09-08)

  • 기간: 2026-09-08 (세션 2개 5206098b-4b03-4aa0-bf43-7b1de9f5f88c Phase 1~5·PR, 44680527-9b9a-43f7-946d-86627fffbfeb 머지·기록. 오너 보고 원문 "지금 좌우로 스와이프해서 오버레이 전환하는거가 좀 문제가 있어보이거든 … 가장 중요한건 화면간 이동을 하면서 뎁스가 꼬이는 부분이 없어야 하고, 직관적이면서 좌우 스와이프 하느라 막 헷갈리거나 엉키는 부분이 없어야해" → 규칙 6개 채팅 결정 → 이슈 #1393 분석·계획 → "go")
  • 랜딩: PR #1395(Phase 1~5 한 PR, squash 머지 17495680 · staging Deploy run 34212623676 성공 — 직전 run 34212401611은 docs PR #1399가 바로 뒤에 머지되어 "더 새 커밋에 밀림" 검사로 중단) — 마이그레이션·엣지 함수·네이티브 셸 변경 없음. 앱 코드 58파일 +1,839 −914. Vercel 앱 배포는 릴리스(v0.17.3)까지 대기
  • 설계서: 없음 — 분석·Phase 계획·예상 효과·오너 결정 D1은 이슈 #1393 본문
  • 정본: docs/contracts/navigation-stack.md(오너 규칙 6개·D1·스택 항목·push/pop/unwind·popstate·history.state 규약·ignoreNextPop·이동 중 호출 미루기·스와이프 판정값·범위 밖) · 코드 src/react/controllers/navigationStore.ts(screens) · src/react/services/barbelicNav.ts(createHistoryDriver) · src/react/controllers/screenKinds.ts(등록표) · src/react/ui/mobile/lib/StackScreen.tsx(공용 셸) · src/react/ui/mobile/lib/slideBack.ts(useSwipeBack)
  • 도구: 없음(가짜 history 픽스처는 테스트 안)
  • 게이트: 단위 — tests/react/navigationStack.test.mjs 16(뒤로가기 4경로·탭바 초기화·드로워 경유·계정 전환·이동 중 호출) · swipeBack.test.mjs 7 · navigationStackRegistry.test.mjs 3(push되는 kind 전부 등록·.pg-slidein은 공용 셸 한 곳·전역 항목 3종). 잠금 테스트 9개 갱신(pending-changes 신고). npm run check 3,299건 fail 0. npm run ci:local -- --full: verify 통과(8단계) · db reset 통과 · schema.sql 스냅샷 --check 통과 · pgTAP 통과 119파일/2059 assert · e2e-local 12/12 · e2e-empty 8/8 · e2e-cardio 6/6 · e2e-persistence 38/38 · e2e-browser 57/57 · e2e-viewport 14/14 · e2e-evidence 통과 · 18분 18초(head b117d564). CI 1회 — 근거: 앱 화면·공용 서비스·e2e 기대값 변경 (run 34208862458, head bca5f6ef)
  • 버그리포트: bug-report/bug-083-20260908.md
  • 계약: navigation-stack.md 신설 · 개정 7종 — shell-frames·bottom-chrome-clearance·feed-props·session-screen-props·group-props·records-props·profile-screen-props(레이어 슬롯·pr 판·원점 인자·왼쪽 진입 서술을 스택 기준으로)

Phase 현황

Phase내용상태
Phase 1규칙 정본 문서 · 실패하는 스택 명세 테스트 12건(현행 코드로 12/12 실패 확인) · "열기 = 적재" 분리beb0b074
Phase 2navigationStore.screens 화면 스택 + 히스토리 드라이버 재작성 + 등록표 + 탭바 = unwindAll · 종전 레이어 API는 스택 어댑터4fa5b093
Phase 3공용 슬라이드 셸 · 탭별 스택 호스트 · 스와이프 훅 한 벌 · 히스토리 없던 오버레이 4종 이관(일지 월간·하루, 홈 리포트, PR 전체 보기)ee9f0b69
Phase 4그룹 5종 · 세션 상세 공용 오버레이 · 종목 상세(가짜 pr 판 제거) · 일 요약 · 친구 페이지(오른쪽 진입, 연 탭 스택 항목)30a0e1bc fb7ca091 12b741c4
Phase 5도구 화면 3종(D1=a) · 레이어 슬롯 은퇴 · 등록표 게이트 · 계약 문서 개정 · e2e 기대값 · 보완 4건9862c8b5 + e0890a75 c0bde974 a95093ff bca5f6ef → PR #1395 17495680

1. 배경

바벨릭 모바일은 탭 7개(홈·일지·피드·그룹·리포트·검색·내 정보)의 첫 화면 위에 "깊이 화면"이 오른쪽에서 슬라이드되어 쌓이는 구조다. 그룹 커피라운지 → 화이트보드, 일지 월간 요약 → 하루 상세 → 세션 상세, 홈 리포트, 종목 상세, 친구 페이지, PR 전체 보기, 드로워에서 여는 도구 화면 3종까지 슬라이드 화면이 12종(친구 페이지 안 일지 포함 15종)이다. 이 화면들은 한 트랙에서 설계된 것이 아니라 기능이 추가될 때마다 각자의 방식으로 열리고 닫혔다.

2026-09-08 오너가 "좌우로 스와이프해서 오버레이 전환하는 것에 문제가 있어 보인다"며 규칙 4개를 제안했고, 채팅에서 6개로 정리했다: ① 첫 화면은 가장 위, 좌우 스와이프로 화면 이동 없음 ② 깊이 화면은 한 겹씩 뒤로 ③ 슬라이드 화면은 전부 오른쪽에서 ④ 탭바 버튼 = 그 탭 첫 화면 + 깊이 스택 초기화 ⑤ 다른 탭에서 연 화면은 연 탭의 스택 위 ⑥ 슬라이드 화면 안에 내용 넘김용 좌우 스와이프 없음.

2. 문제 제기

"몇 겹째인가"를 기억하는 곳이 다섯 군데였고, 시스템 뒤로가기는 그중 한 곳만 봤다

iOS 가장자리 스와이프와 Android 뒤로 버튼은 브라우저 히스토리에 등록된 층만 본다. 등록은 barbelicNav.ts의 히스토리 스택이 맡았는데, 슬라이드 화면 12개 중 3개(세션 상세·종목 상세·친구 페이지)만 등록됐다. 그룹 3종은 groupStore, 일지 월간·하루는 일지 화면 지역 상태, 홈 리포트·PR 전체 보기는 각자 지역 상태에만 열려 있었다. 그래서 그룹 보드에서 Android 뒤로 버튼을 누르면 앱이 백그라운드로 내려가고(네이티브 셸이 history.state를 보고 "첫 화면"으로 판정), iOS 가장자리 스와이프는 무반응이었다.

상태 닫기와 히스토리 소비가 별개 호출이라 고아 항목·죽은 뒤로가기가 생겼다

탭바 이동은 종목 상세 상태만 지우고 히스토리 항목은 남겨, 다음 뒤로가기가 탭바에 없는 "기록" 탭의 빈 판으로 튀었다. 드로워가 위에 있을 때 아래 층을 닫으면 최상단이 아니라서 배열에서만 지워 브라우저 항목이 남았다. 스택이 비었을 때 "열려 있어 보이는 것"을 추측해 닫는 폴백은 손으로 적은 목록이라 친구 페이지·법률·그룹이 빠져 있었다.

스와이프 판정 5벌, 원점 복귀 장치 4종

스와이프 판정은 공용 훅 + 일지 복사본 3벌 + 홈 리포트 인라인으로 5벌이었고 임계값이 45px·70px로 제각각이었다. 세션 상세·종목 상세·일 요약은 여는 탭을 바꾸고 닫을 때 "원점 탭"으로 돌아가는 장치(returnTab·origin·sessionDetailOrigin·daySummary.origin)를 각자 가져서, 그룹 멤버로 연 친구 페이지에서 세션을 닫으면 배경 탭이 피드로 바뀌고, 홈 리포트 안에서 연 종목 상세를 닫으면 리포트 탭에 떨어졌다. 친구 페이지만 왼쪽에서 들어왔다.

탭바가 깊이 화면을 닫지 않았다

탭 판이 살아 있는 채로 숨겨지는 구조(keep-alive)라 하루 상세·그룹 보드·홈 리포트를 열어둔 채 다른 탭에 갔다 와도 그대로 열려 있었고, 같은 탭 버튼을 다시 눌러도 아무 일이 없었다.

어떤 구조라서 가능했나(§22): 화면 하나를 추가하려면 히스토리 등록·닫기 분기·폴백 목록·스크롤 복원 목록·하단 크롬 신고 다섯 곳을 손으로 맞춰야 했다. 그중 하나를 빠뜨린 결과가 미등록 화면 9종이다. 같은 구조가 남아 있으면 다음 화면에서 같은 결함이 다시 난다.

3. 해결 방안

원칙 (오너 규칙 6개 + D1, 2026-09-08)

  • 규칙 ①~⑥은 §1. 정본 navigation-stack.md §1.
  • D1 = (a): "드로워에서 여는 나의 주요 종목·1RM 직접 입력·커스텀 종목 관리 세 화면을 현재 탭 스택 위 화면(오른쪽 슬라이드 진입, 뒤로가기 4경로)으로 바꾼다." 2026-09-03 디자인 지시로 "화면 안 뒤로가기 없음"이었던 것을 이 트랙에서 예외 없는 구조로 되돌린 결정.
  • 묻지 않은 전제 9개(탭바 = 모든 탭 깊이 초기화, 초기화는 애니 없이 즉시, iOS 가장자리 제스처 유지, 친구 페이지 오른쪽 진입, 오버레이는 판 안, 모바일 "기록(pr)" 탭 제거, 일 요약도 스택 항목, 운동 플로우 안 단계·온보딩·웹 루트 뒤로가기는 범위 밖, 네이티브 셸 변경 없음)은 이슈 본문에 적고 그대로 진행.

접근

내용판단
A. 화면 스택 일원화내비게이션 저장소에 화면 스택 하나. push/pop/unwind가 상태와 히스토리를 항상 함께 움직인다. 등록표 1곳. 뒤로가기 4경로 = 같은 pop, 탭바 = 전체 초기화, 원점 복귀 장치 제거채택 — 같은 종류 결함이 구조적으로 불가능
B. 미등록 화면에 히스토리 등록만 추가각 화면 열기/닫기에 push/consume 덧붙임기각 — 분리 구조·다섯 곳 문제 그대로
C. 탭바 핸들러에 닫기 조건문 추가탭 누를 때 상태를 하나씩 닫음기각 — 증상 하나만 가리는 땜질

4. 적용한 내용

Phase 1 — 규칙 정본·명세 테스트·"열기 = 적재" (beb0b074)

  • docs/contracts/navigation-stack.md 작성. 명세 테스트 12건을 써서 현행 코드로 12/12 실패 확인(9건은 없는 API, 3건은 현행 결함 — 빈 스택 뒤로가기가 드로워를 닫음, 고아 히스토리 항목, 계정 전환 리셋에 스택 없음).
  • 탭 진입 적재 정책은 탭 값 변화에만 반응해 스택 push가 사각지대였다. 종목 상세 열기의 표시 위치를 셸이 주입(presentPrExerciseDetail, 데스크톱 = 탭 이동·모바일 = push)하고 PR 개요를 함께 적재, 일 요약은 하루 데이터를 탭 게이트 우회(allowInactive)로 명시 적재, 세션 상세가 일지 선택 날짜·표시 달을 바꾸는 것은 일지 판이 그릴 때만.

Phase 2 — 화면 스택·히스토리 드라이버·등록표·탭바 (4fa5b093)

  • navigationStore.screens: 항목 {id, kind, tab, params, closing}. 메뉴·운동·법률은 전역 항목(tab=null). pushScreen/requestPop/finishPop/unwindAll/unwindTo/screensForTab. 계정 전환 리셋 리터럴에 screens: [].
  • barbelicNav.createHistoryDriver: 히스토리만 움직이는 드라이버. 문자열 스택·추측 폴백·상태 미러 판정 폐지. 빈 스택의 뒤로가기는 아무것도 닫지 않음. 여러 겹은 go(-N) 1회 + ignoreNextPop 1.
  • controllers/screenKinds.ts 등록표에 기존 8종 층 등재. 종전 lgPushLayer/lgConsumeLayer는 스택 어댑터(최상단이 아닌 층을 닫으면 그 위까지 unwind — 배열에서만 지우던 분기 폐지).
  • 탭바 버튼 = unwindAll() + 탭 이동. 증상 D·E가 구조적으로 사라짐.

Phase 3 — 공용 셸·스와이프 한 벌·오버레이 4종 이관 (ee9f0b69)

  • UiStackScreen: 스택 항목 하나를 오른쪽 슬라이드 인 전체 화면으로 그리고 진입/퇴장·재마운트 제자리 복원(pg-restore)·스와이프·뒤로 버튼·하단 크롬 신고·퇴장 완료 처리를 한 곳에서. 항목별 Suspense 경계.
  • TabStackProvider: 자기 탭 항목만 구독하는 판별 호스트(조립 루트 비재렌더). 홈·일지·기록 판과 친구 판(가상 탭)에 배선.
  • useSwipeBack: 이동 중 첫 10px에서 축 고정, 손 뗄 때 60px 또는 30px + 속도 0.4px/ms, 왼쪽 가장자리 20px 제외, touchcancel 포기. 일지 복사본 3벌·홈 리포트 인라인 판정 삭제.
  • 이관: 일지 월간·하루 상세(journalMonth/journalDay), 홈 리포트(homeReport), PR 전체 보기(prTape/prList). 홈 리포트 안에서 연 종목 상세·일 요약은 닫으면 홈으로(증상 G 해소).

Phase 4 — 스토어 소유 화면 이관, 규칙 ②③④⑤ 완성 (30a0e1bc fb7ca091 12b741c4)

  • 그룹 커피라운지·화이트보드·완료 세션 상세·작성창·보드 안 종목 기록 뷰를 그룹 탭 스택 항목으로(닫기 = 컨테이너 등록 closer).
  • 세션 상세를 공용 오버레이(UiStackSessionOverlay) 하나로 — 일지·피드·검색·리포트·종목 상세·일 요약 어디서 열어도 연 탭 판 안에 서고, 모바일은 열 때 탭을 바꾸지 않으며 닫을 때 원점 복귀 장치 4종을 쓰지 않는다(데스크톱은 현행).
  • 종목 상세: 탭바에 없던 "기록" 판을 없애고 연 탭 스택 위 화면으로. 일 요약: 판을 가리던 레이어 슬롯에서 스택 항목으로.
  • 친구 페이지: 판 밖 왼쪽 진입에서 연 탭(피드·그룹) 스택 항목·오른쪽 진입으로. 안의 4탭은 교체이고 전환 시 위 층을 먼저 걷어낸다. 친구 피드 세션은 친구 전용 슬롯.
  • 정리: 원점·prJump·PR 로딩·그룹 세션 오버레이·closeFriendScope 등 미사용 바인딩 제거.

Phase 5 — 도구 화면 3종(D1=a)·레이어 슬롯 은퇴·게이트·계약 개정 (9862c8b5 + 보완 4건)

  • 나의 주요 종목·1RM 직접 입력·커스텀 종목 관리를 탭 스택 위 슬라이드 화면(recordsTool)으로 — 워드마크 행 좌측 뒤로 버튼 + 스와이프 + OS 뒤로가기. 드로워에서 열면 현재 탭 스택을 비운 뒤 push.
  • MobileTabFrame의 레이어 슬롯(.tab-keep off)과 판 밖 친구 판 은퇴. 법률 문서는 종전대로 전역 항목(app.tsx 층 호스트).
  • 게이트 navigationStackRegistry.test.mjs. 계약 문서 7종 개정.
  • 보완: ESLint 억제 검토 게이트(그룹 보드 날짜 이동 이펙트를 ref 패턴으로, 사라진 억제를 기준선에서 제거 e0890a75) · 히스토리 이동 중 호출 미루기(c0bde974, 아래 "작업 중 드러난 것") · 종목 상세 위 세션의 종목 강조를 스택에서 판정 + 디자인 마커 수집기 + 스켈레톤 기대값(a95093ff, 리베이스 뒤 npm run check가 잡음) · CI Node 22의 tsx 로더가 .mjs 테스트와 .tsx 소스에 다른 모듈 인스턴스를 주던 것을 테스트 다리 tests/react/support/tabStackContext.tsx로(bca5f6ef).

주요 결정과 그 근거

  • 열림 상태의 정본은 스택 하나. groupStore·friendScopeStore는 데이터만 갖는다. 계정 전환·관리자 유저 열람의 리셋 지점이 3곳 → 1곳.
  • 스택 구독은 판별 호스트에 격리. 조립 루트가 screens를 읽으면 탭 전환 시 루트 커밋 0 기준(렌더 입자성 계약)이 깨진다. 저장소는 바뀐 탭의 배열만 새 참조로 만든다.
  • 탭바 초기화는 히스토리 즉시(go(-N))·화면 제거는 판이 숨은 뒤. 같은 틱에 화면을 지우면 이전 탭 첫 화면이 한 프레임 보인다.
  • 숨은 탭의 깊이는 남기지 않는다. 브라우저 히스토리가 한 줄이고 하단 크롬 신고가 판 가시성을 모르기 때문. 규칙 ④와 동치.
  • 오버레이는 판 안(.pg-overlay), 새 슬롯 없음. 숨은 판 가드·바닥 여백 유도식·e2e 셀렉터·독 기하 게이트에 영향 없음.
  • 데스크톱은 같은 저장소를 쓰되 스택 동작은 모바일 셸에서만. 데스크톱 PR 탭·일지 탭 동작 불변, 각 Phase에 데스크톱 테스트 회귀 0.

작업 중 드러난 것

  • ci:local e2e dock-geometry가 결함 하나를 잡았다(c0bde974). 유저가 드로워에서 "나의 주요 종목"을 누르면 앱은 "열려 있던 화면 전부 닫기(브라우저 뒤로 이동)"와 "도구 화면 열기(브라우저 항목 추가)"를 연달아 부른다. 브라우저의 뒤로 이동은 나중에 실행되므로 새 항목이 옛 자리 위에 쌓이고 이동이 끝난 브라우저 위치는 그 아래에 섰다. 그 상태에서 탭바를 누르면 뒤로 이동이 앱 밖으로 나가 화면이 사라졌다. 수리 = 히스토리 드라이버가 "이동이 끝나기 전에 들어온 열기/닫기 호출"을 순서대로 미뤄 두었다가 이동이 끝난 뒤 실행한다(계약 §3, 재현 테스트 2건). 화면 쪽 호출 순서를 손보는 땜질 대신 드라이버 규칙으로 고정해 "닫고 바로 열기" 모든 경로에 적용.
  • ESLint 억제 검토 게이트가 npm run check에서는 안 잡고 ci:local verify에서 잡혔다 — 새 억제 대신 ref 패턴으로.
  • 리베이스(main #1367) 뒤 npm run check에서 실패 3건: 종목 상세 위 세션의 종목 강조(prJump를 없애며 빠짐)·디자인 마커 수집기·스켈레톤 기대값 → 수리 뒤 ci:local --full 재실행 통과.
  • CI 1회차(head b117d564) unit-tests 2건 실패: Node 22 + tsx 로더가 .mjs 테스트와 .tsx 소스에 다른 tabStack 모듈 인스턴스를 줌(npx node@22로 로컬 재현) → 테스트 다리(da383bff). browser-journeys CASE-040 1회차 net::ERR_ABORTED get_calendar_month_summary(탭 전환 중 취소된 요청) 재시도 통과 → e2e-evidence가 재시도를 실패로 봄(main 0bac9463에서도 같은 형태) — 재현 불가, 플레이키.
  • 머지 직전 main에 #1394·#1396이 먼저 들어와 리베이스(bca5f6ef, 충돌은 테스트 신고 파일 1개). 리베이스 head의 CI가 GitHub 결제/지출 한도 사유로 잡이 시작되지 않아("The job was not started because recent account payments have failed…") 오너 처리 뒤 재실행 → 통과.
  • Phase 3 커밋에 셸 리다이렉트로 생긴 잡동사니 파일(루트 controllers, 1줄)이 섞여 들어간 것을 머지 전 발견해 제거(b5b83dcd). 코드 참조 없음.
  • 사전 검증 누락: 없음(ci:local이 CI보다 먼저 dock-geometry 결함을 잡았다).

5. 적용 결과

항목전 → 후
시스템 뒤로가기(iOS 가장자리·Android 뒤로 버튼)가 아는 슬라이드 화면3/12 → 12/12 (친구 페이지 안 일지 포함 15/15) — 단위 테스트(가짜 history)·등록표 게이트로 확인
Android 뒤로 버튼이 앱을 백그라운드로 내리는 깊이 화면9 → 0 (히스토리 층이 있어 셸이 history.back()을 보냄; 실기기 실행은 없음)
탭바를 눌러도 남는 깊이 화면9 → 0. 같은 탭 재탭 무반응 → 초기화 + 스크롤 맨 위
히스토리 항목만 남는 경로(고아 항목·죽은 뒤로가기)3 → 0 (navigationStack.test.mjs로 고정)
닫을 때 탭이 바뀌는 경로 / 원점 복귀 장치5 → 0 / 4종 → 0
왼쪽에서 들어오는 화면1(친구 페이지) → 0
스와이프 판정 구현 / 임계값 종류5벌 / 3종 → 1벌 / 1종(60px 또는 30px+속도)
열림 상태 소유자 / 계정 전환 리셋 지점5곳 / 3곳 → 1곳 / 1곳
새 화면 추가 시 손으로 맞출 자리5곳 → 등록표 1곳 (미등록 kind·공용 셸 밖 .pg-slidein은 게이트 실패)
모바일 탭바에 없던 가짜 "기록(pr)" 탭있음 → 없음(데스크톱 PR 탭 유지)
테스트npm run check 3,273 → 3,299(신규 26) fail 0 · ci:local full 통과 · CI 통과
미검증슬라이드·스와이프의 감각적 품질(사람 눈). Android·iOS 실기기 실행. 탭 전환 프로필(tabSwitch.profile)로 조립 루트 커밋 0 유지는 렌더 입자성 계약 테스트로만 확인

6. 이번 개선으로 향상된 것

어느 화면에서 어떤 방법으로 뒤로 가든 한 겹씩 돌아온다

화면 ‹ 버튼·앱 안 스와이프·iOS 가장자리 스와이프·Android 뒤로 버튼이 전부 같은 pop을 부른다. 그룹 보드에서 뒤로 버튼을 눌러도 앱이 내려가지 않고 라운지로 돌아온다.

탭바는 언제나 그 탭의 첫 화면이다

탭바 버튼을 누르는 순간(같은 탭이어도) 열려 있던 깊이 화면이 전부 닫히고 첫 화면이 보인다. 하루 상세·그룹 보드·홈 리포트를 열어둔 채 다른 탭에 갔다 와도 첫 화면이다.

연 자리로 돌아온다

검색·리포트·홈에서 연 세션 상세·종목 상세를 닫으면 그 탭에 그대로 있다. 그룹 멤버로 연 친구 페이지에서 세션을 닫아도 배경 탭이 바뀌지 않는다. 친구 페이지가 오른쪽에서 들어온다.

구조적으로 남는 것

  • 계약 docs/contracts/navigation-stack.md — 규칙·항목 형태·드라이버 규약·스와이프 판정값·범위 밖.
  • 등록표 screenKinds.ts + 게이트 navigationStackRegistry.test.mjs — 새 화면은 등록표 한 줄로만 추가되고, 등록 없이 push하거나 공용 셸 밖에서 슬라이드를 그리면 npm run check가 실패한다.
  • 가짜 history 스택 테스트 navigationStack.test.mjs — 뒤로가기 4경로·탭바 초기화·드로워 경유·계정 전환·이동 중 호출 시나리오.
  • 히스토리 드라이버 규칙 "이동 중 들어온 호출은 popstate 뒤로 미룬다" — "닫고 바로 열기" 모든 경로에 적용.

남은 것

  • 범위 밖(계약 §7): 운동 플로우 안 단계(시작 → 기록 → 완료), 로그인 전 온보딩, 웹 브라우저(네이티브 밖) 루트 뒤로가기는 현행.
  • 후속 이슈 후보: iOS 로그인 재로드 때 about:blank가 뒤로 목록에 남을 수 있는 문제 — 코드 근거만 있고 실측이 없다.
  • 릴리스: 오너가 2026-09-08 release/v0.17.3 브랜치(ed3d6db7, #1395 포함)를 만들어 v0.17.3으로 묶었다 → Production 반영 뒤 이슈 [v0.17.3 반영완료] 전환.