DeepSeek Harness dsh 플러그인 직접 개발 가이드
DeepSeek Harness에서 사용할 dsh 플러그인을 처음부터 직접 개발하는 방법을 설명합니다. package.json 필수 필드 설정부터 패치 파일 구성, 실제 도구 구현 및 두 가지 핵심 훅 사용법까지 0.1.0-rc.7 버전 기준으로 상세히 안내합니다.
dsh 플러그인의 정의
dsh 플러그인은 apply 함수를 내보내고 DeepSeek Harness에 로드를 지시하는 작은 YAML 파일 하나를 포함하는 npm 패키지입니다. 별도로 학습해야 할 플러그인 SDK는 없습니다. dsh는 Cordis 애플리케이션이며, "모든 것이 플러그인"이라는 원칙은 문자 그대로 적용됩니다. 도구 레지스트리, 에이전트 루프, 세션 저장소, 웹 서버 모두가 귀하의 패키지가 합류하게 될 동일한 플러그인 트리 내의 항목들입니다.
Cordis는 독립적으로 구축되어 수년간 Koishi 챗봇 프레임워크의 기반으로 사용되어 온 범용 구성 프레임워크입니다. 이 프레임워크는 플러그인의 로드와 언로드를 처리하며 플러그인 간의 의존성을 해결합니다. Cordis 자체는 에이전트에 대해 알지 못합니다. 에이전트와 관련된 모든 기능은 그 위에 계층화된 harness 패키지에서 제공되므로, 아래에 설명할 플러그인 구조가 매우 간결한 이유가 여기에 있습니다. 제공되는 기능 대부분은 상속받은 것입니다.
플러그인은 두 부분으로 구성됩니다. 호스트 측은 Node에서 실행되며 도구와 이벤트 리스너를 등록하고 자체 서비스를 제공할 수 있습니다. 브라우저 측은 Web UI 내부에서 실행되며 인터페이스 슬롯을 등록합니다. 첫 번째 플러그인은 거의 항상 호스트 전용이므로, 브라우저 측은 필요할 때까지 선택 사항으로 간주하십시오.
이 가이드는 2026년 8월 19일 기준 npm latest 태그인 @deepseek-ai/dsh 버전 0.1.0-rc.7을 기준으로 작성되었습니다. dsh는 개발자 프리뷰 단계이며 README에 명시된 대로 호환성을 깨뜨리는 변경 사항이 발생할 수 있습니다. 아래의 모든 키 이름은 해당 날짜의 업스트림 문서와 저장소에서 확인한 내용입니다. 프리뷰 API는 릴리스 후보(release candidate) 버전 간에도 필드 이름이 변경될 수 있으므로, 특정 기능을 사용하기 전에 반드시 다시 확인하십시오. harness가 아직 실행 중이 아니라면 VPS에서의 DeepSeek Harness 및 dsh API 키와 모델 설정을 통해 먼저 환경을 구성한 뒤 이어서 진행하십시오.
패키징 전에 스크래치 파일 하나를 먼저 로드하십시오
먼저 패키징하는 것은 이 과정을 배우는 느린 방법입니다. 파일 하나를 로드하여 런타임이 코드를 호출하는지 확인한 다음 패키징하십시오.
하네스 체크아웃 외부의 폴더를 생성하고 그 안에 파일 하나를 넣으십시오.
import type { Context } from '@deepseek-ai/cordis'
export const name = 'hello-plugin'
export function apply(ctx: Context) {
console.log('[hello-plugin] plugin loaded')
}export const name는 진단 시 플러그인에 레이블을 지정하는 데 사용되는 메타데이터입니다. apply는 전체 계약입니다. Cordis는 이를 한 번 호출하고 플러그인 범위로 지정된 컨텍스트를 전달합니다. 해당 컨텍스트에 등록한 모든 항목은 플러그인이 삭제될 때 자동으로 정리됩니다.
그 옆에 cordis.yml을 작성하십시오.
- insert:
- id: hello
name: '/absolute/path/to/scratch-plugin/hello.ts'이제 해당 파일이 위에 계층화된 프로파일을 부팅하십시오.
dsh web --patch ./scratch-plugin/cordis.ymlnpx @deepseek-ai/dsh web --patch ./scratch-plugin/cordis.yml이 PATH에 없다면 npx @deepseek-ai/dsh web --patch ./scratch-plugin/cordis.yml도 동일한 역할을 수행합니다. npx 경로는 이 가이드에서 설명하는 버전 대신 캐시된 이전 릴리스 후보를 제공할 수 있습니다. 따라서 하네스가 문서화된 플래그를 완전히 거부한다면, 자신의 파일을 의심하기 전에 dsh 설치 및 버전 오류 해결 방법을 먼저 확인하십시오. dsh를 시작한 터미널에서 [hello-plugin] plugin loaded이 표시되어야 합니다. 아무것도 나타나지 않으면 행이 확인되지 않은 것입니다.
name 필드는 npm 패키지 이름이나 파일 시스템 경로를 사용하며, 업스트림 문서에 따르면 경로는 절대 경로여야 합니다. 스크래치 플러그인에서 출력이 생성되지 않을 때 가장 먼저 확인해야 할 것은 상대 경로인 ./hello.ts입니다. 두 번째는 파일 확장자입니다. 문서화된 루프는 하네스 저장소의 복제본에서 pnpm dsh web --patch ...로 실행되며, 여기서 TypeScript 항목은 tsx를 통해 로드됩니다. dsh를 npm에서 가져온 경우 행을 일반 JavaScript로 지정하거나 파일을 먼저 빌드하십시오.
--patch은 런처 플래그이며, 해당 오버레이는 모든 번들과 사용자 고유의 프로파일 패치 이후 마지막에 적용됩니다. 따라서 스크래치 오버레이는 항상 우선하며, 이는 반복 작업을 수행할 때 정확히 필요한 동작입니다.
유용한 기능을 수행하는 최소한의 도구 작성하기
로그 한 줄은 플러그인이 로드되었음을 증명합니다. 도구는 플러그인이 에이전트의 일부임을 증명합니다.
import type { Context } from '@deepseek-ai/cordis'
import { defineTool } from '@deepseek-ai/dsh-tools'
export const name = 'greet-tool'
export const inject = ['tools']
export function apply(ctx: Context) {
ctx.tools.register(defineTool({
name: 'greet',
description: 'Greet someone by name.',
parameters: {
name: { type: 'string', required: true, description: 'The name to greet' },
},
output: {
schema: { type: 'string' },
render: (_args, value) => [{ type: 'text', text: value }],
},
async execute(args) {
return `Hello, ${args.name}!`
},
}))
}export const inject = ['tools']는 사람들이 자주 빠뜨리는 줄입니다. Cordis 설정의 항목들은 동시에 시작되므로, 파일 내 행의 위치는 로드 순서를 보장하지 않습니다. 순서는 선언된 의존 관계에 따라 결정됩니다. inject는 ctx.tools이 존재할 때까지 Cordis가 대기하도록 지시한 뒤 apply을 호출하게 합니다. 이 설정이 없으면 레지스트리가 등록을 받을 준비가 되지 않은 시점에 코드가 실행될 수 있습니다.
객체의 나머지 부분은 모델이 인식하는 계약입니다. parameters은 인자 스키마이며, execute는 이미 해당 스키마에 따라 파싱된 인자를 전달받습니다. output.schema은 execute이 반환하는 값을 설명하며, render는 해당 값을 모델이 읽을 수 있는 콘텐츠 블록으로 변환합니다. 이 두 가지를 분리해 두어야 인터페이스에는 하나를 보여주면서 모델은 다른 것을 읽게 할 수 있습니다.
프로필을 시작하고 어시스턴트에게 이름을 불러 인사를 하도록 요청하십시오. 응답은 execute을 통해 돌아옵니다. ctx를 통한 등록은 되돌릴 수 있으므로, 플러그인을 폐기하면 도구도 자동으로 등록 해제됩니다. 소켓이나 파일 핸들처럼 Cordis가 알 수 없는 항목의 경우, ctx.effect()를 호출하여 폐기자(disposer)를 전달하십시오.
첫 번째 플러그인이 주로 다루는 두 가지 확장 지점
확장 지점의 전체 목록은 매우 깁니다. 그중 두 가지가 거의 모든 첫 번째 플러그인의 기능을 포괄합니다.
대화 이벤트는 영구적으로 기록되는 스트림입니다. 이름은 session/event, turn/start, turn/end, step/start, step/end, user/message, assistant/message, assistant/chunk, tool/call, tool/result입니다. 일반적인 리스너를 연결하여 사용합니다.
ctx.on('tool/call', (payload) => {
console.log('[my-plugin] tool/call', JSON.stringify(payload))
})페이로드를 한 번 출력하여 내용을 확인하십시오. 본 가이드를 포함한 어떤 문서에서도 페이로드 필드 이름을 그대로 복사하지 마십시오. 페이로드 형태는 프리뷰 API에서 가장 자주 변경되는 부분이기 때문입니다.
두 번째 확장 지점은 워터폴(waterfall)입니다. agent/pre-step, agent/request, agent/request-error, llm/stream 및 tools/* 이벤트는 워터폴 방식이며, 워터폴 리스너는 다른 서명을 가집니다. 이 리스너는 next 콜백을 인자로 받으며, 해당 콜백이 호출되어야만 체인이 계속 이어집니다.
ctx.on('agent/request', async (payload, next) => {
const startedAt = Date.now()
const downstream = await next()
console.log('[my-plugin] model request took', Date.now() - startedAt, 'ms')
return downstream
})await next() 호출을 잊으면 훅이 추가되지 않습니다. 모델 호출을 아무것도 없는 상태로 대체하게 되며, 에이전트는 그 지점에서 멈춥니다. 이는 의도적으로 요청을 거부하는 게이트웨이 플러그인을 위해 설계된 단락(short circuiting) 동작이기 때문입니다. 이 차이 하나가 첫 번째 플러그인을 개발할 때 가장 많은 혼란을 야기합니다. 다른 코드를 작성하기 전에 next() 호출부터 작성하십시오.
agent/request는 모델 호출 자체를 감쌉니다. 이 페이로드에는 호출을 수행하는 에이전트, 현재 열려 있는 턴 번호, 요청이 속한 단계, 그리고 해당 턴의 중단 신호가 포함됩니다. 이러한 특성 덕분에 요청 로거(logger)나 속도 제한(rate limiter)을 구현하기에 적합한 지점입니다. tools/* 워터폴은 한 단계 아래에서 동일한 형태를 가집니다. tools/pre-execute은 디스패치 이전에 허용, 거부 또는 승인을 요청할 수 있게 합니다. tools/execute은 디스패치를 감쌉니다. tools/post-execute은 정규화된 결과를 대체하거나 차단할 수 있습니다. tools/result는 최종적으로 확정된 결과만을 관찰합니다.
다른 사용자가 설치할 수 있도록 번들로 패키징하기
번들은 package.json에 패치 파일을 가리키는 dsh.bundle 필드가 선언된 npm 패키지입니다. 이 선언이 스크래치 파일과 설치 가능한 패키지를 구분하는 유일한 차이점입니다.
{
"name": "dsh-plugin-hello",
"version": "0.1.0",
"type": "module",
"main": "lib/index.js",
"files": ["lib", "cordis.patch.yml", "README.md", "LICENSE"],
"engines": { "node": "^22.19 || >=24", "dsh": ">=0.1.0-rc.6" },
"dsh": { "bundle": { "patch": "./cordis.patch.yml" } },
"keywords": ["dsh-plugin", "deepseek-harness"],
"scripts": { "build": "tsdown", "prepare": "pnpm run build" },
"exports": {
".": { "types": "./lib/index.d.ts", "default": "./lib/index.js" },
"./cordis.patch.yml": "./cordis.patch.yml",
"./package.json": "./package.json"
}
}그 옆에 위치한 cordis.patch.yml는 간단합니다.
- insert:
- id: dsh-plugin-hello
name: dsh-plugin-helloname 행은 패키지 이름이므로, 두 문자열은 반드시 일치해야 합니다. id 행은 사용자가 설정을 재정의할 때 이후 계층에서 참조하는 대상이므로, 안정적인 이름을 선택하고 다른 플러그인에 재사용하지 마십시오.
files에는 반드시 cordis.patch.yml이 포함되어야 합니다. 이를 누락하면 배포된 tarball에 포함되지 않은 파일을 가리키는 dsh.bundle.patch이 생성되므로, 패키지는 설치되더라도 트리에는 아무런 영향을 주지 못합니다.
플러그인 폴더가 포함된 디렉터리에서 프로필로 설치하십시오.
dsh plugin --profile demo add ./dsh-plugin-hello
dsh --profile demo --dump-config
dsh --profile demodsh plugin --profile <name>은 나머지 인수를 해당 프로필 디렉터리 내부의 pnpm으로 전달하므로, add와 remove은 pnpm과 동일하게 동작합니다. dsh plugin --profile demo remove dsh-plugin-hello을 사용하여 제거할 수 있습니다. web와 headless 프로필은 처음 사용할 때 제공된 템플릿으로부터 자동으로 생성되며, 그 외의 프로필 이름은 dsh plugin를 통해 생성해야 합니다.
구성된 트리에서 행이 누락되는 이유
구성은 빈 항목 목록에서 시작하여 고정된 순서로 계층을 쌓습니다. 프로필의 dsh.profile.bundles에 명명된 각 번들이 나열된 순서대로 추가됩니다. 그다음 프로필 자체의 cordis.patch.yml이 추가됩니다. 이어서 $DSH_HOME/cordis.patch.yml이 추가됩니다. 마지막으로 명령줄에서 지정한 --patch 오버레이가 추가됩니다. 이후 계층은 id를 기준으로 이전 행을 대체합니다.
프로필은 $DSH_HOME/profiles/<name> 아래에 위치합니다. 프로필 디렉터리에는 정렬된 bundles 목록이 포함된 dsh.profile 매니페스트를 담은 package.json 파일과 사용자의 패치 파일이 들어 있습니다. 번들 이름은 dsh 설치 경로에서 먼저 확인하고, 그다음 프로필의 node_modules에서 확인합니다. pnpm은 트리에 포함되지 않은 플러그인을 바로 이곳에 배치합니다.
dsh --profile demo --dump-config는 아무것도 부팅하지 않고 완전히 구성된 트리를 출력하며, 이 출력 결과가 디버깅의 기준점이 됩니다. 행 id가 보이지 않는다면 구성 과정의 문제입니다. 이름을 해석하지 못했거나 패치 파일이 패키징되지 않은 경우입니다. 행은 존재하는데 아무런 동작이 일어나지 않는다면, 그것은 코드의 문제입니다. 이 질문에 먼저 답하면 대부분의 추측 과정을 생략할 수 있습니다.
로딩 오류가 실제로 나타나는 곳
apply 내부에서 발생하는 오류는 명확하게 드러납니다. 프로세스는 해당 예외와 함께 종료되며, 사용자의 코드 라인을 가리키는 스택 트레이스를 확인할 수 있습니다.
해결(resolution) 실패는 조용하게 발생합니다. 로더는 충돌하는 대신 Cordis 로거를 통해 해결할 수 없는 모듈을 보고합니다. 상위 튜토리얼에서는 이러한 메시지가 콘솔 익스포터가 연결되기 전에 출력되므로 시작 시점에 누락된 것처럼 보일 수 있다고 경고합니다. 따라서 경로 오타는 로드되었지만 아무 작업도 수행하지 않는 플러그인과 똑같이 보입니다. 이것이 바로 코드를 읽기 전에 위에서 언급한 --dump-config 검사를 실행해야 하는 이유입니다.
개발 중에는 apply의 첫 번째 문장으로 console.log을 유지하십시오. 이 문장이 없으면 문제의 절반이 어디에 있는지 알 수 없으며, 나중에 삭제하는 데 비용이 들지 않습니다. 서버에서는 서비스 관리자 대신 포그라운드에서 하네스를 실행하여 반복 작업을 수행하십시오. 그래야 로더 출력이 직접 확인해야 하는 저널이 아닌 터미널로 전달됩니다.
전체 재시작 없이 반복 작업하기
현재 호스트 측에서 가장 솔직한 해결책은 재시작을 수행하는 것입니다. 웹 애플리케이션 번들은 공유 핫 모듈 리로드(hot module reload) 기능이 비활성화된 상태로 제공되며, 해당 파일에는 리로드 생명주기 테스트가 완료되는 대로 다시 활성화하겠다는 메모가 포함되어 있습니다. 클라이언트 측 리로드 체인은 항상 마운트되어 있지만, 리빌드 감시자(rebuild watcher)가 클라이언트 번들을 다시 작성하기 전까지는 유휴 상태로 유지되므로 Node 측에서도 아무런 동작을 하지 않습니다.
아직 구현되지 않은 리로드 기능을 쫓기보다는 재시작 비용을 낮추는 편이 낫습니다. 플러그인은 하나의 파일로 유지하십시오. 프로필에 설치하는 대신 --patch를 사용하여 로드하면 편집과 실행 사이에 빌드 단계나 pnpm 단계가 개입하지 않습니다. 모든 항목을 ctx을 통해 등록하여 재시작 시 도구가 중복되거나 리스너가 좀비 상태로 남지 않도록 하십시오. 직접 할당하는 모든 리소스는 실제 해제자(disposer)를 포함한 ctx.effect()로 감싸야 합니다. 해제자가 누락되면 첫 번째 실행에서 점유한 포트를 두 번째 실행에서 확보하지 못해 실패하는 현상이 흔히 발생하기 때문입니다.
노트북이 아닌 서버에서 실행 중인 하네스(harness)를 대상으로 개발하더라도 위 내용은 동일하게 적용되지만, 웹 UI 바인딩은 주의가 필요합니다. 포트 3080의 루프백 바인드 문서에서 페이지가 자동으로 열리지 않는 이유와 그 해결 방법을 설명합니다.
브라우저 측 구성 요소와 신뢰 수준
플러그인에 자체 인터페이스가 필요한 경우에만 이 항목을 추가하십시오. 이는 번들과 동일한 dsh 필드에 선언됩니다.
{
"dsh": {
"client": {
"platform": "web",
"inject": [],
"external": [],
"immediately": false
}
},
"exports": {
".": "./src/index.ts",
"./client": "./src/client/apply.ts",
"./package.json": "./package.json"
}
}"platform": "web"은 필수 항목이며, 패키지에 ./client 내보내기가 없으면 스캐너가 오류를 발생시킵니다. 따라서 내보내기 맵은 편의 기능이 아닌 매니페스트의 일부입니다. 클라이언트 진입점은 클라이언트 런타임 유형으로 확장된 Cordis Context를 수신하며, 모든 등록은 ctx.slots.register을 통해 apply 내부에서 이루어집니다. 해당 위치에서는 모듈 수준의 부작용이 허용되지 않습니다.
import type { Context } from 'cordis'
import type { DshClientContext } from '@deepseek-ai/dsh-client-runtime'
export async function apply(ctx: Context & DshClientContext) {
ctx.slots.register({ name: 'domain.entry.slot' }, MyComponent)
}시작하기 전에 알아두어야 할 두 가지 세부 사항이 있습니다. 클라이언트 매니페스트의 inject은 스케줄링이 아닌 문서화를 위한 것입니다. 이는 패키지 수준의 의존성 관계를 기록할 뿐 활성화 순서를 제어하지는 않습니다. external는 기준선 외부의 모듈 요청을 선언하는 곳이며, 플러그인이 요청하기 전에 해당 모듈들이 구체화되도록 합니다. 이 영역은 프리뷰 버전에서 가장 빠르게 변화하는 부분이므로, 가이드를 읽는 날이 아닌 코드를 작성하는 당일에 하니스(harness) 저장소의 packages/client/AGENTS.md을 확인하십시오.
플러그인 게시 및 접근 권한 명시
GitHub 저장소에 dsh-plugin 토픽을 추가하면 사용자가 플러그인을 검색할 때 해당 목록에 노출됩니다. 이는 타인의 신뢰를 전제로 하는 행위이며, 그에 따른 의무가 따릅니다. 이러한 의무는 dsh 플러그인 설치 전 검토 가이드에서 독자들에게 확인을 권장하는 항목들과 정확히 일치하므로, 해당 체크리스트에 맞춰 문서를 작성하는 것이 검토를 통과하는 가장 쉬운 방법입니다.
- 의존성을 고정하십시오. 전이적 의존성에 캐럿(caret) 범위를 사용하면 지난주까지 안전했던 패키지가 이번 주에는 다른 코드를 실행하게 될 수 있으며, 이는 서버를 대상으로 하는 npm 공급망 공격의 핵심 메커니즘입니다.
- 매니페스트에 접근 권한을 명시하십시오.
inject목록은 귀하가 사용하는 하니스(harness) 서비스가 무엇인지 기계가 읽을 수 있는 형태로 정직하게 요약한 것입니다. 검토자는 이를 수 초 내에 읽고 판단을 내립니다. - 암묵적인 네트워크 호출을 금지하십시오. 도구가 API를 호출한다면 README에 호스트 이름을 명시하고 엔드포인트를 설정 가능하게 만드십시오. 언급되지 않은 서버와 통신하는 플러그인은 감사 담당자에 의해 목록에서 삭제됩니다.
files을 엄격하게 관리하십시오. 작업 폴더 전체를 게시하면 실수로 포함된 자격 증명 파일이 레지스트리에 유출될 수 있습니다.- git 설치 사용자에게 개발 전용 가정이 없는
prepare빌드 스크립트를 제공하고, README에 프로필의pnpm-workspace.yaml에서 해당 빌드를 허용 목록(allowlist)에 추가해야 함을 명시하십시오. - 빌드 및 테스트를 완료한 릴리스 후보 버전을 기준으로 README에 날짜를 기록하십시오. 미리 보기 API를 사용하는 독자들은 귀하가 어떤 버전을 기준으로 작업했는지 알아야 합니다.
완성된 플러그인이 외부에서 어떻게 보이는지 확인하려면 설치할 가치가 있는 dsh 플러그인을 읽고 각 README가 설치 전 사용자에게 무엇을 알리는지 확인하십시오. 다른 에이전트용 확장을 작성해 본 경험이 있다면 Claude Code 플러그인 구성 방식이 유용한 대조 자료가 될 것입니다. 하니스는 라이브 객체 그래프와 가역적 등록 기능을 제공하는데, 이는 단순한 파일 매니페스트보다 강력한 권한을 부여하는 만큼 그에 상응하는 책임이 따릅니다.
FAQ
dsh 플러그인을 작성하려면 npm에 배포해야 합니까?
아니요. cordis.yml 오버레이에 파일 시스템 경로를 지정하고 dsh web --patch ./scratch-plugin/cordis.yml로 로드하는 것만으로도 하네스 내부에서 코드를 실행할 수 있습니다. 경로는 반드시 절대 경로여야 합니다. 패키징은 다른 사용자가 플러그인을 설치할 때만 중요하며, 그 경우에도 dsh plugin --profile demo add ./my-plugin을 사용하여 로컬 폴더를 설치하면 레지스트리를 거치지 않고도 패키징된 형태를 테스트할 수 있습니다.
플러그인은 로드되는데 왜 도구가 나타나지 않습니까?
먼저 dsh --profile demo --dump-config를 실행하십시오. 해당 출력에 행 ID가 없다면 플러그인이 마운트되지 않은 것이며, 원인은 코드가 아닌 구성에 있습니다. 행이 존재한다면 export const inject = ['tools']을 확인하십시오. Cordis 설정의 항목들은 동시에 시작되므로 파일 순서가 로드 순서를 결정하지 않습니다. 해당 선언이 없으면 Cordis는 도구 레지스트리를 기다리지 않으며, ctx.tools가 등록을 받을 준비가 되지 않은 시점에 apply이 실행될 수 있습니다.
cordis.yml과 cordis.patch.yml의 차이점은 무엇입니까?
cordis.yml은 전체 항목 목록입니다. cordis.patch.yml는 그 위에 적용되는 레이어로, ID를 기준으로 행을 지정하여 새로운 항목을 삽입하거나 기존 설정을 교체합니다. 번들은 package.json 내의 dsh.bundle.patch를 통해 자체 패치 파일을 가리킵니다. 레이어는 고정된 순서로 적용됩니다. 프로필에 나열된 순서대로 모든 번들이 적용된 후, 프로필의 패치 파일, $DSH_HOME/cordis.patch.yml, 그리고 마지막으로 모든 --patch 오버레이가 적용됩니다. 나중에 적용된 레이어가 우선합니다.
에이전트가 실행 중일 때 dsh 플러그인을 핫 리로드할 수 있습니까?
0.1.0-rc.7 버전 기준으로 웹 프로필의 호스트 측은 불가능합니다. 해당 번들은 공유 핫 모듈 리로드 행이 비활성화된 상태로 제공되며, 파일 내에는 리로드 생명 주기가 검증되면 다시 활성화될 것이라는 메모가 포함되어 있습니다. 대신 빠른 재시작을 고려하여 설계하십시오. 빌드 단계 없이 --patch를 통해 로드되는 단일 파일을 사용하고, 모든 등록은 ctx을 통해 수행하여 이전 실행의 데이터가 다음 실행으로 누출되지 않도록 하십시오. Cordis가 스스로 정리할 수 없는 리소스는 폐기자(disposer)와 함께 ctx.effect()을 사용하십시오.