Skip to content

feat: Apple Watch 실시간 러닝 연동 및 심박수 존 시각화#276

Open
thingineeer wants to merge 19 commits intoRunnect:developfrom
thingineeer:feature/v2.3.2-watch-sync
Open

feat: Apple Watch 실시간 러닝 연동 및 심박수 존 시각화#276
thingineeer wants to merge 19 commits intoRunnect:developfrom
thingineeer:feature/v2.3.2-watch-sync

Conversation

@thingineeer
Copy link
Collaborator

@thingineeer thingineeer commented Feb 9, 2026

Summary

  • iPhone-Watch 러닝 실시간 연동 (카운트다운 동기화, 데이터 전송, 양방향 종료)
  • 심박수 존(Heart Rate Zone) 실시간 시각화 (Z1~Z5, 색상 + 햅틱)
  • Watch 프로그레스바 위치 트래킹 UI (시작/현재위치/끝 마커)
  • Watch 앱 아이콘 추가
  • Watch 레거시 타겟 제거 및 직접 임베딩 구조 개선
  • GPS 기반 실시간 러닝 거리 추적
  • 코스 발견 피드 네이티브 광고 구현
  • 당근마켓 스타일 페이지네이션 성능 개선

변경 내용

Watch 러닝 연동

  • 카운트다운 시작 시 Watch에 동시 3-2-1 카운트다운
  • 5초 간격 러닝 데이터(거리, 시간, 페이스, 진행률) Watch 전송
  • 양방향 러닝 종료 (iPhone↔Watch)

심박수 존 시각화

  • HeartRateZone 모델 (Zone 1~5, MHR% 기반)
  • 러닝 중 심박수 텍스트/아이콘이 존 색상으로 변경 + Zone 배지
  • Zone 4 이상 진입 시 햅틱 알림
  • 러닝 요약에 평균 존 배지 표시

Watch UI

  • CourseProgressBar: S(시작) → 러너 → 깃발(끝) 마커 + 거리 텍스트
  • Runnect 앱 아이콘 적용

GPS 러닝 거리 추적

  • CLLocationManager 기반 실시간 누적 거리 계산 (0.0km에서 시작)
  • Watch에 실제 GPS 거리 전송 (코스 전체 거리 대신)

러닝 UX 개선

  • 러닝 기록 화면 뒤로가기 → 루트 화면으로 이동
  • "러닝 종료" 버튼에 확인 알럿 추가

코스 발견 네이티브 광고

  • 10개 코스마다 네이티브 광고 1개 삽입 (최대 3개)
  • 앱 3회 이상 실행 후부터 광고 노출
  • 릴리즈 빌드에서 테스트 광고 노출 방지

프로젝트 구조

  • watchapp2-container(RNWatch) 레거시 타겟 제거
  • CocoaPods 1.15.0 → 1.16.2

📊 코스 발견 페이지네이션 성능 개선 (Before → After)

데이터 로딩 지연시간

항목 Before After 개선폭
페이지네이션 인위적 지연 700ms 0ms -700ms (100% 제거)
체감 로딩 시간 (네트워크 제외) ~700ms + α ~0ms + α 최소 700ms 단축

셀 렌더링 효율

항목 Before After 개선폭
페이지네이션 업데이트 방식 reloadData (전체 N개 셀 재생성) insertItems (새 10개만 삽입) 렌더링 대상 ~90% 감소
2페이지 로드 시 렌더링 셀 수 20개 전체 재생성 10개만 추가 삽입 -50%
5페이지 로드 시 렌더링 셀 수 50개 전체 재생성 10개만 추가 삽입 -80%
10페이지 로드 시 렌더링 셀 수 100개 전체 재생성 10개만 추가 삽입 -90%

이미지 로딩 최적화

항목 Before After 개선폭
이미지 프리페칭 없음 (셀 표시 시점에 로드) 6개 셀 미리 로드 스크롤 시 이미지 즉시 표시
셀 재사용 시 이미지 매번 재다운로드 URL 비교로 동일 이미지 스킵 중복 다운로드 제거
셀 재사용 시 다운로드 취소 없음 kf.cancelDownloadTask() 불필요한 네트워크 요청 제거

로딩 인디케이터 UX

항목 Before After
첫 페이지 로딩 ✅ 전체 화면 로딩 ✅ 전체 화면 로딩
2페이지 이후 ❌ 매번 전체 화면 로딩 (스크롤 중단) ✅ 백그라운드 로딩 (스크롤 유지)

프리페치 트리거 방식

항목 Before After
트리거 scrollViewDidScroll (매 스크롤 이벤트마다 호출) willDisplay (셀 표시 시점에만 호출)
호출 빈도 초당 수십~수백회 셀 표시당 1회
프리페치 시점 스크롤 끝에 도달 후 남은 6개 셀 시점부터 미리 로드

Test plan

  • iPhone 러닝 시작 시 Watch 3-2-1 카운트다운 동시 표시
  • 러닝 중 Watch 거리/시간/페이스 실시간 업데이트
  • 심박수 존 색상 변경 및 Z 배지 표시
  • Zone 4+ 진입 시 햅틱 알림
  • Watch 프로그레스바 러너 마커 이동
  • 양방향 러닝 종료 동작
  • Watch 앱 아이콘 표시
  • GPS 거리 0.0km에서 시작하여 실시간 증가 확인
  • 러닝 기록 뒤로가기 → 루트 화면 이동
  • 러닝 종료 확인 알럿 동작
  • 코스 발견 네이티브 광고 정상 표시 (10개마다)
  • 코스 발견 페이지네이션 자연스러운 스크롤
  • 실기기 빌드 및 설치 정상 동작

fix: 코스 발견 배너 이미지 미표시 수정
- watchapp2-container(RNWatch) 레거시 타겟 제거
- RNWatch Watch App을 iOS 앱에 직접 임베드하도록 변경
- 빌드 스킴에 Watch App 타겟 추가
- CountDownVC에서 카운트다운 완료 시 Watch에 러닝 시작 알림
- RunTrackingVC에서 5초 간격 러닝 데이터 Watch 전송
- Watch에서 종료 명령 수신 시 iPhone 러닝 종료 처리
- RunningRecordVC에서 기록 저장 후 Watch 상태 초기화
- iPhone 앱과 동일한 Runnect 아이콘을 Watch 앱에 적용
- 1024x1024 원본 이미지로 자동 리사이즈 설정
- CocoaPods 1.15.0 → 1.16.2 업데이트
- rexml 버전 제약 완화
- HealthKit entitlements 정리
- WatchSessionService에 sendCountdownStarted() 메서드 추가
- CountDownVC에서 카운트다운 시작 시점에 Watch 알림 전송
- WatchSessionManager에서 countdownStarted 메시지 수신 처리
- 카운트다운 메시지 누락 시 runStarted fallback 처리
- CourseProgressBar 컴포넌트 신규 생성
- 시작(S), 현재위치(러너 아이콘), 끝(깃발) 마커 표시
- GeometryReader 기반 실시간 위치 애니메이션
- 현재거리/전체거리 텍스트 표시
- ActiveRunView에서 기존 ProgressView를 CourseProgressBar로 교체
- HeartRateZone enum 모델 생성 (Zone 1~5, 색상, MHR% 기반 계산)
- WorkoutManager에 currentZone 프로퍼티 추가 및 자동 존 계산
- Zone 4 이상 진입 시 햅틱 알림 (HapticManager.heartRateZoneAlert)
- 최대 심박수 기본값 190 (220-30세)
- ActiveRunView 심박수 텍스트/아이콘 색상을 현재 존 색상으로 동적 변경
- Zone 배지 캡슐 추가 (Z1~Z5, 존 색상 배경)
- RunSummaryView 평균 심박수 셀에 존 배지 표시
- heartRate 0일 때 Zone 배지 숨김 처리
- RNAlertVC로 "러닝을 종료하시겠습니까?" 확인 다이얼로그 표시
- 종료 시 Watch 데이터 전송 중지 및 상태 초기화
- 취소 시 러닝 계속 진행
- CLLocationManager를 활용한 실제 뛴 거리 누적 계산
- Watch 데이터 동기화 시 코스 총 거리 대신 실제 GPS 거리 전송
- 러닝 종료/취소 시 위치 추적 정리
- 정확도 20m 이내, 최소 이동 거리 5m 필터링 적용
- 러닝 종료 버튼에 확인 알럿 추가 (뒤로가기와 동일한 UX)
- 러닝 기록 화면에서 뒤로가기 시 루트 화면으로 이동하도록 수정
- NativeAdCVC 셀 생성 (CourseListCVC와 동일한 카드 스타일)
- CourseDiscoveryVC 코스 리스트에 매 10개마다 네이티브 광고 삽입
- GADAdLoader로 최대 3개 네이티브 광고 사전 로드
- 광고 위치 계산 헬퍼 메서드 추가 (isAdPosition, courseIndex 등)
- 기존 shouldShowAds 조건 재활용 (3회 이상 실행 + 등록 사용자)
- 광고 로드 실패 시 코스만 표시하는 graceful fallback
- 네이티브 광고 프로덕션 ID를 빈 문자열로 설정하여 릴리즈에서 테스트 광고가 노출되지 않도록 처리
- AdMob 콘솔에서 프로덕션 네이티브 광고 단위 ID 발급 후 교체 필요
- init 시점에 contentView.frame.width가 0일 수 있는 문제 해결
- 고정 높이 대신 snp.width.multipliedBy(124/174) 사용
- 0.7초 인위적 지연 제거하여 즉시 데이터 로딩
- reloadData 대신 performBatchUpdates + insertItems로 자연스러운 셀 삽입
- willDisplay 기반 프리페치 트리거 (threshold: 6)로 미리 다음 페이지 로드
- Kingfisher ImagePrefetcher로 이미지 사전 캐싱
- CourseListCVC 중복 이미지 로딩 방지 및 prepareForReuse 최적화
- 첫 페이지만 로딩 인디케이터 표시 (페이지네이션은 백그라운드 로딩)
- Watch 앱 버전 2.3.1 → 2.4.0
- 한국어 릴리즈 노트 작성 (Watch 출시, 성능 개선, 러닝 UX)
- 영어 리뷰 노트 업데이트
- Watch 스크린샷 교체 (심박수 존, 프로그레스바 반영)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant