본문 바로가기
프로젝트/TailTales

Vue-Quill 텍스트 에디터 사용하기

by wldusdn 2025. 5. 7.

지금 tailtales라는 특수동물 커뮤니티를 미니 프로젝트를 만들고 있는데,

이번 게시판 작성에서는 VueQuill 이라는 텍스트 에디터를 한번 적용해보려고 한다.

 

😑기존의 게시판 폼

<template>
  <Breadcrumb breadcrumb="board" />

  <h2 class="text-xl font-semibold leading-tight text-gray-700">글 작성</h2>

  <div class="mt-5">
    <form @submit.prevent="submitForm" class="space-y-4">
      <div>
        <label for="category" class="block text-gray-700 text-sm font-bold mb-2 sr-only">카테고리:</label>
        <select id="category" v-model="formData.category" class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline">
          <option value="" disabled selected>카테고리</option>
          <option v-for="option in categoryOptions" :key="option.value" :value="option.value">
            {{ option.label }}
          </option>
        </select>
      </div>
      <div>
        <input type="text" id="title" v-model="formData.title" placeholder="제목을 입력하세요" required class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline">
      </div>
      <div>
        <textarea id="content" v-model="formData.content" rows="10" required class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"></textarea>
      </div>
      <div class="bg-gray-100 p-2 rounded-md mb-4 flex items-center space-x-2">
        <label for="fileInput" class="flex items-center cursor-pointer">
            <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"></path></svg>
            첨부파일
        </label>
        <input type="file" id="fileInput" @change="handleFileChange" class="hidden" multiple>
        <div v-if="formData.files && formData.files.length > 0" class="mt-2">
          <div class="flex space-x-2">
            <div v-for="(file, index) in formData.files" :key="index" class="relative">
              <img v-if="file.preview" :src="file.preview" alt="미리보기" class="w-20 h-20 object-cover rounded border">
              <span v-else>{{ file.name }}</span>
              <button type="button" @click="removeFile(index)" class="absolute top-0 right-0 bg-gray-300 hover:bg-gray-400 text-gray-800 text-xs rounded-full w-5 h-5 flex items-center justify-center">
                <svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path></svg>
              </button>
            </div>
          </div>
        </div>
      </div>

      <div class="flex justify-end space-x-2">
        <button type="submit" class="px-4 py-2 font-medium tracking-wide text-white capitalize transition-colors duration-200 transform bg-indigo-600 rounded-md hover:bg-indigo-500 focus:outline-none focus:bg-indigo-500">완료</button>
      </div>
    </form>
  </div>
</template>

<script lang="ts" setup>
import Breadcrumb from '../../partials/AppBreadcrumb.vue';
import { ref } from 'vue';

interface UploadedFile {
  file: File;
  name: string;
  preview?: string;
}

const categoryOptions = ref([
  { value: 'notice', label: '공지' },
  { value: 'free', label: '자유게시판' },
  { value: 'question', label: '질문' },
]);

const formData = ref({
  category: '',
  title: '',
  content: '',
  files: [] as UploadedFile[], // 변경: 여러 파일 저장을 위한 배열
});

const handleFileChange = (event: Event) => {
  const target = event.target as HTMLInputElement;
  const files = Array.from(target.files || []);
  formData.value.files = []; // 기존 파일 목록 초기화

  files.forEach((file: File) => {
    const uploadedFile: UploadedFile = { file: file, name: file.name }; // 파일 이름 저장
    formData.value.files.push(uploadedFile);
    if (file.type.startsWith('image/')) {
      uploadedFile.preview = URL.createObjectURL(file);
    }
  });
};

const removeFile = (index: number) => {
  formData.value.files.splice(index, 1);
};

const submitForm = () => {
  console.log(formData.value);
  alert('글이 작성되었습니다!');
  formData.value.title = '';
  formData.value.content = '';
  formData.value.files = [];
};
</script>

- 그냥 단순히 글을 작성하고 첨부파일을 따로 첨부받고 있어 글자를 꾸민다거나 중간중간 이미지 삽입 등을 할 수 없음!

 

그럼 순서대로 적용해보자!

 

NPM

우선 터미널에 라이브러리 설치하기

npm install @vueup/vue-quill@latest --save

 

Main.ts

그리고 main파일에 아래 코드를 추가해 import 해준다.

여기서 snow는 테마 중 하나라서 원하는 걸 넣으면 됨. (bubble테마도 있다.)

import { QuillEditor } from "@vueup/vue-quill";
import '@vueup/vue-quill/dist/vue-quill.snow.css';

app.component("QuilEditor", QuillEditor);

 

toolbarOptions.ts

텍스트 에디터를 커스텀해 사용하기 위해 option 값들을 정한 파일을 만들어준다.

import { ref } from 'vue';

const toolbarOptions = ref([
    [{ 'header': [1, 2, 3, 4, 5, 6, false] }],
    [{ 'font': [] }],
    [{ 'align': [] }],
    [{ 'indent': '-1'}, { 'indent': '+1' }],
    ['bold', 'italic', 'underline', 'strike'],
    ['blockquote', 'code-block'],
    [{ 'script': 'sub'}, { 'script': 'super' }],
    [{ 'list': 'ordered'}, { 'list': 'bullet' }],
    [{ 'size': ['small', false, 'large', 'huge'] }],
    [{ 'color': [] }, { 'background': [] }],
    ['link', 'image', 'video'],
    ['clean']
]);

export default toolbarOptions;

 

AppBoardWrite.vue

기존의 <textarea>태그를 QuilEditor로 바꿔주고 마운트 됐을 때 getQuill() 메서드를 호출해서 사용

<templete>
  <QuillEditor :toolbar="toolbarOptions" theme="snow" v-model="formData.content" ref="quillEditorRef"/>
</templete>

<script lang="ts" setup>
import { QuillEditor } from '@vueup/vue-quill';
import toolbarOptions from '@/hooks/toolbarOptions';

const quillEditorRef = ref<InstanceType<typeof QuillEditor> | null>(null);
const quillInstance = ref<any | null>(null);

onMounted(() => {
  if (quillEditorRef.value) {
    quillInstance.value = quillEditorRef.value.getQuill();
  }
});

</script>

 

😊 바뀐 게시판 폼

 

AppBoardWrite.vue

formData.content에 html태그 그대로 저장되게 해놔서 나중에 조회할 때는 v-html속성에 넣어줘야 함

<template>
  <Breadcrumb breadcrumb="board" />

  <h2 class="text-xl font-semibold leading-tight text-gray-700">글 작성</h2>

  <div class="mt-5">
    <form @submit.prevent="submitForm" class="space-y-4">
      <div>
        <select id="category" v-model="formData.category" class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline">
          <option value="" disabled selected>카테고리</option>
          <option v-for="option in categoryOptions" :key="option.value" :value="option.value">
            {{ option.label }}
          </option>
        </select>
      </div>
      <div>
        <input type="text" id="title" v-model="formData.title" placeholder="제목을 입력하세요" required class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline">
      </div>
      <div class="shadow appearance-none border rounded w-full 2xl:h-72 md:h-64 py-3 px-3 bg-white">
        <div class="md:h-4/6 2xl:h-5/6"><QuillEditor :toolbar="toolbarOptions" theme="snow" v-model="formData.content" ref="quillEditorRef"/></div>
      </div>
      <div class="flex justify-end space-x-2">
        <button type="submit" class="px-4 py-2 font-medium tracking-wide text-white capitalize transition-colors duration-200 transform bg-indigo-600 rounded-md hover:bg-indigo-500 focus:outline-none focus:bg-indigo-500">완료</button>
      </div>
    </form>
  </div>
</template>

<script lang="ts" setup>
import { QuillEditor } from '@vueup/vue-quill';
import Breadcrumb from '../../partials/AppBreadcrumb.vue';
import { ref, onMounted } from 'vue';
import toolbarOptions from '@/hooks/toolbarOptions';

const categoryOptions = ref([
  { value: 'notice', label: '공지' },
  { value: 'free', label: '자유게시판' },
  { value: 'question', label: '질문' },
]);

const formData = ref({
  category: '',
  title: '',
  content: '',
});

const quillEditorRef = ref<InstanceType<typeof QuillEditor> | null>(null);
const quillInstance = ref<any | null>(null);

onMounted(() => {
  if (quillEditorRef.value) {
    quillInstance.value = quillEditorRef.value.getQuill();
    console.log('Quill Instance:', quillInstance.value);
  }
});

const getEditorHTML = () => {
  if (quillInstance.value) {
    formData.value.content = quillInstance.value.root.innerHTML; // 여전히 root.innerHTML로 접근 가능한지 확인 필요
    console.log('HTML Content:', formData.value.content);
  }
};

const submitForm = () => {
  getEditorHTML();
  console.log(formData.value);
  alert('글이 작성되었습니다!');
  formData.value.title = '';
  formData.value.content = '';
};
</script>

 

 

 

참고 - https://vueup.github.io/vue-quill/

 

VueQuill

VueQuill Rich Text Editor for Vue 3

vueup.github.io