이 문서는 AI가 프로젝트의 리팩토링 및 새로운 기능 개발 시 참고할 수 있도록 작성된 종합 가이드입니다. 현재 코드베이스의 아키텍처, 패턴, 규칙을 정리하여 일관성 있는 개발을 지원합니다.
- 프로젝트 개요
- 기술 스택
- 프로젝트 구조 및 아키텍처
- API 통신 아키텍처
- 상태 관리
- 라우팅 및 네비게이션
- 공통 컴포넌트 및 레이아웃
- 도메인 별 구조 패턴
- 코드 스타일 및 컨벤션
- 개발 시 주의사항 및 베스트 프랙티스
기도함께 - 기도를 함께 나누고 기도로 응원하는 공간
기도방을 만들고, 친구들과 함께 기도 제목을 공유하며 서로의 기도를 응원하는 모바일 애플리케이션
- iOS
- Android
- Production: https://praytogether.site/api
- Preview (스테이징)
- Development
- 인증 (Auth): JWT 토큰 기반 로그인/회원가입, 자동 토큰 갱신
- 기도방 (Rooms): 기도방 생성/관리, 멤버 초대
- 기도 (Prayers): 기도 제목 및 내용 CRUD, 완료 처리
- 친구 (Friends): 친구 추가 및 관리
- 초대 (Invitations): 기도방 초대 수락/거절
- 알림 (Notifications): FCM 기반 푸시 알림
- 앱 업데이트: EAS Update OTA, 강제 업데이트/유지보수 모드
- 코어: React Native, Expo, TypeScript (strict mode)
- 라우팅: Expo Router (파일 기반, (public)/(protected) 그룹)
- 상태 관리: Zustand (전역), React Query (서버)
- 서버 통신: Axios
- UI: React Native Paper, Reanimated, expo-linear-gradient
- Firebase: FCM 푸시 알림, Crashlytics
- 네비게이션: React Navigation (native-stack, bottom-tabs)
- 유틸리티: date-fns, expo-secure-store, expo-notifications
- 빌드/배포: EAS (Build, Update, Submit)
app-client/
├── app/ # Expo Router 파일 기반 라우팅
│ ├── (public)/ # 공개 페이지 (인증 불필요)
│ │ ├── login/ # 로그인
│ │ └── signup/ # 회원가입
│ ├── (protected)/ # 인증 필요 페이지
│ │ ├── (tabs)/ # 탭 네비게이션
│ │ │ ├── rooms/ # 기도방 목록
│ │ │ └── my-page/ # 마이페이지
│ │ ├── prayers/ # 기도 관련
│ │ │ ├── creation/ # 기도 생성
│ │ │ └── [id]/ # 기도 상세
│ │ ├── rooms/ # 기도방 관련
│ │ │ └── [id]/ # 기도방 상세
│ │ ├── friends/ # 친구 관련
│ │ ├── my-page/ # 마이페이지 상세
│ │ └── phone-registration/ # 전화번호 등록
│ ├── index.tsx # 앱 진입점
│ └── _layout.tsx # 루트 레이아웃
├── src/ # 소스 코드
│ ├── common/ # 공통 모듈
│ │ ├── apis/ # API 관련
│ │ │ ├── api.ts # Axios 인스턴스
│ │ │ ├── apiService.ts # API 서비스 래퍼
│ │ │ └── apiUrl.ts # API URL
│ │ ├── components/ # 공통 컴포넌트
│ │ │ ├── loading/ # 로딩 컴포넌트
│ │ │ ├── modal/ # 모달 (Alert, Confirmation 등)
│ │ │ ├── error/ # 에러 컴포넌트
│ │ │ ├── empty/ # Empty State
│ │ │ ├── layout/ # 레이아웃 컴포넌트
│ │ │ ├── header/ # 헤더 컴포넌트
│ │ │ ├── button/ # 버튼 컴포넌트
│ │ │ ├── form/ # 폼 컴포넌트
│ │ │ └── toast/ # 토스트 컴포넌트
│ │ ├── hooks/ # 공통 훅
│ │ ├── services/ # 공통 서비스
│ │ │ ├── fcm/ # FCM 관련
│ │ │ ├── time/ # 시간 관련
│ │ │ ├── keyboard/ # 키보드 관련
│ │ │ └── clear/ # 스토어 초기화
│ │ ├── constants/ # 공통 상수
│ │ ├── styles/ # 공통 스타일
│ │ └── types/ # 공통 타입
│ ├── domain/ # 도메인별 비즈니스 로직
│ │ ├── auth/ # 인증
│ │ ├── prayers/ # 기도
│ │ ├── rooms/ # 기도방
│ │ ├── friends/ # 친구
│ │ ├── invitations/ # 초대
│ │ ├── members/ # 멤버
│ │ ├── fcmToken/ # FCM 토큰
│ │ ├── navigation/ # 네비게이션 상태
│ │ └── appVersion/ # 앱 버전
│ └── config/ # 설정
├── assets/ # 정적 리소스
├── app.config.js # Expo 앱 설정
├── eas.json # EAS 빌드 설정
└── tsconfig.json # TypeScript 설정
프로젝트는 도메인 주도 설계(DDD) 개념을 일부 차용한 레이어드 아키텍처를 따릅니다.
┌─────────────────────────────────────────────┐
│ Presentation Layer (UI) │
│ - React Components (app/) │
│ - Pages & Screens │
└─────────────────┬───────────────────────────┘
│
┌─────────────────▼───────────────────────────┐
│ Application Layer │
│ - React Query Hooks (hooks/) │
│ - Zustand Stores (stores/) │
└─────────────────┬───────────────────────────┘
│
┌─────────────────▼───────────────────────────┐
│ Domain Layer │
│ - Business Logic (services/) │
│ - Domain Types (types/) │
│ - Domain Constants │
└─────────────────┬───────────────────────────┘
│
┌─────────────────▼───────────────────────────┐
│ Infrastructure Layer │
│ - API Client (api.ts) │
│ - External Services (Firebase, etc) │
│ - Utils & Helpers │
└─────────────────────────────────────────────┘
Component
↓ (사용)
React Query Hook (queries/mutations)
↓ (호출)
Service (도메인별 Service)
↓ (사용)
API Service (apiService.ts)
↓ (사용)
Axios Instance (api.ts)
↓ (Request Interceptor: 토큰 추가)
Backend Server
↓ (Response Interceptor: 토큰 갱신)
Component
-
인증 불필요 경로 확인:
/v1/auth/login/v1/auth/signup/v1/auth/otp/email/v1/auth/otp/email/verification/v1/auth/reissue-token
-
인증 필요한 경우:
- SecureStore에서 Access Token 조회
Authorization: Bearer {token}헤더 추가
-
성공 응답: 그대로 반환
-
401 에러 (토큰 만료):
- Refresh Token으로 새 토큰 발급 시도
- 성공 시: 새 토큰 저장 후 원래 요청 재시도
- 실패 시: 로그아웃 처리 및 로그인 화면 이동
-
네트워크 에러:
- "네트워크 연결을 확인해주세요" 메시지
NETWORK_ERROR코드 반환
-
기타 에러:
- 서버 응답의 에러 메시지 전달
ApiError객체로 변환
- Expo SecureStore (암호화된 로컬 저장소)
- Key:
accessToken,refreshToken
let refreshTokenPromise: Promise<AxiosResponse> | null = null;
const fetchNewTokens = async (refreshToken: string) => {
if (!refreshTokenPromise) {
refreshTokenPromise = fetchNewTokensBySingletone(refreshToken)
.finally(() => {
refreshTokenPromise = null;
});
}
return refreshTokenPromise;
};중요: 동시에 여러 API 요청이 401을 받더라도, 토큰 갱신은 단 한 번만 수행됩니다.
- Base URL:
https://praytogether.site/api - 버전: 모든 엔드포인트
/v1/... - 예시:
POST /v1/auth/login- 로그인GET /v1/rooms- 기도방 목록GET /v1/prayer-rooms/:roomId/prayer-titles- 기도 제목 (무한 스크롤)
- Query Keys:
src/common/constants/queryKeys.ts에 도메인별로 정의 - Cache 정책: staleTime 0, cacheTime 5분, refetchOnWindowFocus true, retry 3회
- Infinite Query:
pageParam으로 커서 전달,getNextPageParam으로 다음 페이지 계산
프로젝트는 Zustand와 React Query를 조합하여 상태를 관리합니다.
┌────────────────────────────────────────────────┐
│ Zustand (Client State) │
│ - 인증 상태 (isAuthenticated) │
│ - UI 상태 (모달, 토스트, 네비게이션) │
│ - 임시 폼 데이터 (기도 생성 폼 등) │
└────────────────────────────────────────────────┘
┌────────────────────────────────────────────────┐
│ React Query (Server State) │
│ - API 데이터 (기도방, 기도, 친구 등) │
│ - 캐싱 & 자동 재시도 │
│ - Optimistic Updates │
└────────────────────────────────────────────────┘
각 도메인별로 스토어를 분리하며, 다음 패턴을 따릅니다:
// src/domain/[domain]/stores/use[Domain]Store.ts
interface MyState { data: string | null; }
interface MyActions { setData: (data: string) => void; clear: () => void; }
type MyStore = MyState & MyActions;
export const useMyStore = create<MyStore>((set) => ({
data: null,
setData: (data) => set({ data }),
clear: () => set({ data: null }),
}));주요 스토어: useAuthStore (인증), usePrayerCreationStore (기도 생성 폼), useSelectedRoomStore (선택된 기도방), useBottomNaviStatusStore (탭 네비게이션)
Query Hook (hooks/queries/):
export const useMyDataQuery = () => useQuery({
queryKey: [QueryKeys.MY_DATA],
queryFn: () => myService.fetchData(),
});Mutation Hook (hooks/mutations/):
export const useCreateMutation = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: myService.create,
onSuccess: () => queryClient.invalidateQueries({ queryKey: [QueryKeys.MY_DATA] }),
});
};Infinite Query: useInfiniteQuery + pageParam + getNextPageParam으로 커서 기반 페이지네이션
프로젝트는 Expo Router를 사용하여 파일 시스템 기반 라우팅을 구현합니다.
app/
├── _layout.tsx # 루트 레이아웃 (Provider 설정)
├── index.tsx # 웰컴 화면 ("/")
├── (public)/ # 인증 불필요 그룹
│ ├── login/index.tsx # "/login"
│ └── signup/index.tsx # "/signup"
└── (protected)/ # 인증 필요 그룹
├── _layout.tsx # Protected 레이아웃 (인증 체크)
├── (tabs)/ # 탭 네비게이션
│ ├── _layout.tsx # 탭 레이아웃
│ ├── rooms/index.tsx # "/rooms" (탭1)
│ └── my-page/index.tsx # "/my-page" (탭2)
├── prayers/
│ ├── creation/index.tsx # "/prayers/creation"
│ └── [id]/index.tsx # "/prayers/:id"
├── rooms/
│ └── [id]/index.tsx # "/rooms/:id"
└── friends/index.tsx # "/friends"
주요 책임:
-
Provider 설정:
SafeAreaProvider: Safe Area 관리PaperProvider: React Native Paper 테마CustomQueryClientProvider: React Query 설정ErrorBoundary: 전역 에러 핸들링
-
전역 컴포넌트:
AuthStateListener: 인증 상태 감시 및 리다이렉트AuthEventListener: 인증 이벤트 처리GlobalAlertModal: 전역 알림 모달UpdateModalsManager: 앱 업데이트 모달 관리Toast: 토스트 메시지
-
알림 처리:
useNotificationObserver: FCM 푸시 알림 처리- 앱이 종료된 상태에서 알림 탭 시 딥링크 처리
- 인증 완료 후 pending 알림 처리
설정:
- 2개의 탭: "기도방 목록", "마이페이지"
- 아이콘: FontAwesome6
- 활성화 색상:
color.secondary - 애니메이션: 비활성화 (
animation: "none") - 높이: 반응형 (
Top1Body10Bottom1.tsx레이아웃과 연동)
import { router } from "expo-router";
// 페이지 이동
router.push("/rooms/123");
// 파라미터 전달
router.push({
pathname: "/prayers/[id]",
params: { id: "456", roomId: "123" },
});
// 뒤로 가기
router.back();
// 교체 (히스토리에 남지 않음)
router.replace("/login");FCM 푸시 알림 → 특정 화면 이동:
// app/_layout.tsx의 useNotificationObserver
const data = notification.request.content.data;
if (data && data.roomId && data.prayerTitleId) {
// 1. 기도방으로 먼저 이동
router.push(`/rooms/${roomId}`);
// 2. 약간의 지연 후 기도 상세로 이동
setTimeout(() => {
router.push(`/prayers/${prayerTitleId}?roomId=${roomId}`);
}, 300);
}AuthStateListener 컴포넌트:
- 인증 상태 변화 감지
- 미인증 시:
/login으로 리다이렉트 - 인증 완료 시:
/rooms로 리다이렉트
프로젝트는 비율 기반 레이아웃 시스템을 사용합니다.
Top1Body10Bottom1.tsx: 탭 네비게이션이 있는 화면 (상단 70, 본문 flex:1, 하단 50)Top4Body10.tsx: 헤더가 큰 화면 (상단 4배, 본문 10배)Top1Body10.tsx: 일반 화면 (상단 1배, 본문 10배)ScreenLayout.tsx: 전체 화면 레이아웃 베이스 (SafeAreaView + KeyboardAvoidingView 조합)
TopHeader.tsx: 일반 상단 헤더 (제목 표시)BackButtonHeader.tsx: 뒤로 가기 버튼 포함 헤더AuthHeader.tsx: 인증 화면용 헤더 (로고 및 앱 이름)
AccentCard.tsx: 왼쪽 보더 액센트 카드 컴포넌트- 프로젝트 전체에서 일관된 카드 디자인 유지
- 왼쪽 보더 + 흰색 배경 + elevation 그림자
- 선택적 Press 애니메이션
- 적용된 컴포넌트:
RoomItem,PrayerTitleItem,TitleCard
GlobalAlertModal.tsx: 전역 알림 모달 (Zustand store로 관리)AlertModal.tsx: 로컬 알림 모달 (단일 확인 버튼)ConfirmationModal.tsx: 확인/취소 모달 (위험한 작업 전 확인용)ForceUpdateModal.tsx: 강제 업데이트 모달MaintenanceModal.tsx: 유지보수 모달
LoadingScreen.tsx: 전체 화면 로딩LoadingSpinner.tsx: 인라인 스피너
- 데이터가 없을 때 표시 (아이콘 + 메시지)
react-native-toast-message사용- 설정:
toastConfig.ts
각 도메인(src/domain/[domain]/)은 다음과 같은 일관된 구조를 따릅니다:
domain/[domain-name]/
├── types/ # TypeScript 타입 정의
│ ├── request/ # API 요청 타입
│ ├── response/ # API 응답 타입
│ ├── params/ # 함수 파라미터 타입
│ └── [domain]Store.ts # 스토어 타입
├── stores/ # Zustand 스토어
│ └── use[Domain]Store.ts
├── hooks/ # React 훅
│ ├── queries/ # React Query useQuery 훅
│ │ └── use[Domain]Queries.ts
│ └── mutations/ # React Query useMutation 훅
│ └── use[Domain]Mutations.ts
├── services/ # API 호출 로직
│ └── [domain]Service.ts
├── utils/ # 유틸리티 함수
├── constants/ # 도메인 상수
├── events/ # 이벤트 리스너 (일부 도메인)
└── api/ # API 엔드포인트 (일부 도메인)
1. Types (types/): 타입 안전성 보장
request/: API 요청 타입response/: API 응답 타입[domain]Store.ts: State + Actions 타입
2. Services (services/): API 호출 로직 캡슐화
export const roomService = {
fetchRooms: async () => {
const res = await apiService.get<{ data: RoomResponse[] }>("/v1/rooms");
return res.data.data;
},
};3. Hooks (hooks/): 컴포넌트와 비즈니스 로직 연결
queries/: React Query의 useQuery 훅 (섹션 5 패턴 참고)mutations/: React Query의 useMutation 훅 (섹션 5 패턴 참고)
4. Stores (stores/): 클라이언트 상태 관리
- Zustand로 구현 (섹션 5 패턴 참고)
- Strict Mode: 활성화
- 타입 정의: 모든 함수, 컴포넌트에 명시적 타입 지정
- Any 금지:
any타입 사용 지양
- 컴포넌트: PascalCase (예:
PrayerCard.tsx) - 훅: camelCase with "use" prefix (예:
usePrayerQueries.ts) - 서비스: camelCase with "[domain]Service" (예:
prayerService.ts) - 스토어: camelCase with "use[Domain]Store" (예:
useAuthStore.ts) - 타입: camelCase (예:
prayerTitle.ts)
- 소문자 kebab-case: 여러 단어 (예:
phone-registration/) - camelCase: 단일 도메인명 (예:
prayers/,rooms/)
- 함수형 컴포넌트 사용, default export
- Props는 인터페이스로 정의
StyleSheet.create()사용, 스타일은 컴포넌트 하단 위치
@/:src/디렉토리 (예:import { color } from "@/common/styles/color";)
- React/React Native
- 서드파티 라이브러리
- 내부 모듈 (
@/경로) - 타입 import
- 한국어 주석, TODO/FIXME/NOTE 태그 활용
- 들여쓰기 2 spaces, 세미콜론 사용, 큰따옴표 우선
// api.ts의 interceptor가 자동으로 토큰을 관리하므로,
// 서비스 레이어에서는 토큰을 직접 다루지 않음
export const roomService = {
fetchRooms: async () => {
const response = await apiService.get("/v1/rooms");
return response.data.data;
},
};// 서비스에서 직접 토큰을 가져오지 말 것
import { tokenUtils } from "@/domain/auth/utils/tokenUtils";
export const roomService = {
fetchRooms: async () => {
const token = await tokenUtils.getAccessToken(); // ❌
const response = await apiService.get("/v1/rooms", {
headers: { Authorization: `Bearer ${token}` },
});
return response.data.data;
},
};// React Query의 onError 콜백 활용
export const useCreateRoomMutation = () => {
return useMutation({
mutationFn: roomService.createRoom,
onError: (error) => {
Toast.show({
type: "error",
text1: "오류",
text2: error.message || "기도방 생성에 실패했습니다.",
});
},
});
};// 컴포넌트에서 try-catch로 일일이 처리하지 말 것
const handleCreateRoom = async () => {
try {
await createRoomMutation.mutateAsync({ name: "새 기도방" });
} catch (error) {
// ❌ 중복된 에러 처리
alert("오류 발생");
}
};// 서버 상태는 React Query로
const { data: rooms } = useRoomsQuery();
// 클라이언트 상태는 Zustand로
const { selectedRoom, setSelectedRoom } = useSelectedRoomStore();// 서버 데이터를 useState에 저장하지 말 것
const [rooms, setRooms] = useState([]); // ❌
useEffect(() => {
fetchRooms().then(setRooms); // ❌
}, []);// Mutation 성공 시 관련 쿼리 무효화
export const useCreateRoomMutation = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: roomService.createRoom,
onSuccess: () => {
// 기도방 목록 쿼리 무효화 → 자동 재조회
queryClient.invalidateQueries({ queryKey: [QueryKeys.ROOMS] });
},
});
};// 수동으로 refetch 호출하지 말 것
const { refetch } = useRoomsQuery();
const handleCreateRoom = async () => {
await createRoomMutation.mutateAsync({ name: "새 기도방" });
refetch(); // ❌ invalidateQueries를 사용해야 함
};// 공통 레이아웃 컴포넌트 사용
import { Top1Body10 } from "@/common/components/layout";
import { BackButtonHeader } from "@/common/components/header";
export default function MyScreen() {
return (
<Top1Body10
tops={[<BackButtonHeader key="header" title="제목" />]}
bodies={[<View key="body">{/* 내용 */}</View>]}
/>
);
}// 레이아웃을 매번 직접 구현하지 말 것
export default function MyScreen() {
return (
<View style={{ flex: 1 }}>
<View style={{ height: 70 }}>{/* 헤더 */}</View>
<View style={{ flex: 1 }}>{/* 본문 */}</View>
</View>
);
}// API 응답 타입 명시
const response = await apiService.get<{ data: RoomResponse[] }>(
"/v1/rooms"
);
return response.data.data; // RoomResponse[] 타입 보장// any 타입 사용 금지
const response = await apiService.get("/v1/rooms"); // any
return response.data.data; // any// 재사용 가능한 작은 컴포넌트로 분리
function RoomCard({ room }: { room: RoomResponse }) {
return <View>{/* 카드 내용 */}</View>;
}
export default function RoomsScreen() {
const { data: rooms } = useRoomsQuery();
return (
<View>
{rooms?.map((room) => (
<RoomCard key={room.id} room={room} />
))}
</View>
);
}// 모든 로직을 한 컴포넌트에 넣지 말 것
export default function RoomsScreen() {
const { data: rooms } = useRoomsQuery();
return (
<View>
{rooms?.map((room) => (
<View key={room.id}>
{/* 복잡한 카드 UI가 여기에 모두 들어감 */}
</View>
))}
</View>
);
}import { router } from "expo-router";
// Expo Router 사용
router.push("/rooms/123");import { useNavigation } from "@react-navigation/native";
// React Navigation의 navigation prop 직접 사용 금지
const navigation = useNavigation();
navigation.navigate("Rooms", { id: "123" }); // ❌// StyleSheet.create 사용
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: color.background,
},
});
<View style={styles.container} />// 인라인 스타일 남발 금지
<View style={{ flex: 1, backgroundColor: "#f0f0f0" }} /> // ❌import { useGlobalAlertModalStore } from "@/common/components/modal/stores/useGlobalAlertModalStore";
const { open } = useGlobalAlertModalStore();
open({
title: "알림",
message: "작업이 완료되었습니다.",
confirmText: "확인",
});// 매번 새로운 모달 컴포넌트를 만들지 말 것
const [modalVisible, setModalVisible] = useState(false);
<Modal visible={modalVisible}>
<Text>작업이 완료되었습니다.</Text>
<Button onPress={() => setModalVisible(false)}>확인</Button>
</Modal>문제: PagerView를 SafeAreaView, KeyboardAvoidingView 등의 wrapper로 감싸면 iOS 실제 기기에서 터치 이벤트가 차단됩니다.
증상:
- 시뮬레이터에서는 정상 작동
- 실제 iOS 기기에서 입력 필드가 터치되지 않음
- 키보드가 올라오지 않음
scrollEnabled={false}사용 시 더 심각
관련 이슈: react-native-pager-view#51, #382
// PagerView는 SafeAreaView 바로 아래에 직접 배치
<SafeAreaView style={styles.container}>
<BackButtonHeader style={styles.header} />
<PagerView
style={styles.pagerView}
scrollEnabled={false}
>
{/* 페이지 내용 */}
</PagerView>
</SafeAreaView>
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: backgroundColor.default,
},
header: {
marginTop: RFValue(8),
},
pagerView: {
flex: 11, // 또는 flex: 1
},
});// ScreenLayout으로 감싸지 말 것 (내부에 KeyboardAvoidingView + wrapper View 있음)
<ScreenLayout keyboardAvoiding={false}> // ❌
<PagerView>
{/* 페이지 내용 */}
</PagerView>
</ScreenLayout>
// SafeAreaView + 중간 wrapper View로 감싸지 말 것
<SafeAreaView>
<View style={{ flex: 1, justifyContent: 'space-between' }}> // ❌
<PagerView />
</View>
</SafeAreaView>
// KeyboardAvoidingView로 감싸지 말 것
<KeyboardAvoidingView behavior="padding"> // ❌
<PagerView />
</KeyboardAvoidingView>- 최소한의 wrapper만 사용: SafeAreaView + BackButtonHeader + PagerView 구조
- ScreenLayout 사용 금지: PagerView는 특수 케이스로 직접 구현
- 중간 View 최소화: PagerView를 가능한 한 얕은 레벨에 배치
- 실제 기기에서 테스트: 시뮬레이터에서는 문제가 안 보일 수 있음
중요: EAS Update 시 올바른 브랜치로 배포하는지 확인하세요!
# 프로덕션 배포
eas update --branch production
# 현재 브랜치 확인
eas branch:list
# 잘못된 브랜치로 배포하면 실제 사용자에게 변경사항이 적용되지 않음체크리스트:
-
eas.json에서 production 빌드의 채널 확인 - App Store/Play Store 앱이 어떤 채널을 구독하는지 확인
- 배포 후 실제 기기에서 변경사항 확인
-
API 요청/응답 로깅:
src/common/apis/api.ts의 interceptor에 로그 추가
-
React Query DevTools:
- 개발 중 React Query 상태 확인
CustomQueryClientProvider에서 활성화 가능
-
Zustand DevTools:
- Redux DevTools로 Zustand 상태 모니터링
-
React.memo 사용:
- 불필요한 리렌더링 방지
- 리스트 아이템 컴포넌트에 적용
-
useMemo / useCallback 사용:
- 복잡한 계산 결과 메모이제이션
- 콜백 함수 메모이제이션
-
FlatList 최적화:
keyExtractor명시getItemLayout사용 (고정 높이일 경우)removeClippedSubviews활성화
-
토큰 저장:
- 반드시 SecureStore 사용
- AsyncStorage 사용 금지
-
민감 정보:
- 환경 변수 사용 (
app.config.js) - Git에 커밋하지 않을 것
- 환경 변수 사용 (
-
API 키:
- Firebase 설정은
google-services.json/GoogleService-Info.plist - Git에 커밋하지 않을 것
- Firebase 설정은
이 가이드는 기도함께 프로젝트의 현재 아키텍처와 패턴을 반영한 종합 문서입니다.
- 도메인 디렉토리 구조 따르기 (
types/,services/,hooks/,stores/) - API 서비스에서 타입 명시
- React Query로 서버 상태 관리
- Zustand로 클라이언트 상태 관리
- 공통 레이아웃 컴포넌트 사용
- 에러 처리는 React Query의 onError 활용
- 캐시 무효화 패턴 준수
- TypeScript strict mode 준수
- 코드 스타일 컨벤션 준수
- 주석은 한국어로 작성
- 기존 아키텍처 패턴 유지
- 타입 안전성 개선
- 중복 코드 제거 (공통 컴포넌트/훅 추출)
- 성능 최적화 (React.memo, useMemo 등)
- 에러 처리 개선
- 테스트 코드 작성 (필요시)
문서 버전: 1.2 최종 업데이트: 2025-12-16 변경 이력:
- v1.2 (2025-12-16): AccentCard 공통 컴포넌트 추가
- v1.1 (2025-12-14): PagerView 사용 시 주의사항 및 배포 체크리스트 추가
- v1.0 (2025-12-06): 초기 버전