Skip to content
way edited this page Jul 24, 2026 · 1 revision

DoDo 웹 → React Native 포팅 스펙

출처: dodo-frontend (Vite + React 19 + Tailwind v4 + React Query + react-router v7, FSD 구조) 대상: dodo-app (Expo SDK 54 + expo-router + RN 0.81.5) 이 문서 하나로 원본 레포 없이 RN 작업이 가능하도록 작성됨. 최종 조사: 2026-07-14


-1. 현재 dodo-app 상태 (2026-07-14 확인)

커밋 1개(119a8ec Expo 기반 프로젝트 생성). 스캐폴딩은 이미 상당 부분 완료됨.

이미 갖춰진 것 ✅

  • Expo SDK 54 / RN 0.81.5 / React 19.1 / New Architecture 켜짐 (newArchEnabled: true)
  • expo-router v6 (app/ 디렉토리, typedRoutes + reactCompiler 실험 기능 켜짐)
  • @react-navigation/bottom-tabs 설치 완료 → §3 하단 탭바 바로 구현 가능
  • @tanstack/react-query v5, axios, zustand 설치 완료
  • 딥링크 스킴 dodoapp 이미 설정됨 (app.json) → OAuth 리다이렉트에 사용 (§5)
  • expo-web-browser, expo-linking 설치 완료 → OAuth 흐름에 필요
  • expo-font, expo-image, expo-splash-screen, expo-haptics
  • react-native-reanimated v4 → 웹 CSS 애니메이션 대체용 (§1)
  • 경로 별칭 @/*./* (tsconfig). 웹의 @/ = src/ 와 매핑이 다름에 주의
  • husky + lint-staged + prettier + eslint (웹과 동일한 워크플로)
  • 이미 이식된 파일: shared/api/axios.ts, shared/config/{env,apiConfig,index}.ts, shared/lib/react-query/queryClient.ts

아직 없는 것 / 해야 할 것 ❗

  • app/Expo 기본 템플릿 그대로(tabs)/index.tsx, (tabs)/explore.tsx, modal.tsx, hello-wave.tsx, parallax-scroll-view.tsx 등은 전부 삭제 대상
  • constants/theme.ts 는 Expo 기본 색상 → §1 DoDo 토큰으로 교체 필요
  • app.jsonios.bundleIdentifier / android.package 없음 → §6 네이버 지도 콘솔 등록의 전제. 이걸 먼저 정해야 함 (예: com.dodo.app)
  • env.tsEXPO_PUBLIC_API_BASE_URL 하나만 검증 → §10의 나머지 변수 추가 필요
  • axios.ts 인터셉터가 껍데기 상태 (request/response 둘 다 그냥 통과) → 토큰 주입 + 401 갱신 로직 필요 (§5)
  • 미설치: @mj-studio/react-native-naver-map(§6), @stomp/stompjs(§8), expo-secure-store(§5), expo-auth-session(§5), expo-crypto(§5), expo-image-picker(§7), react-native-svg + transformer(§7)

0. 먼저 알아야 할 것: "모바일 레이아웃"은 없다

원본은 모바일 우선으로 설계된 앱이 아니다. 데스크톱 웹 레이아웃이다.

  • 본문 컨테이너: max-w-5xl (1024px) — 모바일 고정폭 컨테이너가 아님
  • 헤더: 상단 가로 nav. 반응형 분기 없음, 햄버거 없음, 하단 탭바 없음
  • 브레이크포인트 사용량: sm: 192회 / lg: 34회 / xl: 5회 / md: 2회

Tailwind는 모바일 우선이므로 "핸드폰 버전" = sm:/lg: 접두사를 제거한 기본 클래스가 전부다. 별도로 디자인된 모바일 화면이 존재하지 않는다.

결론: 화면 내부 UI(카드/폼/리스트/차트)는 기본 클래스 기준으로 그대로 옮기되, 네비게이션은 하단 탭바로 재설계한다. (결정 완료)


1. 디자인 토큰

원본 src/app/styles/index.css@theme 블록에서 추출. RN 테마 파일로 그대로 옮길 것.

export const colors = {
  brand: '#e56c31',
  brandForeground: '#fefefe',
  secondary: '#f6b93b',
  secondaryForeground: '#fefefe',
  // 앱 전역 배경 (모든 레이아웃이 bg-neutral-50)
  background: '#fafafa',      // neutral-50
  surface: '#ffffff',
  border: '#e5e5e5',          // neutral-200
  textPrimary: '#171717',     // neutral-900
  textSecondary: '#404040',   // neutral-700
  // 지오펜스 상태 색
  fenceInside: '#22c55e',     // green-500
  fenceOutside: '#ef4444',    // red-500
  fenceActiveLabel: '#16a34a',// green-600
  fenceIdleLabel: '#6b7280',  // gray-500
};

export const fonts = {
  sans: 'Pretendard',              // 본문
  display: 'GmarketSans',          // 제목 (GmarketSansTTFBold.ttf, weight 700)
};

폰트 주의: 웹은 Pretendard를 jsDelivr CDN에서, GmarketSans를 /fonts/GmarketSansTTFBold.ttf에서 로드한다. RN에서는 둘 다 번들에 포함해야 한다 (expo-font 또는 네이티브 asset 링크).

애니메이션: 웹은 CSS @keyframes 5종 사용 — skeleton-shimmer(스켈레톤 반짝임 1.4s), enter-soft(진입 시 y+10px 페이드인 0.45s), chart-line-draw(라인 차트 stroke 그리기 1.1s), chart-point-pop, chart-fade-up. → RN에서는 Reanimated로 재작성. 스켈레톤은 react-native-skeleton-placeholder 등으로 대체 가능.


2. 화면 목록 (라우트 → RN 스크린)

원본 src/app/router.tsx 기준. 총 20개 화면.

인증 스택 (AuthLayout — 헤더 없음)

웹 라우트 화면 비고
/auth 로그인 소셜 로그인 (Google/Naver)
/auth/callback/:provider OAuth 콜백 RN에서는 불필요/재설계 (§5 참고)
/auth/signup 회원가입 닉네임/지역/프로필 설정
/auth/family/join 가족 참여 초대코드 입력. 인증 필요

메인 스택 (AppLayout — 헤더 있음)

웹 라우트 화면 인증
/ 메인 홈 선택
/walk 산책 (지도) 산책
/community 커뮤니티 목록 커뮤니티
/community/my 내 활동 필요 커뮤니티
/community/new 글 작성 필요 커뮤니티
/community/:boardId 글 상세 필요 커뮤니티
/community/:boardId/edit 글 수정 필요 커뮤니티
/my 마이도도 필요 마이
/my/notifications 알림 설정 필요 마이
/my/withdrawal 회원 탈퇴 필요 마이
/my/pets/new 반려동물 등록 필요 마이
/my/pets/:petId 반려동물 상세 필요 마이
/my/pets/:petId/edit 반려동물 수정 필요 마이
/my/pets/:petId/notes 특이사항 필요 마이
/my/pets/:petId/weight 체중 기록 (차트) 필요 마이
* 404

메인 홈 특이사항: 비로그인 시 로고만 렌더(MainPage.tsx), 로그인 시 LoggedInHome (반려동물 선택형 건강 레포트).


3. 네비게이션 재설계 (결정됨)

원본 헤더(src/widgets/header/ui/Header.tsx)는 로고 + 가로 링크 3개(gap-8) + 프로필 아바타 드롭다운. 375px에서 폭이 거의 꽉 차 찌그러진다. 하단 탭바로 대체한다.

Root
├─ AuthStack (미인증)
│   └─ Login → Signup → FamilyJoin
└─ MainTabs (인증)
    ├─ 홈       → HomeStack (Main)
    ├─ 산책     → WalkStack (Walk) ※ 전체화면 지도, 탭바 위에 오버레이
    ├─ 커뮤니티 → CommunityStack (List → Detail / New / Edit / MyActivity)
    └─ 마이도도 → MyStack (MyDodo → Notifications / Withdrawal / Pet*)
  • 프로필 드롭다운(ProfileMenu.tsx) → "마이도도" 탭 상단 프로필 섹션으로 흡수
  • 비로그인 상태: 웹은 헤더가 GuestNav(서비스 소개/시작하기)로 바뀜. RN은 AuthStack으로 분기하거나, 탭 진입 시 로그인 모달을 띄우는 방식 중 택1
  • 산책 화면: 원본 AppLayout/walkisFullBleed로 헤더를 제거함 (AppLayout.tsx:13). RN에서도 전체화면 지도로 유지할 것

4. 계층별 이식성 (FSD 구조 활용)

원본 경로 판정 비고
shared/api/axios.ts 거의 그대로 인터셉터의 토큰 저장소만 교체
features/*/api/* 그대로 순수 axios 호출
features/*/model/* (React Query 훅) 거의 그대로 RQ v5는 RN 동일 동작
features/*/model/types.ts 그대로
shared/lib/socket/stompClient.ts 거의 그대로 ✅ SockJS 미사용, 순수 WebSocket
shared/lib/regions, lib/api/errorMessage 그대로
features/*/ui, pages/*/ui (97개 tsx) 전면 재작성 Tailwind className → RN 스타일
shared/ui/* (Modal, Toast, Skeleton, Spinner) 재작성 RN 대응물로
shared/lib/auth/token.ts 재작성 §5
features/auth/lib/oauth.ts 재작성 §5
shared/lib/naver-map/* 재작성 §6 — 최대 리스크
shared/api/files.ts 재작성 §7
app/router.tsx, app/layouts/* 재작성 §3

요약: 데이터/로직 레이어는 대부분 재사용 가능. UI 레이어 97개 파일이 실제 작업량.


5. 인증: 전면 재작성 필요

현재 웹 방식

  • 토큰 저장: accessToken은 localStorage, refreshTokensessionStorage (shared/lib/auth/token.ts). 사용처: localStorage 37회, sessionStorage 12회
  • 저장 키: accessToken, dodo.refreshToken, profileUrl, nickname, region, notificationEnabled, accessTokenExpiresAt, accessTokenTtlMs
  • 만료 처리: ACCESS_TOKEN_REFRESH_BUFFER_MS = 60_000 — 만료 60초 전 갱신
  • 상태 전파: window.dispatchEvent(new Event('dodo:auth-state-change')) — 커스텀 이벤트
  • OAuth: window.location.href = buildSocialAuthUrl(provider) 로 전체 페이지 리다이렉트. CSRF용 state를 sessionStorage에 저장 후 콜백에서 검증

RN 재설계

항목 대응
토큰 저장 expo-secure-store (리프레시 토큰은 반드시 여기) / AsyncStorage(비민감 정보)
sessionStorage 의미 RN엔 세션 개념이 없음 → 앱 시작 시 초기화하는 in-memory 저장소로 흉내내거나, SecureStore로 승격 (보안 정책 재검토 필요)
crypto.randomUUID() expo-crypto 로 교체
OAuth 리다이렉트 expo-auth-session + expo-web-browser. 딥링크 스킴 필요
AuthCallbackPage RN에선 화면 불필요 — expo-auth-session 결과 핸들러로 흡수
dodo:auth-state-change 이벤트 Context/Zustand 등 상태 저장소로 교체
redirectToLogin() 네비게이션 reset() 으로 교체

백엔드 협의 필요: VITE_OAUTH_REDIRECT_URI가 웹 URL 기준이다. RN 딥링크 스킴(dodo://auth/callback/...)을 OAuth 콘솔(Google/Naver)과 백엔드 양쪽에 등록해야 한다. 이거 안 되면 로그인 자체가 막힌다 — 가장 먼저 확인할 것.


6. 네이버 지도 → @mj-studio/react-native-naver-map (결정됨, 2026-07 조사)

결론: WebView 우회나 지도 교체 불필요. 네이티브 SDK 래퍼로 1:1 대응된다.

현재 웹 방식

shared/lib/naver-map/loadNaverMap.ts<script src="https://oapi.map.naver.com/openapi/v3/maps.js?ncpKeyId=...">document.head에 주입. RN에서는 불가능 → 이 파일은 폐기하고 라이브러리로 대체.

features/walk-fence/ui/WalkMap.tsx (196줄)가 하는 일:

  • 지도 생성 + 클릭 리스너 (좌표 콜백)
  • 기존 울타리 원(Circle) 다중 렌더
  • 펫 이름 HTML 라벨 마커 (petLabelContent — innerHTML 문자열)
  • 생성/수정 미리보기 원 (draftCenter + draftRadius)
  • 실시간 위치 점 마커 — 울타리 안=#22c55e / 밖=#ef4444 (liveMarkerContent)
  • 기본 중심: 경복궁 { lat: 37.5796, lng: 126.977 }

대응표

웹 (naver.maps) RN (@mj-studio/react-native-naver-map)
naver.maps.Circle (울타리/미리보기 원) NaverMapCircleOverlayradius(미터), color, outlineWidth, outlineColor
지도 클릭 리스너 onTapMap?: (params: Coord & { x, y }) => void
naver.maps.Marker + HTML 라벨 NaverMapMarkerOverlay커스텀 React 컴포넌트 마커 지원
지도 인스턴스 ref 조작 NaverMapViewRefanimateCameraTo, animateRegionTo, screenToCoordinate
onInitialized, onCameraChanged, onCameraIdle
  • radius가 웹과 동일하게 미터 단위 → 지오펜스 반경 로직 변환 없이 재사용
  • HTML 문자열 마커 2종(petLabelContent, liveMarkerContent)만 RN 컴포넌트로 재작성. 이 과정에서 escapeHtml() XSS 방어 함수는 불필요해져 삭제 가능
  • 그 외 오버레이도 제공: Polygon, Polyline, Path, MultiPath, ArrowheadPath, Ground

라이브러리 상태 (2026-07-14 확인)

  • 최신 2.9.0 (2026-05-28 배포), 최근 커밋 2026-06-05
  • GitHub 158 stars / 열린 이슈 2개 / MIT / 주간 다운로드 2,323
  • peerDeps: react: *, react-native: * (버전 제약 없음)
  • Expo config plugin 지원app.json 설정만으로 iOS/Android 네이티브 파일 직접 수정 불필요

설치

npx expo install @mj-studio/react-native-naver-map

⚠️ 콘솔 등록 필요

  • 웹은 ncpKeyId 파라미터 사용 = 네이버 신규 독립 Maps 상품 키 형식. 라이브러리도 2.4.x+ 부터 이 신규 상품을 지원하므로 방식이 일치한다.
  • 단, 네이버 클라우드 콘솔에 Android 패키지명 / iOS Bundle ID 등록 필요. 등록된 값과 앱의 실제 값이 일치해야 인증이 통과한다. → OAuth 딥링크 등록과 함께 처리할 것.

참고 링크


7. 기타 웹 의존성

  • 파일 업로드 (shared/api/files.ts): File/FormData 기반, multipart 필드명 files(복수). RN에서는 expo-image-picker{ uri, name, type } 형태로 FormData 구성. POST /files/upload{ imageUrls: string[] }. 커뮤니티/프로필 공용
  • window. 38회 / document. 21회 사용 — 대부분 Modal/Toast/드롭다운의 이벤트 리스너(mousedown, Escape)와 리다이렉트. RN 대응물로 교체
  • SVG: vite-plugin-svgr?react import 사용 (Logo_light.svg?react). RN은 react-native-svg + react-native-svg-transformer 필요
  • 경로 별칭: @src (vite.config.ts). RN은 babel module-resolver / tsconfig paths로 재설정

8. 실시간 위치 (STOMP) — 재사용 가능 ✅

shared/lib/socket/stompClient.ts:

  • @stomp/stompjs Client, brokerURL: env.WS_URL (wss://). SockJS 미사용 → RN 호환
  • reconnectDelay: 5000, heartbeatIncoming/Outgoing: 10000
  • beforeConnect에서 매 연결 직전 최신 액세스 토큰을 Authorization: Bearer ... 헤더에 주입 (재연결 시 갱신 토큰 반영 목적)

features/walk-fence/model/useLiveLocation.ts:

  • 구독 토픽: /sub/fence/location/{petId}
  • 메시지: { payload: LiveLocationPayload } — 래핑된 payload만 꺼내 사용
  • petId 변경 시 재연결, 언마운트 시 deactivate()

주의: 위치는 기기(펫 트래커)가 서버로 보내고 앱은 구독만 한다. 앱 자체의 geolocation 사용처는 0건이다. RN에서 위치 권한이 필요하다고 지레 판단하지 말 것.

RN 추가 고려: 백그라운드 진입 시 WebSocket이 끊긴다. AppState 기반 재연결 처리 필요 (웹엔 없던 문제).


9. API 계약

  • Base: VITE_API_BASE_URL, timeout 10초 (shared/config/apiConfig.ts)
  • 인증: Authorization: Bearer {accessToken}

엔드포인트 전체 (52개)

인증/유저

POST   /auth/logout
GET    /users/me
PATCH  /users/me
PUT    /users/me/profile
GET    /users/nickname/check
PATCH  /users/me/setting/notification
POST   /users/me/withdrawal/email
DELETE /users/me

메인

GET    /main

반려동물

GET    /pets/list
POST   /pets
GET    /pets/{petId}
PATCH  /pets/{petId}
DELETE /pets/{petId}
GET    /pets/{petId}/significant
POST   /pets/significant
PATCH  /pets/significant/{noteId}
DELETE /pets/significant/{noteId}
GET    /pet/{petId}/weight/history
POST   /pet/{petId}/weight
PATCH  /pet/{petId}/weight/{weightId}
DELETE /pet/{petId}/weight/{weightId}

⚠️ 체중 API만 /pet/ (단수), 나머지는 /pets/ (복수). 원본 그대로임 — 오타 아님.

가족

POST   /pets/{petId}/invitation-code
GET    /pets/family/applications
GET    /pets/family/pending-users
POST   /pets/family/approval
GET    /pets/family/blocked-users
DELETE /pets/family/block

산책/지오펜스

GET    /fence/boundaries
GET    /fence/{fenceId}/boundary
GET    /fence/{petId}/status
POST   /fence/range
PATCH  /fence/{fenceId}/range
PATCH  /fence/{fenceId}/toggle

커뮤니티

GET    /boards
POST   /boards
GET    /boards/me
GET    /boards/{boardId}
PATCH  /boards/{boardId}
DELETE /boards/{boardId}
POST   /boards/temp-save
GET    /boards/temp-save/{sessionKey}
GET    /comments/{boardId}
GET    /comments/me
POST   /comments
PATCH  /comments/{commentId}
DELETE /comments/{commentId}
POST   /reactions/board
PATCH  /reactions/board/{boardId}
DELETE /reactions/board/{boardId}

파일

POST   /files/upload        (multipart, 필드명: files)

10. 환경 변수

웹(shared/config/env.ts)은 6개를 요구하며 누락 시 throw한다.

RN 대응 비고
VITE_API_BASE_URL EXPO_PUBLIC_API_BASE_URL ✅ 이미 이식됨
VITE_WS_URL EXPO_PUBLIC_WS_URL 추가 필요
VITE_GOOGLE_CLIENT_ID EXPO_PUBLIC_GOOGLE_CLIENT_ID iOS/Android용 클라이언트 ID 신규 발급 필요
VITE_NAVER_CLIENT_ID EXPO_PUBLIC_NAVER_CLIENT_ID 동일하게 앱용 재발급 검토
VITE_OAUTH_REDIRECT_URI 딥링크로 교체. 스킴 dodoapp 이미 설정됨 → dodoapp://auth/callback/{provider} (§5)
VITE_NAVER_MAP_CLIENT_ID app.json config plugin client 신규 Maps 상품 키 형식 동일. 콘솔에 앱 패키지명/Bundle ID 등록 필요 (§6)

11. 권장 작업 순서

블로커 2건 모두 해소됨. OAuth 딥링크 등록 가능 확인(§5), 지도는 네이티브 SDK 래퍼로 결정(§6). 스캐폴딩도 대부분 완료 상태(§-1) — 아래 2번은 대폭 축소됨.

  1. [선행/블로킹] app.jsonios.bundleIdentifier / android.package 확정 후, OAuth 딥링크(Google/Naver 콘솔 + 백엔드) + 네이버 지도(앱 패키지명/Bundle ID) 등록. → 앱 식별자가 정해져야 나머지가 다 굴러감. 나중에 바꾸면 콘솔 재등록
  2. 정리 & 토큰 — Expo 템플릿 잔재 삭제, constants/theme.ts를 §1 DoDo 토큰으로 교체, Pretendard/GmarketSans 번들(expo-font)
  3. 하단 탭 네비게이션 골격 (§3) — (tabs)/_layout.tsx 재작성. bottom-tabs는 이미 설치됨
  4. 데이터 레이어 — axios.ts 인터셉터 채우기 + 토큰 저장소(§5) + features/*/api, features/*/model 이식
  5. 공용 UI 재작성 — Modal / Toast / Skeleton / Spinner
  6. 화면 이식 — 마이도도 → 커뮤니티 → 홈 → 산책 순 (산책이 가장 복잡)
  7. STOMP 연결 + AppState 재연결 (§8)

12. 참고: 두 레포의 @ 별칭이 다르다

레포 별칭 실제 경로
dodo-frontend (웹) @/ src/
dodo-app (RN) @/ ./ (루트)

웹의 @/shared/api/axios → RN의 @/shared/api/axios문자열은 동일하게 유지되지만 디스크 상 위치는 src/shared/... vs shared/... 로 다르다. 복붙 시 import 경로는 대체로 그대로 동작한다.

단, RN은 expo-router 기반이라 app/이 라우팅 전용이고 components/, hooks/, constants/가 루트에 있다. FSD(features/, widgets/, shared/)와 섞이므로 디렉토리 규칙을 먼저 합의할 것. 현재 shared/만 FSD식으로 만들어져 있다.


참고: 원본 레포 위치

c:\Users\soo\dodo-frontend (branch: develop) 파일이 필요하면 /add-dir c:\Users\soo\dodo-frontend 로 붙일 수 있음.