SvelteKit의 MSW

SvelteKit에서 로컬 개발을 위해 MSW를 구현하는 방법

SvelteKit에서 MSW 사용

이것은 SvelteKit 애플리케이션에서 "Mock Service Worker" 라이브러리인 MSW를 구현하는 방법을 보여주는 튜토리얼입니다. 이 가이드는 Jest로 MSW를 설정하는 방법이 아니라 라이브 개발 중에 MSW를 사용하는 방법을 보여줍니다.

완전한 구현은 spikze.club의 리포지토리에서 사용할 수 있으며 페이지 끝에 있는 부록에도 링크가 있습니다.

이 가이드가 처음에는 압도적으로 보일 수 있으니 걱정하지 마세요. 변경 사항은 매우 간단합니다. SvelteKit에서 MSW를 사용하여 로컬 개발 중에 요청을 모의하려면 조금 더 노력해야 합니다.

종속성 설치

먼저 필요한 모든 종속성을 설치하겠습니다.

npm i -D msw ts-node concurrently

우리는 "ts-node"를 사용하여 서버 모의도 시작합니다. 계속 진행하기 전에 SvelteKit 프로젝트의 루트에서 다음 명령을 호출하십시오.

npx msw init ./static

이렇게 하면 MSW의 기본 서비스 작업자 처리기가 있는 Javascript 파일이 생성됩니다. SvelteKit-app에 대해 "정적"을 사용하고 있습니다. 공개 디렉토리와 동일하기 때문입니다. 마지막으로 MSW가 파일을 찾을 위치를 알 수 있도록 package.json-file을 업데이트하겠습니다.

{
  "scripts": ...
  ...
  "msw": {
    "workerDirectory": "static"
  }
}

편리한 스크립트로서 클라이언트와 서버(서버 측 모의용)를 통해 MSW를 시작하려는 경우 필요한 "스크립트" 구성에 다음 명령도 추가했습니다.

{
  "scripts": {
    "dev": "svelte-kit dev",
    "dev:msw-server": "concurrently \"cd msw && ts-node server.ts\" \"npm run dev\"",
    ...
  },
}

MSW 모듈 준비

실제 코드 작성을 시작하기 전에 SvelteKit의 구성과 Typescript-config에서 MSW용 새 작업 디렉터리를 정의해야 합니다. Typescript 파일부터 시작하겠습니다.

{
  "compilerOptions": {
    ...,
    "paths": {
      "$lib": ["src/lib"],
      "$lib/*": ["src/lib/*"],
      "$msw": ["src/msw"],
      "$msw/*": ["src/msw/*"]
    }
  }
}

다음으로 SvelteKit-config를 업데이트하겠습니다.

import adapter from "@sveltejs/adapter-auto";
import preprocess from "svelte-preprocess";
import path from "path";

/** @type {import('@sveltejs/kit').Config} */
const config = {
  // Consult https://github.com/sveltejs/svelte-preprocess
  // for more information about preprocessors
  preprocess: preprocess(),
  kit: {
    adapter: adapter(),
    vite: {
      ...,
      resolve: {
        alias: {
          $msw: path.resolve("./src/msw"),
          $lib: path.resolve("./src/lib")
        }
      }
    }
  }

모의 및 핸들러 추가

자, 가이드의 첫 번째 부분을 완료했습니다! 이제 MSW를 사용할 때 실행되는 실제 코드를 추가합니다. 이것은 최소한의 예이므로 모든 것을 작동시키는 데 필요한 것을 추가했지만 더 이상은 추가하지 않았습니다.

SvelteKit-config에 대한 변경 사항에서 짐작할 수 있듯이 모든 코드는 "src"에 직접 있는 "msw"라는 새 디렉토리에 배치됩니다.

|- app/
|-- src/
|--- pages/
|--- msw/
|---- fixtures/
...

다음은 추가할 모듈에 대한 코드입니다. 단순히 복사하여 붙여 넣을 수 있어야하며 파일 이름 + 디렉토리 경로가 상단의 각 블록에 주석으로 기록됩니다.

//
// app/src/msw/handlers.server.ts
//

import { rest } from "msw";
import { values } from "./fixtures/msw-demo";

export const handlers = [
  // Here, you can mock absolute URL requests,
  // e.g. to a database. For the current implementation,
  // no data is mocked in this place.
  //
  // Note: This is also the place to mock absolute
  // SSR-imports. Everything in 'handlers.workers.ts'
  // is mocked client-side.
];
//
// app/src/msw/handlers.worker.ts
//

import { rest } from "msw";
import { values } from "./fixtures/msw-demo";

// Mock relative URLs that map to your
// routes' data endpoints. This mock only
// happens for client-side requests.
//
// Note that if you use shadow endpoints, this still works
// as the endpoint gets created by SvelteKit.
export const handlers = [
  rest.get("/msw/demo/__data.json", (req, res, ctx) => {
    return res(ctx.status(200), ctx.json({ values }));
  })
];
//
// app/src/msw/server.ts
//

import { setupServer } from "msw/node";
import { handlers } from "./handlers.server";

export const server = setupServer(...handlers);
//
// app/src/msw/worker.ts 
//

import { setupWorker } from "msw";
import { handlers } from "./handlers.worker";

export const worker = setupWorker(...handlers);
//
// app/src/msw/fixtures/msw-demo.ts
//

export const values = ["foo", "bar"];
//
// app/src/msw/index.ts 
//

import { browser, dev } from "$app/env";

/**
 * Lazy-inject the MSW handler
 * so that no errors happen during
 * build/runtime due to invalid
 * imports from server/client.
 */
export async function inject() {
  if (dev && browser) {
    const { worker } = await import("../msw/worker");
    // For live development, I disabled all warnings
    // for requests that are not mocked. Change how
    // you think it best fits your project.
    return worker.start({ onUnhandledRequest: "bypass" }).catch(console.warn);
  }
  if (dev && !browser) {
    const { server } = await import("../msw/server");
    // Same as in worker-mock above.
    return server.listen({ onUnhandledRequest: "bypass" });
  }
}

MSW 시작

라이브 개발 중에 MSW를 사용할 때의 한 가지 문제는 경쟁 조건이 없는지 확인해야 한다는 것입니다. 실행 순서를 정의해야 합니다. 그렇지 않으면 모든 요청이 이미 이루어진 후 MSW의 서비스 작업자가 활성화될 수 있습니다.

이 목표를 달성하기 위해 루트 레이아웃 파일을 수정합니다. 이 파일은 모든 페이지에 마운트되므로 MSW가 완료될 때까지 모든 추가 실행을 차단하는 것이 좋습니다.

<script>
  import "../app.css";
  import { dev } from "$app/env";
  
  // Loaded from .env.local, guide covers this
  // step in a moment.
  const isMswEnabled = dev && import.meta.env.VITE_MSW_ENABLED === "true";
  // Flag to defer rendering of components
  // until certain criteria are met on dev,
  // e.g. MSW init.
  let isReady = !isMswEnabled;
  
  if (isMswEnabled) {
    import("$msw")
      .then((res) => res.inject())
      .then(() => (isReady = true));
  }
</script>

{#if isReady}
  <slot />
{/if}

데모 페이지 추가

다음 코드 스니펫은 이 튜토리얼에서 사용된 두 개의 데모 페이지와 하나의 엔드포인트에 대한 내용을 보여줍니다.

<!-- app/src/routes/msw/index.svelte -->

<script>
  import DisplayProse from "$lib/display/views/DisplayProse.svelte";
  import ProminentDisplayTitle from "$lib/display/views/ProminentDisplayTitle.svelte";
  import PageLayout from "$lib/layout/views/PageLayout.svelte";
  import SectionLayout from "$lib/layout/views/SectionLayout.svelte";
</script>

<PageLayout>
  <SectionLayout withContentTopSpacing withHeaderSpacing>
    <ProminentDisplayTitle slot="header" color="primary">MSW Landing Page</ProminentDisplayTitle>
    <DisplayProse>
      <p>
        This compoonent has no purpose other than being part of an MSW demo implementation. See <a
          href="https://flaming.codes"
          alt="Link to flaming.codes with blog posts">flaming.codes</a
        > for more details.
      </p>
      <p>
        This page doesn't fetch any data for shows how client-side fetches are mocked with MSW in
        SvelteKit.
      </p>
      <p>Simply click the link below to access the page with data.</p>
      <p>
        <a href="/msw/demo" alt="Link to demo page with data">msw/demo</a>
      </p>
    </DisplayProse>
  </SectionLayout>
</PageLayout>
<!-- app/src/routes/msw/demo.svelte -->

<script lang="ts">
  import ProminentDisplayTitle from "$lib/display/views/ProminentDisplayTitle.svelte";
  import PageLayout from "$lib/layout/views/PageLayout.svelte";
  import SectionLayout from "$lib/layout/views/SectionLayout.svelte";
  export let values: string[];
</script>

<PageLayout>
  <SectionLayout withHeaderSpacing withContentTopSpacing>
    <ProminentDisplayTitle slot="header" color="primary">MSW Demo</ProminentDisplayTitle>
    <p>
      This compoonent has no purpose other than being part of an MSW demo implementation. See <a
        href="https://flaming.codes"
        alt="Link to flaming.codes with blog posts">flaming.codes</a
      > for more details.
    </p>
    <p>
      Values: {values}
    </p>
  </SectionLayout>
</PageLayout>
//
// app/src/routes/msw/demo.ts
//

import type { RequestHandler } from "@sveltejs/kit";

// Just for demo purposes.
export const get: RequestHandler = async () => ({
  status: 200,
  body: {
    values: ["production", "data", "not", "msw"]
  }
});

환경 변수 추가

거의 완료되었습니다. 누락된 것은 "VITE_MSW_ENABLED" 플래그를 환경에 추가하는 것입니다. 플래그를 유지하기 위해 ".env.local"을 파일로 사용하고 있습니다. 이는 Vite에서 소비되고 git에 추가되지 않기 때문입니다.

VITE_MSW_ENABLED=true

애플리케이션 실행

자, 이제 모든 준비가 완료되어야 합니다! 클라이언트에서 조롱을 활성화하려면 플래그가 설정되어 있는지 확인하기만 하면 됩니다. 공통 "dev" 명령을 실행한 다음 클라이언트 측 요청을 모의합니다.

npm run dev

서버 측 요청도 모의하려면 처음에 추가한 새 명령을 실행하기만 하면 됩니다.

npm run dev:msw-server

이제 로컬 개발 중에 끝점을 조롱할 준비가 되었습니다. 코드 예제에서 언급했듯이 이 데모 페이지에는 서버 측 모의가 포함되어 있지 않지만 모든 것이 준비되어 있습니다. 즉, 로컬에서 MSW 호출은 링크를 클릭하여 데모 사이트의 인덱스 페이지에서 실제 데모 페이지로 이동하는 경우에만 트리거됩니다.

서버 요청을 시뮬레이션하기 위해 데이터베이스에서 데이터를 가져오는 더 복잡한 끝점도 갖게 됩니다. 그런 다음 이러한 요청을 서버 처리기에서 조롱할 수 있습니다. server-mock의 경우 절대 URL만 유효합니다.