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 modeFront‑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을 구성하고 네트워크 메타데이터를 가져오려다 보니 커밋된 프로젝트 설정을 사용하지 않아 결론을 내지 못함.
이러한 사항들은 이미 프로덕션‑레디 형태를 갖춘 앱에 대한 전형적인 다음 단계 개선 사항입니다.