Skip to content

Latest commit

 

History

History
1042 lines (804 loc) · 31.9 KB

File metadata and controls

1042 lines (804 loc) · 31.9 KB

기도함께 (Pray Together) - AI 개발 가이드

이 문서는 AI가 프로젝트의 리팩토링 및 새로운 기능 개발 시 참고할 수 있도록 작성된 종합 가이드입니다. 현재 코드베이스의 아키텍처, 패턴, 규칙을 정리하여 일관성 있는 개발을 지원합니다.


목차

  1. 프로젝트 개요
  2. 기술 스택
  3. 프로젝트 구조 및 아키텍처
  4. API 통신 아키텍처
  5. 상태 관리
  6. 라우팅 및 네비게이션
  7. 공통 컴포넌트 및 레이아웃
  8. 도메인 별 구조 패턴
  9. 코드 스타일 및 컨벤션
  10. 개발 시 주의사항 및 베스트 프랙티스

1. 프로젝트 개요

프로젝트명

기도함께 - 기도를 함께 나누고 기도로 응원하는 공간

프로젝트 목적

기도방을 만들고, 친구들과 함께 기도 제목을 공유하며 서로의 기도를 응원하는 모바일 애플리케이션

플랫폼

  • iOS
  • Android

배포 환경

주요 기능

  • 인증 (Auth): JWT 토큰 기반 로그인/회원가입, 자동 토큰 갱신
  • 기도방 (Rooms): 기도방 생성/관리, 멤버 초대
  • 기도 (Prayers): 기도 제목 및 내용 CRUD, 완료 처리
  • 친구 (Friends): 친구 추가 및 관리
  • 초대 (Invitations): 기도방 초대 수락/거절
  • 알림 (Notifications): FCM 기반 푸시 알림
  • 앱 업데이트: EAS Update OTA, 강제 업데이트/유지보수 모드

2. 기술 스택

  • 코어: 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)

3. 프로젝트 구조 및 아키텍처

전체 디렉토리 구조

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                   │
└─────────────────────────────────────────────┘

4. API 통신 아키텍처

API 통신 흐름

Component
    ↓ (사용)
React Query Hook (queries/mutations)
    ↓ (호출)
Service (도메인별 Service)
    ↓ (사용)
API Service (apiService.ts)
    ↓ (사용)
Axios Instance (api.ts)
    ↓ (Request Interceptor: 토큰 추가)
Backend Server
    ↓ (Response Interceptor: 토큰 갱신)
Component

인증 처리

Request Interceptor (src/common/apis/api.ts)

  1. 인증 불필요 경로 확인:

    • /v1/auth/login
    • /v1/auth/signup
    • /v1/auth/otp/email
    • /v1/auth/otp/email/verification
    • /v1/auth/reissue-token
  2. 인증 필요한 경우:

    • SecureStore에서 Access Token 조회
    • Authorization: Bearer {token} 헤더 추가

Response Interceptor

  1. 성공 응답: 그대로 반환

  2. 401 에러 (토큰 만료):

    • Refresh Token으로 새 토큰 발급 시도
    • 성공 시: 새 토큰 저장 후 원래 요청 재시도
    • 실패 시: 로그아웃 처리 및 로그인 화면 이동
  3. 네트워크 에러:

    • "네트워크 연결을 확인해주세요" 메시지
    • NETWORK_ERROR 코드 반환
  4. 기타 에러:

    • 서버 응답의 에러 메시지 전달
    • ApiError 객체로 변환

토큰 관리

저장 위치

  • Expo SecureStore (암호화된 로컬 저장소)
  • Key: accessToken, refreshToken

토큰 갱신 로직 (Singleton 패턴)

let refreshTokenPromise: Promise<AxiosResponse> | null = null;

const fetchNewTokens = async (refreshToken: string) => {
  if (!refreshTokenPromise) {
    refreshTokenPromise = fetchNewTokensBySingletone(refreshToken)
      .finally(() => {
        refreshTokenPromise = null;
      });
  }
  return refreshTokenPromise;
};

중요: 동시에 여러 API 요청이 401을 받더라도, 토큰 갱신은 단 한 번만 수행됩니다.

API 엔드포인트 패턴

  • Base URL: https://praytogether.site/api
  • 버전: 모든 엔드포인트 /v1/...
  • 예시:
    • POST /v1/auth/login - 로그인
    • GET /v1/rooms - 기도방 목록
    • GET /v1/prayer-rooms/:roomId/prayer-titles - 기도 제목 (무한 스크롤)

React Query 설정

  • Query Keys: src/common/constants/queryKeys.ts에 도메인별로 정의
  • Cache 정책: staleTime 0, cacheTime 5분, refetchOnWindowFocus true, retry 3회
  • Infinite Query: pageParam으로 커서 전달, getNextPageParam으로 다음 페이지 계산

5. 상태 관리

프로젝트는 ZustandReact Query를 조합하여 상태를 관리합니다.

상태 관리 전략

┌────────────────────────────────────────────────┐
│  Zustand (Client State)                        │
│  - 인증 상태 (isAuthenticated)                 │
│  - UI 상태 (모달, 토스트, 네비게이션)          │
│  - 임시 폼 데이터 (기도 생성 폼 등)            │
└────────────────────────────────────────────────┘

┌────────────────────────────────────────────────┐
│  React Query (Server State)                    │
│  - API 데이터 (기도방, 기도, 친구 등)          │
│  - 캐싱 & 자동 재시도                          │
│  - Optimistic Updates                          │
└────────────────────────────────────────────────┘

Zustand Store 패턴

각 도메인별로 스토어를 분리하며, 다음 패턴을 따릅니다:

// 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 (탭 네비게이션)

React Query 패턴

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으로 커서 기반 페이지네이션


6. 라우팅 및 네비게이션

Expo Router 파일 기반 라우팅

프로젝트는 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"

루트 레이아웃 (app/_layout.tsx)

주요 책임:

  1. Provider 설정:

    • SafeAreaProvider: Safe Area 관리
    • PaperProvider: React Native Paper 테마
    • CustomQueryClientProvider: React Query 설정
    • ErrorBoundary: 전역 에러 핸들링
  2. 전역 컴포넌트:

    • AuthStateListener: 인증 상태 감시 및 리다이렉트
    • AuthEventListener: 인증 이벤트 처리
    • GlobalAlertModal: 전역 알림 모달
    • UpdateModalsManager: 앱 업데이트 모달 관리
    • Toast: 토스트 메시지
  3. 알림 처리:

    • useNotificationObserver: FCM 푸시 알림 처리
    • 앱이 종료된 상태에서 알림 탭 시 딥링크 처리
    • 인증 완료 후 pending 알림 처리

탭 네비게이션 (app/(protected)/(tabs)/_layout.tsx)

설정:

  • 2개의 탭: "기도방 목록", "마이페이지"
  • 아이콘: FontAwesome6
  • 활성화 색상: color.secondary
  • 애니메이션: 비활성화 (animation: "none")
  • 높이: 반응형 (Top1Body10Bottom1.tsx 레이아웃과 연동)

네비게이션 패턴

1. 프로그래매틱 네비게이션

import { router } from "expo-router";

// 페이지 이동
router.push("/rooms/123");

// 파라미터 전달
router.push({
  pathname: "/prayers/[id]",
  params: { id: "456", roomId: "123" },
});

// 뒤로 가기
router.back();

// 교체 (히스토리에 남지 않음)
router.replace("/login");

2. 딥링크 처리

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);
}

3. 인증 기반 리다이렉트

AuthStateListener 컴포넌트:

  • 인증 상태 변화 감지
  • 미인증 시: /login으로 리다이렉트
  • 인증 완료 시: /rooms로 리다이렉트

7. 공통 컴포넌트 및 레이아웃

레이아웃 시스템 (src/common/components/layout/)

프로젝트는 비율 기반 레이아웃 시스템을 사용합니다.

  • Top1Body10Bottom1.tsx: 탭 네비게이션이 있는 화면 (상단 70, 본문 flex:1, 하단 50)
  • Top4Body10.tsx: 헤더가 큰 화면 (상단 4배, 본문 10배)
  • Top1Body10.tsx: 일반 화면 (상단 1배, 본문 10배)
  • ScreenLayout.tsx: 전체 화면 레이아웃 베이스 (SafeAreaView + KeyboardAvoidingView 조합)

헤더 컴포넌트 (src/common/components/header/)

  • TopHeader.tsx: 일반 상단 헤더 (제목 표시)
  • BackButtonHeader.tsx: 뒤로 가기 버튼 포함 헤더
  • AuthHeader.tsx: 인증 화면용 헤더 (로고 및 앱 이름)

카드 컴포넌트 (src/common/components/card/)

  • AccentCard.tsx: 왼쪽 보더 액센트 카드 컴포넌트
    • 프로젝트 전체에서 일관된 카드 디자인 유지
    • 왼쪽 보더 + 흰색 배경 + elevation 그림자
    • 선택적 Press 애니메이션
    • 적용된 컴포넌트: RoomItem, PrayerTitleItem, TitleCard

모달 컴포넌트 (src/common/components/modal/)

  • GlobalAlertModal.tsx: 전역 알림 모달 (Zustand store로 관리)
  • AlertModal.tsx: 로컬 알림 모달 (단일 확인 버튼)
  • ConfirmationModal.tsx: 확인/취소 모달 (위험한 작업 전 확인용)
  • ForceUpdateModal.tsx: 강제 업데이트 모달
  • MaintenanceModal.tsx: 유지보수 모달

로딩 컴포넌트 (src/common/components/loading/)

  • LoadingScreen.tsx: 전체 화면 로딩
  • LoadingSpinner.tsx: 인라인 스피너

Empty State 컴포넌트 (src/common/components/empty/)

  • 데이터가 없을 때 표시 (아이콘 + 메시지)

토스트 컴포넌트 (src/common/components/toast/)

  • react-native-toast-message 사용
  • 설정: toastConfig.ts

8. 도메인 별 구조 패턴

각 도메인(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 패턴 참고)

9. 코드 스타일 및 컨벤션

TypeScript

  • 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";)

Import 순서

  1. React/React Native
  2. 서드파티 라이브러리
  3. 내부 모듈 (@/ 경로)
  4. 타입 import

주석 및 포맷팅

  • 한국어 주석, TODO/FIXME/NOTE 태그 활용
  • 들여쓰기 2 spaces, 세미콜론 사용, 큰따옴표 우선

10. 개발 시 주의사항 및 베스트 프랙티스

1. 인증 처리

✅ DO

// api.ts의 interceptor가 자동으로 토큰을 관리하므로,
// 서비스 레이어에서는 토큰을 직접 다루지 않음
export const roomService = {
  fetchRooms: async () => {
    const response = await apiService.get("/v1/rooms");
    return response.data.data;
  },
};

❌ DON'T

// 서비스에서 직접 토큰을 가져오지 말 것
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;
  },
};

2. 에러 처리

✅ DO

// React Query의 onError 콜백 활용
export const useCreateRoomMutation = () => {
  return useMutation({
    mutationFn: roomService.createRoom,
    onError: (error) => {
      Toast.show({
        type: "error",
        text1: "오류",
        text2: error.message || "기도방 생성에 실패했습니다.",
      });
    },
  });
};

❌ DON'T

// 컴포넌트에서 try-catch로 일일이 처리하지 말 것
const handleCreateRoom = async () => {
  try {
    await createRoomMutation.mutateAsync({ name: "새 기도방" });
  } catch (error) {
    // ❌ 중복된 에러 처리
    alert("오류 발생");
  }
};

3. 상태 관리

✅ DO

// 서버 상태는 React Query로
const { data: rooms } = useRoomsQuery();

// 클라이언트 상태는 Zustand로
const { selectedRoom, setSelectedRoom } = useSelectedRoomStore();

❌ DON'T

// 서버 데이터를 useState에 저장하지 말 것
const [rooms, setRooms] = useState([]); // ❌

useEffect(() => {
  fetchRooms().then(setRooms); // ❌
}, []);

4. 캐시 무효화

✅ DO

// Mutation 성공 시 관련 쿼리 무효화
export const useCreateRoomMutation = () => {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: roomService.createRoom,
    onSuccess: () => {
      // 기도방 목록 쿼리 무효화 → 자동 재조회
      queryClient.invalidateQueries({ queryKey: [QueryKeys.ROOMS] });
    },
  });
};

❌ DON'T

// 수동으로 refetch 호출하지 말 것
const { refetch } = useRoomsQuery();

const handleCreateRoom = async () => {
  await createRoomMutation.mutateAsync({ name: "새 기도방" });
  refetch(); // ❌ invalidateQueries를 사용해야 함
};

5. 레이아웃 일관성

✅ DO

// 공통 레이아웃 컴포넌트 사용
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>]}
    />
  );
}

❌ DON'T

// 레이아웃을 매번 직접 구현하지 말 것
export default function MyScreen() {
  return (
    <View style={{ flex: 1 }}>
      <View style={{ height: 70 }}>{/* 헤더 */}</View>
      <View style={{ flex: 1 }}>{/* 본문 */}</View>
    </View>
  );
}

6. 타입 안전성

✅ DO

// API 응답 타입 명시
const response = await apiService.get<{ data: RoomResponse[] }>(
  "/v1/rooms"
);
return response.data.data; // RoomResponse[] 타입 보장

❌ DON'T

// any 타입 사용 금지
const response = await apiService.get("/v1/rooms"); // any
return response.data.data; // any

7. 컴포넌트 분리

✅ DO

// 재사용 가능한 작은 컴포넌트로 분리
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>
  );
}

❌ DON'T

// 모든 로직을 한 컴포넌트에 넣지 말 것
export default function RoomsScreen() {
  const { data: rooms } = useRoomsQuery();

  return (
    <View>
      {rooms?.map((room) => (
        <View key={room.id}>
          {/* 복잡한 카드 UI가 여기에 모두 들어감 */}
        </View>
      ))}
    </View>
  );
}

8. 네비게이션

✅ DO

import { router } from "expo-router";

// Expo Router 사용
router.push("/rooms/123");

❌ DON'T

import { useNavigation } from "@react-navigation/native";

// React Navigation의 navigation prop 직접 사용 금지
const navigation = useNavigation();
navigation.navigate("Rooms", { id: "123" }); // ❌

9. 스타일링

✅ DO

// StyleSheet.create 사용
const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: color.background,
  },
});

<View style={styles.container} />

❌ DON'T

// 인라인 스타일 남발 금지
<View style={{ flex: 1, backgroundColor: "#f0f0f0" }} /> // ❌

10. 모달 사용

✅ DO

import { useGlobalAlertModalStore } from "@/common/components/modal/stores/useGlobalAlertModalStore";

const { open } = useGlobalAlertModalStore();

open({
  title: "알림",
  message: "작업이 완료되었습니다.",
  confirmText: "확인",
});

❌ DON'T

// 매번 새로운 모달 컴포넌트를 만들지 말 것
const [modalVisible, setModalVisible] = useState(false);

<Modal visible={modalVisible}>
  <Text>작업이 완료되었습니다.</Text>
  <Button onPress={() => setModalVisible(false)}>확인</Button>
</Modal>

11. PagerView 사용 시 주의사항

⚠️ CRITICAL: PagerView는 wrapper와 충돌

문제: PagerView를 SafeAreaView, KeyboardAvoidingView 등의 wrapper로 감싸면 iOS 실제 기기에서 터치 이벤트가 차단됩니다.

증상:

  • 시뮬레이터에서는 정상 작동
  • 실제 iOS 기기에서 입력 필드가 터치되지 않음
  • 키보드가 올라오지 않음
  • scrollEnabled={false} 사용 시 더 심각

관련 이슈: react-native-pager-view#51, #382

✅ DO

// 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
  },
});

❌ DON'T

// 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>

주요 원칙

  1. 최소한의 wrapper만 사용: SafeAreaView + BackButtonHeader + PagerView 구조
  2. ScreenLayout 사용 금지: PagerView는 특수 케이스로 직접 구현
  3. 중간 View 최소화: PagerView를 가능한 한 얕은 레벨에 배치
  4. 실제 기기에서 테스트: 시뮬레이터에서는 문제가 안 보일 수 있음

배포 시 주의사항

중요: EAS Update 시 올바른 브랜치로 배포하는지 확인하세요!

# 프로덕션 배포
eas update --branch production

# 현재 브랜치 확인
eas branch:list

# 잘못된 브랜치로 배포하면 실제 사용자에게 변경사항이 적용되지 않음

체크리스트:

  • eas.json에서 production 빌드의 채널 확인
  • App Store/Play Store 앱이 어떤 채널을 구독하는지 확인
  • 배포 후 실제 기기에서 변경사항 확인

추가 참고 사항

디버깅 팁

  1. API 요청/응답 로깅:

    • src/common/apis/api.ts의 interceptor에 로그 추가
  2. React Query DevTools:

    • 개발 중 React Query 상태 확인
    • CustomQueryClientProvider에서 활성화 가능
  3. Zustand DevTools:

    • Redux DevTools로 Zustand 상태 모니터링

성능 최적화

  1. React.memo 사용:

    • 불필요한 리렌더링 방지
    • 리스트 아이템 컴포넌트에 적용
  2. useMemo / useCallback 사용:

    • 복잡한 계산 결과 메모이제이션
    • 콜백 함수 메모이제이션
  3. FlatList 최적화:

    • keyExtractor 명시
    • getItemLayout 사용 (고정 높이일 경우)
    • removeClippedSubviews 활성화

보안

  1. 토큰 저장:

    • 반드시 SecureStore 사용
    • AsyncStorage 사용 금지
  2. 민감 정보:

    • 환경 변수 사용 (app.config.js)
    • Git에 커밋하지 않을 것
  3. API 키:

    • Firebase 설정은 google-services.json / GoogleService-Info.plist
    • Git에 커밋하지 않을 것

마무리

이 가이드는 기도함께 프로젝트의 현재 아키텍처와 패턴을 반영한 종합 문서입니다.

새로운 기능 개발 시 체크리스트

  • 도메인 디렉토리 구조 따르기 (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): 초기 버전