FE
[SvelteKit] Markdown Editor 구현하기
| 서론
안녕하세요! 팡일입니다.
오늘은 SvelteKit 환경에서 Markdown Editor를 구현하면서 겪은 시행착오와
결국 markdown-it 기반 커스텀 방식으로 정착하게 된 이유 및 사용방법에 대해서 공유해보려고 합니다.
처음에는 이미 존재하는 마크다운 관련 패키지를 이용해 손쉽게 구현해보려고 했지만,
실제로 적용해보니 생각보다 호락호락하지 않다는 것을 느꼈습니다.
| 시도해본 3가지 접근법
1) mdsvex - "Svelte 안에서 마크다운을 컴포넌트처럼 쓰기"
mdsvex는 마크다운 파일을 Svelte 컴포넌트처럼 사용할 수 있게 해줍니다.
즉, .svx 파일을 만들어서 import만 하면 그대로 화면에 렌더링되는 거죠.
하지만, 실시간으로 작성하고 미리보기 하는 구조에는 어울리지 않았습니다.
한마디로 사용자가 textarea에 글을 쓰고 바로 변환한 결과를 보고 싶을 때는 mdsvex가 할 수 있는 게 거의 없었습니다.
-> 결론 : 문서를 보여줄 땐 좋지만, "에디터"로는 부족함.
2) svelte-markdown-input — "편리한데, 타입과 옵션 충돌"
mdsvex의 아픔을 딛고 새로 찾게 되어 정말 기대했던 패키지였습니다.
textarea와 preview를 한 번에 제공해주는 구조라, 제가 원하는 '즉시 미리보기'에 딱 맞을 것 같았습니다.
그런데 막상 TypeScript와 함께 쓰자마자 오류가 폭탄처럼 쏟아져 나왔습니다,,
SvelteKit 빌드 중 micromark 버전 충돌, 타입 인식 오류가 계속 났습니다.
게다가 CSS나 UI를 커스터마이징하기도 어려웠습니다.
-> 결론 : 아이디어는 좋았지만, 실무에 쓰기엔 불안정함.
3) marked — "렌더링은 되지만, 너무 날것 그대로"
marked는 오래된 마크다운 렌더러라서, 기본 기능은 정말 잘 동작했습니다.
marked.parse(text)만 해도 HTML이 바로 나왔습니다.
문제는 너무 기본적이라는 점이었습니다.
코드 하이라이드, 체박스 등을 넣으려면 전부 직접 구현해야 했고, 조금 더 알아보니 XSS라는 보안 처리도 수동으로 해야 했습니다.
-> 결론 : 단순한 마크다운 변환엔 좋지만, 확장성 부족
| 최종 선택 — markdown-it 기반 직접 구현
결국 저는 markdown-it을 사용해서 직접 renderMarkdown 함수를 만들어 쓰기로 했습니다.
이 방식은 “적당한 자유도 + 안정성 + 단순함” 세 마리 토끼를 모두 잡았습니다.
1) 패키지 설치
npm install markdown-it2) src/lib/markdown.ts
import MarkdownIt from 'markdown-it';
const md = new MarkdownIt();
export function renderMarkdown(text: string) {
return md.render(text);
}3) Svelte 페이지 (예 : src/routes/write/+page.svelte)
<script lang="ts">
import { renderMarkdown } from '$lib/markdown';
let raw = '';
let html = '';
function updatePreview() {
html = renderMarkdown(raw);
}
</script>
<textarea bind:value={raw} on:input={updatePreview}></textarea>
<div class="preview">{@html html}</div>이제 입력하는 즉시 미리보기로 반영됩니다.
원한다면 서버에서 불러온 Markdown 데이터도 별도의 {@html}로 표시할 수 있죠.
| 목업 데이터 불러오기
버튼을 누르면 아래처럼, 서버에서 받아온 것처럼 보이는 목업 Markdown을 표시할 수도 있습니다.
즉, markdown-it은 parser 역할만 하고, 렌더링 로직은 내가 완전히 제어할 수 있는 구조였습니다.
<script lang="ts">
import { renderMarkdown } from '$lib/markdown';
let raw = '';
let html = '';
function updatePreview() {
html = renderMarkdown(raw);
}
let fetchedHtml = '';
let isLoading = false;
async function fetchMockMarkdown() {
isLoading = true;
await new Promise((r) => setTimeout(r, 800));
const mockData = {
content: `
# ? 서버 목업 데이터
이 데이터는 실제 서버가 아닌,
**클라이언트 내부에서 만들어진 목업(Mock) 데이터**입니다.
- 비동기 fetch 시뮬레이션
- markdown-it으로 렌더링
- 서버 통신처럼 보이지만 로컬 실행
> ? 이후 Firestore나 API 연동 시 이 구조를 그대로 활용하면 됩니다!
`
};
fetchedHtml = renderMarkdown(mockData.content);
isLoading = false;
}
</script>
<textarea bind:value={raw} on:input={updatePreview}></textarea>
<div class="preview">{@html html}</div>
<button on:click={fetchMockMarkdown}>? 서버 목업 불러오기</button>
{#if isLoading}
<p>⌛ 로딩 중...</p>
{:else if fetchedHtml}
<div class="preview secondary">
{@html fetchedHtml}
</div>
{:else}
<p>아직 불러온 데이터가 없습니다.</p>
{/if}| 결론
세 가지 패키지를 모두 사용해본 결과, markdown-it은 가장 단순하면서도 가장 유연한 선택지였습니다.
mdsvex처럼 복잡한 설정도 없고, svelte-markdown-input처럼 타입 오류에 시달리지도 않으며,
marked보다 훨씬 확장성이 높습니다.
이를 사용하여 추후 Firestore, 서버 API, 코드 하이라이팅 등 다양한 확장을 유연하게 시도해보려고 합니다.