AppOrbit
← blog

육아앱 직접 만든 개발자의 회고 (5) — 성능 최적화와 UX 디테일, 그리고 회고

React Native, 성능 최적화, 위젯, 예방접종, 사이드프로젝트

1. 들어가며: 기능보다 디테일이 앱을 만든다

앱의 기능 목록이 아무리 화려해도, 스크롤할 때 버벅거리거나 버튼을 눌렀을 때 반응이 느리면 사용자는 떠납니다. 특히 육아앱은 하루에도 수십 번 열었다 닫는 앱이기 때문에, 작은 불편함이 쌓이면 빠르게 대체 앱을 찾게 됩니다.

이 마지막 편에서는 기록 목록의 성능 최적화, 홈 화면 위젯, 예방접종 관리, 일과표 공유 등 시리즈에서 다루지 못했던 기능들과 전반적인 회고를 다루겠습니다.

2. 리스트 성능: SectionList 최적화

홈 화면의 기록 목록은 앱에서 가장 많이 보는 화면입니다. 하루에 수유 8~12회, 기저귀 8~10회, 수면 2~4회, 그 외 기록을 합치면 하루 20~30개의 아이템이 쌓입니다. 여기에 시간순으로 "오늘", "어제" 같은 섹션 헤더도 들어갑니다.

고정 높이 + getItemLayout

리스트 아이템의 높이가 고정되어 있다면, React Native의 getItemLayout 속성을 활용할 수 있습니다. 이 속성을 제공하면 SectionList가 아이템의 위치를 미리 계산할 수 있어서, 특정 위치로 즉시 스크롤하거나 빠르게 렌더링할 수 있습니다.

getItemLayout 설정 (수도코드):

ITEM_HEIGHT = 72px  // 모든 RecordItem의 고정 높이

function getItemLayout(data, index):
  RETURN {
    length: ITEM_HEIGHT,
    offset: ITEM_HEIGHT × index,
    index
  }

이 최적화가 없으면 SectionList는 아이템을 렌더링하면서 높이를 동적으로 측정해야 합니다. 목록이 길어질수록 초기 렌더 비용이 커지고, scrollToIndex 같은 동작이 부정확해집니다.

배치 렌더링 튜닝

React Native의 가상화된 리스트는 한 번에 모든 아이템을 렌더링하지 않고, 배치(batch) 단위로 나눠서 렌더링합니다. 이 배치의 크기와 주기를 적절히 조정하면 초기 렌더 속도와 스크롤 부드러움 사이의 균형을 잡을 수 있습니다.

SectionList 성능 속성 (수도코드):

maxToRenderPerBatch = 10
  → 한 번에 최대 10개의 아이템을 렌더링

updateCellsBatchingPeriod = 50ms
  → 배치 간 50ms 간격 (너무 짧으면 프레임 드롭)

windowSize = 10
  → 현재 화면 기준 전후 10배 영역의 아이템을 메모리에 유지

removeClippedSubviews = true
  → 화면 밖의 뷰를 네이티브 레벨에서 분리 (메모리 절약)

initialNumToRender = 10
  → 앱 시작 시 처음 10개만 렌더 (나머지는 스크롤 시 로딩)

이 값들은 기기 성능에 따라 다르게 설정해야 할 수도 있지만, 실제 테스트에서 위 조합이 중저가 안드로이드 기기에서도 안정적인 60fps 스크롤을 보여줬습니다.

LayoutAnimation으로 자연스러운 추가/삭제

기록이 추가되거나 삭제될 때 목록이 "툭" 변하면 어색합니다. LayoutAnimation을 사용하면 아이템이 부드럽게 밀려나면서 공간을 만들거나, 삭제된 아이템 아래의 아이템들이 자연스럽게 올라옵니다.

LayoutAnimation 적용 (수도코드):

이전 기록 개수를 ref로 저장

useEffect(records.length 변경 시):
  IF records.length ≠ 이전 기록 개수:
    LayoutAnimation.easeInEaseOut 적용
    이전 기록 개수 업데이트

주의할 점이 있습니다. Android에서 LayoutAnimation을 사용하려면 앱 시작 시 명시적으로 활성화해야 합니다. 이 설정을 빠뜨리면 iOS에서는 정상 작동하는데 Android에서만 애니메이션이 없는 현상이 생깁니다. 플랫폼 차이를 늦게 발견해서 한참을 디버깅한 경험이 있습니다.

3. 홈 화면 위젯: 앱을 열지 않아도 되는 기록

위젯은 DailyBaby에서 가장 만족스러운 기능 중 하나입니다. 앱을 열지 않고도 홈 화면에서 오늘의 수유량, 수면 시간, 기저귀 횟수를 한눈에 볼 수 있고, 위젯을 탭하면 바로 기록을 시작할 수 있습니다.

위젯 데이터 동기화

위젯에 표시되는 데이터는 앱의 상태가 변경될 때마다 업데이트됩니다.

위젯 동기화 훅 (수도코드):

useWidgetSync():
  감시 대상: 인증상태, 현재아기, 기록목록, 날짜키

  데이터 변경 시:
    오늘의 기록 필터링 (로컬 시간 기준)

    위젯 데이터 = {
      아기이름, 생후일수,
      오늘 총 수유량(ml),
      오늘 수유 횟수,
      오늘 수면 시간(분),
      오늘 기저귀 횟수,
      마지막 기록 유형과 시간
    }

    IF Android → requestWidgetUpdate(위젯 데이터)
    IF iOS → updateIOSWidget(위젯 데이터)

여기서 주의한 점은 시간대 처리입니다. "오늘의 기록"을 필터링할 때 UTC가 아닌 로컬 시간을 기준으로 해야 합니다. 한국(UTC+9) 기준으로 자정 직후에 기록한 수유가 "어제"로 분류되면 안 됩니다.

딥링크를 통한 퀵 기록

위젯에서 버튼을 탭하면 dailybaby://quick-record?type=feeding&action=quick 형태의 딥링크가 실행됩니다. 앱은 이 딥링크를 받아서 별도의 모달 라우트에서 처리합니다.

퀵 레코드 딥링크 처리 (수도코드):

quick-record.tsx (모달 라우트):
  type = URL 파라미터에서 추출
  action = URL 파라미터에서 추출

  IF 미로그인 → 로그인 화면으로 리다이렉트
  IF 아기 미선택 → 아기 선택 안내

  IF action === "quick":
    → 마지막 기록의 기본값을 활용해서 즉시 생성
    → 수유: 이전 수유 방식 + 월령 기반 추천 ml
    → 기저귀: 이전 기록의 패턴 재사용
    → 수면: 수면 유형 자동 판별 (시간대 기반)

이 모달 라우트는 일반 네비게이션 스택 밖에 존재합니다. 위젯에서 바로 진입해야 하기 때문에 드로어나 탭 네비게이션에 속하지 않는 독립적인 화면으로 설계했습니다.

4. 예방접종 관리: 자동 일정 생성

예방접종 관리는 다른 육아앱에서 의외로 지원하지 않는 경우가 많습니다. DailyBaby에서는 아기의 생년월일을 입력하면 질병관리청 기준의 전체 예방접종 일정이 자동으로 생성됩니다.

일정 자동 생성 로직

한국의 국가 예방접종 일정은 생후 개월 수에 따라 정해져 있습니다. BCG는 생후 4주 이내, B형간염은 0, 1, 6개월차, DTaP은 2, 4, 6개월차 등. 이 데이터를 상수로 관리하고, 아기의 생년월일로부터 접종 예정일을 계산합니다.

일정 생성 (수도코드):

VACCINE_SCHEDULE = [
  { 코드: "BCG", 그룹: "BCG", 권장시작월: 0, 권장종료월: 1 },
  { 코드: "HepB-1", 그룹: "B형간염", 권장시작월: 0, 권장종료월: 0 },
  { 코드: "HepB-2", 그룹: "B형간염", 권장시작월: 1, 권장종료월: 1 },
  { 코드: "DTaP-1", 그룹: "DTaP", 권장시작월: 2, 권장종료월: 2 },
  ... (35개 이상)
]

function generateSchedule(아기ID, 생년월일):
  FOR EACH 백신 IN VACCINE_SCHEDULE:
    접종예정일 = 생년월일 + 권장시작개월
    upsert(아기ID, 백신코드, 접종예정일)
    // upsert로 중복 생성 방지

날짜 계산에서 한 가지 주의점이 있습니다. "생후 2개월"은 단순히 60일이 아니라, 생일의 날짜를 유지하면서 월만 더하는 것입니다. 1월 15일생이면 3월 15일이 생후 2개월이지, 3월 16일이 아닙니다.

접종 후 관찰 타이머

예방접종 후에는 보통 30분간 병원에서 이상 반응을 관찰합니다. 이 30분을 정확하게 지키기 위한 관찰 타이머를 구현했습니다.

관찰 타이머 (수도코드):

OBSERVATION_DURATION = 30분

function startObservationTimer(백신코드):
  시작시간 = 현재시간
  스토어에 저장: { 백신코드, 시작시간 }
  → AsyncStorage에 persist (앱 종료 후에도 유지)

매 1초마다:
  남은시간 = OBSERVATION_DURATION - (현재시간 - 시작시간)
  IF 남은시간 <= 0:
    타이머 종료 + 완료 알림
  ELSE:
    UI에 "남은 시간: 25분 30초" 표시

이 타이머가 AsyncStorage에 persist되는 이유는 중요합니다. 병원에서 30분 기다리는 동안 부모가 앱을 닫거나 다른 앱을 사용할 수 있습니다. 앱을 다시 열었을 때 타이머가 초기화되어 있으면 안 됩니다. 시작 시간을 저장해두면 앱을 다시 열었을 때 경과 시간을 정확하게 계산할 수 있습니다.

접종 상태 자동 판별

각 백신의 상태는 아기의 현재 월령과 접종 기록에 따라 자동으로 판별됩니다.

접종 상태 판별 (수도코드):

function getVaccineStatus(백신, 아기월령):
  IF 완료일자가 있으면 → "completed" (완료)
  IF 아기월령 > 권장종료월 → "overdue" (지연)
  IF 권장시작월 <= 아기월령 + 3 → "upcoming" (예정)
  ELSE → "scheduled" (향후)

이 상태에 따라 UI의 색상과 아이콘이 달라집니다. 특히 "overdue" 상태는 눈에 띄는 색상으로 강조해서 놓친 접종을 빠르게 인지할 수 있게 했습니다.

5. 일과표 공유: 데이터를 이미지로

통계 탭에서 하루의 기록을 타임라인 형태로 시각화한 일과표를 볼 수 있습니다. 이 일과표를 이미지로 저장하거나 공유할 수 있는 기능도 넣었습니다.

일과표 공유 흐름 (수도코드):

1. 일과표 뷰를 ViewShot으로 캡처 → PNG 이미지
2. 공유 시에만 헤더/푸터 영역 표시
   → 헤더: 아기 이름 + 날짜
   → 푸터: 앱 이름
3. 네이티브 공유 다이얼로그 실행
   → iOS: UIActivityViewController
   → Android: Intent.ACTION_SEND

ViewShot 캡처 시 주의한 점은 공유 전용 레이아웃입니다. 화면에서 보이는 그대로 캡처하면 헤더나 앱 정보가 빠져서 맥락 없는 이미지가 됩니다. 공유 버튼을 누르면 잠시 헤더/푸터를 추가한 상태로 캡처하고, 완료 후 다시 원래 레이아웃으로 복원합니다.

원형 타임라인 차트

일과표의 핵심은 24시간을 원형으로 표현한 타임라인 차트입니다. 수유, 수면 등의 기록이 시계 모양의 도넛 차트 위에 호(arc)로 표시됩니다.

원형 타임라인 (수도코드):

시간 → 각도 변환:
  angle = (시간 / 24) × 360 - 90
  // -90은 12시 방향이 아닌 0시 방향을 위쪽으로

호(arc) 그리기:
  FOR EACH 기록:
    시작각도 = timeToAngle(기록.시작시간)
    종료각도 = timeToAngle(기록.종료시간)

    IF (종료각도 - 시작각도) < 5도:
      종료각도 = 시작각도 + 5도  // 최소 호 크기 보장

    SVG Path로 도넛 호 그리기
    호 바깥에 기록 유형 아이콘 배치

짧은 기록(5분짜리 기저귀 교체 등)이 너무 작아서 보이지 않는 문제는 최소 5도(약 20분)의 호 크기를 보장해서 해결했습니다. 정확한 시간 비례는 아니지만, 기록의 존재 자체를 시각적으로 확인할 수 있는 것이 더 중요하다고 판단했습니다.

6. 캘린더 뷰의 멀티 도트

캘린더 탭에서는 각 날짜에 최대 3개의 컬러 도트가 표시됩니다. 이 도트는 해당 날짜에 어떤 유형의 기록이 있는지를 빠르게 보여줍니다.

멀티 도트 마킹 (수도코드):

FOR EACH 날짜:
  해당 날짜의 기록 유형들을 Set으로 수집
  우선순위 순으로 최대 3개의 도트 생성:
    1순위: 수유 (파랑)
    2순위: 수면 (청록)
    3순위: 기저귀 (보라)
    4순위: 성장, 투약, 활동, 건강...

  IF 유형이 3개 초과:
    → 3개까지만 표시 (나머지는 생략)

우선순위를 둔 이유는, 수유/수면/기저귀가 매일 가장 빈번하게 기록되는 유형이기 때문입니다. 성장 기록처럼 가끔 있는 유형이 수유 도트를 밀어내면 정보 밀도가 떨어집니다.

7. 통계 차트: 스택 바 차트의 구현

통계 탭에서 가장 구현이 까다로웠던 것은 수면 스택 바 차트입니다. 하나의 막대 안에 낮잠과 밤잠이 쌓여서 표시되어야 합니다.

스택 바 차트 (수도코드):

FOR EACH 요일:
  napPercent = (낮잠시간 / 최대값) × 100
  nightPercent = (밤잠시간 / 최대값) × 100

  막대 그리기:
    하단: 낮잠 영역 (밝은 색, 높이 = napPercent%)
    상단: 밤잠 영역 (어두운 색, 높이 = nightPercent%)

  라운드 코너 처리:
    IF 밤잠 있음 → 상단 코너만 둥글게
    IF 낮잠만 있음 → 상하단 모두 둥글게
    IF 둘 다 있음 → 밤잠 상단만 둥글게

라운드 코너 처리가 의외로 복잡했습니다. 낮잠만 있는 날, 밤잠만 있는 날, 둘 다 있는 날의 코너 처리가 각각 다릅니다. 단순히 모든 코너를 둥글게 만들면, 낮잠과 밤잠이 만나는 경계에 갭이 생기는 시각적 아티팩트가 발생합니다.

8. 앱 전체 회고

1인 개발의 현실

DailyBaby를 만들면서 가장 많이 느낀 것은 **"기능 개발보다 엣지 케이스 처리에 시간이 더 많이 든다"**는 점입니다.

수유 기록을 시작하고 종료하는 기본 기능은 하루면 만들 수 있습니다. 하지만 더블 탭 방지, 앱이 백그라운드에 있을 때의 타이머 보정, Android와 iOS의 날짜 피커 차이, 네트워크가 없을 때의 처리, 자정을 넘기는 시간 범위 비교 등 엣지 케이스를 하나씩 잡아가는 데 몇 배의 시간이 들었습니다.

직접 사용하는 앱의 장점

반면, 자신이 직접 사용하는 앱을 만드는 것의 장점은 확실합니다. 매일 새벽에 직접 쓰면서 "이게 불편하다"는 것을 바로 느끼고, 다음 날 바로 고칠 수 있습니다. 별도의 사용자 리서치나 A/B 테스트 없이도, 자연스럽게 UX가 개선됩니다.

야간모드의 색상 온도, 스낵바의 자동 닫힘 시간(5초), 풀 싱크 쿨다운(30초), 경과 시간 갱신 주기(60초) 같은 수치들은 모두 직접 사용하면서 조정한 값입니다. 문서나 가이드라인으로는 알 수 없는, 실사용에서 나오는 감각적인 수치입니다.

기술 스택 선택의 회고

  • Expo: OTA 업데이트가 정말 편했습니다. 스토어 심사 없이 핫픽스를 배포할 수 있는 것은 1인 개발에서 치명적인 장점입니다
  • Zustand: 선택적 persist와 스토어 간 직접 참조가 프로젝트 규모에 딱 맞았습니다. Redux였다면 보일러플레이트에 시간을 더 쓰고 기능에 시간을 덜 썼을 것입니다
  • Supabase: RLS의 학습 곡선이 있지만, 한번 설정하면 별도 서버 없이 보안이 보장됩니다. 1인 개발의 서버 관리 부담을 크게 줄여줬습니다

아쉬운 점도 있습니다. TypeScript의 타입 안전성을 JSONB 기반 details 필드에서 완벽하게 보장하지 못한 것, 테스트 코드를 충분히 작성하지 못한 것, 초기부터 국제화(i18n)를 고려하지 않아 한국어가 하드코딩된 것 등이 있습니다.

앞으로의 계획

  • AI 기반 패턴 분석: 수유/수면 데이터를 기반으로 아기의 패턴을 예측하는 기능
  • 다국어 지원: 현재 한국어 전용이지만, 일본어와 영어 지원 계획
  • Apple Watch 연동: 워치에서 바로 기록을 시작/종료하는 기능
  • 데이터 내보내기: CSV/PDF 형태로 기록을 내보내서 소아과 상담 시 활용

9. 시리즈를 마치며

5편에 걸쳐 DailyBaby의 개발 과정을 정리했습니다. 기획부터 기술 스택 선정, 실시간 기록 시스템, 야간모드, 가족 공유, 성능 최적화까지. 하나의 육아앱을 만드는 데 이렇게 많은 기술적 결정과 디테일이 필요하다는 것을 글로 정리하면서 저 자신도 다시 한번 느꼈습니다.

사이드 프로젝트의 가장 큰 가치는 "완성"에 있다고 생각합니다. 아이디어만으로는 배울 수 없는 것들이 있고, 실제로 사용자가 쓰는 앱을 스토어에 올려본 경험은 어떤 강의나 책으로도 대체할 수 없습니다.

이 시리즈가 사이드 프로젝트를 고민하고 있는 개발자에게 조금이라도 도움이 되었으면 합니다. 특히 "내가 필요한 앱을 직접 만들어보자"는 생각이 들었다면, 이 글의 목적은 달성된 것입니다.

👉 데일리베이비 - App Store에서 다운로드