본문으로 건너뛰기

001. Pull-to-Refresh 기능 추가 (홈 화면)

Status: 계획 수립 작성일: 2026-02-08 발견 계기: 모바일 앱 사용자가 홈 화면에서 아래로 당겨서 콘텐츠를 새로고침하는 네이티브 UX 패턴 필요 참조 구현: apps/dha-sleep-app-web/components/screens/home/home-screen.tsx 연관 Plan: 없음

배경

dha-sleep-app-web의 홈 화면은 HomeCard(동적 인사말 + 제안 액션)를 표시하지만, 사용자가 수동으로 콘텐츠를 새로고침할 방법이 없다. 현재는 앱 포그라운드 복귀 시 5분 이상 경과한 경우에만 자동 재페치되며, 사용자 주도적인 갱신은 불가능하다.

Pull-to-refresh는 모바일 앱의 핵심 UX 패턴으로, 사용자가 화면 상단에서 아래로 스와이프하여 최신 데이터를 요청할 수 있게 한다.

Goals

  1. 홈 화면에서 pull-to-refresh 제스처로 HomeCard + Prefetch 데이터를 갱신할 수 있다
  2. 새로고침 진행 중 시각적 피드백(스피너/인디케이터)을 제공한다
  3. Framer Motion(이미 의존성에 존재)만으로 구현하여 새 라이브러리 추가 없이 완성한다

Non-Goals

  1. 채팅 화면(/chat)에 pull-to-refresh 추가 — 별도 플랜으로 분리
  2. 네이티브 Capacitor Pull-to-Refresh 플러그인 통합 — 웹 기반으로 구현
  3. 홈 화면 레이아웃 리디자인 — 기존 구조 유지
  4. HomeCard API 백엔드 변경 — 기존 GET/POST /api/home-card 활용

문제 분석

현상

  • 사용자가 홈 화면에서 수동으로 데이터를 갱신할 수 없음
  • visibilitychange 기반 자동 재페치는 사용자 의도와 무관
  • stale 카드가 표시될 때 갱신할 직관적 방법 부재

근본 원인

  • Pull-to-refresh 컴포넌트 미구현
  • 홈 화면의 메인 영역이 스크롤 컨테이너가 아닌 flex-col 고정 레이아웃

기존 구현 현황

항목상태비고
HomeCard 페치 로직✅ 완료useHomeCardStore.fetchCard()
HomeCard 리프레시 로직✅ 완료useHomeCardStore.refreshCard()
Prefetch 로직✅ 완료usePrefetchStore.prefetchAll()
포그라운드 복귀 재페치✅ 완료visibilitychange 이벤트
Pull-to-refresh UI❌ 미구현이번 플랜 대상
Touch gesture 처리❌ 미구현Framer Motion drag 활용 예정

해결 방안

핵심 원칙

  1. 외부 라이브러리 없이 Framer Motion의 drag + onDragEnd로 제스처 감지
  2. 기존 refresh 로직 재활용: fetchCard() (GET) 호출하여 최신 HomeCard 즉시 반영
  3. 접근성 준수: aria-label, prefers-reduced-motion 적용
  4. 터치 최적화: 44px+ 터치 타겟, smooth 애니메이션

데이터 플로우 (C4 Level 3)

사용자 Pull Down (touchstart → touchmove → touchend)


PullToRefresh 컴포넌트 (터치 이벤트 기반)
├─ deltaY > threshold (80px)?
│ ├─ Yes → setIsRefreshing(true) + 스피너 표시
│ │ │
│ │ ▼
│ │ Promise.all([
│ │ fetchHomeCard(), ← HomeCard 재페치
│ │ prefetchAll(), ← Zygote + Profile 재페치
│ │ ])
│ │ │
│ │ ▼
│ │ setIsRefreshing(false) + 스피너 숨김
│ │
│ └─ No → 원위치 복귀 (스프링 애니메이션)


HomeCard 업데이트 → HeroSection re-render

설계 결정

Q1. 구현 방식: Framer Motion drag vs 커스텀 Touch Events?

Context: 프로젝트에 Framer Motion v11.15.0이 이미 설치되어 있고 다양한 컴포넌트에서 사용 중. 그러나 pull-to-refresh는 스크롤 최상단에서만 동작해야 하며, 브라우저의 기본 overscroll 동작과 충돌할 수 있다.

Decision: 커스텀 Touch Events (touchstart, touchmove, touchend) 기반으로 구현하되, 애니메이션은 Framer Motion의 motion.div + animate 속성을 활용한다.

Consequences:

  • 긍정: 스크롤 최상단 감지 로직을 세밀하게 제어 가능, overscroll bounce 방지
  • 긍정: 브라우저 기본 pull-to-refresh(Chrome 등)와 충돌 방지를 위한 overscroll-behavior: none 제어 가능
  • 트레이드오프: Framer Motion drag보다 약간 더 많은 코드량

Alternatives Considered:

  • Framer Motion drag="y" — overscroll과 충돌 이슈가 있으며 스크롤 최상단 감지가 어려움
  • 외부 라이브러리 (react-simple-pull-to-refresh) — 새 의존성 추가 불필요, Framer Motion으로 충분

Q2. 컴포넌트 구조: 독립 컴포넌트 vs HomeScreen 내 인라인?

Context: Pull-to-refresh는 다른 화면에서도 재사용 가능한 범용 패턴이다.

Decision: PullToRefresh 독립 래퍼 컴포넌트로 분리하여 재사용성 확보

Consequences:

  • 긍정: 채팅 화면 등 다른 화면에서도 재사용 가능
  • 긍정: 홈 화면 코드 복잡도 최소화 (HomeScreen은 래퍼만 추가)
  • 트레이드오프: 파일 1개 추가

Q3. Refresh 대상: HomeCard만 vs 전체 데이터?

Context: 홈 화면 마운트 시 HomeCard, Zygote Session, User Profile을 병렬로 페치한다.

Decision: HomeCard(fetchCard) + Prefetch(prefetchAll)를 모두 갱신한다. 단, 외부 건강 데이터 동기화(syncExternalHealthData)는 제외.

Consequences:

  • 긍정: 사용자가 최신 인사말 + 최신 Zygote 세션을 한번에 갱신
  • 트레이드오프: 네트워크 요청 2-3개 동시 발생 (수용 가능한 수준)

Alternatives Considered:

  • HomeCard만 갱신 — Prefetch 데이터가 stale한 채로 유지될 수 있음
  • 전체 데이터(건강 동기화 포함) — 건강 데이터 동기화는 무거워서 사용자 체감 지연 발생

Rabbit Holes

  • Chrome overscroll 동작과의 충돌overscroll-behavior-y: contain CSS로 해결 가능, 깊이 파고들 필요 없음
  • Capacitor 네이티브 pull-to-refresh 통합 — 웹 뷰 기반 앱이므로 웹으로 구현하는 것이 일관성 있음
  • 스크롤 가능한 콘텐츠 내 pull-to-refresh — 홈 화면은 스크롤 영역이 아닌 고정 레이아웃이므로 최상단 감지가 단순

구현 계획

문서내용변경 파일우선순위
01-pull-to-refresh-componentPhase 1: PullToRefresh 컴포넌트 + 홈 화면 통합1 NEW + 2 EDITP0
99-verification빌드/테스트/기능 검증-P0
총계1 NEW + 2 EDIT

영향 범위

변경 대상 파일

apps/dha-sleep-app-web/
├── components/
│ └── ui/
│ └── pull-to-refresh.tsx [NEW] PullToRefresh 래퍼 컴포넌트
├── components/
│ └── screens/
│ └── home/
│ └── home-screen.tsx [EDIT] PullToRefresh 래퍼 적용
└── app/
└── globals.css [EDIT] overscroll-behavior CSS 추가

변경하지 않는 것

파일/모듈이유
stores/home-card-store.ts기존 fetchCard() 재활용, 변경 불필요
stores/prefetch-store.ts기존 prefetchAll() 재활용, 변경 불필요
components/screens/home/hero-section.tsx데이터만 변경되므로 컴포넌트 수정 불필요
components/screens/home/home-top-bar.tsx변경 불필요
백엔드 API (/api/home-card)기존 GET/POST 엔드포인트 활용
capacitor.config.ts웹 기반 구현이므로 변경 불필요

컴플라이언스 체크

항목해당 여부대응
PII 저장/처리No프론트엔드 UI 변경만
감사 로그No해당 없음
삭제 cascadeNo해당 없음
데이터 보존 정책No해당 없음
데이터 레지던시 (europe-west3)No프론트엔드만 변경

예상 효과

시나리오BeforeAfter
사용자가 최신 HomeCard를 보고 싶을 때앱 종료 → 재시작 또는 5분 대기아래로 당겨서 즉시 갱신
Stale 카드가 표시될 때자동 백그라운드 갱신 대기 (3-12초)즉시 수동 갱신 가능
Zygote 세션이 만료됐을 때포그라운드 복귀 시 자동 갱신Pull-to-refresh로 즉시 재생성