01 About
작게 만들고,
끝까지 확인합니다.
안녕하세요, 조현석입니다. 웹 화면에서 데이터베이스까지 연결하며, 실제로 동작하는 서비스를 만듭니다. 구현 이후에 만나는 오류와 성능 문제도 개발의 일부로 생각합니다.
Looking For Friend에서 제품 의사결정과 end-to-end 개발·검증·운영을 책임졌습니다. AI를 코드 작성, 분석, 테스트에 활용하며 설계 판단과 결과 검증을 맡았습니다.
02 Selected project
Looking For Friend
LFFDiscord에서 함께 게임할 친구를 찾는 서비스
게임과 태그로 사람을 찾고, 상대가 수락하면 대화를 시작합니다. 탐색부터 실시간 DM까지의 흐름과 팔로우·차단·신고·관리자 제재 기능을 구현했습니다.
React 19 · React Router 7 · TypeScript · Vite
Supabase Auth / Postgres / RLS / Realtime / Storage
Drizzle · Playwright · Vercel
시스템 구조 살펴보기
브라우저 ↔ Supabase: Discord OAuth 인증 · Realtime 구독
일반 DB 접근에는 Drizzle을 사용하고 앱 프로세스의 기본 풀 크기를 2로 제한했습니다. transaction pooler(6543) 옵션과 마이그레이션용 direct URL을 분리했습니다.
검색은 서버 전용 RPC로 범위를 제한하고 anon 직접 호출을 차단했습니다. 이 검색 경로의 부하 검증은 앱의 Drizzle 풀을 통과하지 않습니다.
03 Decisions & fixes
만들면서 해결한 문제들
01가입 실패를 고치면서 보안 경계 지키기외부 아바타와 Storage 검증 규칙의 충돌
문제Discord 신규 가입 시 provider의 외부 avatar_url을 프로필에 복사했지만, DB media trigger는 본인의 Storage 파일만 허용했습니다. 이 충돌로 프로필 INSERT가 실패했습니다.
해결가입 시 아바타 없는 프로필을 먼저 만들고, 이후 검증된 업로드를 사용하도록 분리했습니다. 내부 DB 오류는 고정된 안내로 처리했습니다.
판단가입을 통과시키기 위해 미디어 검증을 느슨하게 만드는 대신, 초기 프로필 생성과 이미지 등록의 책임을 나눴습니다.
PR #27 · 가입 수정 (새 탭)02차단 상태를 화면과 DB에서 일치시키기DM의 읽기·쓰기·UI 상태를 함께 정리
문제차단·탈퇴·제재된 상대가 DM에 노출되거나, 화면에서 시도한 동작이 DB 오류로 끝나는 상황이 있었습니다.
해결공통 양방향 차단 규칙을 정하고 RLS, RPC, 모든 DM 읽기·쓰기 경로와 UI 상태에 적용했습니다. 허용되는 대화와 금지되는 동작을 함께 E2E로 검증했습니다.
판단화면에서 버튼을 숨기는 것에 그치지 않고, 데이터 접근 규칙과 사용자가 보는 상태가 같은 기준을 따르도록 했습니다.
PR #19 · DM 접근 규칙 (새 탭)03실시간 이벤트가 누락되어도 복구하기알림은 신호로, 읽음 상태는 DB를 기준으로
설계Realtime은 새 데이터를 확인하는 wake-up 신호로 사용했습니다. unread와 high-water marker를 DB에 영속화해 읽음 상태의 정본으로 삼았습니다.
복구재연결이나 이벤트 유실 시 DB 상태를 다시 읽어 복구하도록 구성했습니다.
검증Broadcast 전환은 브라우저 실측에서 회귀가 드러나 철회했습니다. 구조가 단순해 보여도 실제 사용자 경로에서 확인한 결과를 우선했습니다.
04 Measured, with limits
수치와 함께, 측정한 범위도.
합성 프로필 10,000개로 검색 RPC를 검증했습니다. 최고 200개 동시 요청을 5회 실행한 구간의 결과입니다.
- 동시 요청
- 200개
- p95 응답 시간
- 725.95ms
- 오류율
- 0%
2026.08.15 · warm local Data API → Postgres
Vercel·앱 loader·OAuth를 포함한 production E2E 검증은 아닙니다.
측정 조건과 한계
25 / 50 / 100 / 200 동시 요청을 각각 5회 burst로 실행했습니다. 최고 구간은 누적 1,000요청이며, p99 825.68ms, 처리량 182.03req/s를 기록했습니다. 요청 timeout은 5초, retry는 없었습니다.
필터·응답 개수·순서를 확인했고, sampled active query는 최대 11, deadlock과 temp bytes는 0이었습니다.
keyset(bumped_at, profile_id), trigram·partial index, EXISTS 필터를 적용하고 31행 제한 후 aggregate하도록 설계했습니다.
이 결과는 1,000 동시 사용자나 1,000RPS를 뜻하지 않습니다. 1,000 / 2,500 동시 사용자 시험은 계획·하네스 지원 단계이며 통과 근거는 없습니다.
검색 검증 문서 (새 탭) 부하 스크립트 (새 탭)Preview에서 드러난 미달 항목
2026.08.12, 50VU Preview 테스트에서는 모든 경로가 목표를 통과하지는 못했습니다.
| 경로 | 요청 | p95 | p99 |
|---|---|---|---|
| Explore | 1,049 | 461ms | 873ms |
| Navigation | 1,058 | 485ms | 1,253ms |
| DM list | 1,042 | 680ms | 1,291ms |
각 경로의 오류는 0이었고 Explore의 429도 0이었습니다. Navigation은 p99 기준 SLO에 미달했습니다. 세션 6개를 순환한 시험이므로 독립된 50명의 OAuth 처리 능력을 입증하지 않습니다.
Preview 검증 기록 (새 탭)05 Reflection
잘 만드는 것, 그리고 쓰이게 하는 것.
서비스를 구현하고 검증하는 과정과 실제 사용자를 확보하는 과정은 달랐습니다. LFF는 사용자 확보에 어려움을 겪었습니다. 기술적 완성도만으로 제품의 수요와 지속적인 사용을 증명할 수는 없었습니다.
이 경험을 통해 구현 결과와 사업 검증을 구분해 바라보게 됐습니다. 다음에는 더 작은 사용자 흐름부터 만들고, 실제로 쓰이는 이유를 확인하는 실험을 개발과 함께 진행하려 합니다.
