Skip to content
··7분 읽기

빈 화면은 신뢰를 죽입니다. 저에게는 31개가 있었습니다.

SaaS에서 31개의 빈 화면을 발견했습니다—멀티테넌시가 데이터 접근만의 문제가 아니라 URL 컨텍스트의 문제라는 것을 잊었기 때문입니다. Claude Code가 하룻밤 만에 모두 고치는 것을 어떻게 도왔는지 소개합니다.

11월 1일, 오전 8시 47분. 이메일을 받았습니다.

"Chandler씨 - 'View'를 클릭했더니 빈 페이지가 나왔어요. 인터뷰 데이터가 다 사라진 건가요?"

속이 철렁했습니다. 빈 페이지는 버그가 아닙니다. 신뢰 파괴자입니다.

개발자 콘솔을 열었습니다. 라우트를 확인했습니다. 에이전시 사용자가 클라이언트 "Acme Corp"을 보면서 버튼을 클릭했는데... URL이 /clients/acme-corp/agents/agent-name/results/123에서 그냥 /agent-name/results/123으로 바뀌었습니다.

클라이언트 컨텍스트를 잃었습니다. React Router가 라우트를 찾을 수 없었습니다. 빈 화면.

"좋아, 버그 하나네," 라고 생각했습니다. "고치고 넘어가자."

Claude Code를 설정하여 코드베이스에서 유사한 패턴을 분석하게 한 후, 토요일을 가족과 보냈습니다. 점심. 심부름. 아이들. 평소와 같이.

늦은 오후, 다시 확인해 봤습니다. Claude Code가 패턴을 찾았는데—예쁘지 않았습니다.

오후 7시 42분까지 14개의 버그를 수정했습니다. 오후 7시 48분까지 8개 더 발견했습니다. 오후 7시 56분까지 총 31개에 달했습니다.

모두 같은 근본 원인. 모두 같은 수정. 모두 멀티테넌시를 구축할 때 한 가지를 잊었기 때문입니다: 내비게이션은 데이터만의 문제가 아니라 컨텍스트의 문제입니다.

---

얼마나 심각했나요?

솔직하게 이 31개의 버그가 실제로 무엇을 의미했는지 말씀드리겠습니다:

사용자에게:

- 에이전시 사용자가 빈 화면에 부딪힘 (버그가 아니라 깨진 것처럼 보임)

- 세션 중간에 워크플로 진행 상황 손실

- "이 플랫폼이 우리 클라이언트에게 쓸 만큼 안정적인가?"

- 모든 빈 화면 = 트라이얼 취소에 한 걸음 더 가까이

저에게:

- 하루 5건 이상의 버그 보고 (모두 내비게이션 관련)

- 각각 개별적으로 디버깅하는 데 2-3시간

- 새 기능을 출시할 수 없음 (화재 진압에 바쁨)

- 진짜 두려움: "다른 것도 망가뜨렸는데 아직 모르는 거면?"

존재론적 질문: 내비게이션조차 제대로 작동시키지 못한다면, 왜 누군가가 STRAŦUM에 클라이언트의 마케팅 전략을 맡기겠습니까?

빈 화면은 무엇보다 빠르게 신뢰를 죽입니다.

---

모든 것의 시작이 된 버그

정확히 무슨 일이 있었는지 보여드리겠습니다.

사용자 흐름 (작동했어야 하는 것):

1. /clients/acme-corp/agents/analysis로 이동

2. "Start Session" 클릭

3. 분석 완료

4. "View Results" 클릭

5. /clients/acme-corp/agents/analysis/results/123에서 결과 확인

실제로 일어난 일:

1. ✅ /clients/acme-corp/agents/analysis (좋음)

2. ✅ 세션 시작 (작동)

3. ✅ 분석 완료 (데이터 저장됨)

4. ❌ **"View Results" 클릭 → 빈 페이지**

5. ❌ URL이 /analysis/results/123으로 변경 (클라이언트 컨텍스트 손실)

React Router가 /analysis/results/123에서 라우트를 찾았습니다. 에이전시 사용자에게는 존재하지 않았습니다. 아무것도 렌더링하지 않았습니다.

사용자가 보는 것: 빈 흰 화면. 에러 메시지 없음. 로딩 스피너 없음. 그냥... 아무것도 없음.

---

(실제로는 Claude Code가) 파헤치기 시작했을 때 발견한 것

깨진 코드가 어떻게 생겼는지 보여드리겠습니다:

```typescript
// AgentPage.tsx (정제됨 - 깨진 패턴)
import \{ useParams, useNavigate \} from 'react-router-dom';

export function AgentPage() \{
  const { clientSlug \} = useParams<\{ clientSlug: string \}>();
  const navigate = useNavigate();

  const handleViewResults = (sessionId: string) => \{
    // 문제: 하드코딩된 라우트, 클라이언트 컨텍스트 없음
    navigate(`/agent-name/results/${sessionId\}`);
  };

  return (
    // ... 컴포넌트
  );
}
```

문제가 보이시나요?

URL에서 clientSlug를 추출했습니다. 데이터를 가져오는 데 사용했습니다. 하지만 내비게이션할 때, 완전히 잊어버렸습니다.

에이전시 라우트는 이렇게 생겼습니다: /clients/acme-corp/agents/agent-name

SME 라우트는 이렇게 생겼습니다: /agent-name

SME 패턴을 하드코딩했습니다. 에이전시 사용자 = 깨짐.

---

깨달음: 이런 게 31개가 있었다

오후 2시 15분. 첫 번째 버그를 수정했습니다. 커밋. 기분이 좋았습니다.

오후 3시 42분. 다른 에이전트에서 또 하나를 발견했습니다. 같은 패턴. 수정했습니다.

오후 4시 18분. 또 다른 에이전트. 같은 것.

오후 5시 30분. 멈추고 화면을 10분 동안 쳐다봤습니다.

모든 에이전트 페이지에 같은 버그가 있었습니다. 내비게이션하는 모든 중첩 컴포넌트. "Go to..." 버튼이 있는 모든 공유 UI 요소.

하나씩 수정해서 2일을 보낼 수 있었습니다. 아니면 패턴을 찾아서 체계적으로 모두 수정할 수 있었습니다.

체계적을 선택했습니다.

---

수정: 컨텍스트 인식 내비게이션

모든 컴포넌트에서 useParams() 대신, Claude Code에게 Context 프로바이더를 만들어 달라고 했습니다:

```typescript
// contexts/ClientContext.tsx
import \{ createContext, useContext \} from 'react';
import \{ useParams \} from 'react-router-dom';

interface ClientContextValue \{
  clientSlug: string | null;
\}

const ClientContext = createContext<ClientContextValue | null>(null);

export function ClientContextProvider(\{ children \}: \{ children: React.ReactNode \}) \{
  // 레이아웃 레벨에서 clientSlug를 한 번만 추출
  const { clientSlug \} = useParams<\{ clientSlug: string \}>();

  return (
    <ClientContext.Provider value=\{{ clientSlug: clientSlug || null \}}>
      \{children\}
    </ClientContext.Provider>
  );
}

export function useClientContext() \{
  const context = useContext(ClientContext);
  if (!context) {
    throw new Error('useClientContext must be used within ClientContextProvider');
  \}
  return context;
}
```

그런 다음 에이전시 라우트를 래핑했습니다:

```typescript
// App.tsx
<Route path="/clients/:clientSlug/*" element={
  <ClientContextProvider>
    <ClientLayout />
  </ClientContextProvider>
}>
  <Route path="agents/analysis" element={<AnalysisAgent />} />
  <Route path="agents/strategy" element={<StrategyAgent />} />
  \{/* ... 모든 클라이언트 범위 라우트 */\}
</Route>
```

이제 내부의 모든 컴포넌트가 params가 아닌 context를 통해 clientSlug에 접근할 수 있습니다.

라우팅 헬퍼 (같은 if/else를 20번 입력하면 지겹기 때문):

```typescript
// hooks/useContextRoute.ts
import \{ useClientContext \} from '@/contexts/ClientContext';

export function useContextRoute() \{
  const { clientSlug \} = useClientContext();

  const buildRoute = (route: string) => \{
    if (clientSlug) {
      // 에이전시 라우트: /clients/acme-corp/agents/...
      const cleanRoute = route.startsWith('/') ? route.slice(1) : route;
      return `/clients/${clientSlug\}/$\{cleanRoute\}`;
    }
    // SME 라우트: /agent-name/...
    return route;
  };

  return \{ buildRoute, clientSlug \};
}
```

사용법 (훨씬 깔끔):

```typescript
// AgentPage.tsx (정제됨 - 수정된 패턴)
import \{ useClientContext \} from '@/contexts/ClientContext';
import \{ useNavigate \} from 'react-router-dom';
import \{ useContextRoute \} from '@/hooks/useContextRoute';

export function AgentPage() \{
  const { clientSlug \} = useClientContext();
  const \{ buildRoute \} = useContextRoute();
  const navigate = useNavigate();

  const handleViewResults = (sessionId: string) => \{
    // SME와 에이전시 사용자 모두에게 작동
    navigate(buildRoute(`agents/analysis/results/${sessionId\}`));
  };

  return (
    // ... 컴포넌트
  );
}
```

하나의 헬퍼 함수. 컨텍스트 인식. 두 사용자 유형 모두에게 작동.

---

체계적 수정: 하루, 31개 파일

패턴을 확보하자, 기계적인 작업이 되었습니다.

2025년 11월 1일 - 스프린트

오후 (2시 - 7시) - 발견 및 분류:

- 모든 에이전트 페이지에서 패턴 발견

- Context 프로바이더와 라우팅 헬퍼 구축

- 첫 번째 에이전트에서 접근 방식 테스트

저녁 (7:00 PM - 8:00 PM) - 체계적 수정:

7:42 PM - 첫 번째 웨이브 (14개 버그):

```
fix(multi-tenant): 14개의 내비게이션 버그 수정 및 모든 에이전트를 useClientContext()로 리팩토링

프론트엔드 변경 (8개 파일):
- 6개의 에이전트를 useClientContext() hook으로 리팩토링
- 여러 에이전트에서 클라이언트 컨텍스트를 잃는 14개의 내비게이션 버그 수정

백엔드 변경 (5개 파일):
- 모든 저장 작업에 client_id 매개변수 추가
- client_id를 적절히 추출하도록 기본 클래스 업데이트

변경된 파일: 14
추가: +732
삭제: -164
```

7:48 PM - 두 번째 웨이브 (8개 추가 버그):

```
fix(multi-tenant): 전략 페이지에서 8개의 내비게이션 버그 수정

- 클라이언트 컨텍스트를 잃는 8개의 내비게이션 버튼 수정
- 총 수정된 버그: 22개 (14 + 8)

변경된 파일: 1
추가: +71
삭제: -23
```

7:56 PM - 마지막 웨이브 (9개 추가 버그):

```
fix(multi-tenant): 인터뷰 및 도구 페이지에서 9개의 내비게이션 버그 수정

- 중첩 컴포넌트에서 나머지 내비게이션 문제 수정
- 총 수정된 버그: 전체 애플리케이션에서 31개

변경된 파일: 2
추가: +46
삭제: -10
```

모든 에이전트 흐름을 돌려봤습니다. SME 사용자. 에이전시 사용자. 모든 버튼을 클릭했습니다. 빈 페이지 없음.

총 피해: 17개 파일 변경, 849개 삽입, 197개 삭제.

투자 시간: Claude Code와 함께 비연속적으로 약 6시간 (오후 2시 발견 → 오후 8시 최종 커밋).

절약 시간: 아마 30시간 이상의 개별 버그 수정 및 사용자 지원.

---

(어렵게) 배운 것

1. 멀티테넌트 내비게이션은 데이터 격리보다 어렵다

org\_id로 데이터를 필터링할 수 있습니다. 그것은 쉬운 부분입니다.

하지만 내비게이션은? 같은 기능에 대해 두 가지 유효한 URL 구조가 있습니다:

```
SME:    /agent-name/session/123
에이전시: /clients/acme-corp/agents/agent-name/session/123
```

모든 navigate() 호출은 어떤 패턴을 사용할지 알아야 합니다. 한 번이라도 틀리면, 사용자는 빈 페이지를 봅니다.

2. useParams()는 거짓말을 한다

```typescript
const \{ clientSlug \} = useParams();
```

이것은 작동합니다... 라우트가 바뀔 때까지. 그러면 clientSlugundefined가 되고, 다음 내비게이션이 깨집니다.

React Context는 거짓말하지 않습니다. 항상 사용 가능하고, 항상 일관성 있습니다.

3. 승리를 선언하기 전에 모든 버그를 세라

14개의 버그가 있다고 생각했습니다. 그런 다음 8개를 더 찾았습니다. 그런 다음 9개를 더.

교훈: 버그가 있다고 생각하는 파일뿐만 아니라, 전체 코드베이스를 Grep하세요.

4. 같은 버그를 두 번 발견하면, 멈추고 패턴을 만들어라

개별 수정: 31개 버그 = 아마 일주일

체계적 수정: 패턴 찾기 → 헬퍼 만들기 → 모든 인스턴스 수정 = 6시간

오후 5시 30분에 화면을 쳐다보며 보낸 10분이 며칠을 절약했습니다.

5. 내비게이션 버그는 존재론적 위협이다

우리는 상태 관리, 데이터 페칭, API 최적화에 집착합니다.

하지만 깨진 내비게이션 = 빈 화면 = "이 플랫폼은 망가졌어."

사용자들은 여러분의 RLS 정책이나 멀티테넌트 아키텍처에 관심이 없습니다. "View Results"를 클릭하면 결과가 보이는 것에 관심이 있습니다. "기술적으로 작동한다"와 "실제로 완성되었다" 사이의 같은 격차가 AI로 네이티브 iOS 앱을 만들었을 때도 다시 나타났습니다 — Claude Code는 스캐폴드를 빠르게 생성했지만, 사용자가 제품을 신뢰하게 만드는 완성도는? 그것은 인간만이 제공할 수 있는 40%입니다.

---

패턴 (여러분의 멀티테넌트 앱을 위해)

계층적 URL을 가진 멀티테넌트 SaaS를 구축하고 있다면:

✅ 1단계: 레이아웃 레벨에서 Context 프로바이더 생성

```typescript
<ClientContextProvider>
  \{/* 모든 테넌트 범위 라우트 */\}
</ClientContextProvider>
```

✅ 2단계: 라우팅 헬퍼 구축

```typescript
const \{ buildRoute \} = useContextRoute();
navigate(buildRoute('agents/analysis/session/123'));
```

✅ 3단계: 절대 라우트를 하드코딩하지 마라

```typescript
// ❌ 나쁨
navigate('/analysis/session/123');

// ✅ 좋음
navigate(buildRoute('agents/analysis/session/123'));
```

✅ 4단계: 모든 내비게이션 호출을 Grep하라

```bash
# 모든 navigate() 호출 찾기
grep -r "navigate(" src/ > navigation_audit.txt

# 하드코딩된 라우트 찾기
grep -r "navigate('/" src/ | grep -v "buildRoute"
```

✅ 5단계: 모든 사용자 유형으로 테스트

SME 흐름. 에이전시 흐름. 모든 버튼. 모든 링크. 빈 페이지 없음.

---

결과

11월 1일 이전:

- 31개의 내비게이션 버그 잠복

- 하루 5건 이상의 버그 보고

- 에이전시 사용자가 플랫폼 안정성에 의문

- 저: 새 기능 출시가 두려움

11월 1일 이후:

- 0개의 내비게이션 버그

- 0개의 내비게이션 관련 지원 티켓

- 향후 개발을 위한 패턴 확립

- 저: 새 에이전트 라우트 출시에 자신감

개발 속도:

- 새 라우트 추가: 5분 (기존 30분 + "이게 망가지진 않을까?")

- 버그 보고: 0건 (기존 하루 5건 이상)

- 사용자 신뢰: 회복 ("플랫폼이 안정적으로 느껴져요")

이 버그들을 수정하는 데 보낸 6시간은 줄어든 지원 부담과 회복된 사용자 신뢰로 10배의 효과를 냈습니다.

---

이것을 공유하는 이유

멀티테넌트 내비게이션은 화려하지 않습니다. 아무도 "6시간 만에 31개 버그 수정"을 "새 기능 출시"처럼 축하하지 않습니다.

하지만 저처럼 https://stratum.chandlernguyen.com/—에이전시를 위한 AI 마케팅 플랫폼—으로 멀티테넌트 SaaS를 구축하고 있다면, 이것에 부딪힐 것입니다. 31개 버그는 아닐 수도 있습니다. 아마 5개일 수도. 하지만 부딪힐 것입니다.

그때, 기억하세요:

1. 내비게이션 상태에는 Params보다 Context

2. 개별 수정보다 체계적 수정

3. 전체 코드베이스를 Grep하세요 (생각보다 더 많이 찾을 것입니다)

4. 승리를 선언하기 전에 모든 사용자 유형으로 테스트하세요

그리고 빈 화면을 쳐다보며 컨텍스트가 어디로 갔는지 궁금해한다면, 저도 같은 상황에 있었다는 것을 알아주세요. :)

실제 사용자에게 출시한 가장 아픈 버그가 뭔가요 — 누군가가 알려줘서야 발견한 그런 종류의? 정말로 듣고 싶습니다.

감사합니다,

Chandler

STRAŦUM 아키텍처 시리즈: 이 내비게이션 위기는 더 큰 멀티테넌시 여정의 일부였습니다. 2일차에 멀티테넌시 구축하기에서 시작했고, 67일차에 전체 스키마를 재구축해야 했을 때 확대되었으며, 데이터베이스가 정확하지만 296배 느리다는 것을 발견했을 때 마무리되었습니다.

---

코딩하고, 배우고, 여전히 31개 단위로 버그를 발견하고 있습니다.

https://stratum.chandlernguyen.com/request-invitation에서 알파 액세스를 요청하세요

---

추신 - 첫 번째 버그를 보고한 사용자는? 인터뷰 데이터를 잃지 않았습니다. 데이터베이스에 저장되어 있었습니다. 깨진 라우트 때문에 볼 수 없었을 뿐입니다. 오후 7시 42분에 수정했을 때, 모든 작업이 여전히 그대로 있었습니다. 그 작은 안도감이 6시간을 가치 있게 만들었습니다.

---

계속 읽기

나의 여정
연결
언어
환경설정