TOAST UI를 선택한 이유,
나한테 주어진 프로젝트에서 Editor를 구현해야 했다. Vue 기반의 Nuxt 프레임워크와 잘 어울리는 에디터를 찾아봤다. 개발자가 아닌 사람들이 주로 사용하기에 마크다운이 아닌 위지윅 방식의 에디터를 찾아보게 됐다.
그 중에서 tip-tap이 떠오르는 에디터라고 하나 문제는 import를 했을 때 기본적으로 제공하는 플러그인을 스스로 만들어야 했다. 아래 사진은 대표 홈페이지에 들어간 예시들인데 이를 바로 쓸 수 없고 직접 구현해야했다. 예시 자료도 있지만 플러그인들의 이미지는 제공하지 않고 텍스트로 된 버튼으로 보여주었다.
그러다가 찾아보게 된 TOAST Editor, 기본으로 제공되는 플러그인들이 갖춰져있었고 한국에서 만들어졌기에 조금 더 정보들을 쉽고 빠르게 얻어갈 수 있었다. 플러그인을 직접 구현하여 넣을 수 있어 확장성이 있는 에디터였다.
본격적인 플러그인 제작
nuxt다 보니 먼저 nuxt의 플러그인 폴더에 tui-editor.js를 설정해준다.
//tui-editor.js
import Vue from "vue"
import "codemirror/lib/codemirror.css"
import "@toast-ui/editor/dist/toastui-editor.css"
import "@toast-ui/editor/dist/i18n/ko-kr"
import "@toast-ui/editor/dist/toastui-editor-viewer.css"
import { Editor, Viewer } from "@toast-ui/vue-editor"
Vue.component("Editor", Editor)
Vue.component("Viewer", Viewer)
그리고 nuxt.config.js에도 설정을 해주는데 내가 담당하는 프로젝트는 ssr을 활용하지 않는다.
// nuxt.config.js
plugins: [
{ src: "~/plugins/tui-editor", ssr: false },
],
내가 만들어야 하는 플러그인은 유튜브 링크를 넣어서 게시글을 등록하면 게시물에 iframe으로 유튜브 영상을 시청할 수 있도록 만들어야 했다. 먼저 TOAST Editor에 iframe 태그를 강제로 넣어봤더니 구현이 되지 않았다.
내가 생각한 방법은 에디터에서는 썸네일 이미지로 보여주고 게시글에서 iframe으로 변경하여 영상을 보여줄 수 있도록 기획을 했다.
template는 ref가 핵심이라 볼 수 있다. 그래야 에디터 내의 콘텐츠를 markdown으로 혹은 html로 가져올 수 있기 때문이다.
<template>
<div>
<Editor
ref="editor"
v-model="content"
:options="options"
initial-edit-type="wysiwyg"
height="800px"
min-height="500px"
@change="onEditorChange"
/>
</div>
</template>
데이터에는 에디터랑 연결되어있는 options에 플러그인을 넣어주는데 colortSyntax는 TOAST Editor가 제공한 플러그인이고 this.cutomPlugin은 유튜브 영상을 구현하는 플러그인을 의미한다.
data() {
return {
// 에디터 관련 데이터
editor: null,
options: {
language: "ko",
plugins: [[colorSyntax], [this.customPlugin]],
hooks: {
addImageBlobHook: this.addImageBlobHook,
},
},
fileName: "",
imageData: "",
tempImage: { fileName: null, imageData: null },
tempImages: [],
youtubeUrl: "",
youtubeUrls: [],
title: "",
content: "",
}
},
내가 정의한 customPlugin은 아래 코드와 같다. return에서 toolbarIems에 Index로 위치를 지정해주고 name, tooltip, className, popup을 설정하는데 popup에 대한 내용을 위에서 열심히 container부터 만들어준다.
customPlugin() {
const container = document.createElement("div")
container.className = "youtube-div"
container.id = "youtube-div"
const youtubeInput = document.createElement("input")
youtubeInput.className = "youtube-input"
youtubeInput.id = "youtube-input"
youtubeInput.placeholder = "유튜브 링크"
const youtubeButton = document.createElement("button")
youtubeButton.className = "youtube-button"
youtubeButton.id = "youtube-button"
let btnText = document.createTextNode("확인")
container.appendChild(youtubeInput)
container.appendChild(youtubeButton)
youtubeButton.appendChild(btnText)
youtubeButton.onclick = () => {
const value = document.getElementById("youtube-input").value
if (!value) {
youtubeInput.value = ""
return
}
if (!value.includes("https://youtu.be/")) {
alert("'https://youtu.be/' 를 포함한 링크만 가능합니다.")
youtubeInput.value = ""
return
}
const youtubeUrl = value.split("/")[3]
this.youtubeUrl = youtubeUrl
this.youtubeUrls = [...this.youtubeUrls, this.youtubeUrl]
const getHTML = this.$refs.editor.invoke("getHTML")
this.$refs.editor.invoke(
"setHTML",
getHTML + `<img src="https://img.youtube.com/vi/${this.youtubeUrl}/hqdefault.jpg" contenteditable="false" />`
)
youtubeInput.value = ""
}
return {
toolbarItems: [
{
groupIndex: 3,
itemIndex: 3,
item: {
name: "youtube",
tooltip: "유튜브 링크",
className: "youtube",
popup: {
className: "toastui-editor-popup-add-link-youtube",
body: container,
style: { width: "400" },
},
},
},
],
}
},
popup에서는 DOM조작을 해 팝업창을 구현하고 버튼에 onClick 함수를 정의하여 유튜브의 썸네일을 에디터 내에 들어가도록 한다. 다른 링크가 들어갈 수 있기에 youtu.be가 들어가는지의 여부로 예외처리를 해주고 기존의 에디터 내용(getHTML)에 이미지를 넣게 했다. 마우스 포커스에 맞게 들어가도록 해야하는 게 미션인데 기능을 구현하기 위해서 임시로 이렇게 코드를 정리했다.
그래서 유튜브 링크를 넣으면 아래처럼 썸네일이 에디터에 보여진다. 사용하고 나니 Toast Editor는 마크다운으로도 작성가능해 이 부분도 같이 구현해야하는데 위지윅만을 고려한 코드가 되었다. 그리고 아쉬운 점은 마크다운 형식은 왼쪽, 가운데, 오른쪽 정렬이 어렵다는 사실을 알게 되었다.
그렇게 에디터를 다 작성하고 등록하기를 할 때 썸네일의 이미지 주소를 iframe으로 바꿔줘야 하는데 함수를 새로 정의한다. 여기서 인자 value는 getHTML이라 생각하면 된다. 유튜브 플러그인 외 이미지 처리하는데 html을 변경 하고난 뒤의 HTML을 인자로 받아와 iframe로 바꿔주는 작업이다. for문을 돌려 유튜브 링크가 여러개면 바꿔주도록 설계했다.
handleChangeIframe(value) {
let HTMLTextIFrame = value
if (!this.youtubeUrls) {
return HTMLTextIFrame
}
for (let i = 0; i < this.youtubeUrls.length; i++) {
if (value.includes(`${this.youtubeUrls[i]}`)) {
HTMLTextIFrame = HTMLTextIFrame.replace(
`<p><img src="https://img.youtube.com/vi/${this.youtubeUrls[i]}/hqdefault.jpg" contenteditable="false"><img class="ProseMirror-separator" alt=""><br class="ProseMirror-trailingBreak"></p>`,
`<div><iframe width="560" height="315" src='https://www.youtube.com/embed/${this.youtubeUrls[i]}' title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe></div>`
)
}
}
return HTMLTextIFrame
},
그 결과 게시글에는 아래처럼 유튜브 영상이 보여지게 된다. 이렇게 구현을 하고 느낀 것이 TOAST Editor는 사용자가 직접 플러그인 구현할 수 있는 기반을 만들어 놓은 것에 대해서 놀라웠다.
플러그인을 만들고 나서
플러그인을 직접 만들어보면서 평소에 잘 안 하던 DOM조작에 대해서 배울 수 있었고 nuxt.config.js 설정도 살펴보면서 조금은 nuxt의 작동원리에 대해서 이해하는 시간이 되었다. 예전 같았으면 어려워서 포기했을텐데 하나씩 되면서 할 수 있다는 자신감이 붙어 원하는 기능을 만들 수 있어서 좋았다. 개발을 시작하고 나서 뿌듯했던 사례 중 하나로 기억남을 듯 하다.
댓글