스마트 홈트레이닝 IoT App 서비스 개발

2024.11 ~ 2025.04

2024.11 ~ 2025.04

iOS & AOSFSD ArchitectureTypeScript MigrationBluetoothApp Store Management

React NativeTypeScript

목차

  • 프로젝트 개요
  • 트러블 슈팅
  • A. 실시간 데이터 스트림 처리 구조 설계
  • 문제
  • 원인 분석
  • 1. 데이터 처리와 렌더링의 단일 흐름 구조
  • 2. 상태 기반 실시간 처리 구조
  • 해결 방법
  • 1. 데이터 처리 흐름 분리
  • 2. 입력 처리 계층 구조 분리
  • 3. Hook, Built-in Component 기반 렌더링 영향 제거
  • 결과
  • 핵심 포인트
  • B. 운동 상태 인지 UI 구현
  • 문제
  • 원인 분석
  • 1. 상태 중심 UI 부재
  • 2. 인터랙션 흐름 단절
  • 3. 모바일 환경 비최적화 입력 방식
  • 해결 방법
  • 1. 상태 중심 UI 재구성
  • 2. 자동 스크롤 기반 인터랙션 연결
  • 3. Wheel Picker 기반 입력 구조 전환
  • 결과
  • 핵심 포인트

iOS & AOSFSD ArchitectureTypeScript MigrationBluetoothApp Store Management

React NativeTypeScript

목차

  • 프로젝트 개요
  • 트러블 슈팅
  • A. 실시간 데이터 스트림 처리 구조 설계
  • 문제
  • 원인 분석
  • 1. 데이터 처리와 렌더링의 단일 흐름 구조
  • 2. 상태 기반 실시간 처리 구조
  • 해결 방법
  • 1. 데이터 처리 흐름 분리
  • 2. 입력 처리 계층 구조 분리
  • 3. Hook, Built-in Component 기반 렌더링 영향 제거
  • 결과
  • 핵심 포인트
  • B. 운동 상태 인지 UI 구현
  • 문제
  • 원인 분석
  • 1. 상태 중심 UI 부재
  • 2. 인터랙션 흐름 단절
  • 3. 모바일 환경 비최적화 입력 방식
  • 해결 방법
  • 1. 상태 중심 UI 재구성
  • 2. 자동 스크롤 기반 인터랙션 연결
  • 3. Wheel Picker 기반 입력 구조 전환
  • 결과
  • 핵심 포인트

프로젝트 개요


하드웨어 센서 기반 운동 데이터를 실시간으로 처리하고 사용자에게 카운팅 및 인터랙션을 제공하는 홈트레이닝 하이브리드 앱 개발을 전담하였다.

1. 실시간 BLE 데이터 스트림 처리 및 운동 화면 구현

센서로부터 블루투스를 통해 약 200ms 주기로 수신되는 실시간 데이터를 기반으로 푸쉬업, 스쿼트, 플랭크 운동 상태를 실시간으로 처리하고 UI에 반영하는 구조를 구현하였다.

  • 실시간 BLE 데이터 스트림 처리 구조 설계
  • 라우팅 구조 기반 운동 별 처리 로직 분리
  • 상태 기반 처리에서 참조(useRef) 기반 처리로 전환하여 렌더링 지연 최소화
  • 2. 렌더링 성능 최적화 및 상태 관리 개선

    운동 세트 및 목표 횟수가 증가할 때 발생하는 렌더링 지연 문제를 분석하고 최적화하였다.

  • Recoil 기반 상태 업데이트 구조 개선 (batch update)
  • useMemo / useCallback 적용으로 불필요한 재렌더링 제거
  • FlatList 적용으로 리스트 렌더링 최적화
  • 3. TypeScript 마이그레이션 및 FSD 아키텍처 도입

    외주 과정에서 작성된 JSX 코드를 점진적으로 TypeScript로 전환하고 FSD 아키텍처를 도입하여 유지보수성을 개선하였다.

  • 점진적 Migration으로 안정성 확보
  • 운동 카운팅 로직을 custom hook으로 모듈화
  • FSD 아키텍처 도입 및 컴포넌트 역할 분리
  • 4. 사용자 인터페이스 및 공통 컴포넌트 개발

    사용자가 운동 설정 및 진행에 필요한 UI 컴포넌트를 설계 및 구현하였다.

  • 운동 상태(시작/진행/휴식) 및 카운팅 결과 실시간 반영
  • WheelPicker 기반 운동 설정 UI 구현
  • 세트 리스트 및 진행 상태 시각화 UI 구성
  • 운동 결과 및 피드백 표시 인터페이스 개발
  • 5. 앱 빌드 및 스토어 등록

    Android와 iOS 환경에서 빌드 및 스토어 등록을 수행하였다.

    트러블 슈팅


    A. 실시간 데이터 스트림 처리 구조 설계


    문제

    센서 데이터가 약 200ms 주기로 지속적으로 유입되는 환경에서 데이터 처리와 UI 렌더링을 동시에 수행해야 했으며 다음과 같은 문제가 발생했다.

  • 데이터 반영 주기가 점점 느려지다가, 세트 종료 시 데이터가 몰려 들어오며 입력 주기가 깨짐
  • 실시간으로 반영되어야 하는 운동 시간 및 상태가 지연되어 표시됨
  • 데이터 처리 흐름과 UI 렌더링 흐름이 서로 영향을 주는 구조
  • 실시간 데이터는 들어오고 있었지만 처리 구조와 렌더링 구조가 분리되지 않은 상태였다.

    원인 분석

    1. 데이터 처리와 렌더링의 단일 흐름 구조

  • BLE 데이터 수신, 상태 변경, UI 렌더링이 하나의 실행 흐름에서 처리됨
  • 렌더링 비용 증가 시 데이터 처리까지 지연되는 구조
  • 앱에서 센서 데이터 수신과 상태 업데이트를 동시에 처리하면서 입력 처리 주기가 UI 성능에 종속되는 구조였다.

    2. 상태 기반 실시간 처리 구조

  • 실시간 입력 기준과 조건 판단의 대부분이 useState 기반으로 구성됨
  • 상태 반영 이전에 다음 데이터가 들어오면서 기준 값이 계속 변경됨
  • 상태를 기준으로 사용할 경우 입력 주기와 렌더링 타이밍이 엇갈리며 지연이 발생하였다.

    해결 방법

    1. 데이터 처리 흐름 분리

  • 센서 입력 처리, 상태 계산, UI 반영을 분리된 단계로 구성
  • 입력 처리 흐름이 렌더링에 영향을 받지 않도록 구조 재설계
  • 데이터를 “UI 상태”가 아닌 “독립적인 처리 흐름”으로 다루도록 변경하였다.

    1.1. 입력 수신과 처리 단계 분리

  • BLE 수신 단계에서는 데이터 파싱만 수행
  • 실제 처리 로직은 별도 함수에서 실행
  • 입력 수신 단계에서 처리 로직을 직접 실행하지 않고 분기만 수행하도록 구성하였다.

    1.2. 처리 기준과 UI 상태 분리

    판단의 기준이 되는 값들은 useRef에 저장하고, 판단 이후 UI에 반영해야 되는 경우에 useState를 활용하였다.

  • 실시간 처리 기준은 useRef로 관리
  • 화면 표시용 데이터만 useState로 관리
  • 입력 주기와 무관하게 내부 처리 기준이 유지되도록 구성하였다.

    1.3. 상태 업데이트 최소화

  • 연속적으로 변경되는 값은 상태로 올리지 않음
  • 화면 갱신이 필요한 시점에만 상태 업데이트 수행
  • 렌더링 발생 횟수를 줄여 입력 처리 흐름이 끊기지 않도록 구성하였다.

    2. 입력 처리 계층 구조 분리

  • BLE 입력 처리, 데이터 파싱, 처리 실행을 단계별로 분리
  • 입력 라우팅 구조를 통해 처리 흐름 단순화
  • 입력 수신부는 분기만 담당하고 실제 처리는 분리된 구조에서 실행되도록 구성하였다.

    3. Hook, Built-in Component 기반 렌더링 영향 제거

  • useMemo, useCallback을 적용하여 함수 재생성 방지
  • 리스트 렌더링을 FlatList로 변경하여 렌더링 범위 제한
  • 상태 변경을 최소화하여 렌더링 트리 부담 감소
  • 렌더링 비용이 데이터 처리 주기에 영향을 주지 않도록 구조를 정리하였다.

    결과

  • 200ms 단위의 센서 데이터 처리 주기 유지
  • 세트 종료 이후 발생하던 입력 지연 및 UI 반영 지연 제거
  • 입력 처리 흐름과 렌더링 흐름이 분리된 구조 확보
  • 데이터 누적 및 몰림 현상 없이 안정적인 실시간 처리 가능
  • 핵심 포인트

    이 구조 개선의 핵심은 처리 로직 변경이 아니라 구조 분리에 있다.

  • 데이터 처리와 렌더링을 동일 흐름에서 분리
  • 상태 기반 처리에서 참조 기반 처리로 전환
  • 입력 → 처리 → 렌더링을 단계별로 분리
  • 실시간 데이터를 UI 상태의 일부로 처리하던 구조에서 독립적인 처리 파이프라인으로 전환

    B. 운동 상태 인지 UI 구현


    문제

    운동 진행 화면에서 사용자에게 제공되는 정보는 단순 카운트 값 중심으로 구성되어 있었으며 다음과 같은 문제가 존재했다.

  • 현재 진행 중인 세트를 직관적으로 인지하기 어려움
  • 목표 대비 진행 상황을 한눈에 파악하기 어려움
  • 반복적인 목표 설정 과정이 비효율적
  • 사용자 입력과 운동 흐름 간 인터랙션이 단절됨
  • 운동 데이터는 존재하지만 사용자 행동 흐름과 연결되지 않는 UI 구조였다.

    원인 분석

    1. 상태 중심 UI 부재

  • 현재 세트, 목표, 보정 값 등의 상태 정보가 개별적으로 분산되어 표현됨
  • 사용자가 여러 정보를 종합하여 해석해야 하는 구조
  • 2. 인터랙션 흐름 단절

  • 세트 진행 상태가 UI에 즉각적으로 반영되지 않음
  • 사용자 입력과 실제 운동 흐름이 분리되어 있음
  • 3. 모바일 환경 비최적화 입력 방식

  • 숫자 입력 기반 UI는 반복 조작에 비효율적
  • 빠른 설정 및 변경이 어려움
  • 해결 방법

    1. 상태 중심 UI 재구성

  • 분산된 상태(현재 세트 / 목표 / 보정 값)를 하나의 UI 단위로 통합
  • 사용자가 상태를 해석하는 것이 아니라 UI 자체가 상태를 설명하도록 구조 재설계
  • 1.1. 세트 기반 상태 표현 UI 재구성

  • 현재 진행 중인 세트를 시각적으로 강조하여 사용자가 별도의 계산 없이 현재 위치를 즉시 인지할 수 있도록 구성
  • 세트 목표 보정 값 UI의 보정 방향(+/-)을 색상으로 구분하여 표현하여 텍스트가 아닌 시각적 요소로 상태를 전달
  • 2. 자동 스크롤 기반 인터랙션 연결

  • 세트 진행에 따라 리스트가 자동으로 현재 위치로 이동하도록 구성
  • 사용자 개입 없이 운동 흐름이 자연스럽게 이어지도록 설계
  • 운동 중 App UI
    운동 중 App UI

    3. Wheel Picker 기반 입력 구조 전환

    기존 목표 설정 UI는 숫자 입력 기반으로 구성되어 있었으며 모바일 환경에서 키보드 기반 입력은 입력 흐름을 끊고 불편함을 유발하였다.

    3.1. 직접 구현

    외부 라이브러리 및 플랫폼 기본 컴포넌트를 검토하였으나 다음과 같은 제약이 있었다.

  • 라이브러리 → UI 세부 제어가 어려우며 추후 빌드 및 유지보수 과정에서 리스크 존재
  • 빌트인 컴포넌트 → 커스터마이징이 거의 불가능하여 일관성 있는 UI 제공이 어렵고 React Native 환경에서 사용이 어려웠다.
  • 이러한 이유로 요구사항에 맞는 구조를 확보하기 위해 직접 구현하였다.

    3.2. 컴포넌트 구조 설계

  • Scroll 기반 리스트 구조로 구성
  • 중앙 영역을 선택 기준으로 설정
  • 스크롤 위치를 기준으로 선택 값을 계산하도록 설계
  • 3.3. 상태 관리 방식

    입력 이벤트가 아닌 위치 기반으로 상태를 계산하도록 구성하여 상태 변경 흐름을 단순화하였다.

  • 선택 값은 스크롤 위치를 기준으로 계산
  • 렌더링 상태와 선택 상태를 분리하였다.

  • 렌더링: 화면에 표시되는 항목
  • 상태: 선택된 index
  • 이 구조를 통해 불필요한 리렌더링을 줄일 수 있었다.

    3.4. 스크롤 및 snap 처리

  • 스크롤 종료 시점에 선택 값을 확정
  • item 단위 기준으로 index 계산
  • 가장 가까운 값으로 자동 정렬되도록 구성
  • 스크롤 중에는 상태 업데이트를 최소화하고 종료 시점에만 상태를 반영하도록 처리하였다.

    3.5. 렌더링 최적화

  • 동일한 item 구조 유지로 key 안정성 확보
  • 조건부 스타일 적용으로 렌더링 비용 최소화
  • 선택 상태 강조를 레이아웃 변경 없이 스타일로 처리
  • Wheel Picker 적용 App UI
    Wheel Picker 적용 App UI

    결과

  • 현재 운동 상태를 별도 해석 없이 즉시 인지 가능
  • 사용자 입력과 운동 진행 흐름이 자연스럽게 연결됨
  • 반복 조작 감소 및 인터랙션 효율 증가
  • UI와 운동 로직 간 동기화 구조 확보
  • 핵심 포인트

    이 개선의 핵심은 UI 요소를 추가한 것이 아니라,

  • 상태를 나열하는 방식 → 상태를 구조화하는 방식으로 전환
  • 사용자 입력 → 시스템 흐름과 직접 연결
  • 데이터를 보여주는 UI에서 사용자의 행동 흐름을 유도하는 인터랙션 중심 UI로 전환
    const onBleManagerUpdate = useCallback(({ value }: { value: any }) => {
      const data = String.fromCharCode(...value);
      const [ble_header, ble_value] = data.split("=");
    
      if (headerActions[ble_header as keyof typeof headerActions]) {
        headerActions[ble_header as keyof typeof headerActions](ble_value);
      }
    }, []);
    if (!hasCrossedThresholdRef.current) {
      exCrossStartTimeRef.current = Date.now(); // 되는 값들은 useRef에 저장.
      hasCrossedThresholdRef.current = true; 
    }
    
    filteredDataBufferRef.current.push(
      data - standardWeightRef.current
    );
    
    const height =
      filteredDataBufferRef.current.reduce((sum, value) => sum + value, 0) /
      filteredDataBufferRef.current.length;
    
    const width = Date.now() - exCrossStartTimeRef.current;
    
    if (height * width > MINIMUM_AREA) {
      exCountRef.current++;
      sendProtocol("PUSH_CNT=" + exCountRef.current);
      setExCount(exCountRef.current); // 판단 이후 UI에 반영해야 되는 경우에 useState.
    }
    constheaderActions = useMemo(
      () => ({
        PUSH: (ble_value:string) => countPush(parseNumberValue(ble_value)),
        SQUAT: (ble_value:string) => countSquat(parseNumberValue(ble_value)),
        PLANK: (ble_value:string) => countPlank(parseNumberValue(ble_value)),
      }),
      [countPush, countSquat, countPlank]
    );