Skip to content

피드 당겨서 새로고침 — 최신 갱신 수단 부재에서 커밋 0 제스처까지 (2026-08-25)

  • 기간: 2026-08-25 (세션 2 — 구현 6402d603·랜딩 이 세션, 오너 요청 원문: "피드 탭에서 화면 아래로 잡아당기면 새로고침되게 해줘" — 이슈 #774)
  • 랜딩: PR #776(Phase 1, bf45ebd3) · PR #778(Phase 2-1 스프링백) · PR #781(Phase 2-2 문구 통일, 오너 지시 CI 생략 admin 머지·사후 CI 통과) — 마이그레이션 0·서버 신설 0, Vercel 배포 확인(.fd-ptr를 lazy 청크 feed-*.css에서, 문구 통일을 FeedScreen-*.js에서 실측)
  • 설계서: 없음(단일 기능 트랙) — Phase 계획서·예상 효과·개선사항 표는 이슈 #774 본문
  • 정본: src/react/ui/mobile/lib/pullRefresh.ts(createPullRefreshGesture 상태 기계 + usePullRefresh 훅, 튜닝 상수 PULL_REFRESH_TUNING 한 곳) · feed-props.md onRefresh
  • 도구: 없음
  • 게이트: tests/react/feedPullRefresh.test.mjs 8건(스크롤 게이트·슬롭·감쇠·임계·busy 재진입 금지·배선 단언)
  • 버그리포트: 없음(기능 신설)
  • 계약: feed-props.md Callbacks에 onRefresh() → Promise? 등재(미공급이면 제스처 비활성)

Phase 현황

Phase내용상태
Phase 1구현·PR·CI·머지✅ PR #776 (bf45ebd3)
Phase 2실기기 제스처 확인✅ 오너 확인 "잘 되고 … 완료됐음"(08-25)
Phase 2-1오너 피드백 반영 — 놓는 즉시 스프링백(busy 유지 구간 제거)✅ PR #778
Phase 2-2오너 지시 — 문구 "당겨서 새로고침" 하나로 통일✅ PR #781
Phase 3홈 탭·일지 탭 등 다른 탭 확산 여부✅ 결정: 확산 안 함, 피드만 유지(08-25 오너)
Phase 4작업 기록✅ 이 문서

트랙 종결(2026-08-25) — 오너 확인 후 이슈 #774 닫음.

1. 배경

팔로잉 포스트(2026-08-23, 20260821480000) 도입 뒤에도 피드 탭의 갱신 경로는 셋뿐이었다 — 최초 1회 적재(loadedProfileFeedRef 게이트로 재진입 시 재적재 없음), 무한 스크롤(다음 페이지만), 세션 저장/삭제 후 무효화(내 글만). 즉 친구가 새로 올린 글을 사용자가 앱 안에서 받아볼 수단이 없었다. 에러 상태에서만 "다시 시도" 버튼이 노출된다. 오너가 표준 모바일 관용구인 당겨서 새로고침을 요청했다.

2. 문제 제기

최신 피드를 다시 받을 트리거가 UI에 없다

retryProfileFeed(= loadProfileFeed({force:true}), 첫 페이지 강제 재적재)는 이미 있었지만 에러 화면의 "다시 시도"에만 배선돼 있었다. 정상 상태에서는 호출 경로 자체가 없음.

제스처를 새로 달 때의 렌더 예산 제약

렌더 입자성 캠페인(2026-08-23) 이후 제스처 중 React 커밋은 0이 규범이다(useSheetDrag가 선례). useState로 당김 오프셋을 추적하면 touchmove마다 루트 커밋이 터진다 — 같은 화면에서 회귀.

3. 해결 방안

원칙 (오너 요청, 2026-08-25)

"피드 탭에서 화면 아래로 잡아당기면 새로고침" — 적용 범위는 요청 그대로 피드 탭만. 다른 탭 확산은 Phase 3 오너 결정으로 남김.

접근

대안판정
신규 새로고침 RPC/로더기각 — retryProfileFeed가 이미 정확히 "첫 페이지 강제 재적재"다. 신설은 중복
React 상태로 당김 오프셋 추적기각 — touchmove마다 커밋. 입자성 규범 위반
판정 로직을 훅 안에 인라인기각 — 임계·감쇠·재진입 금지를 단위 테스트로 잠글 수 없다
DOM 무의존 상태 기계 분리 + DOM 직접 조작 훅채택createPullRefreshGesture(idle→tracking→pulling→busy)는 순수 로직이라 8건 테스트로 고정, 훅은 transform·data-ptr 속성만 만진다(커밋 0)

인디케이터는 스피너 없이 텍스트 + opacity 펄스(오너 로딩 UX 원칙 = 스피너 폐기). 문구 전환은 CSS가 data-ptr(pull/arm/busy) 속성 셀렉터로 수행 — React 무관여.

4. 적용한 내용

Phase 1 — 구현·랜딩 (#776)

파일변경
src/react/ui/mobile/lib/pullRefresh.ts신설 — 상태 기계 + usePullRefresh(onRefresh) 훅(contentRef 콜백 ref·indicatorRef 반환)
src/react/ui/mobile/screens/FeedScreen.tsxonRefresh prop, .fd-full에 contentRef + 첫 자식 .fd-ptr 밴드
src/react/ui/mobile/styles/screens/feed.css.fd-ptr 규칙(상단 밖 52px 밴드, data-ptr별 문구, busy 펄스)
src/react/mobileApp.tsxonRefresh={retryProfileFeed} 한 줄
docs/contracts/feed-props.mdonRefresh 계약 등재
tests/react/feedPullRefresh.test.mjs신설 8건

동작: .screen.scrollTop > 0이면 즉시 브라우저 스크롤에 넘김 → 아래로 8px(slop) 넘게 끌면 클레임·preventDefault()({passive:false}) → 감쇠 0.5로 최대 110px 당김, 56px 이상에서 놓으면 발동 → 52px 고정 + retryProfileFeed() + 최소 450ms 유지 후 원위치. busy 중 재당김 무시. 튜닝 상수는 PULL_REFRESH_TUNING 한 곳.

Phase 2-1 — 오너 피드백 반영: 놓는 즉시 스프링백 (#778)

오너 피드백(08-25): "그냥 땡기고 나서 바로 위로 올라가게 해줘. 새로고침중 이런 텍스트 때문에 뭔가 애니메이션이 끊기는듯." → 임계 이상에서 놓아도 즉시 원위치로 변경: 52px 유지 오프셋·data-ptr="busy" 표시·450ms 최소 홀드 제거(settleOffsetPx·minBusyMs 상수 삭제, "새로고침 중" span·busy CSS 제거). 새로고침 Promise가 접힐 때까지 재발동을 막는 가드는 유지. 같은 턴 오너 결정으로 Phase 3(다른 탭 확산)은 확산 안 함으로 종결.

이어서 Phase 2-2(#781): 오너 지시 "텍스트만 당겨서 새로고침 으로 바꿔줘. ci하지말고 바로 반영" — 인디케이터 문구를 상태 무관 "당겨서 새로고침" 하나로 통일(arm 문구·.fd-ptr-msg 전환 CSS 제거), 지시대로 CI 대기 없이 admin 머지(로컬 게이트 전체 통과·사후 CI verify 통과). 오너 실기기 확인(08-25)으로 트랙 종결.

주요 결정과 그 근거

  • 에러 표면 없음: onRefresh 실패는 인디케이터만 접힌다 — 오류 표출은 기존 컨테이너 경로(reportedUserError·status="error")가 담당. 당김이 별도 에러 UI를 만들면 관문 밖 표면이 늘어난다.
  • 최소 busy 450ms(#776): 번쩍임 방지 의도였으나 오너 체감은 "애니메이션이 끊긴다" — Phase 2-1에서 busy 표시 자체를 제거하며 함께 삭제.

작업 중 드러난 것

  • 소스 앵커 게이트 오탐: 테스트에서 렌더 출력에 html.indexOf("8자+ 리터럴")을 쓰면 sourceSliceAnchors 게이트가 소스 앵커로 수집해 실패한다 → 렌더 출력 검사는 정규식 단언(assert.match)으로.
  • e.touches[0]는 TS2532: 인덱스 접근은 지역 변수로 좁혀서 사용.
  • 로컬 ESLint 실행 불가(기존 환경 문제)라 lint 판정은 PR CI가 유일했다.
  • Vercel 마커는 메인 app-*.css에 없다: 피드 화면 스타일은 mobileRoot 아래 lazy 청크 feed-*.css로 스플릿된다 — 배포 확인 폴링이 메인 CSS만 봐서 10분간 오탐(lazy 청크 함정 재확인). 마커 검사는 화면 소유 청크를 찾아서 할 것.

5. 적용 결과

항목결과
앱 내 최신 피드 갱신 수단0개 → 1개(에러 전용 "다시 시도" 제외)
제스처 중 React 커밋0 유지(DOM 직접 조작 — 입자성 규범 준수)
서버 변경0(기존 retryProfileFeed 재사용)
해제 애니메이션52px 유지+"새로고침 중" 표시 → 놓는 즉시 스프링백(#778, 오너 피드백)
전체 테스트 스위트1,975 pass / 0 fail(신설 8건 포함, 워크트리 실측) · tsc·dead-CSS·소스 앵커 게이트 통과
CI(lint 포함)✅ scope·verify pass(verify 3m46s) — 로컬 ESLint 불가라 lint는 CI가 판정
Vercel 반영✅ Production assets/feed-CgpQb4vB.css.fd-ptr 규칙 실측 — 마커는 레포에서 이 PR의 4파일에만 존재(고유성 증명)
인디케이터 문구pull/arm/busy 3종 → "당겨서 새로고침" 하나(#781, 오너 지시) — Production FeedScreen-*.js 실측
실기기 제스처 체감오너 실기기 확인(08-25, 스프링백 반영 후 "잘 되고 … 완료됐음")

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

친구 새 글을 화면에서 즉시 받는다

탭 재진입·앱 재시작 없이 표준 제스처 한 번으로 첫 페이지 재적재. 팔로잉 포스트 기능이 실사용 가능한 루프로 닫혔다.

구조적으로 남는 것

  • createPullRefreshGesture는 DOM·React 무의존이라 다른 탭 확산(Phase 3) 시 그대로 재사용된다.
  • onRefreshfeed-props.md 계약 항목이라 컨테이너 교체와 무관하게 유지된다.
  • 제스처 판정 회귀는 테스트 8건이 잠근다(임계·재진입·스크롤 게이트).

남은 것

없음 — 전 Phase 종결(오너 확인 08-25, 이슈 #774 닫음).