50개 치과 관행을 운영하는 치과 DSO는 200개 위치를 가진 짐 체인, 30개 속성을 가진 호텔 그룹, 15개 캠퍼스를 가진 교회 네트워크와 동일한 웹사이트 아키텍처 문제를 가지고 있습니다. 이들 모두 필요로 하는 것은: 중앙화된 브랜드 제어, 위치별 지역화된 콘텐츠, 하나의 관리자 대시보드, 위치별 SEO 페이지, 그리고 아무것도 깨지 않고 모든 것을 동시에 업데이트하는 배포입니다. 아키텍처는 동일합니다. 콘텐츠는 다릅니다.

저는 치과 그룹, 피트니스 프랜차이즈, 수의 네트워크, 레스토랑 체인을 위해 이 패턴을 구축했습니다. 매번 동일한 데이터베이스 스키마, 동일한 Next.js 라우트 구조, 동일한 역할 기반 액세스 제어로 시작합니다. 변하는 것은 시드 데이터와 컴포넌트 레이블입니다. "서비스"는 짐에서는 "클래스"가 되고 레스토랑에서는 "메뉴 항목"이 됩니다. "직원"은 "치과의사" 또는 "트레이너" 또는 "수의사"가 됩니다. 내부의 배관은? 동일합니다.

이 글은 보편적 다중 위치 아키텍처 패턴을 한 번 설명한 다음, 완전히 다른 5개 산업에 어떻게 적응하는지 보여줍니다. 어떤 종류의 다중 위치 비즈니스를 운영하든 — 또는 다중 위치 비즈니스를 위해 개발하는 개발자이든 — 이것이 청사진입니다.

목차

DSO, 수의 체인, 짐 및 프랜차이즈를 위한 다중 사이트 아키텍처

모든 다중 위치 비즈니스가 직면하는 핵심 문제

일반적으로 무슨 일이 일어나는지 솔직하게 말해봅시다. 프랜차이즈 또는 다중 위치 비즈니스는 단일 웹사이트로 시작합니다. 그다음 두 번째 위치를 엽니다. 누군가 두 번째 WordPress 설치를 합니다. 15개 위치가 있을 때쯤이면, 15개의 별도 WordPress 사이트, 15개의 다른 테마(일부는 3개 버전 뒤떨어짐), 15개의 다른 플러그인 세트, 그리고 중앙화된 제어가 전무합니다.

마케팅 담당자가 모든 위치에서 브랜드의 주요 CTA를 업데이트하기를 원합니다. 그것은 15개의 로그인, 15개의 편집, 그리고 아무도 자신의 템플릿을 깨지 않았기를 바라는 것입니다. SEO 팀이 어떤 위치에서 블로그 콘텐츠를 게시하고 어떤 위치에서 6개월 동안 활동이 없었는지 보기를 원합니다. 그에 대한 대시보드는 없습니다 — 3월에 누군가가 업데이트하는 것을 잊은 스프레드시트만 있습니다.

이는 50개 관행을 관리하는 치과 지원 조직(DSO)이든 200개 위치를 가진 레스토랑 그룹이든 동일한 문제입니다. 증상은 동일합니다:

  • 브랜드 편차. 아무도 일관성을 강제하지 않기 때문에 위치가 브랜드를 벗어납니다.
  • SEO 단편화. 구조화된 지역 SEO 페이지 없음, 스키마 마크업 일관성 없음, 중앙화된 사이트맵 없음.
  • 관리자 혼란. 각 위치가 자체 사이트를 관리하거나(잘못됨), 또는 기업이 모든 것을 처리합니다(느림).
  • 배포 위험. 한 위치의 사이트를 업데이트하는 것이 다른 위치를 손상시킬 수 있습니다.

수정은 더 나은 CMS 테마가 아닙니다. 완전히 다른 아키텍처입니다.

보편적 데이터베이스 스키마

모든 것은 locations 테이블로 시작합니다. 이것이 전체 시스템의 앵커입니다. 저는 Supabase를 데이터베이스 및 인증 레이어로 사용합니다. 왜냐하면 이것이 Postgres, 행 수준 보안, 실시간 구독, 그리고 관대한 무료 계층을 제공하기 때문입니다 — 하지만 스키마는 모든 관계형 데이터베이스에서 작동합니다.

핵심 스키마는 다음과 같습니다:

-- 앵커 테이블. 모든 위치별 콘텐츠는
-- location_id를 통해 이것을 참조합니다.
CREATE TABLE locations (
  id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
  name TEXT NOT NULL,
  slug TEXT UNIQUE NOT NULL,
  address TEXT NOT NULL,
  city TEXT NOT NULL,
  state TEXT NOT NULL,
  zip TEXT NOT NULL,
  lat DECIMAL(10, 8),
  lng DECIMAL(11, 8),
  phone TEXT,
  email TEXT,
  hours JSONB DEFAULT '{}',
  photos TEXT[] DEFAULT '{}',
  description TEXT,
  metadata JSONB DEFAULT '{}',
  is_active BOOLEAN DEFAULT true,
  created_at TIMESTAMPTZ DEFAULT now(),
  updated_at TIMESTAMPTZ DEFAULT now()
);

-- 콘텐츠 테이블은 동일한 패턴을 따릅니다:
-- location_id는 NULL입니다.
-- NULL = 모든 위치에서 공유됨
-- 값 = 해당 위치에 특정

CREATE TABLE services (
  id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
  location_id UUID REFERENCES locations(id) ON DELETE CASCADE,
  name TEXT NOT NULL,
  slug TEXT NOT NULL,
  description TEXT,
  price_range TEXT,
  duration TEXT,
  category TEXT,
  sort_order INT DEFAULT 0,
  is_active BOOLEAN DEFAULT true,
  metadata JSONB DEFAULT '{}'
);

CREATE TABLE staff (
  id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
  location_id UUID REFERENCES locations(id) ON DELETE CASCADE,
  name TEXT NOT NULL,
  slug TEXT NOT NULL,
  title TEXT,
  photo TEXT,
  bio TEXT,
  credentials TEXT[],
  specialties TEXT[],
  sort_order INT DEFAULT 0,
  is_active BOOLEAN DEFAULT true
);

CREATE TABLE blog_posts (
  id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
  location_id UUID REFERENCES locations(id) ON DELETE CASCADE,
  title TEXT NOT NULL,
  slug TEXT UNIQUE NOT NULL,
  content TEXT,
  excerpt TEXT,
  author_id UUID REFERENCES staff(id),
  published_at TIMESTAMPTZ,
  is_published BOOLEAN DEFAULT false,
  tags TEXT[] DEFAULT '{}',
  metadata JSONB DEFAULT '{}'
);

CREATE TABLE testimonials (
  id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
  location_id UUID REFERENCES locations(id) ON DELETE CASCADE,
  author_name TEXT NOT NULL,
  rating INT CHECK (rating >= 1 AND rating <= 5),
  content TEXT,
  is_approved BOOLEAN DEFAULT false,
  created_at TIMESTAMPTZ DEFAULT now()
);

CREATE TABLE events (
  id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
  location_id UUID REFERENCES locations(id) ON DELETE CASCADE,
  title TEXT NOT NULL,
  description TEXT,
  event_date TIMESTAMPTZ,
  end_date TIMESTAMPTZ,
  is_active BOOLEAN DEFAULT true
);

NULL 가능한 location_id 패턴이 핵심 통찰입니다. 블로그 게시물이 location_id = NULL을 가질 때, 이것은 네트워크 전체 기사입니다 (모든 50개 치과 관행에서 공유되는 "건강한 치아를 위한 5가지 팁"). location_id가 값을 가질 때, 이것은 해당 위치에 특정적입니다 ("Dr. Smith는 우리의 Austin 관행에 참여합니다"). 동일한 테이블, 동일한 쿼리 패턴이지만, 콘텐츠는 단일 열로 공유되거나 지역화될 수 있습니다.

metadata JSONB 열은 산업별 필드가 있는 곳입니다. 치과 위치는 {"insurance_accepted": ["Delta Dental", "Cigna"], "parking_info": "Free lot behind building"}을 저장할 수 있습니다. 짐은 {"equipment": ["squat racks", "rowing machines"], "peak_hours": "5-7 PM weekdays"}를 저장합니다. 스키마 마이그레이션이 필요하지 않습니다 — 단지 다른 JSON 형태입니다.

Next.js 라우트 아키텍처

Next.js App Router는 이 데이터 모델에 명확하게 매핑됩니다. 모든 산업에 대해 작동하는 라우트 구조는 다음과 같습니다:

app/
├── page.tsx                          # 홈페이지
├── locations/
│   ├── page.tsx                      # 위치 찾기 (지도 + 지역 검색)
│   └── [slug]/
│       ├── page.tsx                  # 위치 상세 페이지
│       ├── staff/page.tsx            # 위치에 대한 직원 목록
│       └── services/page.tsx         # 위치에 대한 서비스
├── services/
│   └── [service]/page.tsx            # 공유 서비스 설명
├── blog/
│   ├── page.tsx                      # 모든 블로그 게시물
│   └── [post]/page.tsx               # 개별 블로그 게시물
├── about/page.tsx
└── contact/page.tsx

위치 상세 페이지(/locations/[slug])는 마술이 일어나는 곳입니다. 단일 generateStaticParams 호출은 모든 활성 위치를 쿼리하고 빌드 시간에 모두 미리 렌더링합니다:

// app/locations/[slug]/page.tsx
import { createClient } from '@/lib/supabase/server'

export async function generateStaticParams() {
  const supabase = createClient()
  const { data: locations } = await supabase
    .from('locations')
    .select('slug')
    .eq('is_active', true)

  return locations?.map((loc) => ({ slug: loc.slug })) ?? []
}

export async function generateMetadata({ params }: { params: { slug: string } }) {
  const supabase = createClient()
  const { data: location } = await supabase
    .from('locations')
    .select('*')
    .eq('slug', params.slug)
    .single()

  if (!location) return {}

  return {
    title: `${location.name} | ${location.city}, ${location.state}`,
    description: location.description,
    openGraph: {
      title: `${location.name} - ${location.city}`,
      images: location.photos?.[0] ? [location.photos[0]] : [],
    },
  }
}

export default async function LocationPage({ params }: { params: { slug: string } }) {
  const supabase = createClient()
  
  const [{ data: location }, { data: staff }, { data: services }, { data: testimonials }] = 
    await Promise.all([
      supabase.from('locations').select('*').eq('slug', params.slug).single(),
      supabase.from('staff').select('*').eq('location_id', params.slug), // 단순화됨
      supabase.from('services').select('*').or(`location_id.is.null,location_id.eq.${locationId}`),
      supabase.from('testimonials').select('*').eq('is_approved', true),
    ])

  // 모든 데이터로 위치 페이지 렌더링
  // 이것은 산업과 관계없이 동일한 컴포넌트 구조입니다
}

서비스 쿼리는 그 or 필터를 사용합니다 — location_id가 null(공유 서비스)이거나 현재 위치와 일치하는 서비스를 가져옵니다. 이는 치과 DSO가 모든 위치에 대해 "치아 청소"를 한 번 정의한 다음 "Invisalign"을 이것을 제공하는 위치에만 추가할 수 있음을 의미합니다. 중복 없음.

위치 찾기 페이지의 경우, 저는 위도/경도 좌표를 저장하고 지역 쿼리에 Supabase의 PostGIS 확장을 사용합니다:

-- 사용자 좌표에서 25마일 이내의 위치 찾기
SELECT *, 
  (point(lng, lat) <@> point($1, $2)) * 1.60934 AS distance_miles
FROM locations
WHERE is_active = true
ORDER BY point(lng, lat) <@> point($1, $2)
LIMIT 20;

DSO, 수의 체인, 짐 및 프랜차이즈를 위한 다중 사이트 아키텍처 - 아키텍처

행 수준 보안 및 관리자 대시보드

여기서 아키텍처가 정말로 빛을 발합니다. Supabase RLS 정책을 통해 응용 프로그램 코드가 아니라 데이터베이스 수준에서 데이터 액세스를 정의할 수 있습니다.

-- 위치 관리자는 자신의 위치 데이터만 볼 수 있습니다
CREATE POLICY "Location managers see own data" ON services
  FOR ALL
  USING (
    location_id IN (
      SELECT location_id FROM user_locations
      WHERE user_id = auth.uid()
    )
    OR
    EXISTS (
      SELECT 1 FROM user_roles
      WHERE user_id = auth.uid() AND role = 'network_admin'
    )
  );

네트워크 관리자는 모든 것을 봅니다. 위치 관리자는 자신의 위치만 봅니다. 이것은 모든 테이블 — 서비스, 직원, 블로그 게시물, 추천글, 이벤트에 적용됩니다. 하나의 정책 패턴, 일관되게 적용됩니다.

관리자 대시보드는 네트워크 수준 메트릭을 표시합니다:

  • 콘텐츠 신선도: 어떤 위치가 지난 30일 이상 블로그를 업데이트하지 않았습니까?
  • 위치별 트래픽: 위치 slug로 집계된 Google Search Console 데이터
  • 위치별 리드: 위치별 양식 제출 및 예약 요청
  • 브랜드 준수: 모든 위치가 승인된 로고, 색상 및 CTA 텍스트를 사용하고 있습니까?

산업 변형 1: 치과 DSO

DSO 웹사이트는 통합 치과 브랜드처럼 느껴져야 하면서 각 관행이 고유한 제공자와 전문성을 강조할 수 있게 해야 합니다.

서비스는 치과 절차에 매핑됩니다: 청소, 충전, 크라운, 임플란트, Invisalign, 응급 치과 치료. 일부는 보편적입니다 (모든 위치가 청소를 수행), 다른 것들은 위치별입니다 (3개 위치만 진정 치과 치료를 제공).

직원은 치과의사, 위생사, 사무실 관리자입니다. 각각은 자격증(DDS, DMD), 전문성, 교육, 및 전문 사진이 있는 프로필을 얻습니다. 어린이 치과의사를 선택하는 부모는 자신의 아이를 치료할 사람을 보기를 원합니다.

CTA는 "예약하기"입니다. 이것은 Calendly, NexHealth 또는 사용자 정의 예약 시스템에 연결됩니다. 예약 위젯은 사용자가 온 위치 페이지에 따라 위치를 사전 선택합니다.

지역 SEO 대상: "dentist in [city]", "[procedure] in [city]", "emergency dentist [city] [state]". 각 위치 페이지는 DentistLocalBusiness 스키마에 대한 구조화된 데이터 마크업을 받습니다.

Metadata JSONB 저장: 수락된 보험 계획, 주차 정보, 접근성 기능, 사용 언어, 새 환자 수락 여부.

산업 변형 2: 짐 및 피트니스 체인

짐 체인은 "서비스"를 "클래스"로 바꾸지만 — 데이터 모델은 동일합니다. 위치 A의 요가 클래스와 위치 B의 HIIT 클래스는 단지 다른 location_id 값을 가진 서비스 테이블의 행일 뿐입니다.

서비스는 일정 데이터가 있는 클래스 유형입니다. 메타데이터는 주간 일정을 JSON으로 저장하고, 강사 할당, 용량 제한, 그리고 드롭인이 허용되는지 여부를 저장합니다.

직원은 자격증(NASM, ACE, CrossFit L2), 전문성, 개인 훈련 예약 가용성을 가진 트레이너와 강사입니다.

CTA는 "지금 참여하기"입니다 — 회원 계층을 처리하고 교차 위치 액세스를 처리하는 Stripe 구독 체크아웃입니다. 다운타운 위치에서 가입한 회원은 교외 위치에서도 체크인할 수 있어야 합니다.

지역 SEO 대상: "gym near me", "fitness classes [city]", "[class type] classes [city]", "personal trainer [city]".

Metadata JSONB 저장: 장비 목록, 클래스 일정, 피크 시간, 편의시설 (사우나, 수영장, 보육), 무료 주차 가용성.

산업 변형 3: 호텔 그룹

부티크 호텔 그룹과 독립적인 호텔 체인은 이 패턴으로 엄청난 이득을 얻습니다 — 특히 직접 예약을 가능하게 하기 때문입니다 (Booking.com 또는 Expedia에서 일반적으로 예약당 15-25% OTA 수수료를 우회합니다).

서비스는 객실 유형이 됩니다: 표준 객실, 킹 스위트, 펜트하우스. 각각은 사진, 편의시설 목록, 평방 피트, 기본 가격을 얻습니다. 위치별 가격은 메타데이터 또는 날짜 범위를 가진 별도의 요금 테이블에 있습니다.

직원은 여기서 더 가벼운 편입니다 — 아마 브랜드의 스토리텔링을 위해 주목할 만한 지배인이나 콘시어주 정도입니다.

CTA는 "직접 예약"입니다 — 손님들이 OTA 대신 호텔의 자체 사이트에서 예약할 이유를 제공하는 FME (찾기, 일치, 참여) 패턴입니다. 일반적으로 "최저 가격 보장" 또는 무료 업그레이드입니다.

지역 SEO 대상: "hotels in [city]", "[hotel name] reviews", "boutique hotel [neighborhood] [city]", "hotels near [landmark]".

Metadata JSONB 저장: 편의시설 (수영장, 스파, 레스토랑, 짐, EV 충전), 근처 명소, 지역 이벤트 캘린더, 체크인/체크아웃 시간, 애완동물 정책.

산업 변형 4: 수의 클리닉 체인

수의 체인은 2025년에 빠르게 증가하고 있습니다 — 수의학의 통합은 10년 전 치과 DSO에서 일어난 일과 유사합니다. 동일한 다중 위치 아키텍처가 완벽하게 적용됩니다.

서비스는 애완동물 관리 서비스입니다: 웰니스 검진, 예방접종, 치과 청소, 수술, 응급 치료, 보육, 미용. 일부 위치는 이국적인 애완동물 관리를 제공합니다; 대부분은 그렇지 않습니다.

직원은 종 전문성(소동물, 말, 이국적), 위원회 인증, 및 교육을 가진 수의사입니다.

CTA는 "예약하기"입니다 (약간의 변형과 함께) — 접수 양식이 애완동물 정보 (종, 품종, 나이, 방문 이유)를 캡처하여 예약을 올바르게 라우팅해야 합니다.

지역 SEO 대상: "veterinarian in [city]", "emergency vet [city]", "[species] vet [city]", "pet dental cleaning [city]".

Metadata JSONB 저장: 수락된 종, 응급 시간 (정기 시간과 다른 경우), 보육 용량, 현장 실험실과 이미징이 있는지 여부.

산업 변형 5: 레스토랑 체인

서비스는 메뉴 섹션이 됩니다: 전채, 주요 요리, 디저트, 음료. 중요한 것은 가격이 위치별로 다를 수 있다는 것입니다. 버거는 Austin에서 $14, Manhattan에서 $19입니다. metadata 열은 위치별 가격 무효화로 이것을 처리합니다.

직원은 주목할 만한 셰프 또는 핏마스터입니다 — 이것은 음식 뒤의 사람들이 이야기의 일부인 브랜드에 가장 좋습니다.

CTA는 "온라인 주문"입니다 — 사용자의 가장 가까운 위치에 대한 올바른 온라인 주문 시스템 (Toast, Square, ChowNow, 또는 사용자 정의)으로 라우팅하는 위치 인식 링크입니다.

지역 SEO 대상: "[restaurant name] [city] menu", "restaurants near me", "[cuisine type] restaurant [city]", "[restaurant name] hours".

Metadata JSONB 저장: 배송 반경, 예약 가용성 (OpenTable 또는 Resy 링크 포함), 주차 세부정보, 개인 식사 용량, 해피 아워 시간.

아키텍처 비교표

컴포넌트 치과 DSO 짐 체인 호텔 그룹 수의 체인 레스토랑
"서비스" 레이블 절차 클래스 객실 유형 애완동물 서비스 메뉴 항목
"직원" 레이블 치과의사 트레이너 관리 수의사 셰프
주요 CTA 예약하기 회원 가입 객실 예약 예약하기 온라인 주문
예약 통합 NexHealth, Calendly Stripe 구독 사용자 정의 / Cloudbeds 사용자 정의 + 애완동물 접수 Toast, Square
주요 지역 데이터 보험, 주차 일정, 장비 편의시설, 명소 종, 응급 시간 메뉴 가격, 배송
주요 SEO 키워드 "dentist in [city]" "gym near me" "hotels in [city]" "vet in [city]" "[brand] [city] menu"
스키마 마크업 Dentist, LocalBusiness SportsActivityLocation Hotel, LodgingBusiness VeterinaryCare Restaurant, Menu
변경된 DB 테이블 0 0 0 0 0

마지막 행이 핵심입니다. 산업 간에 0개의 데이터베이스 테이블이 변경됩니다. locations, services, staff, blog_posts, testimonials, 및 events 테이블을 사용하고 있습니다. UI의 레이블이 변경됩니다. 메타데이터 형태가 변경됩니다. 아키텍처는 변경되지 않습니다.

대규모 배포 및 성능

우리는 ISR (증분 정적 재생성)과 함께 Vercel에 배포합니다. 각 위치 페이지는 빌드 시간에 정적으로 생성되고 60초마다 재검증됩니다. 200개 위치 체인의 경우, 이것은 모든 장치에서 1초 이내로 로드되는 200개의 정적 HTML 페이지입니다.

숫자가 중요합니다. 일반적으로 보는 것은:

  • 200개 위치에 대한 빌드 시간: Vercel Pro에서 ~45초
  • 위치 페이지당 TTFB: < 50ms (엣지 CDN에서 제공됨)
  • Lighthouse 점수: 전체적으로 95+ 점
  • ISR 재검증: 60초 stale-while-revalidate는 전체 재빌드 없이 콘텐츠 업데이트가 1분 내에 표시됨을 의미합니다

새 위치 추가는 데이터베이스 삽입과 선택적 온디맨드 재검증 호출입니다. 새 배포가 필요하지 않습니다. generateStaticParams 함수는 다음 빌드 또는 ISR 사이클에서 새 위치를 인식합니다.

// 위치가 추가/업데이트될 때 재검증을 트리거하는 API 라우트
import { revalidatePath } from 'next/cache'

export async function POST(request: Request) {
  const { slug } = await request.json()
  
  revalidatePath('/locations')
  revalidatePath(`/locations/${slug}`)
  
  return Response.json({ revalidated: true })
}

비용 분석: 2025년에 실제로 드는 비용

실제 숫자로 말해봅시다. 이것은 가격 대화 중에 자주 받는 질문입니다.

컴포넌트 월간 비용 (50개 위치) 월간 비용 (200개 위치)
Supabase Pro $25 $25 (동일한 계층이 둘 다 처리)
Vercel Pro $20 $20
Vercel 대역폭 (초과분) ~$0 ~$40
도메인 + DNS (Cloudflare) $0 $0
이미지 CDN (Cloudflare R2) ~$5 ~$15
모니터링 (Sentry) $26 $26
총 인프라 ~$76/월 ~$126/월

관리 호스팅 시 ~$30/월인 50개의 별도 WordPress 사이트와 비교하세요 — 이것은 유지보수, 플러그인 라이선스, 또는 이들을 모두 업데이트하고 유지하는 사람을 생각하기 전에 $1,500/월입니다.

초기 개발 투자는 더 높습니다 — 우리는 일반적으로 복잡성에 따라 다중 위치 빌드에 $30K-$80K를 인용합니다 — 하지만 진행 중인 운영 비용은 WordPress 멀티사이트 대안의 일부입니다. 그리고 당신은 프랜차이즈 웹사이트 벤더에게 월별 $500/월을 지불하고 있지 않습니다. 이 벤더는 당신을 자신의 플랫폼에 잠그고 있습니다.

헤드리스 CMS 통합을 탐색하거나 더 빠른 정적 빌드를 위해 Next.js 대신 Astro를 고려하는 데 관심이 있는 팀의 경우, 동일한 데이터베이스 아키텍처가 적용됩니다. 프론트엔드 프레임워크는 교체 가능합니다; 데이터 모델은 그렇지 않습니다.

FAQ

이 아키텍처는 다른 시간대의 위치를 처리할 수 있습니까?

물론입니다. hours JSONB 열은 각 위치의 운영 시간을 자신의 현지 시간대에 저장합니다. 우리는 위치 메타데이터에 timezone 필드 (예: "America/Chicago")를 포함하고 "지금 오픈" 배지와 같은 시간에 민감한 디스플레이에 사용합니다. 데이터베이스의 모든 타임스탐프는 UTC로 저장되고 프론트엔드에서 변환됩니다.

다른 서비스를 제공하는 위치를 처리하려면 어떻게 하나요?

그것이 NULL 가능한 location_id 패턴이 작동하는 것입니다. location_id = NULL인 서비스는 모든 위치에서 공유됩니다 — 모든 위치의 페이지에 나타납니다. 특정 location_id를 가진 서비스는 해당 위치에만 나타납니다. 공유 서비스가 맞춤형 가격이나 가용성과 같은 위치별 무효화가 필요한 경우, 연결 테이블 (location_services)을 사용할 수도 있습니다.

새 위치가 열 때 어떻게 되나요?

네트워크 관리자가 대시보드를 통해 위치를 추가합니다. 이것은 locations 테이블에 행을 생성하고, ISR 재검증을 트리거하는 웹훅을 발생시키고, 새 위치 페이지는 60초 내에 라이브됩니다. 개발자, 배포, DNS 변경이 필요하지 않습니다. 위치는 모든 공유 서비스 및 콘텐츠를 즉시 상속합니다.

이것이 프랜차이즈를 위한 WordPress Multisite보다 나은가요?

대부분의 다중 위치 비즈니스의 경우, 예입니다. WordPress Multisite는 10년 동안 선택지였지만, 실제 문제가 있습니다: 단일 플러그인 취약점이 전체 네트워크를 손상시킬 수 있고, 사이트를 추가함에 따라 성능이 저하되고, 건강을 유지하려면 전담 시스템 관리자가 필요합니다. 이 헤드리스 아키텍처는 정적 사이트 성능, 데이터베이스 수준 보안, 위치 간 공유 런타임 위험 없음을 제공합니다.

위치 관리자가 다른 위치를 깨지 않고 자신의 콘텐츠를 편집하려면 어떻게 하나요?

행 수준 보안이 데이터베이스 수준에서 보장합니다. 위치 관리자는 Austin 데이터베이스에서 Denver 위치에 속한 데이터를 보거나 수정할 수 없습니다. 응용 프로그램 코드에서 강제하는 것이 아닙니다 — 버그가 있을 수 있습니다 — Postgres 자체에서 강제합니다. 관리자 UI가 다른 위치의 데이터를 쿼리하려고 하는 버그가 있어도, 데이터베이스는 빈 결과를 반환할 것입니다.

SEO — 각 위치가 자신의 사이트맵을 얻나요?

각 위치 페이지는 빌드 시간에 생성된 단일 동적 사이트맵에 입력됩니다. 우리는 또한 위치별 구조화된 데이터 (JSON-LD)를 LocalBusiness 스키마, 지역 좌표, 운영 시간, 산업별 유형으로 생성합니다. Google은 각 /locations/[slug] 페이지를 별개의 지역 비즈니스 목록으로 취급하므로, 이것이 정확히 지역 팩 순위에 대해 원하는 것입니다.

위치가 자신의 블로그 게시물을 가지면서 네트워크 전체 콘텐츠를 공유할 수 있습니까?

네 — 다시 NULL 가능한 location_id 패턴입니다. location_id = NULL인 블로그 게시물은 모든 위치의 블로그 피드에 나타납니다. 특정 location_id인 게시물은 해당 위치의 피드에만 나타납니다. Miami의 위치는 지역 커뮤니티 이벤트에 대한 게시물을 게시할 수 있으면서 기업 팀은 네트워크 전체 사상 지도자를 게시합니다. 둘 다 Miami 블로그 피드에 나타나고; 기업 게시물만 다른 곳에 나타납니다.

50개의 별도 웹사이트를 관리하는 것과 비교하여 지속적인 유지보수 비용은 얼마입니까?

이 아키텍처를 사용하면, 하나의 코드베이스, 하나의 배포, 그리고 유지보수할 하나의 의존성 세트가 있습니다. 월간 인프라는 규모에 따라 $75-$125로 실행됩니다. 50개의 WordPress 설치와 비교: 단독 호스팅에서 월별 $1,500, 플러그인 업데이트, 보안 패치, 그리고 자동 업데이트 후 고장난 위치를 해결하는 월별 10-20시간. 우리는 다중 위치 비즈니스가 이 패턴으로 마이그레이션한 후 연간 웹 운영 예산을 60-70% 삭감한 것을 보았습니다.