하루냥

하루냥은 "하루를 위로해주는 모바일 속 작은 내 친구" 콘셉트의 AI 기반 감정 일기 앱입니다. 사용자가 그날의 날씨/감정/일기 작성하면, 하루냥은 기록 내용에 맞춰 공감과 위로의 편지를 전해 감정 기록을 지속하게 만드는 경험을 제공합니다.

프로젝트 기간
기여도프론트42%
사용 기술
TypeScript
TypeScript
React
React
Sass
Sass
Zustand
React Query
React Query
상세 내용

프로젝트 개요

하루냥은 AI 기반 감정 일기 앱으로, 사용자가 감정과 일기를 작성하면 AI가 공감 편지를 제공하는 서비스입니다. Frontend Developer로 참여해 Flutter 앱의 주요 화면을 WebView 기반 React 서비스로 전환하고, 현재는 앱인토스 배포를 위해 React + Vite + React Query로 서비스를 개발하고 있습니다.

온보딩부터 일기 작성, AI 편지 응답까지 주요 사용자 플로우를 설계하고 구현했습니다.

하루냥 앱인토스 구현 화면
앱인토스 하루냥 구현 테스트 화면 입니다.

주요 역할 및 기여

핵심 사용자 플로우 구현

앱의 온보딩부터 일기 작성, AI 편지 응답까지 전체 플로우를 구현했습니다.

  • 온보딩 플로우: 닉네임 → 생일 → 직업 → 완료의 4단계 회원가입 프로세스를 useFunnel로 구현, 진행률 표시 및 단계별 데이터 보존 처리
  • 일기 작성 플로우: 날씨 선택 → 감정 선택 → 일기 작성의 3단계 입력 과정을 BottomSheet + IconTabBar 조합으로 구현
  • 3초 간격 자동 저장: setInterval로 주기적 저장 + AppStorage 활용해 작성 중 데이터 유실 방지
  • AI 편지 응답 화면: 감정별 캐릭터·배경색 동적 생성, 편지 이미지 저장 및 북마크 기능 구현

하루냥 온보딩 페이지 예시

디자인 시스템 기반 UI 컴포넌트 구축

앱 전반의 UI 일관성과 개발 효율을 위해 디자인 시스템을 구축하고 핵심 컴포넌트를 제작했습니다.

  • Button: variant/size/fluid 등 확장 가능한 props로 재사용성 확보
  • BottomSheet: ResizeObserver 기반 동적 높이 계산 + 애니메이션 처리로 네이티브에 가까운 UX 구현
  • WheelSelector: PointerEvent 기반 드래그 + 스냅 정렬로 터치/마우스 멀티 디바이스 지원
  • ImageEditor: 크롭 + Canvas 기반 압축으로 원본 비율 유지 및 업로드 최적화
  • Portal 레이어 시스템: 모달/바텀시트 DOM 분리로 z-index 이슈 해소

useFunnel 기반 다단계 폼 시스템 구현

온보딩/일기 작성 등 다단계 프로세스를 관리하기 위해 커스텀 훅을 설계했습니다.

  • URL 쿼리 기반 단계 관리로 뒤로/앞으로가기 자연스럽게 지원
  • LocalStorage 상태 유지로 새로고침/재진입 시 입력 데이터 복원
  • TypeScript 제네릭으로 단계/상태 타입을 추론해 타입 안정성 확보
  • 선언적 API로 단계 UI를 명확하게 분리

WebView 환경 최적화

React 서비스가 React Native WebView에서 동작하는 환경에 맞춰 UX를 보완했습니다.

  • BottomSheet 활성화 시 document.body 스크롤 잠금
  • CSS transition + onTransitionEnd로 부드러운 전환 처리
  • 다양한 디바이스 대응을 위한 fluid 레이아웃 적용

기술적 도전 및 성과

1. 다단계 폼에서 데이터 유실 문제 해결 (useFunnel)

문제 상황

온보딩, 일기 작성 등 다단계 폼에서 사용자가 뒤로가기를 누르거나 새로고침 시 입력 데이터가 사라지는 문제가 있었습니다. 특히 일기 작성 중 실수로 브라우저를 닫으면 모든 내용이 날아가 사용자 경험이 크게 저하되었습니다.

개선 방안

URL 쿼리 파라미터와 LocalStorage를 조합하여 단계별 상태를 관리했습니다.

  • URL 쿼리 파라미터로 현재 단계(step)를 관리하여 브라우저 히스토리 API 활용
  • LocalStorage에 폼 입력 상태를 저장하여 앱 종료 후 재진입 시에도 데이터 복원
  • TypeScript 제네릭으로 stepsstate 타입을 추론하여 타입 안전성 확보
const [Funnel, state, setFunnel] = useFunnel(['닉네임', '생년월일'], {
  defaultStep: '닉네임',
  initialState: { nickname: '', birthdate: '' },
  storageOptions: { key: 'ONBOARDING' }
});
 
<Funnel>
  <Funnel.Step name="닉네임">
    <NicknameForm onNext={(nickname) => setFunnel('생년월일', { nickname })} />
  </Funnel.Step>
  <Funnel.Step name="생년월일">
    <BirthdateForm nickname={state.nickname} />
  </Funnel.Step>
</Funnel>

성과

  • 브라우저 히스토리 API를 활용한 자연스러운 네비게이션 지원
  • 앱 종료 후 재진입 시에도 이전 작성 내용 복원 가능

2. BottomSheet 동적 높이 이슈 해결

컨텐츠 변화/키보드 노출 시 높이가 고정되어 레이아웃이 깨지는 문제를 ResizeObserver로 높이 변화를 실시간 감지해 해결했습니다. 고정 높이 없이도 안정적인 UI 동작을 구현하고, 키보드 상황에서도 자연스럽게 대응했습니다.

3. WheelSelector를 PointerEvent로 직접 구현

네이티브에 가까운 휠 UX가 필요했지만 <select>는 제약이 컸습니다. PointerEvent로 터치/마우스를 통합하고, 드래그 종료 시 스냅 정렬, clamp로 범위를 제한해 외부 의존 없이 경량 구현했습니다.

하루냥 휠 셀렉터 구현 gif

4. 고해상도 이미지 업로드 최적화 (ImageEditor + Firebase Storage)

문제 상황

고용량 이미지를 그대로 업로드하면 업로드 시간이 길고 서버 부담이 증가하는 문제가 있었습니다. 특히 모바일 환경에서 원본 이미지를 업로드하면 네트워크 비용과 시간이 크게 증가했습니다.

개선 방안

크롭 영역을 원본 기준으로 계산하고, Canvas로 픽셀 수 기반 품질을 자동 조정해 5MB 이하로 압축한 뒤 Firebase Storage에 업로드하는 파이프라인을 구현했습니다.

// 원본 이미지 크기로 스케일 변환
const scaleX = naturalWidth / image.width;
const scaleY = naturalHeight / image.height;
 
const sourceX = crop.x * scaleX;
const sourceY = crop.y * scaleY;
const sourceWidth = crop.width * scaleX;
const sourceHeight = crop.height * scaleY;
 
// 픽셀 수 기반 품질 자동 조정 (5MB 이하)
const MAX_SIZE = 5 * 1024 * 1024;
const pixelCount = canvas.width * canvas.height;
const estimatedSize = pixelCount * 3; // RGB 기준
 
let quality = Math.min(1, (MAX_SIZE * 0.8) / estimatedSize);
quality = Math.max(0.1, quality); // 최소 품질 보장
 
// Firebase Storage 업로드
async function uploadImage(image: Blob, userId: number) {
  const storageRef = ref(storage, `업로드 경로`);
  const url = await uploadBytes(storageRef, image, {
    contentType: "image/jpeg",
  }).then(async (snapshot) => await getDownloadURL(snapshot.ref));
 
  return url;
}

성과

  • 업로드 시간 단축 및 네트워크 비용 절감
  • 원본 비율 유지하면서도 파일 크기 최적화
  • Firebase Storage 활용으로 안정적인 이미지 호스팅 및 CDN 자동 적용

5. Snackbar 시스템 구축 (Framer Motion + Zustand)

토스트 UX(애니메이션/위치/액션/Safe Area)를 Zustand로 큐/중복을 관리하고, Framer Motion spring으로 등장/퇴장을 구현했습니다. Safe Area를 반영해 위치를 보정하여 어디서든 호출 가능한 전역 토스트 시스템을 완성했습니다.

하루냥 앱인토스 토스트 구현2
앱인토스 하루냥 토스트 구현화면 입니다.

6. 성능 및 안정성 개선

  • html2canvas → snapdom 전환으로 편지 저장 렌더링 성능 개선
  • React Query queryKey 중앙화 + staleTime 적용으로 캐시/리페칭 일관성 확보
  • 버그 수정: transitionEnd 버블링으로 인해 히스토리 2번 쌓이게 되어 뒤로가기 버튼 오류 수정, 긴 이미지 업로드 시 레이아웃 깨지는 오류 등
  • --safe-area-bottom CSS 변수 도입으로 하단 컴포넌트 전역 Safe Area 처리

7. Firebase Analytics 통합

주요 사용자 행동(화면 진입/일기 등록/북마크/편지 저장 등)을 추적하기 위해 로깅 유틸 함수와 커스텀 훅을 구성했습니다.

기술 스택

  • Frontend: React, TypeScript, Vite, React Router, Sass
  • State Management: Zustand, React Query
  • Animation: Framer Motion
  • Analytics: Firebase Analytics
  • Libraries: react-image-crop, @zumer/snapdom 등