Communitygithub.com

divinevideo/divine-web

서브도메인 루트(/)에서 URL 리디렉션 없이 페이지 컴포넌트를 직접 렌더링합니다. React Router SPA에서 사용합니다.

divine-web란 무엇인가요?

이 스킬은 React Router 기반 SPA에서 서브도메인 루트 경로(/)에 페이지 컴포넌트를 리디렉션 없이 직접 렌더링하는 방법을 제공합니다. 사용자 이름 기반 서브도메인(예: username.example.com)을 사용할 때, 기존에는 /profile/username으로 리디렉션하여 URL이 변경되고 깔끔한 서브도메인 URL의 장점이 사라지는 문제가 있었습니다. 또한 useParams()에 의존하는 컴포넌트가 루트 경로에서 파라미터를 찾지 못해 빈 화면을 보여주는 문제도 해결합니다. 클라우드플레어 워커나 Fastly Compute와 같은 엣지 워커에서 사용자 데이터를 HTML에 주입하고, 클라이언트에서 이 데이터를 폴백(fallback) 소스로 사용하여 라우트 파라미터가 없어도 컴포넌트가 정상 작동하도록 합니다. 라우트 파라미터가 우선권을 가지며, 엣지 워커는 SPA 하이드레이션 전에 데이터를 주입해야 합니다.

지원 대상~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/divinevideo/divine-web/tree/HEAD/.agents/skills/spa-subdomain-root-render

즐겨 사용하는 AI에게 물어보기

이 에이전트 스킬이 미리 로드된 새 채팅을 엽니다.

문서

SPA 서브도메인 루트 렌더링 (리디렉션 없음)

문제

서브도메인 기반 라우팅(예: username.example.com)을 구축할 때 React Router의 <Navigate to="/profile/username" replace />를 사용하면 URL이 변경되어 깔끔한 서브도메인 URL의 목적이 무효화됩니다. 또한 대상 컴포넌트가 /에 존재하지 않는 라우트 파라미터를 얻기 위해 useParams()에 의존하면 빈 화면이 표시될 수 있습니다.

컨텍스트 / 트리거 조건

  • 서브도메인 프로필 페이지가 /에서 콘텐츠를 제공하는 대신 /profile/npub1...로 리디렉션됩니다.
  • 서브도메인 루트에서 페이지가 빈 화면으로 표시됩니다(콘텐츠 로드 안 됨).
  • 컴포넌트가 useParams()를 사용하며 해당 파라미터가 없는 라우트에서 렌더링될 때 실패합니다.
  • 엣지 워커가 window.__SUBDOMAIN_USER__ 또는 유사한 전역 변수에 사용자 데이터를 주입합니다.
  • username.domain.com/이 개인 웹사이트처럼 렌더링되기를 원합니다.

해결 방법

1. 라우터: 탐색 대신 컴포넌트 직접 렌더링

이전 (리디렉션 + URL 변경 발생):

<Route path="/" element={
  subdomainUser
    ? <Navigate to={`/profile/${subdomainUser.npub}`} replace />
    : <Index />
} />

이후 (URL 변경 없이 /에서 렌더링):

<Route path="/" element={
  subdomainUser
    ? <ProfilePage />
    : <Index />
} />

2. 컴포넌트: 폴백 데이터 소스 추가

컴포넌트는 라우트 파라미터가 없을 때 주입된 서브도메인 데이터를 확인해야 합니다.

이전 (라우트 파라미터만 확인):

export function ProfilePage() {
  const { npub } = useParams<{ npub?: string }>();
  const identifier = npub;

  if (!identifier) {
    return <Error message="사용자 식별자가 제공되지 않았습니다" />;
  }
  // ... 컴포넌트 나머지
}

이후 (주입된 데이터로 폴백):

import { getSubdomainUser } from '@/hooks/useSubdomainUser';

export function ProfilePage() {
  const { npub } = useParams<{ npub?: string }>();
  const subdomainUser = getSubdomainUser();

  // 라우트 파라미터가 우선, 서브도메인 데이터는 폴백
  const identifier = npub || subdomainUser?.npub;

  if (!identifier) {
    return <Error message="사용자 식별자가 제공되지 않았습니다" />;
  }
  // ... 컴포넌트 나머지
}

3. 엣지 워커: HTML에 사용자 데이터 주입

엣지 워커(Fastly Compute, Cloudflare Workers 등)는 사용자 데이터를 주입해야 합니다:

// 서브도메인을 처리하는 엣지 워커에서
const userData = await fetchUserData(username);
const html = await fetchSpaHtml();

const injectedHtml = html.replace(
  '</head>',
  `<script>window.__DIVINE_USER__ = ${JSON.stringify(userData)};</script></head>`
);

return new Response(injectedHtml, { headers: { 'content-type': 'text/html' } });

4. 클라이언트 측 데이터 접근

// useSubdomainUser.ts
interface SubdomainUser {
  subdomain: string;
  pubkey: string;
  npub: string;
  username?: string;
  displayName?: string;
  // ... 기타 필드
}

export function getSubdomainUser(): SubdomainUser | null {
  if (typeof window === 'undefined') return null;
  return (window as any).__DIVINE_USER__ || null;
}

검증

  1. username.domain.com/ 방문 - URL이 /에 유지되어야 하며 리디렉션되지 않아야 합니다.
  2. 프로필 콘텐츠가 올바르게 로드되어야 합니다.
  3. 브라우저 콘솔에 식별자 누락 관련 오류가 없어야 합니다.
  4. 개발자 도구에서 window.__DIVINE_USER__에 예상 데이터가 포함되어 있는지 확인합니다.

예시

divine-web의 경우:

  • thecomedianriley.divine.video/는 /에서 ProfilePage를 렌더링합니다.
  • ProfilePage는 먼저 useParams()를 확인합니다(/에서는 비어 있음).
  • 주입된 데이터의 getSubdomainUser()?.npub로 폴백합니다.
  • URL 변경 없이 프로필이 로드됩니다.

참고 사항

  • 이 패턴은 엣지 워커와 클라이언트 코드 간의 조정이 필요합니다.
  • 엣지 워커는 SPA 하이드레이션 전에 데이터를 주입해야 합니다.
  • 일반 /profile/:npub 라우트가 정상 작동하도록 항상 라우트 파라미터를 먼저 확인하세요.
  • SEO 고려: 엣지 워커는 적절한 meta 태그도 설정해야 합니다.
  • 서브도메인의 링크는 소유자 콘텐츠의 경우 로컬(SmartLink 컴포넌트 사용)을 유지하고, 다른 사용자의 콘텐츠는 apex 도메인으로 연결할 수 있습니다.

참조

관련 스킬