본문 바로가기
카테고리 없음

Nuxt Toast Editor, 유튜브 플러그인 구현하기(feat. wysiwyg)

by 지삶앎 2022. 6. 23.
반응형

 

TOAST UI

TOAST UI를 선택한 이유,

나한테 주어진 프로젝트에서 Editor를 구현해야 했다. Vue 기반의 Nuxt 프레임워크와 잘 어울리는 에디터를 찾아봤다. 개발자가 아닌 사람들이 주로 사용하기에 마크다운이 아닌 위지윅 방식의 에디터를 찾아보게 됐다.

 

그 중에서 tip-tap이 떠오르는 에디터라고 하나 문제는 import를 했을 때 기본적으로 제공하는 플러그인을 스스로 만들어야 했다. 아래 사진은 대표 홈페이지에 들어간 예시들인데 이를 바로 쓸 수 없고 직접 구현해야했다. 예시 자료도 있지만 플러그인들의 이미지는 제공하지 않고 텍스트로 된 버튼으로 보여주었다. 

 

tip-tap editor 예시

 

그러다가 찾아보게 된 TOAST Editor, 기본으로 제공되는 플러그인들이 갖춰져있었고 한국에서 만들어졌기에 조금 더 정보들을 쉽고 빠르게 얻어갈 수 있었다. 플러그인을 직접 구현하여 넣을 수 있어 확장성이 있는 에디터였다.

TOAST Editor 기본 제공 기능

 

 

TOAST UI :: Make Your Web Delicious!

TOAST UI is an open-source JavaScript UI library maintained by NHN Cloud.

ui.toast.com

 

 

본격적인 플러그인 제작

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의 작동원리에 대해서 이해하는 시간이 되었다. 예전 같았으면 어려워서 포기했을텐데 하나씩 되면서 할 수 있다는 자신감이 붙어 원하는 기능을 만들 수 있어서 좋았다. 개발을 시작하고 나서 뿌듯했던 사례 중 하나로 기억남을 듯 하다. 

반응형

댓글