TypeScript로 Spectral 사용하는 방법

Audrey Lopez

Audrey Lopez

19 May 2025

TypeScript로 Spectral 사용하는 방법

API를 사용하는 팀에게 일관성, 품질 및 설계 표준 준수는 매우 중요합니다. OpenAPI 및 AsyncAPI와 같은 API 사양은 청사진을 제공하지만, 수많은 서비스와 팀에 걸쳐 이러한 청사진이 올바르게 준수되도록 하는 것은 어려운 과제가 될 수 있습니다. 이때 API 린팅 도구가 등장하며, Spectral은 유연하고 강력한 오픈 소스 옵션으로 두드러집니다. TypeScript와 결합될 때, Spectral은 개발자가 강력하고 타입 안전한 사용자 지정 규칙을 만들 수 있도록 지원하여 API 거버넌스를 새로운 수준으로 끌어올립니다.

이 튜토리얼은 초기 설정부터 정교한 사용자 지정 유효성 검사 로직 작성까지, TypeScript와 함께 Spectral을 활용하는 과정을 안내합니다. TypeScript가 Spectral 규칙 개발을 어떻게 향상시켜 더 유지보수 가능하고 신뢰할 수 있는 API 린팅 솔루션을 제공하는지 살펴보겠습니다.

💡
멋진 API 문서를 생성하는 훌륭한 API 테스트 도구를 원하시나요?

개발 팀이 최고의 생산성으로 함께 작업할 수 있는 통합된 올인원 플랫폼을 원하시나요?

Apidog는 귀하의 모든 요구를 충족하며, 훨씬 저렴한 가격으로 Postman을 대체합니다!
button

Spectral 이해하기: API 사양의 수호자

TypeScript 통합에 대해 알아보기 전에 Spectral이 무엇이며 왜 API 개발 툴킷에서 가치 있는 도구인지 알아보겠습니다.

Spectral은 OpenAPI (v2 및 v3)AsyncAPI와 같은 API 설명 형식에 주로 초점을 맞춘 오픈 소스 JSON/YAML 린터입니다. 그 목적은 API 설계 가이드라인을 적용하고, 일반적인 오류를 감지하며, API 환경 전반에 걸쳐 일관성을 보장하는 데 도움을 주는 것입니다. API 계약을 위한 ESLint 또는 TSLint라고 생각하시면 됩니다.

Spectral 사용의 주요 이점:

Spectral은 규칙 세트(rulesets)를 기반으로 작동합니다. 규칙 세트는 규칙 모음이며, 각 규칙은 API 문서의 특정 부분(JSONPath 표현식 사용)을 대상으로 유효성 검사 로직을 적용합니다. Spectral은 내장 규칙 세트(예: OpenAPI 표준을 위한 spectral:oas)와 함께 제공되지만, 진정한 힘은 사용자 지정 규칙 세트를 정의하는 능력에 있습니다.

Spectral 사용자 지정 규칙에 TypeScript를 사용하는 이유는 무엇인가요?

Spectral 규칙 세트는 YAML 또는 JavaScript(.js 파일)로 정의할 수 있지만, 사용자 지정 함수 개발에 TypeScript를 사용하면 상당한 이점을 얻을 수 있습니다.

사용자 지정 Spectral 함수를 TypeScript로 작성함으로써, 애플리케이션 코드에 적용하는 것과 동일한 엄격함과 도구의 이점을 API 거버넌스 코드에도 가져올 수 있습니다.

Spectral 및 TypeScript 환경 설정하기

이제 직접 해보면서 필요한 도구를 설정해 보겠습니다.

필수 구성 요소:

설치 단계:

먼저 린팅 작업을 실행하고 규칙을 테스트하기 위해 Spectral CLI가 필요합니다. 전역으로 설치하거나 npx를 사용하는 것이 유용합니다.

Bash

npm install -g @stoplight/spectral-cli
# or
yarn global add @stoplight/spectral-cli

사용자 지정 규칙을 프로그래밍 방식으로 개발하고 TypeScript 프로젝트 내에서 Spectral의 코어 라이브러리를 사용하려면 필요한 Spectral 패키지를 설치하세요:

Bash

npm install @stoplight/spectral-core @stoplight/spectral-functions @stoplight/spectral-rulesets typescript ts-node --save-dev
# or
yarn add @stoplight/spectral-core @stoplight/spectral-functions @stoplight/spectral-rulesets typescript ts-node --dev

이 패키지들을 자세히 살펴보겠습니다:

TypeScript 구성:

아직 tsconfig.json 파일이 없다면 프로젝트 루트에 생성하세요. 기본 구성은 다음과 같을 수 있습니다:

JSON

{
  "compilerOptions": {
    "target": "es2020", // Or a newer version
    "module": "commonjs",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "outDir": "./dist", // Output directory for compiled JavaScript
    "rootDir": "./src", // Source directory for your TypeScript files
    "resolveJsonModule": true // Allows importing JSON files
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "**/*.spec.ts"]
}

프로젝트 구조에 따라 outDirrootDir를 조정하세요. 사용자 지정 TypeScript 함수는 src 디렉토리에 위치한다고 가정하겠습니다.

Spectral 핵심 개념: 규칙, 규칙 세트 및 함수

TypeScript 함수를 작성하기 전에 Spectral의 주요 구성 요소에 대한 이해를 확고히 합시다.

규칙:

규칙은 수행할 특정 검사를 정의합니다. 규칙의 주요 속성은 다음과 같습니다:

규칙 세트:

규칙 세트는 YAML 또는 JavaScript 파일(예: .spectral.yaml, .spectral.js 또는 컴파일 시 .spectral.ts)로 규칙을 그룹화합니다. 또한 다음을 수행할 수 있습니다:

함수:

함수는 실제 유효성 검사를 수행하는 핵심 로직 단위입니다. Spectral은 다음과 같은 많은 내장 함수를 제공합니다:

이러한 함수만으로는 충분하지 않고 자체 사용자 지정 함수를 작성해야 할 때 진정한 힘이 발휘됩니다. 바로 여기서 TypeScript가 빛을 발합니다.

TypeScript로 첫 번째 사용자 지정 Spectral 함수 만들기

간단한 사용자 지정 함수를 만들어 보겠습니다. 모든 API 작업 요약이 Title Case여야 하고 70자를 초과하지 않아야 한다는 규칙을 적용한다고 상상해 봅시다.

1단계: TypeScript에서 사용자 지정 함수 정의하기

예를 들어 src/customFunctions.ts 파일을 생성합니다:

TypeScript

import type { IFunction, IFunctionResult } from '@stoplight/spectral-core';

interface TitleCaseLengthOptions {
  maxLength: number;
}

// Custom function to check if a string is title-cased and within a max length
export const titleCaseAndLength: IFunction<string, TitleCaseLengthOptions> = (
  targetVal,
  options,
  context
): IFunctionResult[] | void => {
  const results: IFunctionResult[] = [];

  if (typeof targetVal !== 'string') {
    // Should not happen if 'given' path points to a string, but good practice
    return [{ message: `Value at path '${context.path.join('.')}' must be a string.` }];
  }

  // Check for Title Case (simple check: first letter of each word is uppercase)
  const words = targetVal.split(' ');
  const isTitleCase = words.every(word => word.length === 0 || (word[0] === word[0].toUpperCase() && (word.length === 1 || word.substring(1) === word.substring(1).toLowerCase())));

  if (!isTitleCase) {
    results.push({
      message: `Summary "${targetVal}" at path '${context.path.join('.')}' must be in Title Case.`,
      path: [...context.path], // Path to the violating element
    });
  }

  // Check for length
  const maxLength = options?.maxLength || 70; // Default to 70 if not provided
  if (targetVal.length > maxLength) {
    results.push({
      message: `Summary "${targetVal}" at path '${context.path.join('.')}' exceeds maximum length of ${maxLength} characters. Current length: ${targetVal.length}.`,
      path: [...context.path],
    });
  }

  return results;
};

설명:

2단계: TypeScript 함수 컴파일하기

.spectral.yaml 또는 .spectral.js 규칙 세트와 함께 이 함수를 사용할 계획이라면, functionsDir를 가리키는 경우 TypeScript를 JavaScript로 컴파일해야 합니다.

package.json에 빌드 스크립트를 추가합니다:

JSON

{
  "scripts": {
    "build": "tsc"
  }
}

npm run build 또는 yarn build를 실행합니다. 이렇게 하면 src/customFunctions.tsdist/customFunctions.js로 컴파일됩니다 (tsconfig.json 기준).

3단계: 규칙 세트 파일 생성하기

예를 들어 .spectral.js 규칙 세트를 만들어 보겠습니다 (완전히 TypeScript 기반 설정을 선호하는 경우 .spectral.ts도 가능합니다. 다음 섹션을 참조하세요).

컴파일된 함수를 직접 참조하는 JavaScript 규칙 세트 파일을 사용하는 경우:

JavaScript

// .spectral.js
const { titleCaseAndLength } = require('./dist/customFunctions'); // Path to your compiled JS functions

module.exports = {
  extends: [['@stoplight/spectral-rulesets/dist/rulesets/oas', 'recommended']],
  rules: {
    'operation-summary-title-case-length': {
      description: 'Operation summaries must be title-cased and not exceed 70 characters.',
      message: '{{error}}', // The message will come from the custom function
      given: '$.paths[*][*].summary', // Targets all operation summaries
      severity: 'warn',
      formats: ["oas3"], // Apply only to OpenAPI v3 documents
      then: {
        function: titleCaseAndLength, // Directly reference the imported function
        functionOptions: {
          maxLength: 70,
        },
      },
    },
    // You can add more rules here
  },
};

또는 .spectral.yamlfunctionsDir를 사용하는 경우:

먼저 dist 디렉토리에 index.js가 포함되어 있는지 확인하거나 customFunctions.js가 직접 내보내는지 확인하세요. 예를 들어, dist/customFunctions.jsexports.titleCaseAndLength = ...;가 있다면 다음과 같이 할 수 있습니다:

YAML

# .spectral.yaml
extends:
  - ["@stoplight/spectral-rulesets/dist/rulesets/oas", "recommended"]
functionsDir: "./dist" # Directory containing compiled custom JS functions
rules:
  operation-summary-title-case-length:
    description: "Operation summaries must be title-cased and not exceed 70 characters."
    message: "{{error}}"
    given: "$.paths[*][*].summary"
    severity: "warn"
    formats: ["oas3"]
    then:
      function: customFunctions#titleCaseAndLength # Assuming functions are exported from customFunctions.js
      functionOptions:
        maxLength: 70

여기서 customFunctions#titleCaseAndLength는 Spectral에게 functionsDir에서 customFunctions.js (또는 customFunctions/index.js)를 찾아 내보낸 titleCaseAndLength 함수를 사용하도록 지시합니다.

4단계: 샘플 OpenAPI 문서 생성하기

규칙을 테스트하기 위해 간단한 openapi.yaml 파일을 만들어 보겠습니다:

YAML

# openapi.yaml
openapi: 3.0.0
info:
  title: Sample API
  version: 1.0.0
paths:
  /items:
    get:
      summary: retrieves all items from the store # Incorrect: not title case
      responses:
        '200':
          description: A list of items.
    post:
      summary: Adds A New Item To The Ever Expanding Collection Of Items In The Store # Incorrect: too long
      responses:
        '201':
          description: Item created.
  /users:
    get:
      summary: Get User Details # Correct
      responses:
        '200':
          description: User details.

5단계: Spectral 실행하기

이제 OpenAPI 문서에 대해 Spectral CLI를 실행합니다:

Bash

spectral lint openapi.yaml --ruleset .spectral.js
# or if using YAML ruleset (often auto-detected if named .spectral.yaml)
spectral lint openapi.yaml

예상 출력:

다음과 유사한 경고가 표시될 것입니다:

openapi.yaml
 2:10  warning  operation-summary-title-case-length  Summary "retrieves all items from the store" at path 'paths./items.get.summary' must be in Title Case.   paths./items.get.summary
 6:10  warning  operation-summary-title-case-length  Summary "Adds A New Item To The Ever Expanding Collection Of Items In The Store" at path 'paths./items.post.summary' exceeds maximum length of 70 characters. Current length: 78.  paths./items.post.summary

✖ 2 problems (0 errors, 2 warnings, 0 infos, 0 hints)

이 출력은 JavaScript로 컴파일된 사용자 지정 TypeScript 함수가 위반 사항을 올바르게 식별하고 있음을 확인합니다.

TypeScript 규칙 세트를 사용한 Spectral 프로그래밍 방식 사용

더 복잡한 시나리오나 애플리케이션에 더 긴밀하게 통합하기 위해 Spectral을 프로그래밍 방식으로 사용하고 규칙 세트를 완전히 TypeScript로 정의할 수 있습니다. 이렇게 하면 원하는 경우 별도의 .spectral.yaml 또는 .spectral.js 파일이 필요 없으며 동적 규칙 세트 구성을 허용합니다.

예를 들어 린터용 TypeScript 파일 src/linter.ts를 생성합니다:

TypeScript

import { Spectral, Document } from '@stoplight/spectral-core';
import { oas } from '@stoplight/spectral-rulesets';
import { truthy } from '@stoplight/spectral-functions'; // Example built-in function
import { titleCaseAndLength } from './customFunctions'; // Your custom TS function
import type { ISpectralDiagnostic } from '@stoplight/spectral-core';
import * as fs from 'fs/promises';
import * as path from 'path';

// Define the Spectral instance
const spectral = new Spectral();

// Load built-in rulesets and functions
spectral.setRuleset({
  extends: [[oas, 'recommended']], // Extends the recommended OpenAPI rules
  rules: {
    'operation-summary-title-case-length': {
      description: 'Operation summaries must be title-cased and not exceed 70 characters.',
      message: '{{error}}',
      given: '$.paths[*][*].summary',
      severity: 'warn',
      formats: ["oas3"],
      then: {
        function: titleCaseAndLength, // Directly use the TypeScript function
        functionOptions: {
          maxLength: 70,
        },
      },
    },
    'info-contact-defined': { // Example using a built-in function
        description: 'Info object should have a contact object.',
        message: 'API contact information is missing.',
        given: '$.info',
        severity: 'warn',
        formats: ["oas3"],
        then: {
          field: 'contact',
          function: truthy, // Using a built-in function
        },
      },
  },
});

// Function to lint a document
export async function lintDocument(filePath: string): Promise<ISpectralDiagnostic[]> {
  try {
    const absolutePath = path.resolve(filePath);
    const fileContent = await fs.readFile(absolutePath, 'utf-8');
    
    // Create a Spectral Document object
    // The second argument (URI) is important for resolving relative $refs if any
    const document = new Document(fileContent, undefined, absolutePath); 
    
    const results = await spectral.run(document);
    return results;
  } catch (error) {
    console.error('Error linting document:', error);
    return [];
  }
}

// Example usage (e.g., in a script or another module)
async function main() {
  const diagnostics = await lintDocument('openapi.yaml'); // Path to your API spec
  if (diagnostics.length > 0) {
    console.log('API Linting Issues Found:');
    diagnostics.forEach(issue => {
      console.log(
        `- [${issue.severity === 0 ? 'Error' : issue.severity === 1 ? 'Warning' : issue.severity === 2 ? 'Info' : 'Hint'}] ${issue.code} (${issue.message}) at ${issue.path.join('.')}`
      );
    });
  } else {
    console.log('No API linting issues found. Great job!');
  }
}

// If you want to run this file directly with ts-node
if (require.main === module) {
  main().catch(console.error);
}

실행 방법:

Bash

npx ts-node src/linter.ts

이 접근 방식은 최대의 유연성을 제공합니다:

고급 사용자 지정 함수 기법

사용자 지정 함수 생성의 몇 가지 더 고급 측면을 살펴보겠습니다.

비동기 사용자 지정 함수:

사용자 지정 함수가 비동기 작업(예: 유효성 검사를 위해 외부 리소스 가져오기, 성능을 위해 주의해서 사용)을 수행해야 하는 경우 비동기 함수로 정의할 수 있습니다.

TypeScript

import type { IFunction, IFunctionResult } from '@stoplight/spectral-core';

export const checkExternalResource: IFunction<string, { url: string }> = 
  async (targetVal, options, context): Promise<IFunctionResult[] | void> => {
  try {
    const response = await fetch(`${options.url}/${targetVal}`);
    if (!response.ok) {
      return [{ message: `Resource '${targetVal}' not found at ${options.url}. Status: ${response.status}` }];
    }
  } catch (error: any) {
    return [{ message: `Error fetching resource '${targetVal}': ${error.message}` }];
  }
};

Spectral은 비동기 함수를 올바르게 await할 것입니다.

전체 문서 및 해석된 값 접근하기:

context.document 객체는 전체 파싱된 문서에 접근할 수 있게 해줍니다. 더 강력하게, context.document.resolved는 $ref 포인터를 다룰 때 필수적인 문서의 완전히 역참조된 버전을 제공합니다.

TypeScript

import type { IFunction, IFunctionResult } from '@stoplight/spectral-core';
import {isPlainObject} from '@stoplight/json';

// Example: Ensure a referenced schema has a specific property
export const referencedSchemaHasProperty: IFunction<{$ref: string}, { propertyName: string }> = (
  targetVal, // This will be the object like { $ref: '#/components/schemas/MySchema' }
  options,
  context
): IFunctionResult[] | void => {
  if (!targetVal.$ref) return;

  // The `context.document.resolveAnchor` method can find the resolved value for a $ref
  const resolvedValue = context.document.resolveAnchor(targetVal.$ref);

  if (!resolvedValue || !isPlainObject(resolvedValue.value)) {
    return [{ message: `Could not resolve $ref: ${targetVal.$ref}` }];
  }
  
  const schemaProperties = resolvedValue.value.properties as Record<string, unknown> | undefined;

  if (!schemaProperties || schemaProperties[options.propertyName] === undefined) {
    return [{
      message: `The schema referenced by "${targetVal.$ref}" at path '${context.path.join('.')}' must have property "${options.propertyName}".`,
      path: [...context.path, '$ref'] // Point to the $ref itself
    }];
  }
};

이 함수는 예를 들어 $.paths[*].*.responses.*.content.*.schema와 같이 $ref를 포함하는 객체를 대상으로 하는 given 경로와 함께 사용됩니다.

형식 작업하기:

사용자 지정 규칙이 적용될 형식(예: oas2, oas3, asyncapi2)을 지정하는지 확인하세요. 이렇게 하면 호환되지 않는 문서 타입에서 규칙이 실행되는 것을 방지할 수 있습니다. 함수 내에서 context.document.formats를 통해 감지된 형식에 접근할 수 있습니다.

규칙 세트 구성 및 확장하기

사용자 지정 규칙 모음이 늘어남에 따라 구성이 중요해집니다.

JavaScript

// .spectral.js (main ruleset)
module.exports = {
  extends: ['./rulesets/common-rules.js', './rulesets/security-rules.js'],
  // ... other rules or overrides
};

워크플로우에 Spectral 통합하기

API 린팅의 진정한 힘은 자동화될 때 발휘됩니다.

YAML

# Example GitHub Action step
- name: Lint API Specification
  run: spectral lint ./path/to/your/api-spec.yaml --ruleset ./.spectral.js --fail-severity=error

Spectral 및 TypeScript 사용자 지정 규칙 모범 사례

결론: Spectral 및 TypeScript로 API 거버넌스 수준 높이기

Spectral은 API 린팅을 위한 강력한 프레임워크를 제공하며, 사용자 지정 규칙 개발에 TypeScript를 활용함으로써 API 거버넌스 전략에 향상된 타입 안전성, 개발자 경험 및 유지보수성을 가져옵니다. OpenAPI 모범 사례, 회사별 명명 규칙 또는 API 설계 내의 복잡한 비즈니스 로직을 적용하든 관계없이, Spectral의 유연한 규칙 엔진과 TypeScript의 강력한 타이핑 시스템의 조합은 강력한 솔루션을 제공합니다.

IDE 피드백부터 CI/CD 파이프라인까지 개발 라이프사이클에 Spectral을 통합함으로써 API가 일관되고 고품질이며 정의된 표준을 준수하도록 보장할 수 있으며, 궁극적으로 더 안정적이고 사용하기 쉬운 API로 이어집니다. 작게 시작하고, 규칙 세트를 반복하며, 팀에게 더 나은 API를 구축하도록 권한을 부여하십시오.

Apidog에서 API 설계-첫 번째 연습

API를 더 쉽게 구축하고 사용하는 방법을 발견하세요