안녕하세요, SmartStudio 박진호입니다.
스마트에디터는 플러그인 시스템을 가지고 있지만, 아직은 많이 활성화되지 않았는데요. 좀 더 자유로운 플러그인 사용 방법을 통해 다양한 플러그인이 나왔으면 하는 바람으로 이 글을 적습니다.

스마트에디터 확장하기
스마트에디터는 기본적으로 플러그인 자체를 관리하는 시스템은 없습니다. 다만 몇 가지 커스터마이징을 할 수 있는 경로를 열어놨는데요, 이 기능을 가지고 정해진 규칙에 따라 플러그인 형태로 적용할 수 있도록 해보겠습니다.
먼저 스마트에디터에서 커스터마이징할 수 있는 기능은 어떤 게 있는지 한번 볼까요?

DocumentToolbar 에는 몇 가지 버튼을 제어할 수 있는 기능이 있습니다.
- 버튼 위치 변경
- 버튼 삭제
- 사용자 정의 버튼 추가
이 중에서 사용자 정의 버튼을 추가하는 코드를 보도록 하겠습니다.
productConfig: {
documentToolbar: {
buttonSet: [
"social-media-image",
"sticker",
// 사용자 정의 버튼 삽입 위치
"custom-count",
...
],
additionalButtons: [
{
"name": "custom-count",
"imageUrl": "https://i.imgur.com/sojc9G2.png", // 버튼 이미지 주소
"label": "카운트", // 버튼 레이블
"logId": "count", // 버튼 nClicks 로그.
"title": "카운트 첨부" // 버튼 요소의 title 값
}
]
}
}
productConfig.documentToolbar.additionalButtons
에 추가될 버튼을 정의한 다음, productConfig.documentToolbar.buttonSet
에 정의된 버튼의 이름name
을 등록해줍니다.

DocumentToolbar 에 추가된 카운트 아이콘이 보이시나요? 이렇게 간단하게 Document Toolbar에 원하는 버튼을 추가할 수 있습니다.
그럼 버튼을 클릭하면 어떻게 동작할까요? 버튼에 클릭이벤트를 적용해보겠습니다. 버튼의 클릭은 에디터 전체의 클릭 이벤트로 정의가 되는데요, 아래 코드를 한번 보시죠.
events: {
click: function (editorEvent) {
var dataset = editorEvent.detail.dataset;
console.log(dataset);
}
}
버튼을 클릭하면 dataset 을 넘겨주는데, 이 dataset 에 있는 정보를 통해 클릭한 버튼이 어떤 버튼인지 판단할 수 있습니다.
속성 | 설명 | 예 |
group | 해당 버튼이 위치한 도구 막대 이름 | documentToolbar |
type | 버튼 유형을 알려주는 값 | custom |
name | 해당 사용자 정의 버튼의 button name | custom-count |
log | 버튼 생성 시 전달한 logId | count |
그럼 위에서 본 click 이벤트를 실행하는 코드를 해당 버튼을 클릭하는 코드로 조금 수정해보겠습니다.
events: {
click: function (editorEvent) {
var dataset = editorEvent.detail.dataset;
if (dataset.group === "documentToolbar") {
if(dataset.name === "custom-count") {
seEditor.pauseDocumentEdit(); // 스마트에디터의 문서 편집을 막습니다.
// 클릭 동작하기
console.log(dataset);
seEditor.unpauseDocumentEdit(); // 스마트에디터의 문서 편집을 원래 상태로 변경합니다.
}
}
}
}
이로써 간단하게 document toolbar 를 확장하는 방법을 알아보았습니다.
커스텀 컴포넌트
스마트에디터는 문서를 확장하는 방법으로 커스텀 컴포넌트를 만드는 기능을 제공합니다. 기본 문서 모델 이외에 다른 정보를 넣고 싶을 때, 커스텀 컴포넌트를 사용할 수 있습니다. 아래 0
이라고 적힌 초록색 박스가 보이시나요? 이 박스가 바로 커스텀 컴포넌트입니다.

그럼 실제로 커스텀 컴포넌트가 어떻게 정의되는지 보도록 하겠습니다.
plugins: {
customComponents: [
{
type: "count", // 사용자 정의 컴포넌트 타입 정의
onRender: function(payload) {
var currentCount = payload.data.currentCount;
var render = function(count) {
var innerStyle = "border:1px solid red; width:500px; height:50px; text-align: center; display: flex; justify-content: center; align-items: center;"
return "<div style='" + innerStyle + "'>" + count + "</div>";
}
return render(currentCount);
}
}
]
},
먼저 plugins.customComponents
속성에 몇 가지 값을 지닌 객체를 등록해줍니다.
속성 | 설명 | 예 |
type | 사용자 정의 컴포넌트 타입 | count |
onRender | html 코드 반환 함수 | <div>0</div> |
이렇게 해서 커스텀한 모델을 가지고 커스텀한 뷰를 만들어줄 수 있게 되었습니다.
플러그인 매니저 만들기
이렇게 기능을 하나씩 추가하다 보면 관리하기가 만만치 않아질 수 있습니다. 그래서 이런 기능들을 통합해 관리할 수 있는 플러그인 매니저를 만들어 전체 플러그인을 관리해보겠습니다.
지금까지 총 세 가지 형태의 기능을 조합해 플러그인으로 사용할 수 있다는 것을 보았습니다.
- 툴바
- 이벤트
- 커스텀 컴포넌트
세 요소를 좀 더 정규화해서 사용하기 쉬운 패턴으로 만들어보도록 하겠습니다. 개발을 하기 전에 패턴을 어떤 식으로 만들면 좋을지 같이 고민해보면 좋을 것 같습니다.
productConfig: {
documentToolbar: {
additionalButtons: [
// 플러그인 매니저에 등록된 플러그인들에서 추가될 버튼을 모아서 정의해줍니다.
...PluginManager.buttons
]
}
},
plugins: {
customComponents: [
// 플러그인 매니저에 등록된 커스텀 컴포넌트 리스트를 정의해줍니다.
....PluginManager.components
]
},
events: {
click: function (editorEvent) {
// 플러그인 매니저에 등록된 개별 매니저에 메세지를 보냅니다.
PluginManager.fireEvent('click', editorEvent);
}
}
저는 이렇게 기본 설정 구조를 정해보았습니다. 메인 코드를 고칠 필요 없이, PluginManager에 Plugin만 등록하면 커스텀 컴포넌트를 사용할 수 있습니다. 코드를 보니 PluginManager에 Plugin이 들어있어야 할 것 같습니다.
PluginManager.add(MyPlugin);
자, 이제 플러그인 매니저에 플러그인을 등록하고 사용할 수 있는 기본 형태가 갖추어졌습니다. 실제로 세 가지 동작에 대해서 PluginManager를 정의해보도록 하겠습니다.
export default class PluginManager {
// 등록될 플러그인 리스트
static plugins = [];
// 플러그인 등록하기
static add(pluginInstance) {
PluginManager.plugins.push(pluginInstance);
}
// 추가될 버튼 리스트 구하기
static get buttons() {
return PluginManager.plugins.map((plugin) => plugin.button).filter(Boolean);
}
// 추가될 커스텀 컴포넌트 리스트 구하기
static get components() {
return PluginManager.plugins
.map((plugin) => plugin.component)
.filter(Boolean);
}
// 특정 이벤트 실행하기
static fireEvent(eventName, dataset) {
switch (eventName) {
case 'ready':
PluginManager.plugins.forEach((plugin) => plugin.emit(eventName));
break;
default:
Plugin.fireEvent(eventName, dataset);
return;
}
}
}
이로써 간단한 플러그인 매니저가 만들어졌습니다. 이제 진짜 플러그인을 만들어서 등록하면, 실제로 동작하는 형태가 될 수 있을 것 같습니다.
그렇다면 플러그인은 어떻게 만들어 볼 수 있을까요? 이것도 같이 생각해보면 좋겠습니다.
플러그인 매니저에서 사용되는 코드를 기준으로, 플러그인이 가져야 하는 몇 가지 기능을 정의해보도록 하겠습니다.
export default class Plugin {
constructor(obj = {}) {
this.obj = obj;
}
// DocumentToolbar 에서 사용될 버튼의 정의를 만들어줍니다.
get button() {
if (this.obj.noneButton) {
return undefined;
}
// 문서 도구 막대 버튼 정의
return {
name: this.obj.type,
imageUrl: this.obj.imageUrl || 'https://i.imgur.com/sojc9G2.png', // 버튼 이미지 주소
label: this.obj.label, // 버튼 레이블
logId: this.obj.logId, // 버튼 nClicks 로그.
title: this.obj.title, // 버튼 요소의 title 값
};
}
// 커스텀 컴포넌트가 될 정보를 정의합니다.
// 이 때 onRender 함수를 분리해서 정의할 수 있게 해줍니다.
get component() {
const self = this;
return {
type: this.obj.type, // 사용자 정의 컴포넌트 타입 정의
propertyToolbar: this.obj.propertyToolbar || [],
contextToolbar: this.obj.contextToolbar || [],
onRender: function (payload) {
return self.render(payload.data, this);
},
};
}
// render 결과 리턴
render(data, component) {
if (this.obj.render) {
setTimeout(() => {
// setTimeout 으로 시간축을 다르게 해서 render 이후에 함수를 실행 할 수 있도록 합니다.
// 커스텀 컴포넌트에 부가 기능 넣을 때 유용합니다.
this.emit('afterRender', data, component);
}, this.obj.delayRender || 0);
return this.obj.render(this, data, component);
}
}
// 플러그인 에서 사용될 간단한 이벤트 시스템을 만듭니다.
static fireEvent(eventName, dataset) {
const func = Plugin.getCallback(eventName, dataset);
if (func) {
func();
}
}
}
간단하게 플러그인을 정의해봤습니다. 플러그인 인스턴스를 생성할 때 전달하는 obj
를 통해, 플러그인의 속성을 정의하는 것을 볼 수 있습니다. Plugin 속성은 아래의 표와 같은 형태로 정의됩니다. 이런 속성을 가진 플러그인 인스턴스가 있다면 PluginManager에 등록해 쉽게 스마트에디터에 기능을 추가할 수 있습니다.
속성 | 설명 | 사용하는 곳 |
type | 버튼 유형을 알려주는 값 | button, component |
imageUrl | 버튼 이미지 주소 | button |
label | 버튼 레이블 | button |
logId | 버튼 생성 시 전달한 logId | button |
title | 버튼 요소의 title 값 | button |
render | html 코드 반환 함수 | component |
이제 간단한 클릭 카운터 플러그인을 만들어보면서 플러그인에 어떤 기능들이 필요하고, 어떻게 구현하는지 보도록 하겠습니다. 클릭 카운터 플러그인은 아래처럼 정의할 수 있습니다.
PluginManager.add(new Plugin({
type: 'clickCounter',
imageUrl: "https://i.imgur.com/sojc9G2.png", // 버튼 이미지 주소
label: "클릭 카운터", // 버튼 레이블
logId: "clickCounter", // 버튼 nClicks 로그.
title: "클릭 카운터", // 버튼 요소의 title 값,
render: (instance, { count }, { id }) => {
return `
<button type="button">
${count}
</button>
`;
}
})
위의 표에 있는 플러그인 속성을 사용해 정의했습니다. 하지만 실제로 동작해야 하는 기능이 몇 가지 빠져있습니다. 어떤 기능이 필요한지 같이 생각해볼까요?
첫 번째로 DocumentToolbar 에 버튼은 추가되었지만, 클릭 이벤트를 어떻게 전달할지 알 수 없습니다.
자동으로 render 함수를 수행해주는게 아니냐고 물어볼 수도 있는데요. 클릭 카운터의 경우 현재 count 를 값으로 표현해주고 있어서 누군가는 초기 설정을 해줘야 합니다. 그래서 DocumentToolbar 에 정의된 버튼을 클릭하는 시점에 count를 초기화하고 커스텀 컴포넌트를 넣어줘야 합니다.
두 번째로 Custom Component와 DocumentToolbar의 버튼은 실질적인 연관이 없습니다.
버튼을 클릭하는 시점에 커스텀 컴포넌트를 넣을 수도 있고, 아닐 수도 있기 때문입니다. 클릭하는 행위와 컴포넌트를 입력하는 행위는 별도로 이루어져야 합니다.
세 번째로 커스텀 컴포넌트는 단순히 html만을 정의합니다.
따라서 렌더링 결과물에서 이벤트를 실행하기 위해서는 이벤트를 정의할 수 있어야 합니다.
실제 동작하는 플러그인 이벤트 정의하기
먼저 DocumentToolbar의 버튼을 클릭할 수 있도록 함수를 정의해보도록 하겠습니다. 다른 코드가 나열된 것처럼 사용하는 부분을 먼저 정의해봅니다.
PluginManager.add(new Plugin({
...,
// 다른 속성과 실행되는 함수를 분리하기 위해서 callbacks 라는 그룹으로 관리하고자 합니다.
callbacks: {
// documentToolbar 에서 버튼이 클릭될 때 실행되는 함수
'click': (instance) => {
console.log(`${instance.type} button is clicked`);
}
}
})
플러그인에 callback 이 추가 됐으니 실제로 플러그인 매니저에 실행로직을 정의해보겠습니다.
class PluginManager {
...,
static fireEvent(eventName, dataset) {
if (dataset.group === 'documentToolbar') {
PluginManager.plugins.filter(plugin => plugin.type === dataset.name).forEach(plugin => {
plugin.emit('click', dataset)
});
}
}
}
플러그인 매니저에서 DocumentToolbar 버튼을 클릭한 시점에 플러그인으로 메세지를 보낼 준비가 되었습니다. 이제 플러그인의 emit을 정의해, 플러그인의 callbacks.click 함수를 연결하도록 하겠습니다.
class Plugin {
...,
emit (eventName, ...args) {
if (typeof this.obj.callbacks[eventName] === 'function') {
this.obj.callbacks[eventName](this, ...args);
}
}
}
DocumentToolbar 에서 정의된 버튼을 클릭하면 callbacks에 있는 click 함수가 실행되도록 정의하였습니다. click 함수가 실행될 때 커스텀 컴포넌트를 넣어볼게요.
PluginManager.add(new Plugin({
...,
callbacks: {
click: (instance) => {
// 플러그인 인스턴스에 정의된 api 를 통해서 커스텀 컴포넌트를 추가
instance.insertCustomComponent({
count: 0
})
}
}
})
이제 커스텀 컴포넌트를 넣을 수 있게 되었습니다. insertCustomComponent를 통해서 실행하면 플러그인에서 정의한 type과 render 함수를 통해 에디터에 해당 컴포넌트가 렌더링 됩니다.
마지막으로 렌더링 된 컴포넌트를 변경할 수 있는 이벤트 정의해보도록 하겠습니다. 먼저 렌더링 함수를 살펴볼까요?
PluginManager.add(new Plugin({
...,
render: (instance, { count }, { id }) => {
return `
<button type="button">
${count}
</button>
`;
}
})
render() 함수가 dom이 아닌 html 코드만을 리턴하는 것을 알 수 있습니다. 그렇기 때문에 커스텀 컴포넌트가 그려지는 시점에서는 html 코드가 들어가고 렌더링 된 이후에 적용된 dom 을 가지고 올 수 있어야 이벤트를 설정할 수 있게 됩니다.
render 함수 마지막 파라미터로 id를 가지고 있는 객체를 넘겨줍니다. id는 생성된 커스텀 객체 고유의 id이기 때문에 에디터 문서구조에서 유일한 상태입니다.
이 id를 가지고 실제 dom을 조회하도록 바꿔보겠습니다. 간단한 click 이벤트를 추가해보도록 하겠습니다.
PluginManager.add(new Plugin({
...,
// render 이후 실행 되는 함수
afterRender: (instance, {count}, {id} ) => {
const $el = document.getElementById('custom-component-' + id);
if ($el) {
$el.addEventListener('click', () => {
insert.updateCustomData({ count: count + 1 })
});
}
},
render: (instance, { count }, { id }) => {
return `
<div id="custom-component-${id}">
<button type="button">${count}</button>
</div>
`;
}
})
id를 기반으로 $dom
을 추적해서 이벤트를 적용해보았습니다. 이 방식대로라면 html로 표현할 수 없는 다른 컴포넌트도 넣을 수 있게 되는데요. 리액트 컴포넌트도 한번 넣어보도록 하겠습니다.
PluginManager.add(new Plugin({
...,
// render 이후 실행 되는 함수
afterRender: (instance, {count}, {id} ) => {
const $el = document.getElementById('custom-component-' + id);
if ($el) {
// ReactDOM.render 를 사용해서 간단히 컴포넌트를 렌더링 할 수도 있습니다.
ReactDOM.render(
<div onClick={() => instance.update(id, { count: count + 1 } )}>
{count}
</div>,
$el
);
}
},
render: (instance, { count }, { id }) => {
return `<div id="custom-component-${id}"></div>`;
}
})
마무리
이번 글에서는 플러그인을 통해 스마트에디터를 외부에서도 쉽게 확장할 수 있는 방법을 알아보았습니다. Plugin
객체를 통해 원하는 기능을 설계하고, PluginManager
를 통해 플러그인을 등록하여 스마트에디터에 원하는 컴포넌트를 추가할 수 있었습니다.
기초적인 공사를 했으니 다음에는 좀 더 다양하고 멋진 플러그인으로 찾아뵙도록 하겠습니다. 재미난 플러그인 이야기로 다시 돌아올게요~ ^^ 긴 글 읽어주셔서 감사합니다.