Skip to content

4jades/msw-auto-mock

Repository files navigation

🎯 msw-auto-mock

OpenAPI 스펙을 기반으로 MSW(Mock Service Worker) 핸들러를 자동으로 생성해주는 라이브러리입니다.

✨ 주요 기능

  • 🚀 OpenAPI 스펙 기반 자동 핸들러 생성
  • 📁 엔티티별 핸들러 파일 분리
  • 🎮 프로그래밍 방식 API 지원
  • 🛠 커스텀 컨트롤러를 통한 응답 커스터마이징 (controllers 타입 추론 지원)
  • 🔄 다양한 응답 타입 지원 (JSON, Event Stream 등)

📦 설치 방법

npm registry에 배포 전이므로, portal 방식으로 link합니다.

1️⃣ 프로젝트 구조 설정

먼저 my-projectmsw-auto-mock을 같은 최상위 디렉토리에 위치시킵니다:

/workspace
├── my-project/    # 구현 프로젝트
└── msw-auto-mock/ # 라이브러리

2️⃣ msw-auto-mock 설정

msw-auto-mock 디렉토리에서:

cd msw-auto-mock
pnpm install
pnpm build

3️⃣ my-project 설정

my-project 디렉토리에서:

  1. package.json에 다음 설정을 추가:
{
  "scripts": {
    "generate-msw-mock": "tsx src/app/mocks/mock-generator.ts"
  },
  "devDependencies": {
    "@dhlab/msw-auto-mock": "^0.31.0"
  },
  "resolutions": {
    "@dhlab/msw-auto-mock": "portal:../msw-auto-mock"
  }
}
  1. 의존성 설치:
cd my-project
yarn

4️⃣ my-project에 mock-generator 스크립트 추가

my-project/src/app/mocks/mock-generator.ts 파일을 생성:

import { type TOptions, generateMocks } from '@dhlab/msw-auto-mock';
import type { TControllers } from './__types__/index';
import { controllers } from './controllers/index';

async function autoGenerateMocks() {
  try {
    console.log('[MSW] 목 파일 생성 시작...');

    const options: TOptions<TControllers> = {
      controllers,
      input: './swagger/openapi.yml',
      outputDir: './src/app/mocks',
      environment: 'react',
      baseUrl: 'https://api.example.com/api/v1',
    };

    const result = await generateMocks(options);

    console.log('[MSW] 목 파일 생성 완료!');
    console.log('[MSW] 생성된 파일 경로:', result.targetFolder);

    return result;
  } catch (error) {
    console.error('[MSW] 목 파일 생성 중 오류 발생:', error);
    throw error;
  }
}

autoGenerateMocks();

5️⃣ MSW 핸들러 생성

my-project 디렉토리에서:

yarn generate-msw-mock

🔧 사용 방법

기본 설정

const options: ProgrammaticOptions = {
  /**
   * OpenAPI 스펙 파일 경로
   * YAML 또는 JSON 형식 지원
   * @required
   */
  input: './swagger/openapi.yml',

  /**
   * 생성된 핸들러 파일이 저장될 디렉토리
   * @optional
   * @default 'src/app/mocks'
   */
  outputDir: './src/app/mocks',

  /**
   * API 기본 URL
   * @optional
   * - string: 지정된 URL을 기본 URL로 사용
   * - true: OpenAPI 스펙의 servers[0].url을 기본 URL로 사용
   */
  baseUrl: 'https://api.example.com',

  /**
   * 생성할 mock 파일의 환경 설정
   * @optional
   * @default 'react'
   * - next: Node.js와 브라우저 환경을 위한 mock 파일 생성
   * - react: 브라우저 환경을 위한 mock 파일 생성
   * - react-native: React Native 환경을 위한 mock 파일 생성
   */
  environment: 'react',

  /**
   * 배열 응답의 최대 길이
   * @optional
   * @default 3
   * faker.js로 생성되는 배열 응답의 최대 아이템 개수를 제한합니다.
   * 예: users 배열이 100개의 아이템을 가질 수 있더라도, maxArrayLength: 3으로 설정하면
   *     최대 3개의 사용자 데이터만 생성됩니다.
   */
  maxArrayLength: 3,

  /**
   * 포함할 API 경로 패턴
   * @optional
   * 예: '/api/v1/*' - /api/v1/로 시작하는 경로만 포함
   */
  includes: '/api/v1/*',

  /**
   * 제외할 API 경로 패턴
   * @optional
   * 예: '/api/v1/health' - /api/v1/health 경로 제외
   */
  excludes: '/api/v1/health',

  /**
   * HTTP 상태 코드
   * @optional
   * 예: '200,201' - 200과 201 상태 코드만 사용
   */
  codes: '200,201',

  /**
   * 정적 응답 사용 여부
   * @optional
   * @default false
   * true: faker.js를 사용하지 않고 정적 응답 생성
   */
  static: false,

  /**
   * 사용자 정의 응답 컨트롤러
   * @optional
   * faker.js를 사용하지 않고 API 엔드포인트별 커스텀 응답 생성
   */
  controllers: {
    getGetUsersUsersGet200Response: () => userList,
    ...
  },

  /**
   * 컨트롤러 import 경로
   * @optional
   * @default '@/app/mocks/controllers'
   */
  controllerPath: '@/app/mocks/controllers',

  /**
   * DTO 타입 import 경로
   * @optional
   * @default '@/shared/api/dto'
   * FSD(Feature-Sliced Design) 패러다임에 따라 기본값이 @/shared/api/dto로 설정됩니다.
   */
  dtoImportPath: '@/shared/api/dto',
};

📁 생성되는 파일 구조

src/app/mocks/
├── __types__/
│   ├── users.type.ts
│   ├── chats.type.ts
│   ├── ...
│   ├── index.ts
├── __handlers__/
│   ├── users.handlers.ts
│   ├── chats.handlers.ts
│   └── ...
│   └── index.ts
└── browser.ts

🔄 지원하는 환경

  • 🌐 React (Browser)
  • ⚡ Next.js (Node.js + Browser)
  • 📱 React Native

🚀 ESM 지원 및 기술적 해결 방안

Node.js 전용 기능이 CommonJS인 이유

generateMocks 함수는 다음과 같은 이유로 CommonJS로만 빌드됩니다:

  1. 의존성 제약: @apidevtools/swagger-parser, swagger2openapi 등의 핵심 의존성이 ESM을 지원하지 않음
  2. 파일 시스템 접근: Node.js의 fs, path 모듈을 직접 사용하여 파일 생성 작업 수행
  3. 안정성: CommonJS 환경에서 검증된 라이브러리들과의 호환성 보장

React 프로젝트에서 ESM 사용 가능

React 프로젝트에서는 다음과 같은 방식으로 ESM을 완전히 지원합니다:

// ✅ React 환경에서 ESM 사용 가능
import { selectResponseByScenario, transformJSONSchemaToFakerCode } from '@dhlab/msw-auto-mock';

// ✅ Node.js 빌드 스크립트에서 CommonJS 사용
// (예: React 프로젝트의 scripts/mock-generator.ts)
import { generateMocks } from '@dhlab/msw-auto-mock/node';

이중 패키지 구조의 장점

@dhlab/msw-auto-mock
├── dist/
│   ├── index.js      # ESM (브라우저, React 등)
│   ├── index.cjs     # CommonJS (Node.js 호환)
│   └── node/
│       └── node.cjs  # Node.js 전용 CommonJS

이 구조를 통해:

  • 브라우저 환경: 가벼운 ESM 번들 사용
  • Node.js 환경: 안정적인 CommonJS 사용
  • React 프로젝트: ESM으로 런타임 기능 사용, 빌드 스크립트는 CommonJS로 파일 생성

설치

npm install @dhlab/msw-auto-mock
# 또는
yarn add @dhlab/msw-auto-mock
# 또는
pnpm add @dhlab/msw-auto-mock

환경별 사용법

기본 사용법 (환경 상관없이)

환경에 상관없이 사용할 수 있는 기능들입니다.

// 타입 정의, 시나리오 선택, 가짜 데이터 생성 등
import { 
  selectResponseByScenario, 
  transformJSONSchemaToFakerCode,
  type TOptions,
  type TScenarioConfig,
  type ResponseObject
} from '@dhlab/msw-auto-mock';

Node.js 환경 (코드 생성)

Node.js 환경에서는 OpenAPI 스키마를 기반으로 MSW 핸들러 파일을 생성할 수 있습니다.

// Node.js 전용 기능
import { generateMocks } from '@dhlab/msw-auto-mock/node';

// 환경 상관없이 사용 가능한 기능들 (필요시 별도 import)
import { 
  selectResponseByScenario, 
  transformJSONSchemaToFakerCode,
  type TOptions 
} from '@dhlab/msw-auto-mock';

await generateMocks({
  input: 'path/to/openapi.json',
  outputDir: 'src/mocks',
  environment: 'react' // 'react', 'next', 'react-native'
});

React 컴포넌트에서 사용 예제

import React, { useState, useEffect } from 'react';
import { http, HttpResponse } from 'msw';
import { selectResponseByScenario, transformJSONSchemaToFakerCode } from '@dhlab/msw-auto-mock';

const MyComponent: React.FC = () => {
  const [mockData, setMockData] = useState(null);

  useEffect(() => {
    // 시나리오 설정
    const scenarios = {
      'user-error': {
        description: '사용자 오류 시나리오',
        api: {
          '/api/users': {
            'GET': { status: 400 }
          }
        }
      },
      'success': {
        description: '성공 시나리오',
        api: {
          '/api/users': {
            'GET': { status: 200 }
          }
        }
      },
      'custom-error': {
        description: '커스텀 에러 시나리오',
        api: {
          '/api/users': {
            'GET': { status: 418, allowCustomStatus: true }
          }
        }
      }
    };

    // MSW 핸들러 설정
    const handler = http.get('/api/users', (info) => {
      const responses = [
        { status: 200, responseType: 'application/json', body: JSON.stringify({ users: [] }) },
        { status: 400, responseType: 'application/json', body: JSON.stringify({ error: 'Bad Request' }) },
        { status: 500, responseType: 'application/json', body: JSON.stringify({ error: 'Server Error' }) }
      ];

      const selectedResponse = selectResponseByScenario('GET', '/api/users', responses, info, scenarios);
      
      return HttpResponse.json(
        JSON.parse(selectedResponse.body || '{}'),
        { status: selectedResponse.status }
      );
    });

    // 가짜 데이터 생성 예제
    const userSchema = {
      type: 'object',
      properties: {
        id: { type: 'string', format: 'uuid' },
        name: { type: 'string' },
        email: { type: 'string', format: 'email' },
        age: { type: 'integer', minimum: 18, maximum: 100 }
      }
    };

    const fakerCode = transformJSONSchemaToFakerCode(userSchema);
    console.log('Generated faker code:', fakerCode);

  }, []);

  return (
    <div>
      <h1>MSW Auto Mock React Example</h1>
      {/* 컴포넌트 내용 */}
    </div>
  );
};

Next.js에서 사용 예제

// pages/api/mocks/setup.ts 또는 app/api/mocks/setup/route.ts
import { generateMocks } from '@dhlab/msw-auto-mock/node';
import type { TOptions } from '@dhlab/msw-auto-mock';

export default async function handler(req: any, res: any) {
  if (process.env.NODE_ENV === 'development') {
    const options: TOptions = {
      input: './public/openapi.json',
      outputDir: './src/mocks',
      environment: 'next'
    };
    
    await generateMocks(options);
    
    res.status(200).json({ message: 'Mocks generated successfully' });
  } else {
    res.status(404).json({ message: 'Not found' });
  }
}
// components/MockProvider.tsx
import React from 'react';
import { selectResponseByScenario } from '@dhlab/msw-auto-mock';

export const MockProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
  // 브라우저 환경에서 MSW 설정
  React.useEffect(() => {
    if (typeof window !== 'undefined') {
      import('../mocks/browser').then(({ worker }) => {
        worker.start();
      });
    }
  }, []);

  return <>{children}</>;
};

API 문서

환경 상관없이 사용 가능한 API

selectResponseByScenario

시나리오 기반으로 응답을 선택하는 함수입니다. 헤더 x-scenario를 통해 특정 시나리오를 활성화할 수 있습니다.

function selectResponseByScenario(
  verb: string,
  path: string,
  resultArray: ResponseObject[],
  info: Parameters<HttpResponseResolver<Record<string, never>, null>>[0],
  scenarios?: TScenarioConfig
): ResponseObject

allowCustomStatus 기능

기본적으로 시나리오에서는 OpenAPI 명세에 정의된 status 코드만 사용할 수 있습니다. 하지만 allowCustomStatus: true를 설정하면 명세에 없는 임의의 status 코드도 사용할 수 있습니다.

  • 기본 동작: OpenAPI 명세에 정의된 status 코드만 허용 (타입 안전성 보장)
  • allowCustomStatus: true: 임의의 status 코드 사용 가능 (동적 에러 응답 생성)

사용 예시:

// 시나리오 설정
const scenarios = {
  'success': {
    description: '성공 시나리오',
    api: {
      '/api/users': { 'GET': { status: 200 } }
    }
  },
  'error': {
    description: '에러 시나리오',
    api: {
      '/api/users': { 'GET': { status: 500 } }
    }
  },
  'custom-error': {
    description: '커스텀 에러 시나리오',
    api: {
      '/api/users': { 
        'GET': { 
          status: 418, // "I'm a teapot" - OpenAPI 명세에 없는 status
          allowCustomStatus: true 
        } 
      }
    }
  }
};

// MSW 핸들러에서 사용
const handler = http.get('/api/users', (info) => {
  const responses = [
    { status: 200, responseType: 'application/json', body: '{"users": []}' },
    { status: 500, responseType: 'application/json', body: '{"error": "Server Error"}' }
  ];
  
  // 헤더 기반 시나리오 선택 - 이제 ResponseObject를 직접 반환
  const selectedResponse = selectResponseByScenario('GET', '/api/users', responses, info, scenarios);
  
  return HttpResponse.json(JSON.parse(selectedResponse.body || '{}'), {
    status: selectedResponse.status
  });
});

테스트 시나리오 제어:

# 성공 시나리오 테스트
curl -H "x-scenario: success" http://localhost:3000/api/users

# 에러 시나리오 테스트  
curl -H "x-scenario: error" http://localhost:3000/api/users

# 커스텀 에러 시나리오 테스트 (OpenAPI 명세에 없는 418 status)
curl -H "x-scenario: custom-error" http://localhost:3000/api/users

# 기본 시나리오 (헤더 없음 - 성공 응답 우선)
curl http://localhost:3000/api/users

transformJSONSchemaToFakerCode

OpenAPI 스키마를 Faker.js 코드로 변환하는 함수입니다.

function transformJSONSchemaToFakerCode(
  jsonSchema?: OpenAPIV3.SchemaObject,
  key?: string
): string

🎯 allowCustomStatus 기능 상세 가이드

사용 시나리오

allowCustomStatus: true 기능은 다음과 같은 상황에서 유용합니다:

  1. 테스트 전용 에러 코드: 특정 테스트 시나리오에서만 사용하는 에러 코드
  2. OpenAPI 명세 미정의 응답: 명세에는 없지만 실제 서버에서 발생할 수 있는 응답
  3. 프로토타이핑: 새로운 API 응답을 실험하기 위한 임시 응답

동작 방식

// OpenAPI 명세에 200, 400, 500만 정의되어 있다고 가정
const scenarios = {
  'teapot-error': {
    description: '티팟 에러 테스트',
    api: {
      '/api/users': {
        'GET': { 
          status: 418, // 명세에 없는 status
          allowCustomStatus: true // 허용 플래그
        }
      }
    }
  }
};

생성되는 응답

allowCustomStatus: true로 설정된 시나리오가 활성화되면:

{
  "error": "Client Error", // 400-499는 "Client Error"
  "status": 418
}

또는

{
  "error": "Internal Server Error", // 500+는 "Internal Server Error"  
  "status": 503
}

타입 안전성

TypeScript를 사용하는 경우:

// ❌ 컴파일 에러 - 명세에 없는 status
const badScenario = {
  api: {
    '/api/users': {
      'GET': { status: 418 } // allowCustomStatus 없이는 사용 불가
    }
  }
};

// ✅ 정상 - allowCustomStatus로 명시적 허용
const goodScenario = {
  api: {
    '/api/users': {
      'GET': { status: 418, allowCustomStatus: true }
    }
  }
};

주의사항

  • 명세 일치성: allowCustomStatus는 명세와 목업 간의 일치성을 일부 포기하는 트레이드오프입니다
  • 테스트 전용: 프로덕션 환경이 아닌 테스트/개발 환경에서만 사용하는 것을 권장합니다
  • 문서화: 커스텀 status를 사용하는 경우 팀 내에서 충분한 문서화가 필요합니다

타입 정의

export type TScenarioConfig = {
  [scenarioId: string]: {
    description: string;
    api: Record<string, Record<string, {
      status: number;
      allowCustomStatus?: boolean;
    }>>;
  };
};

export type ResponseObject = {
  status: number;
  responseType: string | undefined;
  body: string | undefined;
};

패키지 구조

이 라이브러리는 기능별로 분리된 패키지로 제공됩니다:

  • 기본 (메인 엔트리): 환경 상관없이 사용 가능한 기능들
  • Node.js 전용: 파일 시스템 접근이 필요한 코드 생성 기능
@dhlab/msw-auto-mock
├── dist/
│   ├── index.js      # 기본 ESM (환경 상관없이)
│   ├── index.cjs     # 기본 CommonJS (환경 상관없이)
│   └── node/
│       ├── node.js   # Node.js 전용 ESM
│       └── node.cjs  # Node.js 전용 CommonJS

사용법 요약

  • 환경 상관없이 사용 가능: import { ... } from '@dhlab/msw-auto-mock'
  • Node.js 전용 기능: import { generateMocks } from '@dhlab/msw-auto-mock/node'

실제 사용 예시

// React/Vue/Angular 등 모든 환경에서
import { selectResponseByScenario, transformJSONSchemaToFakerCode } from '@dhlab/msw-auto-mock';

// Node.js에서 파일 생성 기능이 필요한 경우
import { generateMocks } from '@dhlab/msw-auto-mock/node';

// 둘 다 필요하면 각각 import
import { selectResponseByScenario } from '@dhlab/msw-auto-mock';
import { generateMocks } from '@dhlab/msw-auto-mock/node';

라이선스

MIT

Packages

 
 
 

Contributors

Languages