- 프로젝트 기간: 2025.03
- 인원: 개인 프로젝트
- 기술스택: React Native · Expo · TypeScript · Expo Router · React Query · Zustand · MMKV · Axios · Zod
iOS로 구현했던 Dogether의 주요 앱 플로우를 React Native로 재구현하며 크로스플랫폼 화면 구성, 파일 기반 라우팅, 서버 상태 캐싱, 전역 상태 관리, 데이터 계층 분리를 학습한 프로젝트
flowchart LR
Route["Expo Router<br/>app route"] --> Screen["Screen"]
Screen --> Hook["Custom Hook<br/>ViewModel 역할"]
Hook --> Query["React Query<br/>Server State"]
Hook --> Store["Zustand<br/>Client State"]
Query --> UseCase["UseCase"]
UseCase --> Repository["Repository Interface"]
Repository --> Data["API / Mock Repository"]
Data --> Core["Axios Client / MMKV Storage / AppError"]
-
Screen · Custom Hook · UseCase · Repository 계층 분리로 화면 책임 정리
- Screen은 UI 조립과 사용자 이벤트 연결에 집중
- Custom Hook은 화면에 필요한 파생 상태와 핸들러를 조합
- UseCase는 화면에서 호출하는 기능 흐름을 담당
- Repository는 실제 API 또는 Mock 데이터 접근을 담당
-
React Query와 Zustand로 서버 상태 / 클라이언트 상태 분리
- 그룹 목록, 투두, 랭킹, 통계 등 원격 데이터는 React Query에서 관리
- 로딩, 에러, 캐싱, 리패치 흐름은 Query Hook 단위로 분리
- 선택 그룹, 날짜 오프셋, 필터, 토스트 등 UI 상태는 Zustand에서 관리
- 로그인 세션처럼 앱 재실행 후에도 필요한 값은 MMKV와 함께 관리
-
Expo Router 기반 파일 라우팅 흐름 정리
app/폴더의 파일 구조를 화면 경로로 사용_layout.tsx에서 Stack 화면과 전역 Provider를 구성- 앱 시작, 그룹 생성/참여, 메인, 인증, 설정 플로우를 route 단위로 분리
- 화면 이동은
router.push/router.replace기반으로 정리
-
Repository Interface와 API/Mock 구현체 분리
contracts/에서 Repository 계약을 TypeScript interface로 정의impl/에는 실제 API 호출 구현체를 배치mock/에는 서버 없이 동작하는 Mock 구현체와 더미 데이터를 배치- 환경변수에 따라 Repository factory에서 API 구현체와 Mock 구현체를 선택
-
공통 Axios Client · MMKV Storage · AppError로 외부 의존성 표준화
- Axios Client에서 baseURL, timeout, 인증 헤더 주입을 공통 처리
- MMKV Storage 계층에서 세션과 선택 그룹 같은 로컬 값을 관리
- 서버 에러 응답은 AppError 모델로 변환
- Screen은 네트워크와 저장소 구현 세부사항에 직접 의존하지 않도록 구성
- 기존 iOS 구조
- ViewController가 ViewModel 상태를 구독
- 상태 변경 시 UIKit view 속성과 이벤트를 직접 연결
- React Native에서의 접근
- Screen은 JSX로 현재 상태에 맞는 화면을 선언
- 화면에 필요한 파생 상태는 Custom Hook에서 계산
- 사용자 이벤트 핸들러도 Custom Hook에서 조합해 Screen에 전달
- 전환하며 느낀 차이
- RN의 화면 컴포넌트는 상태 변경마다 다시 실행되는 함수에 가까웠음
- ViewModel 역할을 클래스 인스턴스가 아니라 Custom Hook의 반환값으로 표현
- 화면 상태는 직접 소유하기보다 계산해서 전달하는 방식으로 바라보는 것이 중요했음
- 기존 iOS 구조
- ViewModel이 Relay/Observable로 상태를 전달
- ViewController가 상태 스트림을 구독해 화면을 갱신
- React Native에서의 접근
- 서버에서 받아오는 데이터는 React Query Hook으로 분리
- 앱이 직접 소유하는 UI 상태와 세션은 Zustand에서 관리
- 화면 내부에서만 필요한 값은
useState로 유지
- 전환하며 느낀 차이
- 하나의 ViewModel에 모든 상태를 모으는 방식보다 상태 성격별 분리가 자연스러웠음
- 서버 상태는 캐싱, 만료, 리패치 정책까지 함께 고려해야 했음
- 화면은 데이터를 직접 관리하기보다 query result를 구독하고 표현하는 역할에 가까워졌음
- 기존 iOS 구조
- Coordinator가 ViewController 생성과 의존성 주입을 담당
- push/pop, root 교체, modal 전환 정책을 중앙에서 관리
- React Native에서의 접근
app/main.tsx,app/group-create.tsx처럼 파일을 route로 사용router.push/router.replace로 경로 기반 화면 전환을 수행_layout.tsx에서 공통 Stack 설정과 전역 Provider를 구성
- 전환하며 느낀 차이
- 화면 인스턴스를 직접 생성하기보다 URL path를 기준으로 플로우를 설계
- 파일 구조만으로 화면 목록과 route 흐름이 드러나는 점이 직관적이었음
- 전환 책임이 분산될 수 있어 route 이름과 파일 배치를 일관되게 관리하는 것이 중요했음
- 기존 iOS 구조
- ViewModel이 UseCase를 호출해 데이터를 요청
- 로딩, 성공, 실패 상태를 Relay로 직접 관리
- React Native에서의 접근
useGroupsQuery,useMyTodosQuery,useRankingQuery처럼 조회 단위별 Query Hook을 구성- UseCase 호출은 Query Hook 내부로 모아 Screen에서 분리
- 캐싱, 로딩, 에러, 리패치 상태는 React Query에 위임
- 전환하며 느낀 차이
- 서버 상태를 전역 store에 저장하면 캐시 만료와 재요청 정책까지 직접 관리해야 했음
- React Query를 사용하니 화면은 query result를 구독하고 표현하는 역할에 집중할 수 있었음
- 데이터 요청 로직이 Hook 단위로 모이면서 화면별 로딩/에러 처리 흐름을 더 명확하게 볼 수 있었음
- 기존 iOS 구조
- DIManager가 Protocol 타입 의존성을 조립
- 실제 구현체 또는 Mock 구현체를 화면 흐름에 맞게 주입
- React Native에서의 접근
- Repository contract를 TypeScript interface로 정의
- 실제 API 구현체와 Mock 구현체가 같은 contract를 따르도록 구성
env.useMockGroups같은 런타임 환경값에 따라 factory에서 구현체를 선택
- 전환하며 느낀 차이
- Swift의 Protocol 기반 의존성 분리 기준은 RN에서도 유효했음
- TypeScript interface와 factory 함수만으로도 구현체 교체 구조를 만들 수 있었음
- 화면과 UseCase가 API/Mock 여부를 알지 않아도 되는 점이 유지보수에 도움이 됐음
- 기존 iOS 구조
- Keychain/UserDefaults에서 인증 상태를 읽음
- 앱 시작 플로우에서 로그인 여부와 초기 화면 분기를 결정
- React Native에서의 접근
- MMKV에 로그인 세션과 마지막 선택 그룹을 저장
- 앱 시작 시 MMKV 값을 Zustand store로 hydrate
- hydrate 이후 Splash, Onboarding, Start, Main 플로우를 분기
- 전환하며 느낀 차이
- 저장소에 값이 있는지와 메모리 store가 준비됐는지를 구분해야 했음
hydrated상태를 따로 두어 앱 시작 시점의 화면 분기를 안정적으로 관리- 세션성 데이터와 화면 UI 상태를 같은 store에 섞지 않는 것이 중요했음
- 기존 iOS 구조
- 카카오/애플 로그인 흐름을 iOS 네이티브 API와 직접 연결
- 이미지 선택, 인증 업로드 같은 기능도 iOS 환경에 맞춰 처리
- React Native에서의 접근
- Expo와 RN 라이브러리로 로그인, 이미지 피커, 파일 업로드, 권한 요청을 연결
- 공통 화면 플로우 안에서 iOS와 Android 동작을 함께 처리
- 네이티브 모듈 설정은 app config와 플랫폼별 설정 파일에서 관리
- 전환하며 느낀 차이
- RN이 플랫폼 차이를 줄여주지만 완전히 없애주지는 않았음
- 로그인 복귀, 이미지 권한, 파일 경로처럼 플랫폼 경계와 맞닿는 부분에서 차이가 드러났음
- 안정적으로 디버깅하려면 JS 코드뿐 아니라 iOS와 Android 네이티브 동작도 함께 이해해야 했음
- Dogether RN 기술 정리
- Dogether RN 프로젝트 읽는 법
- iOS 개발자를 위한 RN 개념
- TypeScript 언어 읽는 법
- JSX 컴포넌트 읽는 법
- React Hook 읽는 법
- Expo Router 화면이동 읽는 법
- React Native Layout 읽는 법
- Zustand 상태관리 읽는 법
- React Query 서버상태 읽는 법
- Repository / UseCase 구조 읽는 법
- RN 앱 생명주기
- RN 화면 생명주기
- 네트워크 API 흐름 읽는 법
- 로컬저장소 MMKV 읽는 법
- Mock API 전환 읽는 법
- 인증 로그인 흐름 읽는 법
- 메인화면 데이터흐름 읽는 법
- 투두작성 인증 흐름 읽는 법
- 리뷰 랭킹 통계 흐름 읽는 법
- RN 렌더링과 OTA 업데이트 읽는 법
- Expo와 RN CLI 차이 읽는 법
- RN 실행방법 읽는 법