수정: Qwik가 빈 sitemap.xml을 생성

발행: (2025년 12월 2일 오후 02:50 GMT+9)
7 min read
원문: Dev.to

Source: Dev.to

문제

Node.js 어댑터(nodeServerAdapter)로 Qwik(또는 Qwik City) 애플리케이션을 배포할 때, sitemap.xml은 로컬에서는 정상 작동하지만 프로덕션에서는 비어 있습니다:

<!-- empty sitemap -->
  • 로컬 빌드: ✅ 정상 작동
  • 개발 서버: ✅ sitemap을 올바르게 제공
  • 프로덕션 빌드: ❌ 빈 sitemap, 경고 없음

Node.js 어댑터는 **정적 사이트 생성(SSG)**을 빌드 단계에서 실행합니다(SSR 빌드에서도). 이 단계에서:

  1. 정적으로 내보낼 수 있는 페이지를 찾기 위해 라우트를 스캔
  2. 찾은 라우트로 sitemap을 생성
  3. 직접 public/sitemap.xml에 넣어 둔 파일을 덮어씀

SSG 단계에서 내보낼 수 있는 라우트를 찾지 못하면(예: 동적 라우트, SSR 전용 라우트, 혹은 Node.js 어댑터 사용 시) 빈 sitemap이 생성됩니다.


빠른 해결책 – SSG 비활성화

SSR 전용 애플리케이션이라면 어댑터 설정에서 SSG를 완전히 끌 수 있습니다.

adapters/node-server/vite.config.ts

import { nodeServerAdapter } from "@builder.io/qwik-city/adapters/node-server/vite";
import { extendConfig } from "@builder.io/qwik-city/vite";
import baseConfig from "../../vite.config";

export default extendConfig(baseConfig, () => ({
  build: {
    ssr: true,
    rollupOptions: {
      input: ["src/entry.express.tsx", "@qwik-city-plan"],
    },
  },
  plugins: [
    nodeServerAdapter({
      name: "express",
      // ⚠️ CRITICAL: Completely disable SSG – use a static sitemap.xml
      ssg: null,
    }),
  ],
}));

핵심 변경점: ssg: null은 자동 sitemap 생성을 중단하므로 public/에 넣은 파일이 그대로 제공됩니다.


정적 Sitemap 제공

public/sitemap.xml에 인덱싱하고 싶은 URL을 작성합니다.

<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
  <url>
    <loc>https://yourdomain.com/</loc>
    <lastmod>2024-12-01</lastmod>
    <changefreq>weekly</changefreq>
    <priority>1.0</priority>
  </url>
  <url>
    <loc>https://yourdomain.com/about/</loc>
    <changefreq>monthly</changefreq>
    <priority>0.8</priority>
  </url>
</urlset>

다국어 예시

<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
  <url>
    <loc>https://yourdomain.com/</loc>
    <priority>1.0</priority>
  </url>
  <url>
    <loc>https://yourdomain.com/es/</loc>
    <priority>0.8</priority>
  </url>
  <url>
    <loc>https://yourdomain.com/de/</loc>
    <priority>0.8</priority>
  </url>
</urlset>

빌드 스크립트 조정

package.jsonqwik city collect가 들어 있다면 제거하세요—그렇지 않으면 SSG가 다시 강제로 실행됩니다.

{
  "scripts": {
    // Before
    // "build:deploy": "npm run build.production && qwik city collect"

    // After
    "build:deploy": "npm run build.production"
  }
}

빌드를 실행하고 확인합니다:

npm run build:deploy
ls -lh dist/sitemap.xml
cat dist/sitemap.xml

빈 파일이 아니라 전체 sitemap 내용이 보일 것입니다.

Node.js 서버는 이제 dist/sitemap.xml을 정적 에셋으로 제공합니다:

node server.js
curl http://localhost:3000/sitemap.xml

동적 Sitemap (SSR 라우트)

CMS 등에서 실시간으로 sitemap을 생성해야 한다면 SSR 라우트를 만들면 됩니다.

src/routes/sitemap.xml/index.ts

import type { RequestHandler } from '@builder.io/qwik-city';

const SUPPORTED_LOCALES = ['en-US', 'es', 'ru', 'de', 'fr', 'ja'];

export const onGet: RequestHandler = async ({ url }) => {
  const origin = url.origin;

  const urls = [
    // Root URL
    `  
    ${origin}/
    ${new Date().toISOString().split('T')[0]}
    weekly
    1.0
  `,
    // Language‑specific URLs
    ...SUPPORTED_LOCALES
      .filter(l => l !== 'en-US')
      .map(l => `  
    ${origin}/${l}/
    weekly
    0.8
  `),
  ].join('\n');

  const sitemap = `

${urls}
`;

  return new Response(sitemap, {
    status: 200,
    headers: {
      'Content-Type': 'application/xml; charset=utf-8',
      'Cache-Control': 'public, max-age=86400', // 1 day
    },
  });
};

Note: 어댑터 설정에 ssg: null을 유지해 정적 생성 단계가 이 동적 라우트를 덮어쓰지 않도록 합니다.


왜 Node.js 어댑터에서도 SSG가 실행되는가

  • Static Site Generation (SSG) – 빌드 시 페이지를 미리 렌더링하고, sitemap을 생성하며, 정적 파일을 씁니다.
  • Server‑Side Rendering (SSR) – 런타임에 페이지를 즉시 렌더링합니다.

Node.js 어댑터는 SSR을 위한 것이지만 기본적으로 sitemap 생성과 onStaticGenerate가 있는 페이지를 미리 렌더링하기 위해 SSG를 트리거합니다. 라우트가 동적이거나 onStaticGenerate가 없으면 SSG는 내보낼 라우트가 0개라고 판단해 빈 sitemap을 기록합니다.


흔히 저지르는 실수

문제원인해결책
ssg: { exclude: ['*'] }파일을 제외하지만 여전히 SSG가 실행돼 sitemap을 덮어씀ssg: null 사용
빌드 스크립트에 qwik city collect빌드 후 SSG를 강제로 실행해 정적 파일을 덮어씀해당 명령 제거
동적 콘텐츠를 public/에만 의존동적 라우트는 정적 파일이 아니라 SSR 핸들러가 필요위의 SSR 라우트 구현
dist/가 매 빌드마다 비워짐정적 파일은 public/에 두고 dist/에 복사돼야 함public/sitemap.xml 유지

적용 후 체크리스트

  • ssg: null 설정 후 “Starting Qwik City SSG…” 로그가 나타나지 않음
  • dist/sitemap.xml 파일이 존재하고 빈 109바이트 파일보다 큼
  • cat dist/sitemap.xml 출력에 기대한 URL들이 포함됨
  • 프로덕션 서버가 전체 sitemap을 반환 (curl https://yourdomain.com/sitemap.xml)
  • 빌드 로그에 SSG 관련 경고나 오류가 없음

선택 사항: /sitemap.xml//sitemap.xml 로 리다이렉트

Trailing slash 로 요청될 경우 간단히 리다이렉트를 추가합니다:

import { createServer } from "http";

createServer((req, res) => {
  if (req.url === "/sitemap.xml/") {
    res.writeHead(301, { Location: "/sitemap.xml" });
    res.end();
    return;
  }

  // ...rest of server logic
}).listen(3000);

검색 엔진에는 무해하지만 URL을 깔끔하게 정리할 수 있습니다.


정적 Sitemap vs. SSR 라우트 선택 가이드

사용 사례권장 방법
순수 SSR 앱, 라우트가 개발 시점에 고정, sitemap 변경이 드물 경우정적 sitemap (public/sitemap.xml + ssg: null)
CMS, DB, 사용자 생성 콘텐츠 등에서 sitemap 데이터를 가져와야 할 때SSR 라우트 (src/routes/sitemap.xml/index.ts)
실시간 업데이트 또는 로케일별 로직이 필요할 때SSR 라우트
별도 빌드 단계 없이 간단히 배포하고 싶을 때정적 sitemap

프로젝트에 맞는 방식을 적용하면 빈 sitemap 문제는 해결됩니다.

Back to Blog

관련 글

더 보기 »

core.async: 심층 탐구 — 온라인 밋업

이벤트 개요: 12월 10일 GMT+1 기준 18:00에 Health Samurai가 온라인 밋업 “core.async: Deep Dive”를 주최합니다. 이번 강연은 clojure.core의 내부를 파헤칩니다....

모뎀의 복수

첫 번째 연결 1994년 겨울, 홍콩의 작은 아파트에서, 14세 소년이 US Robotics Sportster 14,400 Fax Modem을 연결했다.

JavaScript 첫 걸음: 간단한 정리

JavaScript에서 변수 let: 나중에 값이 변경될 수 있는 경우에 사용합니다. 예시: ```javascript let age = 20; age = 21; ``` const: 값이 절대로 변경되지 않아야 할 때 사용합니다.