Skip to content

CH 19 · 플러그인의 세 가지 형태: 함수 / 객체 / 클래스

전체 글자 수약 2,870 자예상 소요 시간약 20분선행CH 18 (첫 플러그인)난이도재현 가능

본 장 목표

CH 18에서는 플러그인을 작성하는 가장 흔한 방법을 다루었습니다. 이 장에서는 플러그인의 세 가지 형태(함수, 객체, 클래스)를 한 번에 정리합니다: 각 형태의 외형은 어떠한지, 언제 어떤 형태를 쓰는 게 맞는지, 그리고 클래스 형태가 왜 "공용 창구를 여는" 방식인지 — 이것이 플러그인 협업의 핵심입니다. 이 장을 마치면, 단 하나의 템플릿만 쓸 수 있는 상태를 넘어 플러그인에 어떤 형태가 적절한지 판단할 수 있게 됩니다.

한눈에 보는 세 가지 형태

개요를 하나의 표로 정리하고, 자세한 내용은 아래에서 하나씩 다루겠습니다:

형태외형한 줄 이해사용 시점
함수export function apply(ctx) {}직접 일을 처리기본 선택, 90%의 경우로 충분
객체export default { name, inject, apply(ctx) }이름과 흐름을 한 데 묶음플러그인에 정적 선언을 함께 담고 싶을 때
클래스export default class extends Service {}공용 창구를 열고, 누구나 일을 맡기러 옴다른 플러그인이 본인의 능력을 호출하도록 하고 싶을 때

한 가지 기준이면 충분합니다: Agent에 능력을 더하려면 함수 형태를, 다른 플러그인이 본인에게 의존하도록 하려면 클래스 형태를 쓰세요. 객체 형태는 그 중간으로, 기능적으로는 함수 형태와 거의 같으며 name 같은 정적 메타데이터만 더합니다.

함수 형태: 이미 알고 있는 형태

CH 18의 hello-plugin이 바로 함수 형태입니다:

js
export function apply(ctx) {
  // 여기에 능력을 등록합니다
}

프레임워크는 플러그인을 적재할 때 apply(ctx)를 호출하여 컨텍스트를 전달합니다. 대부분의 플러그인 — 도구 등록, 이벤트 청취, 타이머 마운트 — 은 이것 하나로 충분합니다. 다른 플러그인의 의존 대상이 된다는 확신이 들 때까지는 항상 함수 형태를 먼저 사용하세요.

객체 형태: 이름과 흐름을 함께 묶기

객체 형태는 name, inject, apply를 한 객체에 모아 함께 export합니다:

js
export default {
  name: 'my-plugin',
  inject: ['tools'],
  apply(ctx) {
    // 여기서는 ctx.tools가 사용 가능함이 보장됩니다
  },
}

함수 형태와 거의 동등하며, 유일한 차이점은 정적 선언(이름, 의존성)과 apply를 "패키지된 명세"로 정리한다는 점입니다. 언제 쓸까요? 플러그인의 "나는 누구인가, 어떤 서비스가 필요한가, 무엇을 하는가"가 한눈에 드러나기를 바랄 때, 객체 형태가 더 깔끔합니다. 기능적으로 함수 형태가 못하는 것은 없습니다.

클래스 형태: 플러그인이 "서비스를 제공"하고자 할 때

이 절이 이 장의 핵심입니다. 먼저 한 가지 개념을 잡고 갑시다:

Service는 ctx에 마운트된 이름 붙은 능력입니다. 우리는 매일 서비스를 사용합니다 — ctx.tools(도구), ctx.llm(모델), ctx.agents(서브에이전트)가 모두 서비스입니다. 어떤 플러그인이라도 자체 서비스를 제공하여 다른 플러그인이 호출하도록 할 수 있습니다.

함수/객체 형태는 "본인이 직접 일을 처리"하는 방식이고, 클래스 형태는 공용 창구를 열어 다른 플러그인이 와서 일을 맡기는 방식입니다. 비유하자면: 함수 형태는 본인이 직접 서비스 창구에 가서 본인 일을 처리하는 것이고, 클래스 형태는 본인이 그 창구에서 창구를 열어, 다른 플러그인(타인)이 와서 신청서를 내고 결과를 받아 가는 형태입니다.

Provider: Service 서브클래스 작성하기

js
import { Service } from '@deepseek-ai/cordis'

export default class MetricsService extends Service {
  constructor(ctx) {
    super(ctx, 'metrics')  // metrics라는 이름의 서비스를 등록합니다
  }

  record(event, value) {
    console.log('[metrics]', event, value)
  }
}

두 가지 핵심 포인트:

  • extends Service + super(ctx, 'metrics'): ctxmetrics라는 이름을 마운트하고, 다른 플러그인은 ctx.metrics로 접근할 수 있습니다.
  • record(event, value)는 이 서비스가 외부로 제공하는 메서드입니다 — 다른 플러그인이 ctx.metrics.record(...)를 호출하면 여기에 도달합니다.

Consumer: inject로 의존성 선언하기

js
export const name = 'consumer'
export const inject = ['metrics']

export function apply(ctx) {
  ctx.metrics.record('plugin_loaded', 1)
}

inject: ['metrics']는 "metrics 서비스를 사용하겠다"는 선언입니다. 프레임워크는 보장합니다: apply가 실행될 때, inject에 선언된 서비스는 반드시 준비되어 있다. 서비스가 준비되지 않았다면 플러그인은 기다리며 미리 실행되지 않습니다.

협업을 한 그림으로 이해하기

Provider가 서비스를 ctx에 마운트하고, Consumer는 의존성을 선언해 직접 호출합니다. tools, llm, agents 같은 내장 능력도 본질적으로 같은 메커니즘이며, 그것들을 사용할 때 본인은 "Consumer"입니다.

실습: 두 플러그인을 실제로 대화시키기

위 내용은 모두 개념이었습니다. 이제 provider와 consumer를 직접 작성해 실제로 대화시켜 봅니다. 플러그인을 둘 워크스페이스에서(먼저 그곳으로 cd) 디렉터리를 만듭니다:

powershell
New-Item -ItemType Directory -Path "service-demo\src" -Force

1단계: 플러그인 디렉터리에서 의존성 설치

클래스 형태는 프레임워크의 @deepseek-ai/cordis 패키지를 import해야 합니다. 워크스페이스에는 이 패키지가 없으므로 그대로 적재하면 Cannot find package '@deepseek-ai/cordis'가 발생합니다. 따라서 플러그인 디렉터리로 들어가 먼저 설치합니다:

powershell
cd "E:\software-workspace\DeepSeek harness demo\service-demo"   # 본인 디렉터리로 바꾸세요
npm init -y
npm install @deepseek-ai/cordis

설치 후 package.json을 수정해 루트 객체에 "type": "module" 한 줄을 추가합니다 — 그렇지 않으면 Node가 모듈 형식을 매번 추측하면서 경고가 끊임없이 출력됩니다:

json
{
  "name": "service-demo",
  "version": "1.0.0",
  "type": "module"
}

2단계: Provider 작성

service-demo\src\provider.js를 만듭니다:

js
import { Service } from '@deepseek-ai/cordis'

export default class MetricsService extends Service {
  constructor(ctx) {
    super(ctx, 'metrics')
  }

  record(event, value) {
    console.log('[metrics]', event, value)
  }
}

3단계: Consumer 작성

service-demo\src\consumer.js를 만듭니다:

js
export const name = 'consumer'
export const inject = ['metrics']

export function apply(ctx) {
  ctx.metrics.record('plugin_loaded', 1)
}

4단계: 선언 및 검증

service-demo\cordis.yml을 만듭니다(경로는 본인 것으로 바꾸고, 공백은 %20으로 적어야 함에 주의):

yaml
- insert:
    - id: provider
      name: 'file:///E:/your-workspace/service-demo/src/provider.js'
    - id: consumer
      name: 'file:///E:/your-workspace/service-demo/src/consumer.js'

빠른 검증을 위해 headless를 사용합니다:

powershell
cd your-workspace-directory
dsh --profile headless --patch "./service-demo/cordis.yml" "Just reply: hi"

출력에서 다음을 보게 됩니다:

text
[metrics] plugin_loaded 1
hi

[metrics] plugin_loaded 1은 consumer가 provider의 메서드를 호출한 것입니다 — 두 플러그인이 실제로 대화했습니다. 이 시점에서 함수, 객체, 클래스 세 가지 형태를 모두 직접 검증하셨습니다.

터미널 출력의 노란색 줄 [metrics] plugin_loaded 1은 consumer가 ctx.metrics.record(...)로 provider의 메서드를 호출한 결과입니다 — 두 플러그인이 실제로 대화했습니다.

dsh에게 맡기기: 한 줄 프롬프트로 끝내기

위 디렉터리, 의존성, 두 개의 파일은 모두 본인이 손으로 직접 만들었습니다. 늘 그렇듯이 플러그인 작성은 dsh가 스스로 할 수 있는 일이며, "서비스를 어떻게 정의해야 하는지, 의존성을 어떻게 주입해야 하는지"를 본인은 본인보다 더 잘 알고 있습니다.

Web UI의 입력창에 다음 프롬프트를 그대로 보내세요:

text
현재 워크스페이스에서 "서비스 형태" 플러그인 예시를 작성해 줘: 서비스를 제공하는 플러그인(Service 서브클래스)과 그것을 사용하는 플러그인(inject로 의존)을 함께 만들고, 실제로 대화하도록 해 줘. 먼저 공식 dsh 플러그인 개발 문서를 읽고, 서비스를 어떻게 정의하는지, 의존성을 어떻게 주입하는지 파악한 다음, 공식 명세에 따라 구현하고, 정상적으로 적재 및 호출되는지 검증한 후, 마지막에 결과와 파일 위치를 알려 줘.

dsh가 공식 문서를 직접 찾아보고, 의존성을 스스로 설치하고, 두 플러그인을 직접 작성하고, 서로 대화할 수 있는지 직접 검증합니다. 그저 작성한 내용을 확인하시면 됩니다.

자주 겪는 함정

문제현상대응
Cannot find package '@deepseek-ai/cordis' 보고플러그인 디렉터리에 의존성을 설치하지 않음플러그인 디렉터리에서 npm install @deepseek-ai/cordis
MODULE_TYPELESS_PACKAGE_JSON 경고가 줄줄이 출력package.json이 모듈 타입을 선언하지 않음"type": "module" 추가
Consumer가 서비스를 가져올 수 없음서비스 이름 불일치super(ctx, 'xxx')의 이름과 inject: ['xxx']가 정확히 같은지 확인
클래스 플러그인 적재 실패extends Service가 없거나 super를 호출하지 않음클래스 형태는 반드시 Service를 상속하고 super(ctx, '서비스-이름') 호출
ctx.metrics가 undefinedConsumer가 inject를 선언하지 않음Consumer에서 export const inject = ['metrics'] 작성

이 장에서 배운 것

아래 항목들을 직접 해낼 수 있으면 합격입니다:

  • [ ] 세 가지 형태의 외형과 각각의 사용 시점을 말할 수 있다
  • [ ] Service = ctx에 마운트된 이름 붙은 능력임을 알며, tools/llm/agents가 모두 서비스임을 안다
  • [ ] Provider로서 Service 서브클래스를 작성하고, super(ctx, '이름')로 서비스를 등록
  • [ ] Consumer에서 inject를 작성하고, apply에서 ctx.서비스-이름.메서드()로 직접 호출
  • [ ] 클래스 형태 플러그인은 플러그인 디렉터리에서 먼저 @deepseek-ai/cordis 의존성을 설치해야 함을 안다

Open Source · MIT · Community Driven