본문 바로가기

프로젝트/Chillog (Blog)

Toast UI Editor (tui editor) plugin 적용하기 (#2)

서론

지난 글에서 tui editor에 plugin을 적용해 봤었다.
다만 해당 plugin은 css가 없는 단순 기능 추가였어서 현재 코드의 문제가 드러나지 않았었는데
이번에 적용하는 code-syntax-highlight에서 아주 대차게 고생시켰다.

해당 플러그인은 codeblock에서 문법 강조 효과를 주는 플러그인으로,
블로그 글도 올리고(js, vue) swift도 포스팅도 올리고, 후에 올릴 다른 프로젝트에 관한 글들까지 생각하면
문법적으로나 시각적으로나 구별이 좀 필요할 것 같다고 생각이 들었다.

책을 안 읽은 사람보다 한 권만 읽은 사람이 더 무섭다고 했던가...
어줍잖은 지식으로 기고만장하면 어떻게 되는지 뼈저리게 느꼈다.

이번 포스팅은 tui editor의 기본 플러그인인 code-syntax-highlight를 적용하며 생긴 문제점과,
블로그 운영에 대해서다.

본론

설치

설치는 이전의 게시물과 동일하다.
tui editor의 git 문서를 참고하자.

$ npm install @toast-ui/editor-plugin-code-syntax-highlight

or

$ yarn add @toast-ui/editor-plugin-table-merged-cell

지난번과 마찬가지로 환경에 맞게 사용하자.
저번도 그렇고 이번도 그렇고 yarn으로는 추가가 안 되는데...
이건 내가 그냥 쓰는 것 밖에 못해서 그런 것 같기도 하고...
npm으로 설치해도 증발하는 dependancies는 다시 추가해 주면 되니 크게 문제 될 건 없다. (이전 글)

import & instance

css import

import 하는 것도 크게 다를 것 없다.
서론에서 말 했듯 해당 플러그인은 css와 함께 동작하니 css를 import해 주지 않는다면
제대로 동작하지 않는다.
chillog는 \plugins\editor.js로 에디터를 관리하고 있으니 해당 파일에 추가해 주도록 하자.

import Vue from 'vue'
import 'codemirror/lib/codemirror.css'
import 'highlight.js/styles/github.css' //code-syntax-highlight css import
import '@toast-ui/editor/dist/toastui-editor.css'
import '@toast-ui/editor/dist/toastui-editor-viewer.css'
import '@toast-ui/editor/dist/i18n/ko-kr'
import { Editor, Viewer } from '@toast-ui/vue-editor'

Vue.component('viewer', Viewer)
Vue.component('editor', Editor)

이건 내가 참고하고있는 강의와 나만의 규칙이니 개인의 방식이 있으면 그에 맞게 적용하거나,
plugin을 import하는것과 동시에 진행해도 무방한 것을 확인했다.
해당 css는 코드블럭의 테마에 관련 된 것을 보이는데 현재까지 사용한 코드블럭이 웹페이지와 조화스럽지 않아 바꿔보는 것도 좋을 것 같다.

plugin import

plugin import는 실제 에디터를 사용하는 곳으로 가서 코드를 수정한다.
chillog의 경우 viewer를 사용하는 list-normal.vue와 content.vue, editor를 사용하는 form-normal.vue에서 코드를 수정해야 한다.
본격적으로 plugin을 사용하기 위해 다음의 코드를 추가한다.

import codeSyntaxHighlight from '@toast-ui/editor-plugin-code-syntax-highlight'
import hljs from 'highlight.js'

css import는 여기서 이루어 져도 괜찮다.

plugin을 editor의 option에 추가해 줘야 하는데 이 때 codeblock내의 언어를 감지하는 hljs와 함께 넘겨 줘야 highlight가 제대로 작동 한다.
위에서부터 나온 쌩뚱맞은 highlight.js는 이번 plugin의 의존성 모듈로 다행히 tui-editor 설치시에 함께 설치 되다.
때문에 이전에 셀병합 plugin을 추가했던 것과는 조금 다른 형대로 작성한다.

export default {
  components: { DisplayTime, DisplayUser, DisplayTitle, DisplayCount },
  props: ['items', 'boardId', 'category'],
  data () {
    return {
      tuiOptions: {
        plugins: [tableMergedCell, [codeSyntaxHighlight, { hljs }]], //plugin import
        linkAttribute: {
          target: '_blank'
        }
      },
      getSummary
    }
  },
  computed: {
    fireUser () {
      return this.$store.state.fireUser
    }
  },
  methods: {
    read (item) {
      this.$router.push({ path: this.$route.path + '/' + item.id })
    },
    liked (item) {
      if (!this.fireUser) return false
      return item.likeUids.includes(this.fireUser.uid)
    },
    onViewerLoad (v) {
      addYoutubeIframe(v.preview.el, this.$vuetify.breakpoint)
    }
  }
}

기본적으로 plugin들은 스퀘어 브라켓 내에서 컴마로 구별 되고,
배열형으로 전달해야 할 때는 다시 스퀘어 브라켓에서 컴마와 브레이스로 전달한다.
아래는 list-normal.vue의 최종 코드이다.

<template>
    <div>
        <template v-for="(item, i) in items">
            <v-card :key="item.id" :class="$vuetify.breakpoint.xs ? '' : 'ma-4'" :flat="$vuetify.breakpoint.xs">
                <v-card color="transparent" flat :to="category ? `${boardId}/${item.id}?category=${category}` : `${boardId}/${item.id}`">
                    <v-card-subtitle class="text--primary body-1" :class="item.important > 0 ? 'text-truncate': ''">
                        <display-title :item="item"/>
                        <v-spacer/>
                        <display-count v-if="item.important > 0" :item="item" :column="false"></display-count>
                    </v-card-subtitle>
                    <template v-if="!item.important">
                        <v-card-text>
                            <viewer class="tui-dark" v-if="item.summary" :initialValue="item.summary" @load="onViewerLoad" :options="tuiOptions"></viewer>
                            <v-container v-else>
                                <v-row justify="center" align="center">
                                    <v-progress-circular indeterminate></v-progress-circular>
                                </v-row>
                            </v-container>
                        </v-card-text>
                        <v-card-actions class="d-flex justify-center">
                            <v-btn text color="default" class="mb-4">
                                <v-icon left>mdi-dots-vertical</v-icon>
                            </v-btn>
                        </v-card-actions>
                    </template>
                </v-card>
                <template v-if="!item.important">
                    <v-card-actions>
                        <span class="font-weight-black caption ml-3"><display-time :time="item.createdAt"></display-time></span>
                        <v-spacer/>
                        <display-user :user="item.user"></display-user>
                    </v-card-actions>
                    <v-card-actions>
                        <v-spacer/>
                        <display-count :item="item" :column="false"></display-count>
                    </v-card-actions>
                    <v-card-text>
                        <v-row justify="start" align="center" class="px-4">
                            <v-btn
                                color="info"
                                depressed
                                small
                                outlined
                                class="mr-4"
                                :to="`${$route.path}?category=${item.category}`"
                                width="100"
                            >
                            {{item.category}}
                                <v-icon right>mdi-menu-right</v-icon>
                            </v-btn>
                            <v-chip small label outlined color="default" class="mt-2 mr-2 mb-2" v-for="tag in item.tags" :key="tag" v-text="tag"></v-chip>
                        </v-row>
                    </v-card-text>
                </template>
            </v-card>
            <v-divider v-if="i < items.length -1 && $vuetify.breakpoint.xs" :key="i"/>
        </template>
    </div>
</template>

<script>
import DisplayTime from '@/components/display-time'
import DisplayUser from '@/components/display-user'
import DisplayTitle from '@/components/display-title'
import DisplayCount from '@/components/display-count'
import getSummary from '@/util/getSummary'
import addYoutubeIframe from '@/util/addYoutubeIframe'
// tui-editor plugins
import tableMergedCell from '@toast-ui/editor-plugin-table-merged-cell'
import codeSyntaxHighlight from '@toast-ui/editor-plugin-code-syntax-highlight'
import hljs from 'highlight.js'

export default {
  components: { DisplayTime, DisplayUser, DisplayTitle, DisplayCount },
  props: ['items', 'boardId', 'category'],
  data () {
    return {
      tuiOptions: {
        plugins: [tableMergedCell, [codeSyntaxHighlight, { hljs }]],
        linkAttribute: {
          target: '_blank'
        }
      },
      getSummary
    }
  },
  computed: {
    fireUser () {
      return this.$store.state.fireUser
    }
  },
  methods: {
    read (item) {
      this.$router.push({ path: this.$route.path + '/' + item.id })
    },
    liked (item) {
      if (!this.fireUser) return false
      return item.likeUids.includes(this.fireUser.uid)
    },
    onViewerLoad (v) {
      addYoutubeIframe(v.preview.el, this.$vuetify.breakpoint)
    }
  }
}
</script>

<style>

문제

삽질하기

용케 적용해 놨더니 무언가 이상하다.

같은 코드를 codeblock에 작성해도 결과가 다르게 나온다.
생각할 수 있는 가능성은

  1. plugin이 설치가 안 됐거나, import가 제대로 안 됐거나 아무튼 plugin이 문제다.
  2. css가 적용이 안됐다.

두 가지였다.

plugin이 제대로 작동하는지 확인 하는 방법은 간단하다.
첫 번째로 오류인데, 일단 콘솔에서 뜨는 오류는 없었다.
빌드도 별 내용 없이 잘 구동됐다.
두 번째는 소스를 뜯는 것이다.

웹개발의 장점은 브라우저의 개발자 도구들이 정말 잘 되어 있다는 점이다.
플로팅만 하면 소스까지 이동을 해 주다니... 감동이다...
쉽게 찾아간 곳에서 highlight.js의 작동 여부를 볼 수 있다.
사진의 오른쪽을 보면 코드의 키워드 별로 hljs의 적용 문법에 따라 제대로 분리하고 있는 것을 볼 수 있다.
결론은 css가 문제일 것 같다는 생각이 들었다.

확인 받기

모르는 걸 아는 채 했다가 뽀록나면 그게 창피한 거다.
난 누가봐도 초보이니 당당히 물어 보기로 했다.
tui가 네이버에서 튀어나온 걸로 알고 있는데 일단은 문의 내용들도 그렇고 답변하는 것도 그렇고 영문이 기본인 것 같아 영문으로 진행했다.

tui-editor GitHub 문의

답변 내용은 이러하다.

답변 내용

코드 작성한 부분들은 정상이고,코드 강조는 이미 적용이 되어 있는 것 같다.css가 제대로 적용 됐는지 확인을 한 번 해 보는 것이 좋을 거다.

(이제 와서 하는 말이지만 멍청이에게 친절한 답변 감사드립니다. js87zz님...)

말 그대로 이젠 css의 차례이다.
일단은 삽질에 너무 많은 시간을 쏟았고, css는 또 어떻게 확인해야 할 지 피곤한 상황이라 swift 공부에 잠시 쏟았던 것 같다.
어제에야 문득 내가 다크모드 관련해서 tui의 css를 건드렸다는 것이 생각이 났다.

해결하기

해당 코드는 앱 최상위인 \src\app.vue에 작성되어 있다.
다크모드 관련이라 앱 전반에 걸쳐 적용이 되면 되겠구나 하는 생각에 한 발상이었다,

<style>
.white-space{
  white-space: pre-wrap;
}
.tui-dark h1, .tui-dark h2, .tui-dark h3, .tui-dark h4, .tui-dark h5, .tui-dark h6, .tui-dark li, .tui-dark ol, .tui-dark p, .tui-dark pre, .tui-dark table, .tui-dark ul {
  color: var(--v-primary-base) !important
}
.tui-dark code, .tui-dark span {
  color: #323232 !important
}
.scroll::-webkit-scrollbar {
  display: none;
}
.scroll {
  height: 215px;
  overflow-y: auto;
}
</style>

일단은 크게 건들지 않고 조금씩 바꿔 보면서 진행하기로 한다.
아마 내 생각인데 해당 코드를 옮기면 이번같이 거지같은 상황은 생기지 않으리라...
너무 상위 코드에 적용을 해 둔 거지 싶다.

<style>
.white-space{
  white-space: pre-wrap;
}
.tui-dark h1, .tui-dark h2, .tui-dark h3, .tui-dark h4, .tui-dark h5, .tui-dark h6, .tui-dark li, .tui-dark ol, .tui-dark p, .tui-dark pre, .tui-dark table, .tui-dark ul {
  color: var(--v-primary-base) !important
}
.tui-dark code {
  color: #323232 !important
}
.scroll::-webkit-scrollbar {
  display: none;
}
.scroll {
  height: 215px;
  overflow-y: auto;
}
</style>

하나씩 건들여 보며 span을 뺐을 때 css가 잘 적용 되는 것을 확인했다.
code를 뺐을 경우 글씨가 흰 색으로 변하는 걸 봐선 vuetify의 dark-mode에 영향을 받는 것으로 보인다.
이후에 테마를 바꾸면서 확인해 볼 필요가 있어 보인다.

결론

훨~~씬 보기 좋아졌다.


Log

2021.07.23.
블로그 이전으로 인한 글 옮김 및 수정.