Markdown 에디터 구현 (ByteMD)
Vue 3 프로젝트에서 높은 성능과 확장성을 가진 Markdown 에디터를 구현하기 위해 ByteMD를 활용할 수 있습니다. ByteMD는 ByteDance에서 관리하는 라이브러리로, 다양한 플러그인을 통해 기능을 확장하기 용이합니다.
패키지 설치
에디터 본체와 더불어 GFM(GitHub Flavored Markdown) 지원 및 코드 하이라이팅을 위한 플러그인을 함께 설치합니다.
npm install @bytemd/vue-next @bytemd/plugin-gfm @bytemd/plugin-highlight
에디터 컴포넌트 작성
ByteMD를 래핑하여 재사용 가능한 MarkdownEditor.vue 컴포넌트를 생성합니다.
<template>
<Editor
:value="content"
:plugins="enabledPlugins"
:mode="viewMode"
@change="updateContent"
/>
</template>
<script setup lang="ts">
import { ref, defineProps, withDefaults } from 'vue';
import { Editor } from '@bytemd/vue-next';
import gfm from '@bytemd/plugin-gfm';
import highlight from '@bytemd/plugin-highlight';
import 'bytemd/dist/index.css';
import 'highlight.js/styles/github.css';
interface EditorProps {
content: string;
viewMode?: 'split' | 'tab' | 'fullscreen';
onUpdate: (val: string) => void;
}
const props = withDefaults(defineProps<EditorProps>(), {
content: '',
viewMode: 'split'
});
const enabledPlugins = [
gfm(),
highlight(),
];
const updateContent = (v: string) => {
props.onUpdate(v);
};
</script>
Monaco Editor를 이용한 코드 편집기 구축
VS Code의 핵심 엔진인 Monaco Editor를 사용하여 강력한 코드 편집 환경을 제공할 수 있습니다.
의존성 및 환경 설정
Monaco Editor를 설치하고, Webpack 환경에서 워커 설정을 간소화하기 위해 플러그인을 추가합니다.
npm install monaco-editor monaco-editor-webpack-plugin
vue.config.js 파일에 Monaco Editor 설정을 추가하여 모든 언어 기능을 활성화합니다.
const { defineConfig } = require("@vue/cli-service");
const MonacoEditorPlugin = require("monaco-editor-webpack-plugin");
module.exports = defineConfig({
transpileDependencies: true,
chainWebpack(config) {
config.plugin("monaco").use(new MonacoEditorPlugin());
},
});
코드 에디터 컴포넌트 구현
반응형 이슈를 방지하기 위해 toRaw를 사용하여 Monaco 인스턴스를 관리합니다.
<template>
<div ref="editorContainer" class="code-container" style="height: 450px; border: 1px solid #ddd" />
</template>
<script setup lang="ts">
import * as monaco from 'monaco-editor';
import { onMounted, ref, toRaw, onBeforeUnmount } from 'vue';
interface CodeProps {
initialCode: string;
language?: string;
onCodeChange: (value: string) => void;
}
const props = withDefaults(defineProps<CodeProps>(), {
initialCode: '',
language: 'javascript',
});
const editorContainer = ref<HTMLElement | null>(null);
let instance: monaco.editor.IStandaloneCodeEditor | null = null;
onMounted(() => {
if (editorContainer.value) {
instance = monaco.editor.create(editorContainer.value, {
value: props.initialCode,
language: props.language,
theme: 'vs-dark',
automaticLayout: true,
minimap: { enabled: true },
fontSize: 14,
});
instance.onDidChangeModelContent(() => {
const currentVal = toRaw(instance)?.getValue();
if (currentVal !== undefined) {
props.onCodeChange(currentVal);
}
});
}
});
onBeforeUnmount(() => {
instance?.dispose();
});
</script>
통합 사용 예시
위에서 정의한 두 컴포넌트를 부모 컴포넌트에서 조합하여 데이터 흐름을 제어합니다.
<template>
<div class="editor-workspace">
<section>
<h2>문서 편집기</h2>
<MarkdownEditor :content="markdownText" :onUpdate="handleMdChange" />
</section>
<section style="margin-top: 40px;">
<h2>소스 코드 편집기</h2>
<CodeEditor :initialCode="sourceCode" language="typescript" :onCodeChange="handleCodeChange" />
</section>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import MarkdownEditor from './components/MarkdownEditor.vue';
import CodeEditor from './components/CodeEditor.vue';
const markdownText = ref('# Hello Markdown');
const sourceCode = ref('const greeting = "Hello World";');
const handleMdChange = (val: string) => {
markdownText.value = val;
};
const handleCodeChange = (val: string) => {
sourceCode.value = val;
};
</script>