CH 19 · 플러그인의 세 가지 형태: 함수 / 객체 / 클래스
본 장 목표
CH 18에서는 플러그인을 작성하는 가장 흔한 방법을 다루었습니다. 이 장에서는 플러그인의 세 가지 형태(함수, 객체, 클래스)를 한 번에 정리합니다: 각 형태의 외형은 어떠한지, 언제 어떤 형태를 쓰는 게 맞는지, 그리고 클래스 형태가 왜 "공용 창구를 여는" 방식인지 — 이것이 플러그인 협업의 핵심입니다. 이 장을 마치면, 단 하나의 템플릿만 쓸 수 있는 상태를 넘어 플러그인에 어떤 형태가 적절한지 판단할 수 있게 됩니다.
한눈에 보는 세 가지 형태
개요를 하나의 표로 정리하고, 자세한 내용은 아래에서 하나씩 다루겠습니다:
| 형태 | 외형 | 한 줄 이해 | 사용 시점 |
|---|---|---|---|
| 함수 | export function apply(ctx) {} | 직접 일을 처리 | 기본 선택, 90%의 경우로 충분 |
| 객체 | export default { name, inject, apply(ctx) } | 이름과 흐름을 한 데 묶음 | 플러그인에 정적 선언을 함께 담고 싶을 때 |
| 클래스 | export default class extends Service {} | 공용 창구를 열고, 누구나 일을 맡기러 옴 | 다른 플러그인이 본인의 능력을 호출하도록 하고 싶을 때 |
한 가지 기준이면 충분합니다: Agent에 능력을 더하려면 함수 형태를, 다른 플러그인이 본인에게 의존하도록 하려면 클래스 형태를 쓰세요. 객체 형태는 그 중간으로, 기능적으로는 함수 형태와 거의 같으며 name 같은 정적 메타데이터만 더합니다.
함수 형태: 이미 알고 있는 형태
CH 18의 hello-plugin이 바로 함수 형태입니다:
export function apply(ctx) {
// 여기에 능력을 등록합니다
}프레임워크는 플러그인을 적재할 때 apply(ctx)를 호출하여 컨텍스트를 전달합니다. 대부분의 플러그인 — 도구 등록, 이벤트 청취, 타이머 마운트 — 은 이것 하나로 충분합니다. 다른 플러그인의 의존 대상이 된다는 확신이 들 때까지는 항상 함수 형태를 먼저 사용하세요.
객체 형태: 이름과 흐름을 함께 묶기
객체 형태는 name, inject, apply를 한 객체에 모아 함께 export합니다:
export default {
name: 'my-plugin',
inject: ['tools'],
apply(ctx) {
// 여기서는 ctx.tools가 사용 가능함이 보장됩니다
},
}함수 형태와 거의 동등하며, 유일한 차이점은 정적 선언(이름, 의존성)과 apply를 "패키지된 명세"로 정리한다는 점입니다. 언제 쓸까요? 플러그인의 "나는 누구인가, 어떤 서비스가 필요한가, 무엇을 하는가"가 한눈에 드러나기를 바랄 때, 객체 형태가 더 깔끔합니다. 기능적으로 함수 형태가 못하는 것은 없습니다.
클래스 형태: 플러그인이 "서비스를 제공"하고자 할 때
이 절이 이 장의 핵심입니다. 먼저 한 가지 개념을 잡고 갑시다:
Service는
ctx에 마운트된 이름 붙은 능력입니다. 우리는 매일 서비스를 사용합니다 —ctx.tools(도구),ctx.llm(모델),ctx.agents(서브에이전트)가 모두 서비스입니다. 어떤 플러그인이라도 자체 서비스를 제공하여 다른 플러그인이 호출하도록 할 수 있습니다.
함수/객체 형태는 "본인이 직접 일을 처리"하는 방식이고, 클래스 형태는 공용 창구를 열어 다른 플러그인이 와서 일을 맡기는 방식입니다. 비유하자면: 함수 형태는 본인이 직접 서비스 창구에 가서 본인 일을 처리하는 것이고, 클래스 형태는 본인이 그 창구에서 창구를 열어, 다른 플러그인(타인)이 와서 신청서를 내고 결과를 받아 가는 형태입니다.
Provider: Service 서브클래스 작성하기
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'):ctx에metrics라는 이름을 마운트하고, 다른 플러그인은ctx.metrics로 접근할 수 있습니다.record(event, value)는 이 서비스가 외부로 제공하는 메서드입니다 — 다른 플러그인이ctx.metrics.record(...)를 호출하면 여기에 도달합니다.
Consumer: inject로 의존성 선언하기
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) 디렉터리를 만듭니다:
New-Item -ItemType Directory -Path "service-demo\src" -Force1단계: 플러그인 디렉터리에서 의존성 설치
클래스 형태는 프레임워크의 @deepseek-ai/cordis 패키지를 import해야 합니다. 워크스페이스에는 이 패키지가 없으므로 그대로 적재하면 Cannot find package '@deepseek-ai/cordis'가 발생합니다. 따라서 플러그인 디렉터리로 들어가 먼저 설치합니다:
cd "E:\software-workspace\DeepSeek harness demo\service-demo" # 본인 디렉터리로 바꾸세요
npm init -y
npm install @deepseek-ai/cordis설치 후 package.json을 수정해 루트 객체에 "type": "module" 한 줄을 추가합니다 — 그렇지 않으면 Node가 모듈 형식을 매번 추측하면서 경고가 끊임없이 출력됩니다:
{
"name": "service-demo",
"version": "1.0.0",
"type": "module"
}2단계: Provider 작성
service-demo\src\provider.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를 만듭니다:
export const name = 'consumer'
export const inject = ['metrics']
export function apply(ctx) {
ctx.metrics.record('plugin_loaded', 1)
}4단계: 선언 및 검증
service-demo\cordis.yml을 만듭니다(경로는 본인 것으로 바꾸고, 공백은 %20으로 적어야 함에 주의):
- 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를 사용합니다:
cd your-workspace-directory
dsh --profile headless --patch "./service-demo/cordis.yml" "Just reply: hi"출력에서 다음을 보게 됩니다:
[metrics] plugin_loaded 1
hi[metrics] plugin_loaded 1은 consumer가 provider의 메서드를 호출한 것입니다 — 두 플러그인이 실제로 대화했습니다. 이 시점에서 함수, 객체, 클래스 세 가지 형태를 모두 직접 검증하셨습니다.

터미널 출력의 노란색 줄 [metrics] plugin_loaded 1은 consumer가 ctx.metrics.record(...)로 provider의 메서드를 호출한 결과입니다 — 두 플러그인이 실제로 대화했습니다.
dsh에게 맡기기: 한 줄 프롬프트로 끝내기
위 디렉터리, 의존성, 두 개의 파일은 모두 본인이 손으로 직접 만들었습니다. 늘 그렇듯이 플러그인 작성은 dsh가 스스로 할 수 있는 일이며, "서비스를 어떻게 정의해야 하는지, 의존성을 어떻게 주입해야 하는지"를 본인은 본인보다 더 잘 알고 있습니다.
Web UI의 입력창에 다음 프롬프트를 그대로 보내세요:
현재 워크스페이스에서 "서비스 형태" 플러그인 예시를 작성해 줘: 서비스를 제공하는 플러그인(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가 undefined | Consumer가 inject를 선언하지 않음 | Consumer에서 export const inject = ['metrics'] 작성 |
이 장에서 배운 것
아래 항목들을 직접 해낼 수 있으면 합격입니다:
- [ ] 세 가지 형태의 외형과 각각의 사용 시점을 말할 수 있다
- [ ] Service =
ctx에 마운트된 이름 붙은 능력임을 알며,tools/llm/agents가 모두 서비스임을 안다 - [ ] Provider로서
Service서브클래스를 작성하고,super(ctx, '이름')로 서비스를 등록 - [ ] Consumer에서
inject를 작성하고,apply에서ctx.서비스-이름.메서드()로 직접 호출 - [ ] 클래스 형태 플러그인은 플러그인 디렉터리에서 먼저
@deepseek-ai/cordis의존성을 설치해야 함을 안다
