이종석

Software Engineer

헬스케어 스타트업에서 2년간 앱·웹·서버·인프라를 혼자 만들고 운영했습니다. 수영 기록 서비스에서는 테크리드로 Apple Watch 앱 개발을 이끌었습니다. 통신 네트워크(LG유플러스), 반도체 설계(삼성전자), 블록체인, 헬스케어, 피트니스까지 서로 다른 도메인에서 개발해 왔습니다.

삼신 (RHABO)

Tech Lead · 2024.10 — 현재 · Team 1

Flutter · NestJS · PostgreSQL · Next.js · AWS

집에서 채혈해 난소 나이(AMH)를 확인하는 자가검사키트를 만드는 헬스케어 스타트업입니다. 2025년 2월부터 회사의 유일한 개발자로 앱·웹·서버·운영 도구를 맡았습니다.

분기 결제 주문
1,200건+합류 분기 20건 · 최고 2025 Q4
분기 키트
1,700건+합류 분기 280건 · 최고 2026 Q2
회원
1만+합류 후 10배 이상
누적 매출
2.7억 원

삼신 (RHABO) ·

Flutter · Swift · Kotlin

검사 항목별 결과 구조

난소 나이 검사 리포트

AMH 수치만 알려 주는 병원 검사지와 달리, 사용자가 내 상태를 이해하고 문진에 답하며 상호작용하는 검사 결과를 만들었습니다. 그 사이 비타민 D·여성호르몬 옵션 검사로 상품이 늘면서, 검사 항목이 추가돼도 그대로 확장되는 데이터 모델이 필요했습니다. 지금까지 리포트 약 7,000건이 발급됐습니다.

검사 항목별 결과 구조

난소 나이 검사 리포트

AMH 수치만 알려 주는 병원 검사지와 달리, 사용자가 내 상태를 이해하고 문진에 답하며 상호작용하는 검사 결과를 만들었습니다. 그 사이 비타민 D·여성호르몬 옵션 검사로 상품이 늘면서, 검사 항목이 추가돼도 그대로 확장되는 데이터 모델이 필요했습니다. 지금까지 리포트 약 7,000건이 발급됐습니다.

  1. 검사 항목별 결과키트마다 오류를 하나만 기록해서는 "AMH는 정상인데 비타민 D 검체만 용혈" 같은 결과를 담을 수 없었습니다. 검사 항목별 오류와 등급(Basic·Plus·Premium)으로 모델을 바꾸고, 앱은 등급마다 카드 배치만 다른 한 화면으로 합쳤습니다.
  2. 내 위치와 한계 시점나이대의 하위 10%·중앙값·상위 10% 사이를 보간해 내 수치의 위치를 계산합니다(피임약을 복용 중이면 서버가 AMH를 먼저 보정). 한계 시점은 ML 서버가 예측한 나이별 AMH 곡선이 기준값 아래로 처음 내려가는 나이로 정합니다.
  3. 한 번에 다시 계산문진은 답을 모아 한 번에 보내고 서버가 한 트랜잭션에서 바꿉니다.
AMH 분석 결과 리포트 화면
AMH 분석 결과

채혈 실패 데이터 분석

키트 사용과 채혈 가이드

고객 문의를 유형별로 나눠 보니 채혈 문의가 약 29%로 가장 많았습니다. 종이 설명서의 채혈 안내를 앱 안의 12단계 가이드와 영상으로 옮기고, 단계마다 머문 시간과 이탈을 계측해 실제 채혈 실패 기록과 맞춰 봤습니다. 가이드를 시작한 사람의 80% 이상이 마지막 단계까지 갔습니다.

채혈 실패 데이터 분석

키트 사용과 채혈 가이드

고객 문의를 유형별로 나눠 보니 채혈 문의가 약 29%로 가장 많았습니다. 종이 설명서의 채혈 안내를 앱 안의 12단계 가이드와 영상으로 옮기고, 단계마다 머문 시간과 이탈을 계측해 실제 채혈 실패 기록과 맞춰 봤습니다. 가이드를 시작한 사람의 80% 이상이 마지막 단계까지 갔습니다.

  1. 실물 크기채혈 튜브를 실제 크기로 보여 주려고 Android는 화면의 실제 밀도를 읽습니다. iOS는 이를 알려 주는 API가 없어, 기기 모델별 화면 밀도 표를 원격 설정에 두고 찾습니다.
  2. 실패는 준비에서채혈침 단계에서는 이탈이 거의 없었습니다. 채혈에 실패한 사람들은 준비 운동 단계를 건너뛴 비율이 58%로 정상 채혈한 사람들(41%)보다 높았고, 재검사 키트는 첫 키트보다 3.4배 더 실패했습니다. 재검사 때 준비 단계를 건너뛰지 않게 하는 것이 가장 효과가 클 지점이라는 근거가 됐습니다.
채혈 가이드: 구성품을 사용 순서대로 나열하기(손난로)
채혈 가이드

앱 밖에서 도는 네이티브 위젯

월경 주기와 홈 위젯

결과지를 받고 나면 앱을 다시 열 이유가 없었습니다. 검사는 몇 달에 한 번이지만 월경 주기는 매달 돌아옵니다. 주기 기록을 검사와 잇고, 앱을 열지 않아도 오늘의 주기 상태가 홈 화면에 맞게 떠 있게 했습니다.

앱 밖에서 도는 네이티브 위젯

월경 주기와 홈 위젯

결과지를 받고 나면 앱을 다시 열 이유가 없었습니다. 검사는 몇 달에 한 번이지만 월경 주기는 매달 돌아옵니다. 주기 기록을 검사와 잇고, 앱을 열지 않아도 오늘의 주기 상태가 홈 화면에 맞게 떠 있게 했습니다.

  1. 검사와 연결주기 기록이 문진 응답으로 이어져, 검사 결과를 받을 때 다시 입력하지 않습니다.
  2. 건강 앱과 양방향HealthKit·Health Connect와 기록을 주고받으면, 내가 보낸 기록이 다음 가져오기 때 되돌아옵니다. 출처를 비교해 걸러 내고(Android는 출처 ID가 비어 와서 이름도 비교), 앱에서 지운 주기는 삭제 표시를 남겨 되살아나지 않게 했습니다.
  3. 위젯은 Dart를 실행하지 못한다처음 위젯은 앱이 열려 있을 때만 갱신됐습니다. 주기 계산을 Swift와 Kotlin으로 옮기고 같은 테스트를 붙였습니다. iOS는 며칠 치 표시를 미리 계산해 두고, 그런 방법이 없는 Android는 하루 한 번 백그라운드 작업이 다시 계산합니다. Android의 달력 이미지는 앱을 열 때만 갱신되는 것으로 두었습니다.
월경 주기 캘린더 화면
월경 주기

실패를 대비한 푸시 알림

검사 진행 상황을 잠금화면에(Live Activity)

검체가 검사실에 도착하면 택배 조회는 "배송 완료"에서 멈춥니다. 실제 검사는 그때부터 시작이라, 결과를 기다리는 사람은 그 사이가 궁금하고 불안했습니다. 그 뒤의 진행 상황을 iOS 잠금화면과 Dynamic Island에 보여 주게 했습니다.

실패를 대비한 푸시 알림

검사 진행 상황을 잠금화면에(Live Activity)

검체가 검사실에 도착하면 택배 조회는 "배송 완료"에서 멈춥니다. 실제 검사는 그때부터 시작이라, 결과를 기다리는 사람은 그 사이가 궁금하고 불안했습니다. 그 뒤의 진행 상황을 iOS 잠금화면과 Dynamic Island에 보여 주게 했습니다.

  1. 성공 응답인데 보이지 않을 때사용자가 닫은 진행 표시에도 APNs는 성공을 돌려줍니다. 서버가 일정 시간 뒤 갱신 토큰이 등록됐는지 확인하고, 없으면 일반 알림으로 대신 보냅니다.
잠금화면 Live Activity: 검체 확인 중
검사 진행 상황

스트리밍 AI 상담과 지원금 판정

난자동결 허브

검사 결과를 받은 사람의 큰 고민 중 하나는 난자동결이지만, 비용이나 지원금 같은 정보를 한곳에서 알기 어려웠습니다. AI 상담과 지원금 찾기, 비용 계산 같은 도구를 하나의 허브로 묶었습니다.

스트리밍 AI 상담과 지원금 판정

난자동결 허브

검사 결과를 받은 사람의 큰 고민 중 하나는 난자동결이지만, 비용이나 지원금 같은 정보를 한곳에서 알기 어려웠습니다. AI 상담과 지원금 찾기, 비용 계산 같은 도구를 하나의 허브로 묶었습니다.

  1. 지원금 판정지자체마다 다르고 자주 바뀌는 지원 조건을 코드가 아닌 데이터로 둡니다. 판정은 예/아니오 대신, 확실히 안 되는 조건이 있을 때만 제외하고 입력이 빈 조건은 "확인 필요"로 남겨 네 단계로 보여 줍니다. 혼인 여부처럼 한 조건만 어긋난 사업도 함께 알려 줍니다.
  2. 답변 스트리밍답변은 서버를 거쳐 조각으로 흘러옵니다. 조각 경계에서 한글이 깨지지 않게 스트림 파서를 직접 짜서 테스트를 붙였고, 사용자가 화면을 나가면 서버가 위쪽 스트림까지 끊습니다.
난자냉동 허브 화면
난자냉동 허브

삼신 (RHABO) ·

Next.js · rhabo.care

할인과 결제 금액 정합성

자사몰

MVP 단계에서는 스마트스토어에서만 키트를 팔았습니다. 고객과 구매 데이터가 플랫폼에 남아, 누가 샀고 언제 채혈했고 결과가 어땠는지를 하나로 이을 수 없었습니다. MVP를 인계받아 웹·앱 결제와 주문 API까지 혼자 맡아 자사몰로 판매를 옮기고, 구매부터 결과까지를 하나의 고객 기록으로 이었습니다. 합류한 분기에 20건이던 결제 주문이 2025년 4분기 1,200건+까지 늘었고, 누적 매출은 2.7억 원입니다.

쿠폰, 선물하기, 친구 추천처럼 플랫폼에서는 어렵던 판매 정책도 직접 운영하게 됐습니다. 결제 주문의 34%가 쿠폰을 썼습니다.

할인과 결제 금액 정합성

자사몰

MVP 단계에서는 스마트스토어에서만 키트를 팔았습니다. 고객과 구매 데이터가 플랫폼에 남아, 누가 샀고 언제 채혈했고 결과가 어땠는지를 하나로 이을 수 없었습니다. MVP를 인계받아 웹·앱 결제와 주문 API까지 혼자 맡아 자사몰로 판매를 옮기고, 구매부터 결과까지를 하나의 고객 기록으로 이었습니다. 합류한 분기에 20건이던 결제 주문이 2025년 4분기 1,200건+까지 늘었고, 누적 매출은 2.7억 원입니다.

쿠폰, 선물하기, 친구 추천처럼 플랫폼에서는 어렵던 판매 정책도 직접 운영하게 됐습니다. 결제 주문의 34%가 쿠폰을 썼습니다.

  1. 할인 규칙 통합쿠폰·번들·프로모션 할인 계산을 가격 화면과 주문 생성이 같은 규칙으로 쓰게 한 곳에 모았습니다.
rhabo.care 스토어 메인
rhabo.care 스토어: 상품 상세에서 리뷰를 보고 옵션을 고르기까지

로그인·인앱 브라우저를 넘는 결제 흐름

난자동결 사전 예약

회사가 난자동결 전문병원을 준비하면서, 개원 전에 사전 예약을 받는 페이지를 만들었습니다. 예약금 결제가 걸린 페이지라 "돈은 나갔는데 예약은 없는" 상태가 생기면 안 됐고, 아직 없는 병원이 신뢰를 얻으려면 페이지가 매끄러워야 했습니다. 약 6주 동안 600명+가 사전 예약을 신청했습니다.

로그인·인앱 브라우저를 넘는 결제 흐름

난자동결 사전 예약

회사가 난자동결 전문병원을 준비하면서, 개원 전에 사전 예약을 받는 페이지를 만들었습니다. 예약금 결제가 걸린 페이지라 "돈은 나갔는데 예약은 없는" 상태가 생기면 안 됐고, 아직 없는 병원이 신뢰를 얻으려면 페이지가 매끄러워야 했습니다. 약 6주 동안 600명+가 사전 예약을 신청했습니다.

  1. 로그인을 다녀와도카카오·애플 로그인은 페이지를 통째로 다시 불러 화면 상태가 사라집니다. 받을 쿠폰을 브라우저 저장소 한 곳에만 두고, 돌아오면 자동으로 발급합니다.
난자동결 사전 예약 페이지
사전 예약 페이지

페이지별 렌더링 전략

콘텐츠 · EGG WIKI

검색으로 들어오는 사람을 위해 난소 나이와 난자동결에 관한 위키, 실제 후기 모음, 뉴스레터를 만들었습니다. 글은 서버 DB에서 오고, 검색 노출과 최신성 사이에서 페이지마다 렌더링 방식을 골랐습니다.

페이지별 렌더링 전략

콘텐츠 · EGG WIKI

검색으로 들어오는 사람을 위해 난소 나이와 난자동결에 관한 위키, 실제 후기 모음, 뉴스레터를 만들었습니다. 글은 서버 DB에서 오고, 검색 노출과 최신성 사이에서 페이지마다 렌더링 방식을 골랐습니다.

  1. 페이지마다 렌더링 방식사이트 전체에 걸려 있던 동적 렌더링을 걷어 내고 페이지별로 정했습니다. 위키 글은 미리 만들어 두고 한 시간마다 갱신하고, 홈은 인앱 브라우저 감지를 클라이언트로 옮겨 정적으로 바꿨습니다.
  2. 갱신 주기의 함정주기적 갱신은 만료 뒤 첫 방문자에게 이전 페이지를 보여 줍니다. 방문이 적은 뉴스레터는 발행 직후에도 이전 호가 보여서, 독자가 보는 페이지는 매번 새로 받고 사이트맵만 캐시합니다.
  3. API가 멈춰도 배포빌드 중 상품 API가 늦으면 배포 전체가 실패했습니다. 빈 목록으로 빌드하고 첫 요청 때 채우게 했습니다.
EGG WIKI 난소 건강 지식 목록
EGG WIKI: 주제를 골라 글 하나를 읽기까지

삼신 (RHABO) ·

NestJS · PostgreSQL · AWS

서버 구성

외부 연동 약 20곳

지금까지 본 앱과 웹의 화면 뒤에는 서버 하나가 있습니다. 결제와 주문, 키트 배송과 수거, 검사 결과, 알림, 본인인증을 하나의 NestJS 서버가 처리하고, 외부 서비스 약 20곳과 연결됩니다. 이 서버와 인프라도 혼자 만들고 운영했습니다.

서버 구성

외부 연동 약 20곳

지금까지 본 앱과 웹의 화면 뒤에는 서버 하나가 있습니다. 결제와 주문, 키트 배송과 수거, 검사 결과, 알림, 본인인증을 하나의 NestJS 서버가 처리하고, 외부 서비스 약 20곳과 연결됩니다. 이 서버와 인프라도 혼자 만들고 운영했습니다.

  1. 하나의 APIFlutter 앱, Next.js 웹, 어드민이 모두 같은 NestJS API를 씁니다.
  2. 큐검사 단계 진행이나 알림 확인처럼 나중에 할 일은 Redis 기반 큐의 지연 작업으로 돌려, 서버가 재시작돼도 남게 했습니다.
  3. 저장소운영 데이터는 PostgreSQL, 파일은 S3에 두고, 분석용 데이터는 매일 BigQuery로 옮깁니다.
  4. 외부 연동 약 20곳결제, 물류, 알림, 인증, AI까지 연동이 많아, 응답이 중복되거나 늦거나 거절되는 경우를 연동마다 따로 다뤘습니다.
Flutter 앱Next.js 웹어드민NestJS APIEC2 · Docker큐 · BullMQRedis · 지연 작업PostgreSQLS3 · CDNBigQuery매일 ETL외부 연동결제토스페이먼츠물류우체국 수거 예약 · 배송 조회알림알림톡 · APNs · FCM · 메일 · 해피톡 · Slack인증·주소NICE 본인인증 · 카카오 · 도로명주소AI·문서Claude · ML 서버 · CLOVA OCR · Textract · 네이버 검색
서버 구성 · 외부 연동은 코드의 실제 연동

키트 사용 플로우

주문부터 리포트까지

주문이 들어오면 결제를 확정하고, 우체국 수거를 예약하고, 배송 조회로 키트의 위치를 갱신하고, 검사 결과가 들어오면 리포트를 만들어 알립니다. 이 흐름이 끊기면 사람이 직접 확인해야 해서, 단계마다 상태를 남겼습니다.

키트 사용 플로우

주문부터 리포트까지

주문이 들어오면 결제를 확정하고, 우체국 수거를 예약하고, 배송 조회로 키트의 위치를 갱신하고, 검사 결과가 들어오면 리포트를 만들어 알립니다. 이 흐름이 끊기면 사람이 직접 확인해야 해서, 단계마다 상태를 남겼습니다.

  1. 주문과 결제결제사 웹훅이 중복되거나 늦게 와도 주문은 한 번만 확정됩니다.
  2. 수거 예약고객이 수거를 신청하면 서버가 우체국 수거를 예약하고 전날 안내 알림톡을 예약해 둡니다. 신청을 거두면 예약된 알림톡도 취소합니다.
  3. 배송 조회우체국 배송 기록을 주기적으로 조회해 상태를 바꾸고, 수거 전·오래 조회되지 않음·반송처럼 막힌 상태를 구분해 기록합니다.
  4. 입고 이후운영팀이 입고를 처리하면 채혈 안내 알림을 거두고, 잠금화면 진행 표시를 푸시로 시작하고, 검사실 세부 단계를 지연 작업으로 예약합니다. 처음엔 타이머로 이어 두어 서버가 재시작되면 예약이 사라졌습니다.
  5. 리포트검사 결과가 들어오면 ML 서버가 분석하고, 서버가 리포트를 만들어 진행 표시를 끝내고 푸시로 알립니다.
고객 앱서버외부운영PAYMENT_COMPLETEDPICKUP_REQUESTEDPICKUP_SHIPPING_IN_PROGRESSRECEIVEDREPORT_GENERATED주문결제 승인토스웹훅토스수거 신청수거 예약우체국전날 안내 예약알림톡배송 조회 (주기)우체국입고 처리진행 표시 시작APNs검사 단계 예약 (큐)검사 결과분석ML리포트 · 푸시
주문부터 리포트까지 · 서버의 실제 상태 이름

한 사람이 운영할 수 있는 구조

운영하는 사람이 한 명이면 들인 모든 구성 요소가 곧 장애 대응 범위입니다. 그래서 MVP 단계에는 PM2로 빠르게 배포하고, 주문이 늘어난 뒤에는 Docker 이미지·ECR·SSM 기반 배포로 단계적으로 옮겨 느리고 비용이 더 들어도 재현 가능하고 관리 가능한 구조로 정비했습니다. 테스트를 정리해 통과하지 않으면 배포되지 않게 했고, 쓰지 않는 검증 환경은 꺼 둘 수 있게 했습니다.

삼신 (RHABO) ·

React · BigQuery · AWS CDK

운영 어드민

키트 운영 흐름

키트 주문부터 수거, 입고, 검사, 결과 입력까지 운영팀이 매일 하는 일을 단계별 화면으로 만들었습니다.

운영 어드민

키트 운영 흐름

키트 주문부터 수거, 입고, 검사, 결과 입력까지 운영팀이 매일 하는 일을 단계별 화면으로 만들었습니다.

  1. 키트 현황첫 화면은 단계마다 몇 개가 머물러 있는지 보여 주고, 단계를 누르면 그 키트들을 펼쳐 봅니다.
  2. 수거 일정수거 예정 키트를 날짜와 검사 등급별로 보여 줍니다.
  3. 입고 처리검사실에 도착한 키트를 골라 입고 처리합니다. 이 한 번의 변경으로 서버가 채혈 알림 정리, 잠금화면 진행 표시, 검사 단계 예약을 이어서 합니다.
  4. 결과 입력결과는 검사 항목마다 따로 넣고, 항목별로 혈액 부족·손상을 표시합니다. 리포트의 항목별 결과 구조가 여기서 시작됩니다.
  5. 물류 담당운영팀에 물류를 맡은 파트타임 직원이 있어, 물류 담당 역할에는 발송 처리 화면만 보이게 나눴습니다.
목업 데이터로 띄운 화면(이름·고유번호·전화번호는 가짜)

같은 숫자를 보는 데이터 환경

BigQuery · 지표 기준

개발자가 아닌 동료들도 데이터를 직접 보고 같은 숫자로 이야기할 수 있게, 운영 데이터와 광고·유입 데이터를 한곳에 모으고 자연어로 질문하는 환경을 만들었습니다.

같은 숫자를 보는 데이터 환경

BigQuery · 지표 기준

개발자가 아닌 동료들도 데이터를 직접 보고 같은 숫자로 이야기할 수 있게, 운영 데이터와 광고·유입 데이터를 한곳에 모으고 자연어로 질문하는 환경을 만들었습니다.

  1. 소스운영 DB와 GA4 이벤트를 모읍니다. 광고 성과를 재기 위해 GA4, 서버 측 GTM, Meta 전환 추적도 연결했습니다.
  2. 매일 옮기기운영 DB를 읽기 전용으로 매일 BigQuery에 옮깁니다. 같은 날을 다시 돌려도 결과가 같도록 덮어쓰기로 만들었습니다.
  3. 지표 기준가입자 수나 매출을 사람마다 다르게 계산하지 않도록, 지표마다 기준 SQL·주의사항·검증일을 담은 지표 정의 테이블을 BigQuery에 두고 사람과 AI가 이 정의만 쓰게 했습니다.
  4. 팀 공유 Claude 프로젝트동료들은 팀 공유 Claude 프로젝트에서 질문합니다. 분석 지침, 스키마 설명, 지표 기준을 넣어 두고 BigQuery에 바로 연결해, 같은 질문에는 같은 정의로 답하게 했습니다.
운영 DBPostgreSQLGA4 이벤트서버 측 GTM 포함BigQuery매일 · 덮어쓰기운영 DB 사본GA4 events정리된 뷰스키마 설명지표 기준지표 정의 테이블Claude 프로젝트 · 팀 공유BigQuery 직접 연결분석 지침 · 스키마 설명 · 지표 기준지난달 신규 가입자는 몇 명이야?지표 기준의 SQL로 BigQuery를 조회해 답
운영 DB · GA4 → BigQuery → 팀 공유 Claude 프로젝트

EMR과 DUR 연동

병원 개원 시스템

회사가 난자동결 병원을 준비하면서, 진료 기록과 처방을 다루는 병원 전산(EMR)과 직원용 웹을 만들었습니다. 처방 시 의약품 안전 점검을 위해 건강보험심사평가원의 DUR 시스템과 연동했고, 인프라는 AWS CDK로 코드화했습니다.

EMR과 DUR 연동

병원 개원 시스템

회사가 난자동결 병원을 준비하면서, 진료 기록과 처방을 다루는 병원 전산(EMR)과 직원용 웹을 만들었습니다. 처방 시 의약품 안전 점검을 위해 건강보험심사평가원의 DUR 시스템과 연동했고, 인프라는 AWS CDK로 코드화했습니다.

  1. 오늘 할 일진료 큐와 결과 확인 대기 옆에, DUR 확인이 시간 초과나 오류로 지연된 처방을 모아 직원이 마저 처리하게 했습니다. 보내지 못한 점검 결과는 1분마다 자동으로 다시 보냅니다.
  2. 처방 입력의약품은 성분명 코드로 검색해 고르고, 청구나 DUR 전송에 필요한 항목이 비어 있으면 발급 전에 알려 줍니다.
  3. 발급과 DUR 확인발급과 동시에 DUR(병용금기·중복처방) 확인이 진행되고, 결과와 전자서명이 처방에 남습니다.
목업 데이터로 띄운 직원용 웹(환자·의약품 코드는 가짜)

굿노즈 (셩)

Tech Lead · 파트타임 · 2023.05 — 현재

Swift · React Native · Go · PostgreSQL

수영 기록 앱 셩(Syeong)을 만드는 스타트업입니다. 2023년 5월부터 파트타임 테크 리드로 개발자 채용·온보딩과 기술 방향을 맡았고, Apple Watch 앱을 처음부터 설계·구현했습니다.

회원
5만+
누적 수영 기록
300만+
Garmin 연결 사용자
1,400명+

굿노즈 (셩) ·

Swift · watchOS · CoreMotion

실시간 수영 트래킹 엔진

Apple Watch 루틴 진행

워치가 루틴을 진행하려면, 즉 다음 세트를 안내하고 햅틱을 울리고 휴식을 세려면 수영장 한 편도를 마친 순간을 알아야 합니다. HealthKit 구간 기록만으로는 그 순간을 알 수 없어서, 손목 움직임으로 먼저 판정하고 HealthKit 기록과 나중에 맞춰 보는 엔진을 설계했습니다. PRO 구독의 핵심 기능으로 출시했습니다.

실시간 수영 트래킹 엔진

Apple Watch 루틴 진행

워치가 루틴을 진행하려면, 즉 다음 세트를 안내하고 햅틱을 울리고 휴식을 세려면 수영장 한 편도를 마친 순간을 알아야 합니다. HealthKit 구간 기록만으로는 그 순간을 알 수 없어서, 손목 움직임으로 먼저 판정하고 HealthKit 기록과 나중에 맞춰 보는 엔진을 설계했습니다. PRO 구독의 핵심 기능으로 출시했습니다.

  1. HealthKit은 한 구간 늦다실측해 보니 구간 기록은 다음 구간을 마친 뒤 1~2초 후에야 도착했고, 벽에서 쉬는 동안에는 오지 않았습니다. 누적 거리 통계도 같은 시각에 바뀌어 대안이 되지 못했습니다.
  2. 손목 신호로 먼저 판정가속도에서 스트로크를 세어 수영과 멈춤을 나누고, 학습한 한 편도 시간만큼 헤엄친 뒤 멈추면 벽에 닿은 것으로 잠정 판정합니다. 세트 완료와 햅틱이 이때 바로 나갑니다. 쉬지 않고 이어 헤엄칠 때는 다음 구간이 절반쯤 진행된 것을 확인한 뒤에야 앞 구간을 셉니다.
  3. 나중에 맞춰 보기잠정 판정은 대기열에 두고 HealthKit 기록이 도착하면 하나씩 지워, 같은 구간을 두 번 세지 않습니다. 업로드와 결과에는 HealthKit으로 확정된 값만 씁니다.
  4. 틀린 안내보다 늦은 안내배영은 HealthKit이 실제 벽보다 일찍 구간을 닫아, 그대로 따르면 아직 헤엄치는 중에 휴식이 시작됐습니다. 손목 신호가 아직 수영 중이면 벽을 확인할 때까지 휴식 시작을 미룹니다.
  5. 수영장에서 디버깅직접 수영하며 진단 로그를 모으고, 같은 신호를 재현 하네스로 다시 돌려 회차마다 고쳤습니다. 페이스로 거리를 채워 보여 주는 방법은 멈추면 앞서 나갔다 되돌아가서 버렸습니다.
실제 수영손목 신호엔진 판정HealthKit구간 기록워치 화면접영 25m접영 25m벽에서 휴식배영 25m첫 편도 기록휴식 중 기록 없음25 / 50m멈춤 → 벽 판정50m ✓ · 햅틱둘째 편도 기록다시 세지 않음배영 25m벽에서 휴식 30초구간을 닫음(벽보다 일찍)멈춤 → 벽 확인그대로 따르면 헤엄치는 중에 휴식 시작벽 확인 후 휴식 시작보류
워치 화면은 튜토리얼 목업 · 시간축은 설명용 예시

젖은 손으로 쓰는 화면

워치 루틴 진행 화면

수영 중에는 손이 젖어 있고 화면을 오래 볼 수도 없습니다. 진행 화면은 손대지 않아도 읽히고, 잘못 눌러도 빠져나가지 않게 만들었습니다.

젖은 손으로 쓰는 화면

워치 루틴 진행 화면

수영 중에는 손이 젖어 있고 화면을 오래 볼 수도 없습니다. 진행 화면은 손대지 않아도 읽히고, 잘못 눌러도 빠져나가지 않게 만들었습니다.

  1. 스크롤하지 않아도 되는 목록지금 랩은 늘 같은 자리에 있고, 랩을 마칠 때마다 목록이 한 칸씩 올라가 다음 세트의 첫 랩을 미리 보여 줍니다. 진행 판정과 분리된 표시 층이라 판정 로직은 건드리지 않았습니다. 루틴 전체를 스크롤로 훑는 시안은 이 원칙에 맞지 않아 구조는 가져오지 않았습니다.
  2. 나가는 길이 없는 운동 화면운동이 시작되면 내비게이션 대신 운동 화면만 그려, 잘못 눌러도 빠져나갈 출구 자체가 없습니다. 전체 화면 모달로 덮던 방식을 바꾸자 앱 복귀 때 상태를 되살리는 로직도 필요 없어졌습니다.
  3. 햅틱 두 가지단계 완료와 휴식 종료를 다른 햅틱으로 알립니다. 휴식은 별도 단계가 아니라 각 단계의 속성으로 두었고, 휴식 종료 햅틱은 화면이 어두워진 상시 표시 상태에서도 울리도록 화면 갱신 주기에 걸었습니다.
튜토리얼의 워치 화면 목업을 옮긴 것

굿노즈 (셩) ·

React Native · Swift · Kotlin

워치와 폰의 상태 동기화

WatchConnectivity · HealthKit

폰 앱에서 로그인하고 루틴과 수영장을 고르면, 워치는 그 상태를 받아 혼자 운동을 진행하고 기록을 남깁니다. 두 기기는 서로 켜져 있다는 보장이 없어서, 무엇을 보내느냐에 따라 전달 방식을 나눴습니다.

워치와 폰의 상태 동기화

WatchConnectivity · HealthKit

폰 앱에서 로그인하고 루틴과 수영장을 고르면, 워치는 그 상태를 받아 혼자 운동을 진행하고 기록을 남깁니다. 두 기기는 서로 켜져 있다는 보장이 없어서, 무엇을 보내느냐에 따라 전달 방식을 나눴습니다.

  1. 상태는 마지막 값만로그인 토큰, 수영장 목록, 단위, PRO 상태처럼 지금 값만 의미 있는 것은 한 묶음의 상태로 보냅니다. 여러 번 바뀌어도 마지막 값 하나만 남고, 워치가 꺼져 있었어도 다음 실행 때 받습니다.
  2. 사건은 놓치지 않게기록과 루틴이 바뀌었다는 알림은 워치가 닿아 있으면 바로 보내고, 실패하거나 닿지 않으면 순서가 보장되는 대기열로 넘깁니다. 갱신 시각을 상태에도 함께 남겨, 둘 중 하나만 도착해도 워치가 새로 받아 옵니다.
  3. 늦게 오면 안 되는 명령튜토리얼 단계는 워치 앱이 켜져 있을 때만 의미가 있어 대기열 없이 보냅니다. 운동 중이면 워치가 거절하고, 폰이 워치 화면 예시를 대신 보여 줍니다.
  4. 워치를 거치지 않는 길워치 운동은 HealthKit에도 저장되므로, 폰은 백그라운드에서 HealthKit 변경을 감지해 서버로 올립니다. 워치가 직접 올리다 실패하면 기록을 워치에 저장해 두고, 네트워크가 돌아오면 다시 올립니다.
폰 앱React Native · Swift워치 앱SwiftUI상태 · applicationContext로그인 토큰 · 수영장 · 단위 · PRO 상태마지막 값만 남는다사건 · sendMessage → 대기열기록·루틴 갱신 · 로그아웃닿으면 바로, 아니면 순서대로명령 · sendMessage만튜토리얼 단계 · 대기열 없음운동 중이면 거절 → 폰이 화면 예시로HealthKit셩 서버Go운동 저장백그라운드 감지업로드직접 업로드실패하면 저장 후다시 올림
폰 앱과 워치 앱 사이의 실제 전달 경로

굿노즈 (셩) ·

Go · PostgreSQL

여러 경로로 들어오는 기록의 중복 제거

수영 기록 저장

같은 수영 1회가 워치의 직접 업로드, 폰의 HealthKit 동기화, 실패 뒤 재전송으로 서버에 여러 번 들어와 기록이 중복 생성됐습니다. 경로마다 막는 층을 두었습니다.

여러 경로로 들어오는 기록의 중복 제거

수영 기록 저장

같은 수영 1회가 워치의 직접 업로드, 폰의 HealthKit 동기화, 실패 뒤 재전송으로 서버에 여러 번 들어와 기록이 중복 생성됐습니다. 경로마다 막는 층을 두었습니다.

  1. 식별자 우선순위HealthKit ID, 워치가 만든 ID, 서버 ID 순으로 기록의 식별자를 정하고 사용자별 유니크 인덱스를 걸어, 같은 기록이 다시 와도 한 번만 저장됩니다.
  2. 시작 시각으로 잇기워치 기록이 먼저 올라와 아직 HealthKit ID가 없으면 시작 시각이 같은 기존 기록에 연결합니다. 식별자가 없는 출처(Samsung·Garmin·수동 입력)는 이 규칙에서 빼서 서로 다른 기록이 합쳐지지 않게 했습니다.
  3. 겹치면 같은 운동시작 시각과 구간이 겹치면 같은 운동으로 보고 하나로 합치되, 연달아 한 별개의 운동은 따로 남깁니다.
워치 직접 업로드폰 HealthKit 동기화실패 뒤 재전송Samsung · Garmin수동 입력식별자 + 유니크 인덱스HealthKit > 워치 > 서버시작 시각으로 연결식별자 없는 출처 제외구간이 겹치면 합침연달아 한 운동은 따로②를 건너뜀수영 기록1개
같은 수영 1회가 서버에 닿는 세 경로

헥사고날 구조와 DB 전환

서버 구조

서버 기능을 늘리기 전, 도메인 로직이 DB 쿼리와 외부 호출 사이에 섞여 있었습니다. 구조를 먼저 나눠 두어, 이후 DB 전환이 어댑터 교체로 끝나게 했습니다.

헥사고날 구조와 DB 전환

서버 구조

서버 기능을 늘리기 전, 도메인 로직이 DB 쿼리와 외부 호출 사이에 섞여 있었습니다. 구조를 먼저 나눠 두어, 이후 DB 전환이 어댑터 교체로 끝나게 했습니다.

  1. 섞여 있던 도메인요청 처리 코드 안에서 비즈니스 규칙과 MongoDB 쿼리, 외부 호출이 한데 섞여 있어, 저장소를 바꾸려면 서비스 전체를 고쳐야 했습니다.
  2. 도메인·포트·어댑터도메인과 포트, 어댑터 골격을 세우고 사용자·인증·기록·리포트 서비스를 다시 썼습니다(2024.09~10).
  3. 어댑터만 바꾼 DB 전환이후 팀원이 저장소 포트를 구현하는 PostgreSQL 어댑터를 추가하는 것만으로 MongoDB에서 전환을 마쳤습니다(2025.04~05).
HTTP 요청요청 처리 코드비즈니스 규칙MongoDB 쿼리외부 호출도메인 · 서비스사용자 · 인증 · 기록 · 리포트포트포트HTTP 핸들러어댑터MongoDB어댑터PostgreSQL어댑터 추가팀원 · 2025.04~05
도메인·포트·어댑터 · DB 전환은 팀원이 실행

Garmin 연동

워치 기록 가져오기

Apple Watch와 Galaxy Watch 외에 Garmin 워치로 수영하는 사용자까지 받으려 했지만, Garmin 기록을 앱으로 가져올 방법이 없었습니다. 연동을 설계·구현했고, 1,400명 이상이 연결했습니다.

Garmin 연동

워치 기록 가져오기

Apple Watch와 Galaxy Watch 외에 Garmin 워치로 수영하는 사용자까지 받으려 했지만, Garmin 기록을 앱으로 가져올 방법이 없었습니다. 연동을 설계·구현했고, 1,400명 이상이 연결했습니다.

  1. 연결앱에서 Garmin 계정을 연결하면 서버가 받은 권한을 확인해 함께 저장합니다.
  2. 웹훅으로 받는 새 기록Garmin이 새 활동을 알리면 활동 파일(FIT)을 받아 영법과 구간 정보를 꺼내 셩의 수영 기록으로 바꿉니다.
  3. 과거 기록 가져오기연결 직후 과거 기록을 요청하면 Garmin이 웹훅으로 나눠 보냅니다. 요청 횟수 제한과 요청 가능한 시작 시점 제한을 따로 처리했습니다.
  4. 권한이 바뀔 때사용자가 Garmin에서 권한을 바꾸면 웹훅으로 받아 갱신하고, 과거 기록 권한이 새로 생기면 그때 가져옵니다. 연결 해제도 웹훅으로 받아 동기화를 끕니다.
셩 앱셩 서버Garmin연결계정 연결권한 조회허용한 권한새 기록활동 웹훅활동 파일(FIT) 받기영법·구간 → 수영 기록과거 기록과거 기록 요청웹훅으로 나눠 도착권한 변경권한 변경 웹훅과거 기록 권한이 새로 생기면 가져오기연결 해제 웹훅 → 동기화 끔
연결부터 권한 변경까지 · 서버의 실제 웹훅

수호아이오

Blockchain Core Engineer · 2022.11 — 2024.06

Go · Cosmos SDK · IBC · NestJS

블록체인 인프라 스타트업입니다. Evmos 해커톤 수상작 StayKing을 Cosmos SDK 기반 자체 앱체인으로 다시 만들어 메인넷을 운영한 코어 팀에서 대출 풀과 체인 업그레이드를 맡았고, 이후 리브랜딩한 Mitosis 앱체인에서 청산 로직과 대출 모듈을 맡았습니다.

수호아이오 ·

Go · Cosmos SDK · IBC

운영 중인 체인의 모듈 분할

StayKing 체인 업그레이드

운영 중이던 대출 풀 모듈 하나를 레버리지 대출, 즉시 언스테이킹, 예치 세 모듈로 나눠야 했습니다. 모든 노드가 같은 결과를 내야 하는 체인에서, 자금이 걸린 상태를 업그레이드 블록 한 번에 옮기는 업그레이드를 설계·구현했습니다.

운영 중인 체인의 모듈 분할

StayKing 체인 업그레이드

운영 중이던 대출 풀 모듈 하나를 레버리지 대출, 즉시 언스테이킹, 예치 세 모듈로 나눠야 했습니다. 모든 노드가 같은 결과를 내야 하는 체인에서, 자금이 걸린 상태를 업그레이드 블록 한 번에 옮기는 업그레이드를 설계·구현했습니다.

  1. 모듈 하나에 세 역할레버리지 대출, 즉시 언스테이킹, 예치가 한 모듈의 상태와 계정을 함께 쓰고 있어, 하나를 바꾸려면 나머지까지 건드려야 했습니다.
  2. 한 블록 안의 이관업그레이드 높이에 도달하면 모든 노드가 같은 순서로 기존 상태를 읽고, IBC 채널 권한의 소유자를 바꾸고, 잔고를 옮기고, 새 모듈 셋을 변환된 상태로 초기화합니다.
  3. 실패하면 멈춘다잔고 이체가 실패하면 체인을 멈추게 했습니다. 자금이 반쯤 옮겨진 채 블록이 계속 쌓이는 것보다, 멈춘 지점에서 고쳐 다시 올리는 편이 복구가 쉽습니다.
  4. 테스트와 회고기존 상태를 새 모듈 상태로 바꾸는 변환 테스트를 붙여 메인넷에 적용했습니다. 이후 다른 체인에서 업그레이드 절차 없이 바이너리만 바꾼 수정이 노드 재동기화 때 문제가 될 수 있다는 점을 회고 문서로 남겼습니다.
lendingpool운영 중 · 모듈 하나레버리지 대출즉시 언스테이킹예치업그레이드 블록 한 번기존 풀·설정·다음 ID 읽기IBC 권한 소유자 변경잔고 전액 이체기존 풀 삭제새 모듈 셋 초기화모듈 마이그레이션 실행levstakepool레버리지 대출instantunstakingpool즉시 언스테이킹lending예치이체가 실패하면 체인을 멈춘다변환 테스트기존 상태 → 새 모듈 상태메인넷 적용운영 회고 문서
업그레이드 핸들러가 한 블록 안에서 하는 일 · 코드의 실제 순서

이상 가격에서 멈추는 청산

Mitosis 레버리지 트레이딩

StayKing을 리브랜딩한 Mitosis 앱체인에서 레버리지 트레이딩의 청산 로직과 대출 모듈의 핵심 로직을 맡았습니다. 가격 하나만 믿으면 잘못된 가격 때문에 정상 포지션이 청산될 수 있어, 여러 가격을 교차 확인한 뒤에 집행하게 했습니다.

이상 가격에서 멈추는 청산

Mitosis 레버리지 트레이딩

StayKing을 리브랜딩한 Mitosis 앱체인에서 레버리지 트레이딩의 청산 로직과 대출 모듈의 핵심 로직을 맡았습니다. 가격 하나만 믿으면 잘못된 가격 때문에 정상 포지션이 청산될 수 있어, 여러 가격을 교차 확인한 뒤에 집행하게 했습니다.

  1. 주기 점검12블록(약 1분)마다 전체 포지션을 점검해 청산, 손절, 익절 순으로 처리합니다. 매 블록 모든 포지션을 도는 비용을 줄이려고 주기 실행으로 바꿨습니다.
  2. 가격 넷을 비교온체인과 오프체인의 평균 가격, 시장가와 DEX 가격을 비교합니다. 두 쌍이 모두 5% 이상 어긋나면 가격 데이터가 이상하다고 보고 청산을 미룹니다.
  3. 보류에도 끝이 있다처음 감지한 블록을 기록해 두고, 약 6시간(4,320블록)이 지나면 가격이 충분히 확인된 것으로 보고 집행합니다. 보류가 끝없이 이어져 부실이 쌓이지 않게 했습니다.
  4. 테스트대출 모듈의 예치·대출·상환 핵심 로직과 이자 모델을 구현하고, 금융 로직이라 테스트 약 1,400줄을 함께 작성했습니다.
12블록(약 1분)마다 전체 포지션 점검청산 > 손절 > 익절청산 후보 감지A · 온체인 TWAPD · 오프체인 TWAPB · 시장가C · DEX 가격A와 D가 5% 이상 차이?B와 C도 5% 이상 차이?둘 다 그렇다아니면가격 이상 → 보류청산 집행처음 감지한 블록+4,320블록(약 6시간)보류해도 이 시간이 지나면 가격이 확인된 것으로 보고 청산
청산 판단 흐름 · 숫자는 체인의 기본 설정값

수호아이오 ·

NestJS · AWS ECS

짧은 기간의 신사업 백엔드

2024년 초에는 회사가 시도한 블록체인 밖의 신사업 백엔드를 연달아 만들었습니다. 실물 자산 위탁 서비스의 백엔드는 혼자 NestJS 헥사고날 구조로 세우고, GitHub Actions에서 ECR과 ECS로 배포되는 파이프라인까지 만들었습니다. 탈중앙 거래소의 유동성 보상률을 계산하는 배치는 쓰기 충돌이 나면 다시 시도하게 했고, 방탈출 예약 사이트에는 결제 취소와 환불을 붙였습니다.

삼성전자

ASIC Design Engineer · 2017.07 — 2021.05

Verilog · Python

데이터센터용 엔터프라이즈 SSD 컨트롤러를 설계했습니다.

  • NVMe 명령 완료(Completion) 처리 모듈 설계, PCIe Gen4 성능 튜닝
  • Microsoft·Google·Meta·AWS 등 데이터센터 고객이 요구한 신규 기능 개발
  • 하드웨어와 소프트웨어를 함께 보는 디버깅 플랫폼과 자동화 도구(Python)로 해외 고객사 랩의 검증 과정 개선

LG유플러스

Network Engineer · 2017.01 — 2017.06

LTE 데이터망을 운영하며 트래픽 분석과 장애 대응을 맡았습니다.