커스텀 스킬 개발
나만의 스킬(Skill)을 개발하여 OpenClaw의 기능을 확장하세요.
커스텀 스킬이란?
커스텀 스킬은 사용자가 직접 만든 확장 기능입니다:
- 특정 도메인의 작업 자동화
- 회사 내부 시스템 연동
- 개인화된 워크플로우
| 장점 | 설명 |
|---|---|
| 재사용성 | 한 번 개발하고 여러 번 사용 |
| 공유 가능 | ClawHub에 공유하여 커뮤니티 기여 |
| 통합 관리 | 여러 스킬을 조합하여 복잡한 작업 자동화 |
| 버전 관리 | Git으로 변경 이력 추적 |
Hello World 스킬
// ~/.openclaw/skills/hello-world/skill.js
export default {
name: 'hello-world',
description: 'Hello World 메시지를 반환합니다',
async execute(context) {
const { message } = context;
return {
success: true,
result: 'Hello, World!',
data: {
user: message.from,
timestamp: new Date().toISOString()
}
};
}
};
스킬 등록:
{
"skills": {
"custom": [
"~/.openclaw/skills/hello-world/"
]
}
}
스킬 구조
필수 요소
export default {
name: 'skill-name', // 스킬 이름 (필수)
description: '설명', // 스킬 설명 (필수)
version: '1.0.0', // 버전 (권장)
author: 'Author Name', // 작성자 (권장)
async execute(context) {
return {
success: true,
result: '결과'
};
}
};
Context 객체
context = {
message: {
from: 'user-id',
text: '사용자 메시지',
channel: 'telegram'
},
workspace: '~/.openclaw/workspace',
agent: {
id: 'agent-id',
model: 'claude-sonnet-4'
},
config: {
// openclaw.json 설정
}
}
실전 예시
날씨 스킬
import fetch from 'node-fetch';
export default {
name: 'weather',
description: '도시의 날씨를 조회합니다',
async execute(context) {
const { message } = context;
const city = message.text.match(/날씨 (.+)/)?.[1] || 'Seoul';
const response = await fetch(
`https://api.weather.gov/current?city=${city}`
);
const weather = await response.json();
return {
success: true,
result: `${city}의 날씨는 ${weather.condition}, ${weather.temp}도입니다.`,
data: weather
};
}
};
CSV 분석 스킬
import fs from 'fs/promises';
import path from 'path';
export default {
name: 'csv-analyzer',
description: 'CSV 파일을 분석합니다',
async execute(context) {
const { workspace, message } = context;
const filename = message.text.match(/(.+\.csv)/)?.[1];
if (!filename) {
return {
success: false,
error: 'CSV 파일 이름을 찾을 수 없습니다'
};
}
const filepath = path.join(workspace, filename);
const content = await fs.readFile(filepath, 'utf-8');
const rows = content.split('\n');
const firstRow = rows[0].split(',');
return {
success: true,
result: `파일 분석 완료: ${rows.length}행, ${firstRow.length}열`,
data: {
filename,
rows: rows.length,
columns: firstRow.length
}
};
}
};
스킬 템플릿
기본 템플릿
export default {
name: 'my-skill',
description: '내 스킬 설명',
version: '1.0.0',
author: 'Your Name',
async execute(context) {
try {
const input = this.parseInput(context);
const result = await this.performTask(input);
return { success: true, result, data: input };
} catch (error) {
return { success: false, error: error.message };
}
},
parseInput(context) {
return context.message.text;
},
async performTask(input) {
return '완료';
}
};
웹 API 스킬 템플릿
import fetch from 'node-fetch';
export default {
name: 'api-client',
description: '외부 API를 호출합니다',
async execute(context) {
const { message } = context;
const url = this.extractUrl(message.text);
const response = await fetch(url, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${process.env.API_KEY}`
}
});
if (!response.ok) {
throw new Error(`API 호출 실패: ${response.status}`);
}
const data = await response.json();
return { success: true, result: this.formatResult(data), data };
},
extractUrl(text) {
const match = text.match(/(https?:\/\/[^\s]+)/);
return match ? match[1] : null;
},
formatResult(data) {
return JSON.stringify(data, null, 2);
}
};
테스트 및 디버깅
cd ~/.openclaw/skills/my-skill/
node test.js
테스트 파일 예시:
import mySkill from './skill.js';
const mockContext = {
message: { from: 'test-user', text: '테스트 메시지' },
workspace: '/tmp/test-workspace',
agent: { id: 'test-agent' }
};
mySkill.execute(mockContext)
.then(result => console.log('결과:', result))
.catch(error => console.error('에러:', error));
배포
ClawHub에 공유
git init
git add skill.js README.md
git commit -m "Initial commit"
git push origin main
다른 사용자의 설치:
{
"skills": {
"custom": [
"https://github.com/username/skill-repo"
]
}
}
모범 사례
에러 처리:
try {
const result = await riskyOperation();
return { success: true, result };
} catch (error) {
return { success: false, error: `작업 실패: ${error.message}` };
}
파일 경로는 반드시 workspace를 기준으로:
// 올바른 예
const filepath = path.join(context.workspace, 'file.txt');
비동기 I/O 사용:
// 올바른 예
const data = await fs.readFile(file);
참고: