사진을 전송하지 않는 얼굴 평가 도구를 만들면서 배운 온디바이스 ML에 대한 것
요약
개인 정보 보호를 최우선으로 하는 얼굴 평가 도구 'FaceRuler'를 개발한 경험을 공유합니다. 이 도구는 서버에 이미지를 업로드하지 않고, 브라우저 캔버스 내에서 MediaPipe와 같은 온디바이스 ML 기술만을 사용하여 얼굴 측정을 수행합니다. 점수 계산은 LLM 없이 순수한 수학 함수로 이루어져 신뢰성과 투명성을 확보했습니다.
핵심 포인트
- 온디바이스 ML을 활용하여 개인 정보 유출 위험을 원천 차단함.
- MediaPipe와 WASM을 사용해 서버 추론 없이 로컬에서 얼굴 측정이 가능함.
- 점수 계산은 LLM 대신 수학적 함수로 구현되어 투명성과 재현성을 높임.
- 좌표 스케일링 등 ML 결과 처리 시 놓치기 쉬운 기술적 함정을 공유함.
이것을 만든 이유
한 친구가 자신의 얼굴형이 내것보다 낫다고 말했습니다.
저는 그에게 반박할 생각이 없습니다. 데이터가 없으니까요. 그래서 저는 '어떤 얼굴형인가' 같은 도구를 찾아봤습니다. 모든 도구가 제게 제 얼굴 사진을 업로드하도록 요구했습니다. 서버로요. 제가 모르는 누군가가 운영하는 곳에 말입니다.
보세요, 저는 개발자입니다. 업로드된 이미지에 무슨 일이 일어나는지 압니다. 그것들을 저장하는 코드를 작성해봤습니다. 저는 제 자신의 얼굴 사진이 Date.now() 파일명과 제가 볼 수 없는 ACL(Access Control List)을 가진 누군가의 버킷에 놓여 있는 것을 원하지 않았습니다.
그래서 제가 원하는 버전, 즉 얼굴을 측정하지만 절대 업로드하지 않는 버전을 만들었습니다. '24시간 후에 삭제합니다' 같은 방식이 아닙니다. 사진은 말 그대로 브라우저 탭을 벗어나지 않습니다. 캔버스에 들어가고, MediaPipe가 로컬에서 읽어들이며, 그게 끝입니다.
이것은 FaceRuler라고 불립니다. 무료입니다. 가입 절차가 없습니다. 이 부분은 성장(growth) 결정이라기보다는, 계정이 필요 없는 도구의 인증(auth) 기능을 구축하기에 제가 너무 게을렀기 때문입니다.
스택(The Stack)
- Next.js 16 (App Router)
- MediaPipe FaceLandmarker — 478개의 랜드마크(landmarks), WASM으로 컴파일; 모델은 약 3.6 MB이며, 모델과 WASM 런타임 모두 제 자체 오리진에서 제공됩니다.
- 실제로 필요한 몇 개의 테이블을 위해 OpenNext를 통한 Cloudflare Workers 및 D1
- 유료 심층 보고서(free report는 전체 측정값입니다)를 위한 호스팅된 결제 처리
중요한 부분은 이겁니다: 서버 측 추론(server-side inference)이 없습니다. 거부할 것도, 저를 믿을 필요도 없습니다. 네트워크 탭은 조용합니다.
점수 계산은 순전히 수학입니다 (그것이 요점)
사람들을 가장 놀라게 한 결정은 이것입니다: 점수 계산 과정에 LLM(Large Language Model)이 전혀 사용되지 않습니다.
저는 모델에게
여덟 가지 세부 지표(대칭성, 얼굴 3분할, FWHR, 안와 경사, 턱 비율, 눈 간격, 눈 종횡비, 황금 비율)가 다섯 가지 가중치 차원으로 통합됩니다. 이 가중 평균값은 로지스틱 곡선에 맞춰 PSL 0–8 스케일에 매핑되며, 평균적인 얼굴이 정확히 4.0에 위치하도록 고정되어 있습니다:
// overall 50 -> exactly 4.0 on the 0-8 scale
export const overallToPsl = (overall: number): number =>
8 / (1 + Math.exp(-(overall - 50) / 13));
평균값은 5가 아닌 4.0입니다. PSL 스케일은 일반적인 1–10점 평가보다 의도적으로 더 엄격하며, 백분위는 정규 분포(평균 4.0, 표준편차 SD 0.9)를 사용합니다.
모두 순수 함수(pure functions)이기 때문에 전체 엔진이 단위 테스트(unit-tested)를 거쳤고(12/12 녹색), '재보정(recalibrate)'은 재학습(retrain) 대신 한 줄의 코드 변경으로 이루어집니다. 모든 경우에 동일한 랜드마크가 입력되면 동일한 보고서가 나옵니다.
보기보다 어려웠던 두 가지 점
MediaPipe 좌표는 당신의 얼굴을 조용히 축소시킵니다. 랜드마크들은 정규화되어 돌아옵니다. 즉, x축은 이미지 너비로, y축은 이미지 높이로 나눈 값입니다. 단순히 높이/너비 비율을 계산하면 3:4 세로 사진이 정사각형 캔버스에 있는 같은 얼굴보다 약 25% '짧게' 보이게 되어 점수를 뒤집기에 충분합니다. 당신은 y 성분을 다시 픽셀 등가 공간으로 스케일링해야 합니다. 아무도 이 부분에 대해 경고하지 않습니다.
기울어진 사진이 점수에 영향을 주어서는 안 됩니다. 따라서 어떤 계산을 수행하기 전에, 이마 상단에서 턱까지의 중심선 각도를 측정하고 모든 랜드마크를 기울어짐 보정된 프레임(roll-corrected frame)으로 회전시킵니다. 수직 거리들—예를 들어 FWHR의 분자—은 이것 없이는 무너집니다.
버그는 아니지만 문서화할 가치가 있는 또 다른 점: 학술적인 FWHR 비율(1.85–1.9)은 실제 사진을 기준으로 측정됩니다. 정규화된 랜드마크 공간에서 나지온-상순 거리는 양측광대 너비의 약 36%를 차지하므로, 이 수치들은 전혀 비교할 수 없습니다. 저는 논문의 값을 적용하는 척하기보다 랜드마크 배치(landmark batches)로부터 저만의 중심값(2.75)을 보정했습니다.
하루 전체를 망친 프론트엔드 버그
React 19의 합성(synthetic) onChange는 프로그램적으로 전송된 변경 이벤트에 대해서는 발생하지 않습니다.
제 업로드 컴포넌트는 실제 사용자용으로 작동해야 할 뿐만 아니라, DataTransfer를 통해 File을 주입하는 저 자신의 자동화 테스트를 위해서도 작동해야 합니다. 그런데 합성 핸들러가 주입 경로에서는 침묵했습니다. 실제 사용자는 괜찮았겠지만 — 바로 그것이 위험한 이유입니다. 테스트가 저에게 거짓말을 할 수 있었기 때문입니다.
수정: 합성 핸들러를 제거하고, ref를 통해 네이티브(native) addEventListener를 연결했습니다. 알고 보니 이것이 실제 사용자에게도 더 견고하다는 것을 알게 되었습니다.
그리고 제가 이제 모든 곳에서 지키는 규칙: 조건부 렌더링 안에 ref-의존적인 DOM 노드를 절대 넣지 마세요. 제 결과물 <canvas>가 report && (...) 안에 있었습니다. 첫 보고서가 나오기 전에는 canvasRef.current가 null이었고, 그리는 함수(draw function)의 가드 문이 조기에 반환되어... 아무것도 없습니다. 오류도 없고, 출력도 없습니다. 저는 이 정확한 버그를 규칙으로 기록하기 전에 서로 다른 두 개의 컴포넌트에서 두 번이나 겪었습니다.
다음 계획
- 서버 측 라이선스 검증(Server-side license verification). 현재 권한은
localStorage에 저장되어 있어, 캐시를 지우면 구매 내역이 사라집니다. 저는 이것을 숨기는 대신 문서에서 소리 내어 말할 것입니다. - 동일한 크레딧 시스템을 공유하는 두 번째 도구.
- 익명 집계 데이터(anonymous aggregate data)를 통한 분기별 재보정(Quarterly recalibration).
사용해 보세요
가입 없이 라이브이며 무료입니다: https://faceruler.com
사진을 하나 넣으면 얼굴 모양, PSL 점수, 백분위수, 그리고 여덟 가지 측정값 모두를 얻게 됩니다 — 단순히 판정만 내리는 것이 아니라 각 숫자의 뒤에 있는 공식까지 함께요. 당신의 사진은 당신의 기기에 남아 있습니다; 믿지 못하신다면 네트워크 탭을 확인해 보세요.
진심으로 궁금합니다: 만약 '우리는 이것을 업로드하지 않는다'가 전체 판매 포인트인 제품을 출시한 경험이 있다면, 어떻게 사람들이 당신을 믿게 만들었나요? 저는 결국 계산 과정을 보여주는 방식으로 끝냈지만, 그것이 최선의 답변이라고 확신하지는 않습니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기