Ilere: Expo와 Supabase를 활용한 투명한 렌탈 마켓플레이스 구축
I’m happy to translate the article for you, but I need the full text that you’d like translated. Could you please paste the content you want converted to Korean? Once I have the text, I’ll keep the source line unchanged and translate the rest while preserving all formatting, markdown, and technical terms.
Ilere – 모바일 주택 마켓플레이스
Ilere는 이바단, 라고스, 아부자, 포트하코트와 같은 나이지리아 도시를 대상으로 하는 모바일 주택 마켓플레이스입니다. 핵심 아이디어는 코드에서 확인할 수 있듯이, 세입자는 명확한 수수료 내역이 표시된 매물을 탐색하고, 매물을 열어 에이전트들의 경쟁 입찰을 받으며, 가장 좋은 제안을 선택하고 앱 내에서 대화를 이어갑니다. 실제 문제는 단순히 매물을 찾는 것이 아니라, 특히 에이전트 가격 책정 및 조정과 같은 임대 시장의 불투명한 부분을 비교할 수 있을 만큼 가시화하는 것입니다.
현재 코드베이스는 이 흐름을 넘어선 기능도 제공합니다. 세입자는 다음을 할 수 있습니다:
- 주택 저장
- 집주인 매물에 직접 연락
- 인앱 및 푸시 알림 수신
- 남용 사용자 신고 또는 차단
에이전트와 집주인은 매물을 게시할 수 있습니다. 그 결과, 프론트엔드와 데이터베이스 모두에 실제 워크플로우 경계가 내장된 꽤 의견이 반영된 모바일 제품이 탄생합니다.
아키텍처 개요
아키텍처는 좋은 의미에서 직관적입니다.
-
Shell –
src/app/_layout.tsx는 폰트를 로드하고, 공유QueryClient를 보유하며,authService.getCurrentUser()를 통해 인증 상태를 초기화하고, 인증 변경을 구독하며, 알림 핸들러를 등록하고, 토스트 호스트를 마운트합니다. -
Routing –
src/app아래 Expo Router를 사용한 파일 기반 라우팅입니다. 라우트 그룹은 의미가 있습니다:(auth)– 온보딩 및 로그인(tabs)– 기본 앱 쉘- 상세 라우트 –
house/[id].tsx,request/[id]/bids.tsx,chat/[id].tsx는 더 깊은 흐름을 위해 사용됩니다
-
State Management – 전역 클라이언트 상태는 거의 존재하지 않습니다.
// src/features/auth/store.ts import { create } from "zustand"; interface AuthState { user: User | null; isLoading: boolean; isResettingPassword: boolean; // … } export const useAuthStore = create((set) => ({ user: null, isLoading: false, isResettingPassword: false, // … }));그 외 모든 것은 서버 상태로 간주되며 React Query에 존재합니다. 주택, 요청, 입찰, 채팅, 메시지, 알림, 저장된 주택은 모두 Supabase에서 생성되며 실시간 구독으로 인해 클라이언트 아래에서 자주 변경되므로 이를 클라이언트 스토어에 보관하면 불필요한 중복이 발생합니다.
-
API Layer – 화면은 기능 훅을 호출하고, 훅은
src/services/supabase/*에 있는 서비스 모듈을 호출하며, 서비스 모듈이 실제 Supabase 쿼리를 담당합니다.// src/services/supabase/client.ts import { createClient } from "@supabase/supabase-js"; import AsyncStorage from "@react-native-async-storage/async-storage"; export const supabase = createClient(supabaseUrl, supabaseAnonKey, { auth: { storage: AsyncStorage, autoRefreshToken: true, persistSession: true, detectSessionInUrl: false, }, });이 분리 구조는 대부분의 흥미로운 동작이 단순한 fetch가 아니기 때문에 효과적입니다. 서비스 레이어는 관계형 select를 많이 사용합니다.
housesService.getHouses()는house_photos와 게시 사용자를 한 번의 라운드 트립으로 조인하고,src/services/supabase/chat.ts는 참여자와 최신 메시지를 모두 로드한 뒤 클라이언트에서last_message와unread_count를 파생합니다. Supabase는 순수 데이터베이스 클라이언트라기보다 백엔드 쿼리 인터페이스에 가깝게 동작합니다.
Source:
주목할 만한 구현 세부 사항
Auth State Change 우회
// src/services/supabase/auth.ts
return supabase.auth.onAuthStateChange((event, session) => {
// …
setTimeout(async () => {
const { data, error } = await supabase
.from("users")
.select("*")
.eq("id", userId)
.single();
// …
}, 0);
});
콜백은 의도적으로 동기적으로 유지되고 setTimeout으로 후속 작업을 연기합니다. 이는 인증 상태 핸들러가 Supabase에 동기적으로 다시 호출될 때 발생하는 교착 상태 경고에 대한 실용적인 우회 방법입니다.
검색 정규화
// src/services/supabase/houses.ts
function sanitizeSearchTerm(input: string) {
return (
input
.trim()
.replace(/\s+/g, " ")
.replace(/[^a-zA-Z0-9\s-]/g, " ")
.replace(/\s+/g, " ")
.trim() || undefined
);
}
리스트 피드는 PostgREST .or(...) 필터를 만들기 전에 자유 텍스트 쿼리를 정규화합니다. 디바운싱(useDebounce(search, 350))과 src/features/houses/hooks/useHouses.ts의 useInfiniteQuery와 결합하여 백엔드에 과도한 요청을 보내지 않으면서도 반응성을 유지합니다.
입찰 흐름
세입자가 입찰을 위해 리스트를 열 때(src/app/house/[id].tsx), 화면은:
housesService.getFeeBreakdown()으로 수수료 내역을 로컬에서 계산합니다.- 해당 리스트가 집주인 전용 직접 연락인지 확인합니다.
useCreateRequest()를 사용해 요청을 엽니다.
뮤테이션은 requestsService.createRequest()(src/services/supabase/requests.ts)에 도달하며, 여기서는:
- 가용성 확인
- 입찰 흐름에 대한 집주인 리스트 거부
- 중복 활성 요청 방지
- 데이터베이스 고유 제약 조건이 발생하면 재조회하여 레이스 상황을 허용
낙찰 입찰 선택
// src/services/supabase/requests.ts
async function selectBid(requestId: string, bidId: string) {
// 1. Update request status
// 2. Mark chosen bid as accepted
// 3. Fetch winning agent
// 4. Create chat between tenant and agent
await chatService.createChat(tenantId, bid.agent_id, requestId);
}
제품이 “마켓플레이스 협상”에서 “진행 중인 대화”로 전환되는 과정이 코드에 명시적으로 드러납니다.
채팅 – 실시간 및 낙관적 업데이트
// src/features/chat/hooks/useChat.ts
const optimistic: Message = {
id: `optimistic-${Date.now()}`,
chat_id: chatId,
sender_id: user!.id,
message: payload.message,
message_type: payload.messageType,
data: payload.data,
read: false,
created_at: new Date().toISOString(),
};
useSendMessage()는 낙관적인 메시지를 즉시 삽입하고, 성공 시 서버에서 확인된 메시지로 교체하며, 오류 시 이를 제거합니다. 이 조정 방식은 단순하면서도 효과적입니다.
요약
Ilere의 코드베이스는 관심사의 명확한 분리를 보여줍니다:
- Routing & layout은 파일 기반이며 선언적입니다.
- State는 최소한의 클라이언트‑사이드 Zustand 스토어(auth)와 서버‑사이드 React Query로 나뉩니다.
- Service layer는 Supabase 쿼리를 추상화하여 관계형 선택, 실시간 구독, 비즈니스 로직(입찰, 채팅, 요청 생성)을 처리합니다.
- Defensive patterns(인증 상태 타임아웃, 검색 정규화, 옵티미스틱 UI)는 앱을 반응형이고 견고하게 유지합니다.
전체적으로, 이 아키텍처는 제품의 핵심 문제와 잘 맞습니다: 나이지리아 세입자들에게 불투명한 임대 시장 가격과 조정을 가시화하고 비교 가능하게 만드는 것.
// Example snippet (original context was truncated)
false,
created_at: new Date().toISOString(),
};
전체‑screen Controls
Enter fullscreen mode
Exit fullscreen mode
Front‑end: src/app/chat/[id].tsx
message_type: "house_context"를 지원합니다.- 목록이나 선택된 요청에서 채팅을 시작할 때, 화면은 첫 번째 메시지에 구조화된 부동산 카드를 시드(seed)하여 양쪽 모두 자유 텍스트 회상에 의존하지 않고 공유 컨텍스트를 제공합니다.
- 로직:
- 페이로드를 정규화합니다.
- 시드된 컨텍스트를 중복 제거합니다.
- 메시지를 날짜별로 그룹화합니다.
- 모더레이션 상태를 조정합니다.
백엔드: Supabase
기본 스키마 및 정책 – supabase/migrations/001_initial.sql
- 테이블, 인덱스, RLS 정책, 트리거 및 실시간 퍼블리케이션을 정의합니다.
- 신뢰 모델:
- 사용자는 공개 엔티티(주택, 프로필)를 읽을 수 있습니다.
- 쓰기는
auth.uid()에 의해 엄격히 제한됩니다.
채팅 중재 – supabase/migrations/033_chat_moderation.sql
user_blocks,user_reports를 추가합니다.- 트리거는 차단된 쌍에 대한 메시지 삽입을 거부합니다.
알림 흐름
- 새로운 입찰 및 메시지는 트리거를 통해
notifications에 행을 생성합니다. - 이후 마이그레이션에서 추가:
- 푸시 팬아웃.
- 도시 기반 신규 매물 알림.
- 엣지 함수 –
supabase/functions/push-notify/index.ts:- 얇은 래퍼; 알림 시점을 Postgres가 결정합니다.
- 함수는 페이로드를 Expo로 전달합니다.
Practical Performance & UX Details
-
Saved homes –
src/features/houses/hooks/useSavedHomes.ts에서 낙관적 캐시 업데이트. -
Media uploads –
src/services/supabase/storage.ts의 상수를 사용해 제한:export const MAX_PHOTO_SIZE_BYTES = …; export const MAX_VIDEO_SIZE_BYTES = …; -
Notification display –
notificationService에서 라우트 인식; 사용자가 이미/inbox또는/chat/...에 있을 때 포그라운드 알림을 억제. -
Safe‑area handling & keyboard avoidance – 다음 전반에 걸쳐 일관되게 적용:
- App shell
- Tabs
- Posting flow
- Chat UI
리팩토링 생각
- Stack – 재작성 필요 없음; Expo Router, React Query, Zustand(인증 전용), Supabase가 일관된 스택을 이룸.
- Opportunities:
- 쿼리‑키 규칙 통합 (현재 얇고 부분적으로 중복됨).
- 비즈니스 규칙 중앙화 (현재 서비스, 화면, SQL 트리거에 흩어져 있어 불변성 감사를 단순화).
- 스토리지 업로드 개선: 전체 파일을 Base64로 읽는 대신 스트리밍하여 메모리 사용량을 줄임.
- 도구 체인 강화:
expo lint가 자동으로 ESLint을 구성하고 네트워크 메타데이터를 가져오려다 보니 커밋된 프로젝트 설정을 사용하지 않아 결론을 내지 못함.
이러한 사항들은 이미 프로덕션‑레디 형태를 갖춘 앱에 대한 전형적인 다음 단계 개선 사항입니다.