Tech
2026.03.01

AI 헬스 코치

#React Native Web#FastAPI#Spring Boot#MediaPipe

프로젝트 소개

웹캠만으로 운동 자세를 실시간 분석해 피드백하는 AI 헬스 코칭 서비스입니다. PT의 비용·시간 부담 때문에 전문가의 도움을 포기하는 사용자와, 혼자 운동하다 잘못된 자세로 부상 위험에 노출되는 문제를 해결하고자 제작했습니다. 기존 피트니스 앱이 루틴 추천과 횟수 기록에 머무는 것과 달리, 운동하는 순간의 관절 각도를 분석해 그 자리에서 교정 피드백을 제공합니다.

MediaPipe를 서버가 아닌 브라우저에서 실행해 영상은 전송하지 않고 관절 좌표만 서버로 보내는 구조로 설계했습니다. 프라이버시와 대역폭, 서버 비용을 동시에 해결하기 위한 선택입니다.

프로젝트 개요

항목내용
기간2026.03 ~ (개발 중)
인원4명 (캡스톤 디자인 3조)
서비스https://ai-health-coach-swart.vercel.app/
구성프론트엔드 · 판정 서버 · 백엔드 · AI 연구 모듈 (모노레포)

기술 스택

레이어사용 기술
프론트엔드Expo SDK 54, React 19, React Native Web, Expo Router, TypeScript
자세 추정MediaPipe Pose (브라우저 실행), Azure Kinect Body Tracking SDK (선택)
판정 서버Python 3.10, FastAPI, Uvicorn, WebSocket, NumPy, OpenCV
백엔드Java 21, Spring Boot 3.2.4, Spring Data JPA, JJWT, BCrypt
데이터PostgreSQL 15, Redis
AI 연구PyTorch (지식증류 보정 모델), Fit3D 모션캡처 데이터셋
외부 연동Solapi (SMS 인증), Resend (이메일 인증)
배포Docker Compose, Vercel (프론트), Azure VM + Caddy, GitLab CI/CD

주요 기능

실시간 자세 코칭

  • 4종 운동 판정 — 스쿼트, 푸시업, 랫 풀다운, 사이드 레터럴 레이즈. 운동마다 코치 클래스를 분리해 새 종목 추가 시 3개 지점만 수정
  • 반복 횟수 자동 카운팅 — 단순 임계값이 아닌 상태 머신(READY → DEEP → READY)으로 세어 경계에서 흔들려도 중복 카운트가 발생하지 않음
  • 자세 경고 · 관절별 시각화 — 경고 문구를 관절 단위 점수로 환산해 스켈레톤 색상으로 표시(정상 초록, 위험 빨강)
  • 렙 품질 점수 — 경고 개수와 동작 속도로 0~100점 산출, 각도 이탈 정도에 비례하는 보정을 최대 10점 범위로 추가 적용
  • 입력원 이원화 — 웹캠(MediaPipe) 기본, Azure Kinect는 로컬 브릿지로 지원. 어댑터 패턴으로 분리해 입력원이 바뀌어도 판정 로직은 불변
  • 프라이버시 설계 — 영상은 서버로 전송하지 않고 관절 좌표만 전송, Kinect 모드도 영상을 로컬에 유지

기록 · 게이미피케이션

  • 운동 기록 관리 — 세션·기록 저장, 캘린더 달성 현황, 기간별 통계, 자극 근육 시각화(근육맵), 체중 기록
  • 경험치 · 레벨 — 종목 난이도, 정확도 스케일, 완주 보너스, 노력 점수(칼로리·시간), 스트릭, 루틴 배수를 조합해 산출. 완주 보너스에도 정확도를 반영해 "자세 대충, 횟수만 채우기"가 유리해지지 않도록 설계
  • 시즌 랭킹 · 챌린지 · 친구 — 월간 초기화 스케줄러, 챌린지 일일 인증, 친구 검색·요청·피드
  • 기록 무결성 검증 — 클라이언트가 보낸 기록을 판정 서버 세션의 실측치와 대조. 서버 값을 권위로 채택하되, 세션 조회 실패는 거부하지 않고 verified=false로 표시

회원 · 운영

  • 인증 — 휴대폰·이메일 6자리 코드 인증, JWT 로그인, 소셜 로그인 연동, 관리자 2단계 재인증
  • 인가 — 인터셉터 단일 지점에서 역할 기반 접근 제어 + 리소스 소유권 검증(IDOR 차단)
  • 관리자 — 회원·운동·챌린지 관리, 1:1 문의 답변, AI 서버 모니터링(접속 수·에러율·워커 상태)
  • 점검 모드 — 관리자 토글로 503 응답 차단 + 프론트 자동 안내 화면, 관리자만 우회 가능
  • 동시 접속 관리 — WebSocket 접속 상한과 대기열(Redis FIFO), Redis 미가용 시 로컬 단독 모드로 폴백

AI 정확도 개선 (연구 모듈)

  • 정량 검증 — Fit3D 모션캡처 정답과 대조해 MediaPipe 실제 오차 측정(2D 12px / 3D 133~175mm)
  • 판정 로직 검증 — 실측 기준으로 판정 버그 3건 발견·수정(푸시업 오경고율 92.8% → 2%)
  • 보정 모델 — 지식증류 기반 경량 MLP로 3D 좌표 보정. 처음 보는 사람 기준 오차 243mm → 84mm (약 65% 개선)
  • 일반화 검증 — 카메라 각도 과적합을 교차 평가로 발견하고 다각도 학습으로 대응

아키텍처

front/         Expo (React Native Web) — 웹캠 캡처 · MediaPipe 실행 · 스켈레톤 렌더 · 전체 화면
   │ WebSocket (관절 좌표)          │ REST
python_ai/     FastAPI — 좌표 → 각도 계산 → 운동 판정 (렙·경고·품질 점수)
back/          Spring Boot — 회원 · 기록 · EXP · 랭킹 · 챌린지 · 문의
   └ PostgreSQL + Redis
pytorch_kd_calib/  PyTorch — 관절 보정 모델 학습 및 정확도 검증 (연구 영역)
  • 클라이언트 사이드 추론 — 자세 추정을 브라우저에서 수행해 서버는 좌표 연산만 담당. GPU 없이 일반 인스턴스로 운영 가능하며, 사용자가 늘어도 한계 비용이 크게 늘지 않음
  • 경로 분리 — Spring Boot는 /api/**, 판정 서버는 /ai/**·/ws/**. 두 서버가 같은 접두사를 쓰다 발생한 충돌을 구조적으로 해소
  • 어댑터 패턴 — MediaPipe·Kinect·로컬 브릿지를 어댑터로 분리하고 공통 프레임 처리 함수에서 합류. 입력원 추가 시 판정 로직 수정 불필요
  • 로컬 브릿지 — Azure Kinect SDK가 호스트 GPU를 요구해 클라우드에서 센서를 열 수 없는 제약을, UI 없는 로컬 브릿지가 좌표만 공급하는 구조로 우회(화면은 기존 웹 프론트가 담당해 UI 이중 관리 회피)
  • 계층 분리 — 백엔드는 controller/service/repository/dto로 분리하고, 컨트롤러에서 예외를 던지면 전역 예외 처리기가 상태 코드로 매핑
  • 서비스 계층 단일화 — 프론트의 모든 HTTP 호출을 apiClient 한 곳으로 모아 토큰 주입·401 전역 로그아웃·점검 모드 감지를 중앙 처리

개발 경과

버전 태그를 따로 운영하지 않아 커밋 기록 기준의 주요 단계로 정리합니다.

2026.03 — 기획 주제 선정 및 요구사항 명세서 작성. 기능 요구사항을 도메인별 ID와 우선순위(필수/권장/선택)로 분류해 개발 범위를 고정

2026.05 — 기반 구축 회원 인증 체계, 운동 판정 엔진, Kinect 캡처 파이프라인 구현. WebSocket 프록시 구조에서 버퍼 오버플로가 반복되어 프론트가 판정 서버에 직접 연결하는 구조로 전환

2026.06 초 — 구조 개선 판정 서버 경로를 /ai/로 분리해 경로 충돌 해소, 운동 상태값을 문자열에서 Enum으로 전환, 리버스 프록시 구성 및 CORS 정비

2026.06 중 — 보안 · 데이터 정합성 비밀번호 BCrypt 해싱, JWT 시크릿 외부화, 전역 인증·인가 도입, IDOR 차단. 운동 시각을 UTC 기준으로 통일해 타임존 오차 수정, DB 무결성 제약 추가

2026.06 말 — 배포 자동화 GitLab CI/CD로 빌드·배포 파이프라인 구성, Vercel + Azure VM 분리 배포, 점검 모드 도입

2026.07 — 정확도 검증 및 무결성 (진행 중) 모션캡처 정답 데이터로 판정 임계값 검증, 보정 모델 학습, 기록 서버 권위 검증 구현. 일부 작업은 커밋 정리 대기 중

트러블 슈팅

1. 감으로 정한 판정 임계값이 실측과 어긋나 상시 오경고 발생

  • 문제 상황: 푸시업 판정에서 "몸이 한쪽으로 비틀렸다"는 경고가 사용자 자세와 무관하게 거의 항상 표시되고 있었습니다. 화면상으로는 판정이 동작하는 것처럼 보여 개발 중에는 문제를 인지하지 못했습니다.
  • 원인 분석: 비틀림을 판단하기 위해 어깨·골반·발목의 깊이축(z) 범위를 재고 임계값 55mm를 두고 있었습니다. 그러나 푸시업은 엎드린 자세라 몸 자체가 카메라 깊이 방향으로 길게 눕습니다. 모션캡처 정답으로 실측하니 이 값의 중앙값이 1094mm였습니다. 즉 비틀림이 아니라 몸 길이를 재고 있었고, 55mm 임계값은 구조적으로 넘을 수밖에 없었습니다. 전체 프레임의 92.8%에서 오경고가 발생하고 있었습니다.
  • 해결 방법:
    1. 측정 대상을 바꿔 어깨선 기울기와 골반선 기울기의 차이를 비틀림으로 정의했습니다. 이것이 실제 상하체 비틀림에 해당합니다.
    2. 측정 축도 깊이(z)에서 높이(y)로 옮겼습니다. 정답 데이터와 대조한 결과 축별 신뢰도가 z 0.217, y 0.653, x 0.764로 깊이축이 사실상 무의미한 수준이었기 때문입니다.
    3. 같은 검증 과정에서 스쿼트의 가상 발끝 오프셋(250mm → 실측 210mm)과 경고 문구·구현 불일치도 함께 수정했습니다.
  • 결과: 오경고율이 92.8%에서 2% 수준으로 감소했습니다.
  • 배운 점: 동작하는 것과 맞는 것은 다르며, 그 차이는 정답 데이터를 갖기 전까지 드러나지 않습니다. 이후 판정 로직을 수정할 때는 임계값 옆에 근거 수치를 주석으로 남기고, 깊이축 기반 지표는 새로 만들지 않는다를 팀 규칙으로 정했습니다.

2. WebSocket 버퍼 오버플로 — 증상 대응에서 구조 변경으로

  • 문제 상황: Kinect 모드에서 실시간 코칭 중 WebSocket 연결이 반복적으로 끊어졌습니다. 버퍼 크기를 늘리는 방식으로 네 차례 대응했지만 조건만 바뀔 뿐 문제가 계속 재발했습니다.
  • 원인 분석: 당시 구조는 프론트가 Spring Boot에 연결하고, Spring이 이를 판정 서버로 중계하는 프록시 방식이었습니다. Kinect 경로에서는 이미지 프레임까지 이 경로로 흐르는데, 대용량 스트림을 중계하는 계층이 하나 더 있다는 것 자체가 원인이었습니다. 버퍼 크기 조정은 임계점을 미룰 뿐이었고, 중간 홉으로 인한 지연도 실시간 피드백 품질을 떨어뜨리고 있었습니다.
  • 해결 방법:
    1. 프록시 계층을 제거하고 프론트가 판정 서버에 직접 WebSocket으로 연결하도록 구조를 변경했습니다. 인증이 필요한 시점은 세션 시작이지 프레임 단위가 아니라는 판단이었습니다.
    2. 캡처 단계에서는 해상도를 640×360으로 낮추고, 큐가 찼을 때 가장 오래된 프레임을 버리는 Drop-Oldest 방식으로 전환했습니다. 실시간 스트림에서 밀린 프레임은 처리 가치가 낮기 때문입니다.
  • 결과: 구조 변경 이후 동일 계열의 버퍼 문제가 재발하지 않았습니다.
  • 배운 점: 같은 종류의 수정이 세 번 반복되면 증상이 아니라 구조를 의심해야 합니다. 대응 커밋이 쌓이는 것 자체가 설계 문제의 신호였습니다.

3. 보정 모델의 카메라 각도 과적합 — 지표가 놓친 실패

  • 문제 상황: MediaPipe 좌표를 보정하는 모델을 학습해 오차를 243mm에서 84mm로 줄였습니다. 그런데 실제 서비스는 사용자마다 카메라 각도가 다른 환경이라, 학습 조건과 다른 각도에서도 성능이 유지되는지 확인이 필요했습니다.
  • 원인 분석: 1차로 카메라 각도별 MediaPipe 오차 크기를 비교했을 때 156mm, 161mm, 133mm, 142mm로 차이가 3.5% 수준이었습니다. 이 지표만 보면 "각도에 둔감하다"는 결론이 나옵니다. 그러나 이 지표는 오차의 크기만 측정하며, 보정 모델이 학습하는 것은 오차의 방향입니다. 30도 각도로만 학습한 모델을 18도 영상에서 평가하자 개선율이 73.5%에서 27.5%로 무너졌습니다. 12도 차이만으로 모델이 사실상 무력화된 것입니다.
  • 해결 방법:
    1. 두 각도의 데이터를 함께 학습시킨 결과, 30도에서 73.1%, 18도에서 69.8%로 양쪽 모두 회복했습니다.
    2. 근본 대응으로 yaw 정규화를 설계했습니다. 사용자의 골반 벡터로 방향을 추정해 정면으로 회전 정규화하는 방식이며, 회전량을 입력 데이터만으로 계산하므로 정답이 없는 서비스 환경에서도 적용 가능합니다.
  • 배운 점: 자동 지표는 검증 가능한 실패만 잡아냅니다. 지표를 만들 때 "이 지표가 침묵하는 조건은 언제인가"를 먼저 확인해야 하며, 일반화 성능은 학습하지 않은 조건에서 평가해야만 확인됩니다. 같은 이유로 검증에 쓴 회전 정렬 기법은 학습에 사용하지 않았습니다. 서비스 시점에는 정답이 없어 회전을 맞출 수 없으므로, 그대로 썼다면 좋은 지표를 얻고도 배포가 불가능한 모델이 됐을 것입니다.

4. CORS 오류로 보였으나 원인은 인증 인터셉터

  • 문제 상황: 프론트(Vercel)와 백엔드(Azure)를 분리 배포한 뒤 API 호출이 전부 실패했습니다. 브라우저 콘솔에는 CORS 정책 위반 오류가 표시됐습니다.
  • 원인 분석: CORS 설정을 여러 차례 점검했지만 문제가 없었습니다. 실제 원인은 인증 인터셉터였습니다. 브라우저는 교차 출처 요청 전에 OPTIONS 프리플라이트 요청을 보내는데, 이 요청에는 Authorization 헤더가 포함되지 않습니다. 인터셉터가 이를 인증 실패로 판단해 401을 반환했고, 브라우저는 본 요청을 아예 전송하지 않았습니다. 결과적으로 CORS 문제처럼 보인 것입니다.
  • 해결 방법: 인터셉터가 프리플라이트 요청을 인증 검사 없이 통과시키도록 수정했습니다.
  • 배운 점: 증상이 나타나는 지점과 원인이 있는 지점이 다를 수 있습니다. 브라우저가 보고하는 오류 분류를 그대로 믿고 CORS 설정만 파고들었다면 훨씬 오래 걸렸을 것입니다. 이후 배포 환경 문제는 요청이 실제로 어디까지 도달했는지 서버 로그로 먼저 확인하는 순서로 접근하고 있습니다.

Capstone2026 시리즈

댓글