chorock.page
소개글시리즈프로젝트
© 2026 chorock.page
← 프로젝트 목록

Project

Chrono-Derm

멋쟁이사자처럼 애니멀리그 중앙해커톤 출전작, 피부 시술 이후의 관리를 기록하는 React Native(Expo) 앱의 프론트엔드 전담

작업 기간

2026.08

프로젝트 정보

팀 구성

해커톤 팀 5명 — 프론트엔드 1명(본인) · 백엔드 3명 · 기획/디자인 1명

담당 역할

프론트엔드 (단독)

GitHub

목차

맡은 일성과와 한계주요 기능설계 판단문제 해결발표 슬라이드
목차
맡은 일성과와 한계주요 기능설계 판단문제 해결발표 슬라이드

사용 기술

모바일 클라이언트

React Native 0.86Expo SDK 57TypeScriptExpo Router

카메라·이미지

expo-cameraReact Native SkiaML Kit Face DetectionReact Native SVG

기기 기능

expo-notificationsExpo SecureStoreAsyncStorage

품질·검증

Jestjest-expoReact Native Testing Library

프로젝트 개요

피부 시술을 받은 뒤의 관리 기록을 한곳에 모으는 앱입니다. 시술 이력을 등록하고 셀카 체크인과 데일리 체크리스트를 남기면 "보존지수"와 오늘의 케어, 재시술 D-day를 보여줍니다. 멋쟁이사자처럼 애니멀리그 중앙해커톤에 나가 만든 팀 프로젝트이고, 저는 프론트엔드를 전담했습니다. 서버는 팀의 Django/DRF API를 붙였습니다.

보존지수는 의료 진단이나 시술 효과를 단정하는 값이 아닙니다. 같은 기준으로 반복 기록한 사후관리 상태를 비교하기 위한 서비스 지표로 설계했고, 앱 안에도 그렇게 명시했습니다.

맡은 일

5명 팀에서 프론트엔드는 저 혼자였습니다(백엔드 3명, 기획/디자인 1명). 화면 15개와 서버 연동까지 React Native(Expo) 앱 전 범위를 맡았습니다 — 인증, 셀카 촬영·검증, 체크리스트, 보존지수 시각화, 케어·제품 추천, 알림.

성과와 한계

  • 프론트엔드를 혼자 맡아 화면 15개와 서버 연동까지 마쳤습니다. 코드 커밋 114건, 테스트 파일 87개에 케이스 439개를 함께 남겼고 lint · typecheck · test를 매 작업의 통과 기준으로 삼았습니다.
  • 다만 카메라·얼굴 검출·기기 알림은 자동 테스트로 덮이지 않습니다. 네이티브 개발 빌드에서만 동작해서 이 영역의 회귀 확인은 실기기 수동 검증에 의존합니다.
  • 앱스토어 출시는 이번 범위가 아니었습니다. iOS는 Release 데모 빌드, Android는 내부 배포용 APK 프로필까지입니다.

주요 기능

  • 회원가입·로그인 — JWT를 SecureStore에 보관하고 AsyncStorage에 평문으로 두지 않습니다.
  • 시술 이력 — 등록·조회·수정·삭제, 이력별 보존지수 변화와 산출 근거 확인
  • 셀카 체크인 — 촬영 전 가이드 영역의 조명 밝기를 실시간으로 안내하고, 촬영본에서 얼굴 수를 검증한 뒤 분석을 요청합니다. 분석은 비동기라 5초 간격으로 상태를 조회하고, 완료되면 기기 로컬 알림을 보냅니다.
  • 보존지수 — 셀카 분석과 데일리 체크리스트를 바탕으로 시술별 산출. 반원 게이지와 변화 이력으로 표시합니다.
  • 오늘의 케어와 Pith 제품 추천, 시술 기준일에 따른 재시술 D-day와 알림

설계 판단

서버가 붙기 전에 전 플로우를 돌 수 있는 로컬 목 백엔드

해커톤이라 서버와 클라이언트가 동시에 만들어졌습니다. 화면만 먼저 쌓아두면 "로그인 → 시술 등록 → 셀카 → 체크리스트 → 보존지수"로 이어지는 흐름이 말이 되는지를 서버가 끝날 때까지 확인할 수 없었습니다.

그래서 앱 안에 임시 목 백엔드를 두고 전 플로우를 끝까지 밟을 수 있게 했습니다. 모든 호출에 지연을 넣은 게 핵심이었습니다(일반 400ms, 셀카는 업로드 1.2초 + 분석 3초) — 즉시 응답하는 목은 로딩 표시와 버튼 비활성 처리를 검증하지 못합니다.

교체 비용도 미리 줄여뒀습니다. 조회는 shared의 셀렉터에, 쓰기는 각 도메인의 api/ 파일에 몰아둬서 실제 API가 붙을 때 구현만 바꾸면 화면 코드는 손대지 않아도 되게 했습니다. 실제로 서버가 열린 뒤 도메인별로 교체했고, 화면 수정은 응답 계약이 어긋난 한 곳에서만 필요했습니다.

문제 해결

셀카에서 얼굴을 못 찾던 문제 — 원인을 세 번 갈아엎었습니다

촬영본에서 얼굴을 검출해 미검출·다중 얼굴이면 재촬영을 안내하도록 만들었는데, 실기기에서는 정면으로 잘 나온 사진도 계속 미검출로 떨어졌습니다. 에러가 아니라 빈 배열이 와서 조용히 실패했습니다.

앞의 두 번은 제가 잘못된 기준을 골라서 틀렸습니다. 1차로 "iOS는 픽셀을 회전하지 않고 EXIF에만 방향을 적는다"고 보고 EXIF 기준으로 회전시켰지만 실패했고, 2차로 캡처 버퍼가 가로로 고정된 것이라 보고 판별 기준을 프리뷰 방향으로 바꿨는데 변경 자체는 옳았지만 증상은 그대로였습니다.

진짜 원인은 진단 로그가 확정해줬습니다. photo=1080x1468 decoded=1080x1468 exif=6 turns=0 → 얼굴 0개. EXIF는 90° 돌리라고 하는데 Skia가 디코딩한 크기는 이미 세로였습니다. Skia는 EXIF를 적용해서 디코딩하고 ML Kit은 적용하지 않는데, 저는 두 값을 비교해 회전량을 구하고 있었습니다. 회전량은 항상 0이 나왔고, 0이면 원본 파일을 그대로 넘기게 되어 있어서 ML Kit은 끝까지 누운 픽셀을 봤습니다.

고칠 곳은 회전량 계산이 아니라 파일을 넘기는 방식이었습니다. 회전량이 0이어도 Skia로 디코딩한 결과를 새로 인코딩해서 넘기니 방향이 픽셀에 구워지고 EXIF 태그가 사라집니다. 실기기에서 "0회전에서 검출 성공"을 확인했습니다. 여기서 배운 건 진단 로그를 먼저 넣었어야 한다는 것입니다. 로그가 없어서 두 번을 헛짚었고, 넣자마자 한 줄로 끝났습니다.

로그인 버튼이 끝까지 활성화되지 않던 문제

첫 실기기 테스트에서 로그인·회원가입 버튼이 입력을 다 채워도 비활성으로 남았습니다. 웹과 시뮬레이터에서는 재현되지 않았습니다.

원인은 폼 훅이 { ...form, isValid: form.formState.isValid } 형태로 반환하던 것이었습니다. useForm()이 주는 객체는 참조가 고정돼 값이 바뀌어도 참조는 그대로입니다. 그런데 이 프로젝트는 React Compiler를 켜둔 상태였고, 컴파일러는 반환 객체 생성을 form 하나에만 의존하는 메모 블록으로 감쌌습니다. 그래서 마운트 시점 값인 false가 영구히 캐시됐습니다.

추측으로 끝내지 않고 컴파일 결과를 직접 확인했습니다. babel에 supportsReactCompiler: true를 주고 산출물을 비교했더니 의존성 비교가 $[3] !== form에서 $[3] !== form || $[4] !== isValid로 바뀌는 것이 보였습니다. useFormState()로 받으면 원시값이라 의존성에 잡히는 것이 확인된 셈입니다. 테스트는 컴파일러를 거치지 않은 코드를 돌리기 때문에 원리적으로 못 잡는 종류의 버그였습니다.

발표 슬라이드

애니멀리그 중앙해커톤 제출 자료 중 일부입니다. 기획과 디자인은 팀원이 맡았습니다.

핵심 구조 — 시술 이력 · 셀카 분석 · 데일리 체크인을 보존지수 하나로 모아 케어 · 제품 · 재시술 알림으로 연결

홈 화면 — 시술별 보존지수와 오늘의 케어 추천

시술 이력 — 시술별 보존지수와 재시술 D-day

체크인과 케어 추천 — 셀카 · 데일리 체크리스트, 케어 action과 제품 추천