3분에서 500ms로: 말이 안 되는 가입 버그
3분의 가입 지연을 추적했더니 쓰기와 읽기 간의 데이터베이스 복제 지연으로 인해 존재하면서 동시에 존재하지 않는 슈뢰딩거의 사용자였습니다.
사용자가 전혀 말이 안 되는 버그를 보고할 때의 그 느낌 아시죠? "가입 후 앱이 로드되는 데 3분이 걸려요." 3분? 그건 로딩 시간이 아니라 커피 한 잔 마실 시간입니다. 이것은 DIALØGUE 역사상 가장 이상한 버그 중 하나를 추적한 이야기입니다.
미스터리의 시작
순진하게 시작되었습니다. 새 사용자가 Google SSO로 가입하고 DIALØGUE를 시도해보려고 흥분해있었습니다. 그런데... 아무것도. 정확히 아무것도가 아니라 - 아름답게 디자인된 로딩 스켈레톤을 받았습니다. 3분이나요.
이상한 점? 새 사용자에게만 발생했습니다. 기존 사용자는 즉시 로그인할 수 있었습니다. 그리고 일관적이지도 않았습니다 - 때로는 1분, 때로는 3분, 가끔은 즉시 작동했습니다.
첫 번째 생각: "콜드 스타트 문제겠지." (내레이터: 콜드 스타트 문제가 아니었습니다.)
조사
라운드 1: 프론트엔드 탓
```typescript
// 첫 번째 용의자: 프로필 로딩 훅
useEffect(() => {
if (user) {
fetchUserProfile(); // 이것이 영원히 걸리고 있었음
}
}, [user]);
```
여기저기에 타이머를 추가했습니다. API 호출이 실제로 3분이 걸리고 있었습니다. 하지만 왜? 백엔드는 데이터를 반환하거나 에러를 내야지, 그냥... 기다려서는 안 됩니다.
라운드 2: 백엔드 탓
Supabase Edge Functions를 파고들었습니다:
```typescript
// 사용자 프로필 가져오기 Edge Function
const { data: profile } = await supabase
.from('users')
.select('*')
.eq('id', userId)
.single();
if (!profile) {
// 새 사용자 - 프로필 생성
await createUserProfile(userId);
}
```
이것은 괜찮아 보였습니다. 빠를 것이 맞죠? 로깅을 더 추가할 시간입니다.
라운드 3: 미스터리가 깊어지다
모든 곳에 로그를 추가한 후(정말 모든 곳에), 기이한 것을 발견했습니다:
[00:00] 사용자가 Google로 로그인
[00:01] Auth 트리거 실행 - 사용자 레코드 생성
[00:01] 프론트엔드가 프로필 요청
[00:01] Edge Function이 사용자 조회... 결과 없음
[00:02] Edge Function이 사용자 생성 시도...
[00:02] 데이터베이스 제약 조건 에러: 사용자가 이미 존재함
[00:03] Function 재시도...
[03:00] Function이 최종 타임아웃
잠깐, 뭐? 사용자가 존재하지 않는데, 이미 존재한다고? 슈뢰딩거의 사용자? T.T
깨달음
눈이 아플 때까지 데이터베이스 로그를 응시한 후, 마침내 보았습니다. 데이터베이스에 경쟁하는 프로세스가 있었습니다:
- Supabase Auth 트리거: 가입 시 사용자 레코드 생성
- Edge Function: 찾을 수 없으면 사용자 생성 시도
- 데이터베이스 복제 지연: 트리거의 INSERT가 읽기 복제본에 아직 복제되지 않음
일어나고 있었던 일:
-- Auth 트리거 (기본 데이터베이스에서)
INSERT INTO users (id, email) VALUES ($1, $2);
-- Edge Function (복제본에서 읽기)
SELECT * FROM users WHERE id = $1; -- 아무것도 반환하지 않음!
-- Edge Function (도우려고 시도)
INSERT INTO users (id, email) VALUES ($1, $2); -- 충돌!
함수는 지수 백오프로 재시도하며, 각 시도가 같은 경합 조건에 부딪혔습니다:
- 복제가 마침내 따라잡거나 (1-3분)
- 함수가 타임아웃되거나 (3분)
업데이트: 진짜 원인과 최종 견고한 솔루션
초기 게시 후, 디버깅을 계속했고, 백엔드 변경 사항이 개선이기는 했지만 미스터리의 근본은 여전히 찾기 어려웠습니다. 돌파구는 백엔드에서 클라이언트 측 하이드레이션 및 인증 흐름으로 초점을 옮겼을 때 왔습니다.
진짜 원인: 클라이언트 측 경합 조건
문제는 느린 데이터베이스 트리거나 콜드 Edge Function이 아니었습니다. 진짜 문제는 전형적인 클라이언트 측 경합 조건이었습니다:
- OAuth 리디렉트: 새 사용자가 Google로 로그인하고 앱으로 다시 리디렉트됩니다.
- 비동기 세션: Supabase 클라이언트 라이브러리(
supabase-js)가 세션을 설정하기 위해 URL에서 토큰을 처리하기 시작합니다. 이것은 비동기 프로세스입니다. - 조기 렌더링: 그러나 React 앱은 즉시 렌더링합니다. 2단계의 비동기 프로세스가 완료되기 전에 사용자의 세션을 요청합니다.
- 실패: 앱은
null세션을 받고, 사용자가 로그인하지 않았다고 결론 짓고, 빈 화면이나 에러 상태를 렌더링합니다. 잠시 후 세션이 사용 가능해지지만, 이미 늦었습니다—UI가 이미 결정을 내렸습니다.
클라이언트 측 재시도를 추가하는 것과 같은 초기 해결책은 이 근본적인 경합 조건과 싸우는 증상일 뿐이었습니다.
솔루션: 인증의 단일 진실 소스
올바르고 최종적인 솔루션은 프론트엔드 인증 상태 관리를 진정으로 이벤트 기반이고 견고하게 재설계하는 것이었습니다.
1. SupabaseProvider의 중앙집중화된 로직:
SupabaseProvider를 인증의 단일 권위적 진실 소스로 리팩토링했습니다. 다른 훅의 모든 다른 리스너와 검사를 제거했습니다.
2. onAuthStateChange를 올바르게 사용:
수정의 핵심은 Supabase의 onAuthStateChange 리스너에 전적으로 의존하는 것이었습니다.
// SupabaseProvider.tsx의 단순화된 로직
export function SupabaseProvider({ children }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true); // 로딩 상태로 시작
useEffect(() => {
const { data: { subscription } } = supabase.auth.onAuthStateChange(
(event, session) => {
setUser(session?.user ?? null);
// 이 리스너가 실행되어야만 인증이 "완료"된 것으로 간주
setLoading(false);
}
);
return () => subscription.unsubscribe();
}, []);
// ...
}
이 패턴은 Supabase가 사용자의 세션이 유효한지 null인지 확인할 때까지 전체 애플리케이션이 loading 상태를 유지하도록 보장합니다. 더 이상 경합 조건이 없습니다.
해피 엔딩
DIALØGUE는 이제 1초 이내에 새 사용자를 온보딩합니다. 가입 중 더 이상 커피 브레이크가 없습니다. 뭔가 망가뜨렸나 궁금해하는 혼란스러운 사용자도 없습니다.
이 수정은 3주째 프로덕션에서 운영되고 있습니다. 타임아웃 이슈 제로. 경합 조건 제로. 처음부터 그래야 했던 것처럼 부드럽고 빠른 가입.
이것을 디버깅하는 데 일주일을 보낸 가치가 있었나요? 새 사용자가 가입 후 몇 분 내에 원활하게 첫 팟캐스트를 만드는 것을 볼 때 — 절대적으로요. :D
무언가가 동시에 존재하면서 존재하지 않는 버그를 추적해본 적 있으신가요? 모든 개발자에게 적어도 하나의 슈뢰딩거의 버그 이야기가 있을 것 같습니다. 여러분의 이야기를 듣고 싶습니다!
감사합니다,
Chandler
이제 빨라진 가입을 시도해 보고 싶으시나요? DIALØGUE에서 AI 팟캐스트를 만들어 보세요. 더 이상 3분이 걸리지 않을 것을 약속합니다 :)





