최근 전자문서+에 14종의 문서가 업데이트되었습니다! 🥳
새로운 문서들은 어린이집에서 서식을 자유롭게 수정할 수 있도록 에디터 기능을 제공하고 있는데요, 이 에디터는 RoosterJs 라이브러리를 사용해서 개발했어요.
RoosterJs는 마이크로소프트에서 활발하게 유지 보수하고 있는 오픈 소스 라이브러리지만, 문서가 친절하지 않고 레퍼런스가 없어서 단기간에 개발하는데 어려움이 있었어요.
제가 개발한 표 기능에 대해 레퍼런스를 만들어 보고자 이번 글을 작성하게 되었어요.
전자문서+는 리액트로 만들어졌기 때문에 앞으로 리액트를 기준으로 설명할게요.
RoosterJs에서 roosterjs-react
패키지를 제공하고 있지만, 이는 데모용 패키지이기 때문에 새로운 기능을 제공하지 않거나 종속성 문제가 발생할 수 있다고 안내하고 있어요.
따라서 rooster-react
패키지를 꼭 사용해야 하는 경우가 아니라면 기본적으로 rooster
패키지를 사용했어요.
에디터의 뼈대를 만들기 위해 <Rooster />
컴포넌트를 추가하고 editorCreator
props를 넣어주었어요. 이 함수에서 에디터 인스턴스를 리턴하면 에디터의 기본 기능이 만들어져요.
에디터 인스턴스는 new Editor()
와 new EditorAdapter()
두 가지 방식으로 생성할 수 있어요. EditorAdapter
가 더 다양한 인터페이스를 제공하기 때문에 전자문서+에서는 EditorAdapter
를 사용했어요.
import { Rooster } from 'roosterjs-react';
import { EditorAdapter } from 'roosterjs-editor-adapter';
export default function Editor() {
return <Rooster editorCreator={(div) => new EditorAdapter(div)} />;
}
지금은 뼈대만 만들어졌기 때문에, 아래처럼 아무런 기능도 사용할 수 없는 상태예요.
RoosterJs는 기능을 직접 구현할 수 있도록 다양한 함수를 제공해요. 이를 통해 글자 색을 바꾸거나, 이미지를 추가하는 기능을 만들 수 있어요.
또한 RoosterJs에서 제공하는 플러그인을 사용하면 원하는 기능을 쉽게 추가할 수 있어요.
import { Rooster } from 'roosterjs-react';
import { EditorAdapter } from 'roosterjs-editor-adapter';
import { WatermarkPlugin, ShortcutPlugin } from 'roosterjs-content-model-plugins';
export default function Editor() {
const editorCreator = (div) => {
const plugins = [
new WatermarkPlugin('내용을 작성해 주세요.'), // placeholder를 추가하는 플러그인
new ShortcutPlugin(), // Ctrl + Z와 같은 단축키를 지원하는 플러그인
];
return new EditorAdapter(div, { plugins });
};
return <Rooster editorCreator={editorCreator} />;
}
insertTable
함수를 사용해서 표를 만들 수 있어요.
파라미터
1. editor: 에디터 인스턴스
2. columns: 열의 수
3. rows: 행의 수
import { insertTable } from 'roosterjs';
export default function InsertTableButton() {
return (
<button onClick={() => insertTable(editor, 5, 4)}>
표 만들기
</button>
);
}
getSelectedRegions 함수를 사용해서 셀 선택 여부를 알 수 있어요. 이를 통해 특정 기능들은 셀을 선택했을 때만 노출하도록 만들 수 있어요.
export default function getSelectedCells() {
const cells = editor
.getSelectedRegions() // 선택된 범위 가져오기
.filter((cell) => ['TD', 'TH'].includes(cell.rootNode.nodeName)) // 선택된 범위 중 <th>, <td>만 가져오기
.map((cell) => cell.rootNode); // 엘리먼트 리턴
return cells;
}
editTable
함수를 사용해서 행/열을 추가하거나 지울 수 있어요.
파라미터
1. editor: 에디터 인스턴스
2. operation: 적용할 작업 (insertAbove
, deleteColumn
등)
import { editTable } from 'roosterjs';
export default function InsertRowButton() {
return (
<button onClick={() => editTable(editor, 'insertAbove')}>
위쪽에 줄 추가하기
</button>
);
}
editTable
함수를 사용해서 셀을 합치거나 나눌 수 있어요.
파라미터
1. editor: 에디터 인스턴스
2. operation: 적용할 작업 (mergeCells
, splitVertically
, splitHorizontally
)
import { editTable } from 'roosterjs';
export default function MergeCellButton() {
return (
<button onClick={() => editTable(editor, 'mergeCells')}>
셀 합치기
</button>
);
}
createContextMenuProvider와 ContextMenuPluginBase를 사용해서 표 우클릭 동작을 커스텀할 수 있어요.
// constants.ts
import { editTable } from 'roosterjs';
const onClick = (editor, key) => {
editTable(editor, key); // key: insertAbove, insertBelow 등 적용할 작업
};
const insertMenu = {
key: 'insertMenu',
unlocalizedText: '줄/칸 추가하기',
subItems: {
insertAbove: '위쪽에 줄 추가하기',
insertBelow: '아래쪽에 줄 추가하기',
insertLeft: '왼쪽에 칸 추가하기',
insertRight: '오른쪽에 칸 추가하기',
},
onClick,
};
export const tableEditMenu = [insertMenu];
// Editor.tsx
import ReactDOM from 'react-dom';
import { EditorAdapter } from 'roosterjs-editor-adapter';
import { Rooster, createContextMenuProvider } from 'roosterjs-react';
import { ContextMenuPluginBase } from 'roosterjs-content-model-plugins';
import { tableEditMenu } from './constants';
// 편집중인 표 정보 가져오기
const getEditingTable = (editor, node) => {
const domHelper = editor.getDOMHelper();
const td = domHelper.findClosestElementAncestor(node, 'TD,TH');
const table = td && domHelper.findClosestElementAncestor(td, 'table');
return table?.isContentEditable ? { table, td } : null;
};
export default function Editor() {
const editorCreator = (div) => {
const plugins = [
// 표 우클릭 시 노출할 메뉴 정의
createContextMenuProvider(
'tableEdit',
tableEditMenu,
undefined,
(editor, node) => !!getEditingTable(editor, node),
),
// 표 우클릭 시 컴포넌트 노출
new ContextMenuPluginBase({
render: (container, items) => {
ReactDOM.render(
<Dropdown
items={items}
onClose={() => ReactDOM.unmountComponentAtNode(container)}
/>,
container,
);
},
}),
];
return new EditorAdapter(div, { plugins });
};
return <Rooster editorCreator={editorCreator} />
}
render
의 items
는 아래와 같은 형태를 갖고 있어요.
[
{
key: 'insertMenu',
text: '줄/칸 추가하기',
data: {
key: 'insertMenu',
unlocalizedText: '줄/칸 추가하기',
subItems: {
insertAbove: '위쪽에 줄 추가하기',
insertBelow: '아래쪽에 줄 추가하기',
insertLeft: '왼쪽에 칸 추가하기',
insertRight: '오른쪽에 칸 추가하기',
},
onClick,
},
},
]
// Dropdown.tsx
export default function Dropdown({ items, onClose }) {
return (
<div>
{items.map((item) => (
<div>
<button>
{item.text} /* 1 depth */
</button>
{Object.keys(item.data.subItems).map((key) => (
<button onClick={() => item.data.onClick(editor, key)}>
{item.data.subItems[key]} /* 2 dpeth */
</button>
))}
</div>
))}
</div>
);
}
editTable 함수를 사용해서 표를 정렬할 수 있어요.
파라미터
1. editor: 에디터 인스턴스
2. operation: 정렬 위치 (alignLeft
, alignCenter
, alignRight
)
import { editTable } from 'roosterjs';
export default function AlignTableButton() {
return (
<button onClick={() => editTable(editor, 'alignCenter')}>
표 가운데 정렬하기
</button>
);
}
applyTableBorderFormat 함수를 사용해서 셀에 테두리를 적용할 수 있어요.
파라미터
1. editor: 에디터 인스턴스
2. border: 적용할 테두리 (color
, style
, width
)
3. operation: 적용할 테두리 위치 (leftBorders
, rightBorders
, topBorders
등)
테두리 색상은 HEX 코드와 RGB 등 CSS에서 제공하는 표기법을 사용할 수 있으나, RoosterJs 내부적으로 RGB로 변환하기 때문에 실제로는 모두 RGB로 적용돼요. #FFFFFF
→ rgb(255, 255, 255)
import { applyTableBorderFormat } from 'roosterjs';
export default function ApplyBorderButton() {
return (
<button onClick={() => {
applyTableBorderFormat(
editor,
{ color: '#FFFFFF', style: 'solid', width: '1pt' },
'outsideBorders',
);
}}>
표 테두리 적용하기
</button>
);
}
setTableCellShade 함수를 사용해서 셀에 배경색을 적용할 수 있어요.
파라미터
1. editor: 에디터 인스턴스
2: color: 적용할 색상 (색상을 제거하려면 null
)
HEX 코드와 RGB 등 CSS에서 제공하는 표기법을 사용할 수 있으나, RoosterJs 내부적으로 RGB로 변환하기 때문에 실제로는 모두 RGB로 적용돼요. #FFFFFF
→ rgb(255, 255, 255)
import { setTableCellShade } from 'roosterjs';
export default function FillColorButton() {
return (
<button onClick={() => setTableCellShade(editor, '#FFFFFF')}>
표 채우기
</button>
);
}
RoosterJs처럼 레퍼런스가 없는 라이브러리는 처음 사용해 봤는데요, 처음에는 동작을 이해하기 어려워서 머리를 쥐어뜯곤 했지만... 시간이 지날수록 점점 이해도가 올라가서 적응이 되더라구요. 그리고 막상 완성된 에디터를 보니 힘들었던 기억이 싹~ 씻기진 않았지만 💦 뿌듯했어요!
RoosterJs 레퍼런스가 많이 생기길 바라며 여기서 마치겠습니다.