엘가토 스트림덱과 핫스팟 스트림독에서 동작되는 플러그인을 타입스크립트로 만들어본다. 타입스크립트를 사용하는 이유는 C# 개발에 익숙하고, 자바스크립트로 저장되기 때문에 호환성이 매우 좋으며, Node.js로 시스템 접근이 가능하며, 자바스크립트 데이터 형식인 JSON을 변환이나 정의 없이 즉시 사용할 수 있기 때문이다. 여기서는 스트림독을 기준으로 한다.(스트림독만 보유 중🤣)
개발 환경 준비
Visual Studio Code
https://code.visualstudio.com/
최신 버전을 설치한다.
Node.js
https://nodejs.org/ko/download
터미널(관리자)에서 winget install OpenJS.NodeJS 명령을 실행하거나 Windows 설치 프로그램으로 설치한다.
설치 후 다음 명령으로 버전을 확인할 수 있다.
> node -v
v25.5.0VSCode 확장 설치
- Korean Language Pack for Visual Studio Code: 한국어 팩
- Visual Studio Keymap: Visual Studio에 익숙해서
- npm Intellisense: npm 자동 완성
- Prettier – Code formatter: 코드를 예쁘게~
- Path Intellisense: 경로 자동 완성
- ESLint: 디버깅을 빠르게~
- Error Lens: 오류 강조
- Better Comments Next: 코멘트를 컬러풀하게~
터미널에서 다음 명령어를 실행하여 엘가토 명령줄 도구(CLI)를 설치한다.
> npm install -g @elgato/cli
changed 75 packages in 8s
13 packages are looking for funding
run `npm fund` for detailsnpm은 nodejs 설치에서 함께 설치되며, -g 옵션은 전역 설치이다. @는 조직 표시이고 엘가토의 cli 패키지를 설치하겠다는 것이다.
프로젝트 폴더로 이동 후 터미널에서 다음 명령을 실행한다.
> streamdeck create
___ _ ___ _
/ __| |_ _ _ ___ __ _ _ __ | \ ___ __| |__
\__ \ _| '_/ -_) _` | ' \ | |) / -_) _| / /
|___/\__|_| \___\__,_|_|_|_| |___/\___\__|_\_\
Welcome to the Stream Deck Plugin creation wizard.
This utility will guide you through creating a local development environment for a plugin.
For more information on building plugins see https://docs.elgato.com.
Press ^C at any time to quit.
? Author:
> Please enter the author.필요한 정보를 모두 입력한다.
UUID는 플러그인에서 사용될 도메인 이름을 거꾸로 입력한 것이다. 플러그인에는 여러 기능이 포함될 수 있으며 그 그룹에 대한 고유 이름을 지정하는 것이다. 마켓 플레이스에 공개한 후에는 UUID를 변경하면 안 된다.
✔ Author: DevAny
✔ Plugin Name: MyPlugin
✔ Plugin UUID: kr.devany.myplugin
✔ Description: 테스트 플러그인
✔ Create Stream Deck plugin from information above? Yes
Creating MyPlugin...
✔ Enabling developer mode
✔ Generating plugin
✔ Installing dependencies
✔ Building plugin
✔ Finalizing setup
Successfully created plugin!
? Would you like to open the plugin in VS Code? (Y/n)Y를 입력하면 VSCode가 실행된다.

VSCode의 탐색기 패널을 보면 프로젝트 폴더 아래에 샘플 파일이 자동으로 생성된 것을 볼 수 있다.
컴파일
파일이 변경될 때마다 자동으로 컴파일을 하려면 터미널에서 다음 명령어를 실행한다.
>npm run watch
rollup v4.57.1
bundles src/plugin.ts → kr.devany.myplugin.sdPlugin/bin/plugin.js...
created kr.devany.myplugin.sdPlugin/bin/plugin.js in 911ms
watch.onEnd $ streamdeck restart kr.devany.myplugin
ℹ Stream Deck is not running. Starting Stream Deck.
[2026-02-03 17:05:10] waiting for changes...플러그인이 설치될 위치
프로젝트 폴더의 플러그인 폴더를 다음과 같은 경로에 복사한다.
Maker | Path |
|---|---|
엘가토 | %AppData%\Elgato\StreamDeck\Plugins\kr.devany.myplugin.sdPlugin |
핫스팟 | %AppData%\HotSpot\StreamDock\plugins\kr.devany.googleapi.sdPlugin\bin |
rollup.config.mjs
컴파일 대상 경로를 위 플러그인 폴더의 bin으로 지정하면 수동으로 복사할 필요가 없다.
rollup.config.mjs 파일을 열고 다음과 같이 sdPlugin 경로를 편집한다.
const sdPlugin = path.join(process.env.APPDATA, "Elgato/StreamDeck/Plugins/kr.devany.myplugin.sdPlugin");sourcemap
const isWatching = !!process.env.ROLLUP_WATCH;기본값은 위와 같이 설정되어 있으며, watch일 때에만 .map 파일이 생성된다. .map 파일에는 개인 정보가 포함되어 있으므로 git에 공개되지 않도록 한다. 파일이 필요 없으면 false로 설정한다.
이제 npm run watch를 실행할 때마다 해당 폴더에 컴파일된 파일이 생성되며, 스트림덱에서 즉시 테스트가 가능하다. bin이 아닌 폴더와 파일은 수동으로 복사해야 한다.
> npm run watch
rollup v4.57.1
bundles src/plugin.ts → kr.devany.myplugin.sdPlugin/bin/plugin.js...
created kr.devany.myplugin.sdPlugin/bin/plugin.js in 1s
watch.onEnd $ streamdeck restart kr.devany.myplugin
✔ Restarted kr.devany.myplugin
[2026-02-03 23:30:32] waiting for changes...위와 같은 상태에서 파일이 변경되면 package.json에서 설정한 watch 명령이 실행된다.
키에 플러그인 할당


⬆️핫스팟의 스트림독은 프로그램을 재시작 해야 적용된다.
오른쪽 플러그인 목록에 추가된 플러그인이 나타나며, 스트림독 화면으로 배치한 다음 버튼을 누를 때마다 아래와 같이 값이 증가하는 것을 볼 수 있다.

manifest.json
{
"Name": "MyPlugin",
"Version": "0.1.0.0",
"Author": "DevAny",
"Actions": [
{
"Name": "Counter",
"UUID": "kr.devany.myplugin.increment",
"Icon": "imgs/actions/counter/icon",
"Tooltip": "Displays a count, which increments by one on press.",
"PropertyInspectorPath": "ui/increment-counter.html",
"Controllers": [
"Keypad"
],
"States": [
{
"Image": "imgs/actions/counter/key",
"TitleAlignment": "middle"
}
]
}
],
"Category": "MyPlugin",
"CategoryIcon": "imgs/plugin/category-icon",
"CodePath": "bin/plugin.js",
"Description": "테스트 플러그인",
"Icon": "imgs/plugin/marketplace",
"SDKVersion": 2,
"Software": {
"MinimumVersion": "6.4"
},
"OS": [
{
"Platform": "mac",
"MinimumVersion": "12"
},
{
"Platform": "windows",
"MinimumVersion": "10"
}
],
"Nodejs": {
"Version": "20"
},
"UUID": "kr.devany.myplugin"
}Actions[]->UUID 값은 스트림독 키에 할당한 동작에 대한 고유 이름이다. 다른 동작을 하는데 UUID가 같으면 오동작하므로 이름을 신중하게 지정해야 한다. 배열인 이유는 하나의 플러그인에 여러 동작을 만들 수 있기 때문이다.
Software->MinimumVersion 값은 스트림독(3.10.197.1210)에 영향을 주지 않는다.
plugin.ts
SDK 기본값일 뿐이다. 보통은 index.ts로 이름을 지어야 시작 파일을 구분하기 쉽다.
import streamDeck from '@elgato/streamdeck';
import { IncrementCounter } from './actions/increment-counter';
streamDeck.logger.setLevel('trace');
streamDeck.actions.registerAction(new IncrementCounter());
streamDeck.connect();엘가토/스트림덱 패키지에는 윈소켓과 통신 관련 코드가 포함되어 있으므로 매우 깔끔하게 코딩할 수 있다.
.logger는 플러그인 폴더\logs\ 로그를 기록하는 기능을 한다. 스트림독에서도 동작한다.
.actions는 플러그인의 동작 클래스를 등록한다.
.connect()는 스트림독에 연결한다.
increment-counter.ts
플러그인에서 스트림독 키에 할당되어 실제로 동작을 하는 클래스 파일이다.
import { action, KeyDownEvent, SingletonAction, WillAppearEvent } from '@elgato/streamdeck';
@action({ UUID: 'kr.devany.myplugin.increment' })
export class IncrementCounter extends SingletonAction<CounterSettings> {
override onWillAppear(ev: WillAppearEvent<CounterSettings>): void | Promise<void> {
return ev.action.setTitle(`${ev.payload.settings.count ?? 0}`);
}
override async onKeyDown(ev: KeyDownEvent<CounterSettings>): Promise<void> {
// Update the count from the settings.
const { settings } = ev.payload;
settings.incrementBy ??= 1;
settings.count = (settings.count ?? 0) + settings.incrementBy;
// Update the current count in the action's settings, and change the title.
await ev.action.setSettings(settings);
await ev.action.setTitle(`${settings.count}`);
}
}
type CounterSettings = {
count?: number;
incrementBy?: number;
};import
SDK 샘플 코드와 같이 매우 간단한 플러그인이면 1 줄과 같이 클래스에서 필요한 기능만 나열하여 사용하는 것이 성능면에서 좋지만, 스트림덱 패키지는 용량이 작고 대부분의 기능을 사용한다면 다음과 같이 와일드 카드(*)를 사용하는 것이 편리하다.
import * as StreamDeck from '@elgato/streamdeck';해당 패키지의 모든 export 항목을 호출할 수 있으며, StreamDeck.action 처럼 사용해야 한다.
@action({ UUID: ” })
@는 데코레이션을 나타내며 C#에서 attribute와 같은 기능을 한다. plugin.ts에서 streamDeck.actions.registerAction(new IncrementCounter());는 옵션이지만 클래스 위의 데코레이션은 필수이다.
manifest.json에서 각 동작별로 지정한 UUID를 이곳에 입력해야 스트림독에서 해당 클래스를 호출할 수 있다.
export class IncrementCounter extends SingletonAction<CounterSettings> {};
IncrementCounter 클래스를 SingletonAction에서 상속 받고 외부 파일에서 import 할 수 있도록 export를 붙인다. CounterSettings는 PropertyInspect와 주고 받을 데이터의 Json 객체이다.
override onWillAppear(ev: WillAppearEvent): void | Promise {},
override async onKeyDown(ev: KeyDownEvent): Promise {}
스트림독에서 어떠한 행위를 하면 이벤트가 발생하고, 그 이벤트에 해당되는 메서드가 호출된다.
싱글톤에서 정의된 메서드를 오버라이드하여 새로 정의한다.
- onDialDown 🟰 다이얼을 누를 때
- onDialRotate 🟰 다이얼을 회전할 때
- onDialUp 🟰 다이얼을 뗄 때
- onDidReceiveResources 🟰 리소스를 받을 때
- onDidReceiveSettings 🟰 설정을 받을 때
- onKeyDown 🟰 키를 누를 때
- onKeyUp 🟰 키를 뗄 때
- onPropertyInspectorDidAppear 🟰 속성이 표시될 때
- onPropertyInspectorDidDisappear 🟰 속성이 사라질 때
- onSendToPlugin 🟰 PI가 값을 보낼 때
- onTitleParametersDidChange 🟰 제목이 변경될 때
- onTouchTap 🟰 터치할 때
- onWillAppear 🟰 키에 플러그인이 표시될 때
- onWillDisappear 🟰 키에서 플러그인이 사라질 때
onWillAppear는 한 번만 발생하면 되므로 동기 호출이지만, onKeyDown은 메서드가 실행 중에도 여러번 호출 될 수 있으므로 async를 붙여서 비동기로 실행해야 한다.
ev는 : 뒤에 정의한 타입으로 지정된 매개변수이고 메서드내에서 ev.action 또는 ev.payload 처럼 사용할 수 있다.
ev.action.setTitle(${ev.payload.settings.count ?? 0});
action에는 coordinates, isInMultiAction, setImage, setState, setTitle, showOk, toJSON, getResources, getSettings, isDial, isKey, setResources, setSettings, showAlert, controllerType, device, id, manifestId 메서드를 사용할 수 있다.
setTitle은 제목을 설정하는 메서드이다.
settings는 클래스를 정의할 때 제네릭으로 지정한 CounterSettings에 연결된 것이며, 그 타입에 포함된 count에 값이 없으면 0으로 설정하는 코드이다.
await ev.action.setSettings(settings);
플러그인의 설정값을 스트림독으로 전송한다. 스트림독은 PropertyInspect로 값을 전달한다.
전체 소스 코드에서는 키를 누를 때마다 값을 증가시키고 제목에 표시 한다.
스트림독 프로그램을 처음 또는 재실행 했을 때, 키를 누르지 않았을 때에도 마지막으로 사용된 값과 설정이 적용되어야 한다. 해당 payload 데이터는 스트림독에 저장된다. 위 코드에서는 플러그인이 데이터를 받아서 처리한 후 다시 전달하기 때문에 스트림독을 재시작하거나 화면을 전환해도 누적된 값을 기억한다.
이벤트 동작 순서


