AI 트레이너를,
인터넷 없이도.
세트마다 다음 무게·휴식을 정해주는 AI 헬스 트레이너. 실시간 처방은 순수 로컬 엔진으로 오프라인 100%, AI는 그 위에 코칭을 얹습니다.

AI 트레이너 · 제안엔 근거를 붙이고, 적용은 사용자가 정합니다
// the brief
기록하는 앱은 많다. 정해주는 앱이 없을 뿐.
헬스 앱은 넘치지만 대부분 '기록'만 합니다. 정작 세트 중에 "다음은 몇 kg으로?"를 정해주는 앱은 드물죠. 게다가 AI 앱은 인터넷이 끊기면 무용지물인데, 헬스장은 지하·구석이라 신호가 약합니다.
운동하는 그 순간에 작동하지 않는 코치는 의미가 없습니다. 그래서 목표를 하나로 잡았어요. 인터넷이 없어도, 서버가 죽어도, 다음 세트를 정확히 정해주는 트레이너.
// approach
실시간 결정은 AI가 아니라, 엔진이 한다.
휴식 시간·다음 세트 무게·횟수 같은 실시간 처방을 LLM이 아니라 순수 TypeScript 로컬 엔진으로 만들었습니다. 인터넷도 서버도 없이 폰 안에서 100% 돕니다. AI는 그 위에 코칭·분석·대화를 얹는 보조 역할이고요.
엔진이 처방의 정본, AI는 코칭 보조. 이 원칙을 처음부터 끝까지 지켰습니다. 운동 중 결정에는 LLM이 한 번도 끼어들지 않습니다.

분석 화면 · 엔진이 계산한 비율과 목표 일치도를, 판단 근거와 함께 보여줍니다
// key decisions
믿을 수 있는 코치를 만든 세 가지.
실시간 처방 = 로컬 엔진 (오프라인 100%)
다음 세트 무게·휴식·프로그레션을 순수 TS 엔진이 계산합니다. 인터넷이 끊겨도, 서버가 죽어도 운동은 안 멈춰요. 처방 로직은 709개 엔진 유닛 테스트로 고정해, 앱이 바뀌어도 결정이 흔들리지 않게 했습니다.
AI는 코칭 보조 (비용까지 설계)
챗·세션 분석·주간 리포트는 AI가 맡습니다. 사용자가 늘어도 원가가 터지지 않게 비용 구조를 처음부터 설계했습니다. 한도를 넘으면 더 가벼운 모델로 내려가고, 실시간 처방은 애초에 AI를 쓰지 않으니 그 축은 사용량과 무관합니다.
웹 한 벌로 다섯 표면 (PWA · Android · iOS · 워치 둘)
화면과 처방 엔진은 한 벌입니다. 오프라인 우선 웹을 Capacitor 껍데기로 감싸고, 플랫폼마다 필요한 것만 직접 만든 네이티브 플러그인으로 붙였습니다. 건강 데이터 쓰기·삭제, GPS 트랙, 패스키, 위젯, 워치 연동은 생태계 플러그인이 없거나 부족해 자체 구현했습니다. 안드로이드는 Health Connect에 쓰고(삼성헬스가 이를 읽어 표시합니다), iOS는 HealthKit에 씁니다. 워치는 갤럭시 워치와 Apple Watch가 기능이 같고, Apple Watch는 사각 화면에 맞춰 다시 설계했습니다.
실시간 조절도 세밀합니다. 휴식이 길어지면 다음 세트를 자동으로 줄이고, 컨디션이 나쁜 날은 볼륨을 낮춥니다. 드롭세트·디로드 같은 특수 세트는 강도 지표에서 정확히 걸러내 다음 처방이 오염되지 않게 했고요. 전부 엔진이, 오프라인에서.
// offline-first
'오프라인 우선'은 구호가 아니라 설계다.
운동 기록·타이머·세트 처방이 전부 폰 안(로컬 DB + 엔진)에서 돕니다. 서버(Cloudflare D1)는 여러 기기 동기화 대상일 뿐이에요. 그래서 동시 접속이 1만 명이어도 '운동하는 순간'의 서버 부하는 0입니다. 부하가 걸리는 건 동기화와 AI 잡뿐이고, 그건 큐로 분리했습니다.
// exercise library
처방만 잘해선 부족했다.
다음 세트를 정확히 정해줘도, 그 운동이 뭔지 모르면 따라 할 수 없습니다. 그래서 종목 라이브러리를 2,884종까지 넓히고 자세 시연 애니메이션 2,868개를 앱에 실었습니다. 정식 상용 라이선스 시연 자산을 앱에서 바로 재생되는 형식으로 다시 인코딩한 것이고, 인코딩은 두 대에 나눠 병렬로 돌렸습니다.
라이브러리 밖 종목은 온디맨드로 가져옵니다. 최근엔 처방 가능한 스트레칭이 하나도 없던 근육 5개를 메우면서, 온디맨드에만 있던 스트레칭 5종을 정식 라이브러리로 올렸습니다. 그래도 없으면 직접 만들어 쓰면 됩니다.
검색은 한글 오타를 잡습니다. 자모 축으로 편집거리를 재는 방식이고, 허용 오타 수는 감으로 정하지 않고 측정해서 정했습니다.
그리고 이 라이브러리를 로그인 없이 볼 수 있게 /exercise 허브로 열었습니다. "로그인 없이 볼 수 있습니다"를 문구가 아니라 사실로 만든 셈이고, 검색엔진이 종목 하나하나를 읽어갑니다.
// trust
처방이 제품이라면, 믿게 만드는 것도 제품이다.
기록만 하는 앱은 틀려도 티가 안 납니다. 처방하는 앱은 다릅니다. 왜 그 무게인지 설명하지 못하면 사용자는 다음 세트에서 앱을 닫습니다. 그래서 최근 작업은 기능을 늘리기보다 처방을 믿게 만드는 쪽으로 갔습니다.
휴식 처방 옆에 '왜?'를 답니다. 근거는 검증된 원칙만 담은 지식베이스에서 옵니다. 엔진이 계산한 값 옆에 그 값이 나온 이유가 붙으니, 오프라인 로컬 엔진이라는 결정이 사용자 눈에도 보입니다.
운동 이름을 초보자가 알아들을 우리말로 바꿨습니다. 그런데 설계를 한 번 되돌렸습니다. 처음엔 쉬운 이름을 기본으로 두려 했는데, 그러면 초보자가 헬스장에 가서 그 기구를 못 찾습니다. 정식 명칭을 기본으로 두고, 길게 누르면 쉬운 말로 바뀌게 했어요.
종목을 고를 때 점수가 같아 결국 사전순으로 정해지던 비율이 20%였습니다. 그걸 2.3%로 낮췄고, 이 값은 게이트가 상한으로 지킵니다. 되돌아가면 배포가 막힙니다. '처방이 제품'이라고 말하려면 그 처방이 알파벳 순서여선 안 됩니다.
어젯밤 수면을 준비도 신호로 읽습니다. 남의 몸 데이터를 다루는 앱이라 권한도 코드로 지킵니다. 요청하는 권한과 실제 코드가 어긋나지 않도록 자동 검사가 매 배포 전에 대조하고, 쓰지 않는 건강 권한은 요청하지 않습니다.
좌우 차이를 처방에 바로 연결했다가 그 연결을 끊었습니다. 확정 기준을 다시 보니 측정 잡음을 '비대칭'으로 확정하고 있었고, 그 근거로 무게를 건드리는 건 사용자에게 해로울 수 있었기 때문입니다. 지금은 차이를 관측해 알려 주고, 굳어진 차이에만 무게를 깎는 대신 약한 쪽부터 하는 순서를 제안합니다. 초보자에게는 묻지 않습니다.
임신·산후 모드도 같은 원칙입니다. 해당될 수 있는 사용자에게만 묻고, 켜면 처방을 보수적으로 낮추고 종목별로 제외합니다.
// result
지금도 굴러가는, 상용 준비된 앱.
Google Play 프로덕션 심사 중이고 App Store도 심사 중입니다. 워치 앱은 각 제출본에 동봉했습니다. 웹에서도 정기결제를 붙였고 체험은 양쪽 모두 14일입니다. 최근에는 gymkc.org를 Workers로 옮기고, 온보딩에서 장비와 아픈 곳을 그림으로 고르게 바꿨습니다.
검증은 빌드가 통과했다고 끝나지 않습니다. 자동 게이트 112종과 자동 테스트 4,268개를 두고, 웹 5개 뷰포트와 Android·iOS·Wear·watchOS 각 표면에 검증 도장이 없으면 배포가 막힙니다. 갤럭시 S25와 아이폰·아이패드 실기기로도 반복 확인합니다.
// 이런 걸 만들어 드립니다.