NextJsProseMirrorShiki

[ Introduction > Blog ]

[블로그 소개] 프론트엔드 - 에디터

 Carrot Yoon
 2025-03-16
 118

[블로그 소개] 프론트엔드 - 에디터

안녕하세요? 첫 번째 블로그 글입니다. 읽어주셔서 감사합니다.
저는 직접 홈페이지를 운영해보고 싶었고, 프로그래밍 하면서 고민하고 해결한 것들을 기록하고 공유하고 싶어 블로그를 만들었습니다.
화학공학과 찐 이과 출신이라 많이 글을 못씁니다. 쓴 글을 수정해가며 더 효율적으로 지식을 공유할 수 있도록 노력하겠습니다.
함께 더 쉽고 높게 성장할 수 있으면 좋겠습니다.

에디터를 만드는데 1달 가까이 사용할 정도로 공들였습니다. Toast UI, Editor.js, React-md-editor,Tiptap까지 커스텀 플러그인으로 원하는 기능을 추가 해보고 사용성과 확장성을 확인할 정도로 진심 이였습니다.
저에게 간택된 블로그 에디터는 ProseMirror 엔진 기반의 Tiptap 에디터입니다. 특징은 확장성이 엄청나지만, 그만큼 작은 기능도 많은 코드가 필요하다는 점입니다. 그리고 Viewer를 JSON과 Text 형식 모두 SSR로 렌더링할 수 있어서 NextJs, 백엔드와 찰떡궁합 에디터입니다!
(※ 간단한 기능의 확장만 필요하면 플러그인이 많은 다른 에디터 강력 추천)

에디터

SSR

확장성

엔진

WYSIWYG

Markdown

RIch Text

기타

editor.js

🟡

中上

자체

🟡

노션, Json, 문서 좋음, 추천

react-md-editor

자체

이지윅 때문에 사용성 떨어짐

toast UI

ProseMirror

문서화가 나쁨, 플러그인 적음.

quill

🟡

中上

Delta

🟡

문서 좋음, JSON, 추천.

TipTap

中上

ProseMirror

🟡

기능 잘 나뉨. 문서 보통. 추천.

ProseMirror

자체

🟡

문서 최고. 러닝 커브. 힘듬.

Toast UI Editor, Editor.js, React-md-editor는 내가 생각한 블로그 설계와 적합한지 확인하기 위해 사용했고 quill은 회사에서 사용해봤습니다. 모두 나름의 장단점이 있고 상황에 따라 적절한 에디터를 고르면 된다고 느꼈습니다. 하지만 react-md-editor와 toast UI에디터는 개인적으로 비추천합니다. react-md-editor는 이지윅이 안 돼서 만약 고객에게 제공할 에디터라면 엄청 구식으로 보일 것입니다. 그리고 toast-ui v3.0은 다운로드 수에 비해 플러그인이 적었고 문서화도 커스텀 기능을 추가하기엔 매우 부족했습니다. 위 두 에디터 제외하고는 전부 좋았고 각자 강점과 특색이 있었습니다.

  • editor.js는 노션으로 유명한 에디터입니다. 하지만 막상 사용해보면 노션과 달리 부자연스럽게 느껴졌습니다. 더 매끄럽게 작동하도록 만들려면 많은 플러그인을 추가해야할 것 같습니다. 문서화는 깔끔해서 제대로 만들면 엄청 좋은 에디터가 만들어 질 것 같습니다.

  • quill 에디터는 탄탄한 느낌의 에디터입니다. 일반적으로 필요한 기능의 플러그인은 다 갖췄고, 탄탄하게 설계되었으니 만약 회사에서 고객에게 에디터를 제공해야 한다면 '이 에디터가 맞아!' 라고 말할 것 같은 에디터입니다. 문서화도 잘 되어있어서 커스텀 기능 추가도 수월해 보입니다.

  • Tiptap 에디터는 문서화는 평범하지만 ProseMirror를 엔진으로 사용하며 그 구조를 거의 그대로 사용합니다. 그래서 문서가 부족해서 플러그인 추가에 어려움이 있을 수 있지만 ProseMirror가 자세히 문서화 되어있어서 그 문서를 참고할 수 있습니다.
    하지만 ProseMirror는 기능 추가에 많은 노력이 필요합니다. 예를 들면 글자를 굵게 만드는 기능을 추가하고 싶으면 Schema, Command, HTML Render 방식, parse방식 등 모두 설정해야 합니다. 그래서 TipTap의 플러그인들을 사용한다면 기본 기능들은 굳이 어렵게 구현할 필요가 없기 때문에 서로 상호보완적인 면이 있습니다.
    만약 회사에서 이 에디터를 사용한다면 그 이유는 최고의 제품을 위해 커스텀한 기능이 많이 필요하기 때문일 것입니다.

위 장단점은 모두 제 개인적인 의견이고, 실제 사람마다 생각은 다를 수 있으니 참고 용으로 봐주셨으면 좋겠습니다.
추가적으로 저가 TipTap을 이용한 또 다른 이유는 서버에서 TEXT, JSON 데이터 모두 HTML로 서버에서 바꿀 수 있다는 점입니다. 반면 quill과 editor.js는 window객체를 참조하고 있어 서버에서 JSON을 HTML로 파싱은 가능하지만 TEXT(원본)는 불가능합니다. 서버에서 데이터를 바꿀 수 있다는 것은, Sanitize(HTML 소독)을 백엔드에서 더 수월하게 할 수 있고, 저장 데이터 형식에서도 자유로워 집니다.

1. 기능 정의

저의 에디터에 필요한 기능들을 정리해 봤습니다.
기본적인 글씨체 변경, 문단 정렬, 테이블 기능, 이미지 업로드 등은 필수였고, 프로그래밍 개발자 블로그로서 다양한 언어를 지원하는 Code Syntax Highlighter가 가장 중요합니다.
이 외에 평소 쓰는 마크다운 문법, S3에 업로드하여 URL을 받는 기능, 사용자를 위한 TOC 기능이 에디터에 들어갈 기능입니다.

  1. 기본 텍스트 편집 기능

    • 글씨체 변경 (굵게, 밑줄, 기울임, 색깔, 배경색)

    • 글머리 기호, 번호 기호, 체크 리스트

    • 문단 좌, 우, 가운데 정렬

    • 마크다운 문법 지원 (#적고 띄우면 h1 태그.. ```js 작성시 js 코드 블록 자동 생성)

    • 들여쓰기, 내어쓰기

    • 하이퍼 텍스트 생성

  2. Viewer의 서버 사이드 렌더링

    • Viewer는 서버사이드 렌더링(SSR)이 가능하여 NextJs의 ISR, SSG로 캐싱가능 ★★★★★

  3. 코드 블록 및 코드 하이라이팅

    • 코드 블록에 설정한 코딩 언어에 맞는 문법 강조

    • Code Syntax Highlighter는 SSR이 가능하여, NextJs의 ISR, SSG로 캐싱가능. ★★★★★

    • 기본 Syntax Highlighted된 코드에 서식 추가.(형광팬, 밑줄 등)

    • VS Code에서 복사한 코드는 자동으로 해당 파일의 언어를 감지하여 코드 블록 생성

  4. 이미지 업로드 및 관리

    • 이미지 정렬 및 크기 조정 기능 지원

    • 복사-붙여넣기 시 편하게 S3에 업로드하여 URL을 받아 사용 ★★★

    • 업로드 시, WebP 압축 후 비교하여 효율적인 포멧 사용.

    • 기존의 이미지 URL을 재활용하여 불필요한 업로드 방지.

  5. 목차(TOC) 자동 생성

    • 헤더를 통해 자동으로 목차(TOC) 생성

  6. 테이블 기능

    • 셀 병합 및 분리 기능 지원

    • 직관적인 테이블 조작 기능 제공

2. 기능별 소개

6가지 기능을 구현하면서 어려웠던 점 및 구현 방법을 간단히 소개하겠습니다.
위 기능 외에도 블록 단위로 컨텐츠를 드래그 앤 드롭(D&D)으로 옮기는 기능, 브라우저의 detail 태그를 사용하는 기능 등 다양하게 있지만 생략했습니다.

2.1 Code Syntax Highlighter

이 기능은 플러그인 구현을 하면서, 다양한 에러 및 예외 사항들을 만나 2주 넘게 걸려 겨우 완성한 기능입니다.
그 핵심 기능 리스트를 정리했습니다.

  • 언어에 맞게 Syntax를 강조하는 기능(100개 이상 언어 지원)

  • 원하는 theme 적용 가능(github-black theme 등..)

  • "```언어" 라고 입력하면 해당 언어에 맞는 블록 자동 생성

  • VS Code에서 복붙시 클립보드 MIME 정보로 해당 언어에 맞는 코드블럭 자동 생성.

  • 별도 CSS 추가 적용.

  • CodeHighlighter의 파싱없이 그대로 출력 가능. (별도 데이터 가공 필요X)

const a = 1;const b = 2;function add(num1, num2) {    return num1 + num2;}const answer = add(1, 2);
#include <iostream>int main() {    int** arr = new int*[100];  // 행 포인터 배열 동적 할당    for (int i = 0; i < 100; ++i) {         arr[i] = new int[100]();  // 각 행을 동적 할당 및 0으로 초기화    }    arr[0][0] = 1;         std::cout << arr[0][0] << std::endl;      // 메모리 해제    for (int i = 0; i < 100; ++i) {        delete[] arr[i];    }    delete[] arr;    return 0;} 

코드 블럭을 보면 언어에 맞게 예약어와 변수들이 강조된 것과 별도로 형광팬이 칠해진 모습을 볼 수 있습니다.
Syntax에 맞게 코드를 보여주는 기능과 theme에 맞는 UI를 보여주는 기능은 Shiki라는 Syntax Highlighter를 사용했습니다. Shiki를 사용한 이유는 다른 Syntax Highlighter와 달리 Line 단위로 묶어서 파싱하며, 100개 이상의 언어를 처리할 수 있기 때문입니다.
Line단위로 묶어 파싱되는 것이 좋은 이유가 궁금할 수 있는데, 순수 CSS로 Line Number 출력이 가능하기 때문입니다.

이 기능을 Toast UI Editor 등 다양한 에디터에 녹여보았지만 에디터에 맞게 녹인 느낌이 아니라 따로 노는 느낌이였습니다. 왜냐하면 에디터와 한몸처럼 작동하도록 녹일 방법을 못 찾았기 때문입니다. 하지만 ProseMirror는 확장성이 좋아서 위 기능을 에디터와 한 몸처럼 작동하게 녹일 방법이 보였고, ProseMirror를 더 제대로 공부하게된 계기가 되었습니다.
저가 말하는 에디터와 한 몸처럼 작동한다는 것은 별도 데이터 처리 없이 Text 데이터 그대로 수정할 때 사용할 수 있다는 것을 의미합니다. 별도로 처리를 하면 에디터 형식이 에디터 형식과 달라져서 수정할 때 사용하지 못하기 때문에 이중으로 데이터 보관이 필요하게 됩니다.
이 기능을 에디터에 녹일 때 Tiptap으로 추가한 플러그인들과 얽히고 설켜 어디서 잘못된건지 파악하는 것이 가장 힘들었습니다. 그러다 보니 어떤 라이프 사이클로 돌아가는지 더 자세히 알게 되었고 다른 사람들이 다양한 플러그인 코드들을 보면서 공부하게 되었습니다. 이 과정으로 알아낸 것을 통해 Highlighter가 강조한 텍스트를 ProseMirror Mark(서식 객체)로 변환하여 적용했고, Schema 정의가 충돌하지 않도록 정의했습니다. 그리고 Line마다 Span 태그와 className을 추가하여 css로 line number를 더 수월하게 처리할 수 있도록 하여 Syntext Highlighter와 Editor가 한 몸처럼 동작하도록 만들었습니다.

ProseMirror Mark로 실시간 변환해줬기 때문에, 다른 Mark(서식)도 추가할 수 있게 되어서 형광팬 등의 기능을 코드 블럭에 추가할 수 있게 만들 수 있었습니다. 이 기능을 정상 작동시킬때 까지 2주가 넘게 소요되어서 완성할 때 얼굴에 미소가 자동으로 생길 정도로 기분이 좋았고 앞으로 ProseMirror로 더 많은 것을 능숙하고 빠르게 만들 수 있다는 것에 설레었습니다.

코드블럭에 형광팬, 밑줄 같은 서식을 적용할 수 있는 텍스트 에디터는 찾기 힘듭니다. 그리고 코드 블럭 기능 만을 위해 2주 넘는시간을 투자하여 완성한 기능입니다. 이 기능의 구현은 저에겐 엄청 값진 경험이고 앞으로 더 잘할 수 있는 원동력이 된 것 같습니다.

2.2 Viewer의 서버 사이드 렌더링

Viewer의 서버 사이드 렌더링(SSR) 때문에 여러 에디터를 사용하며 많은 시행착오를 겪었습니다. SSR이 가능해야만 NextJs에서 캐싱하여 빠르게 사용자에게 보여줄 수 있기 때문입니다. 저가 생각한 Viewer의 SSR 구현은 아래와 같습니다.

  1. Client 단에서 HTML로 파싱하여 Editor데이터와 출력용 데이터 Server에 전달하기.

    • Editor 데이터와 화면 출력용 데이터로 2중으로 보관된다.

    • 에디터 기능 일부가 수정 되었을 때, HTML을 정적으로 별도 보관하기 때문에 자동 반영되지 않는다.

    • 서버는 HTML 그대로 보내주면 된다.

    • 서버는 클라이언트가 보낸 정적 HTML에 대한 Sanitize만 가능하다.

  2. Server에서 Editor의 데이터로 직접 Parsing후 출력하기.

    • 에디터 일부 기능 변경 시, 해당 변경 사항에 맞게 HTML을 그리기 때문에 자동 반영된다.

    • 데이터를 2중으로 보관할 필요가 없다.

    • 서버에서 파싱하기 때문에 서버에 부담을 줄 수 있다.

    • Server에서 변경된 에디터 기능으로 HTML 렌더링이 달라져도 Sanitize 할 수 있어 보안이 훨씬 강화된다.

  3. Server에서 Editor 데이터로 파싱하여 HTML을 만든 후, 2중으로 보관하며, 에디터 기능 업데이트 시 HTML 갱신하기.

    • 에디터 일부 기능 변경 시, 해당 변경 사항에 맞게 HTML을 그리기 때문에 자동 반영된다.

    • 서버는 HTML 그대로 보내주면 된다.

    • 변경된 HTML에 대한 Sanitize가 가능하다.

    • 데이터는 2중으로 보관된다.

1번 방법은 에디터가 SSR이 불가능할 때 사용할 방법이며, 클라이언트가 서버 Sanitize가 방어하지 못하는 방식으로 직접 HTML을 조작하여 보낼 수 있어 보안에 구멍이 발생할 수도 있고 , 에디터 업데이트로 변경된 파싱 방법으로 만들어진 HTML을 보여줄 수 없을 것입니다.
2번 방법은 더 안전하고, 최신 업데이트 사항을 반영할 수 있지만, 서버에 부담을 줄 수 있기 때문에 적절하게 캐싱하여 보여줄 수 있어야 합니다. 그래서 저는 위 두 가지 장점을 합친 3번째 방법을 채택했습니다. 빠른 속도와, 보안, 안정성을 가질 수 있는 방법이라고 생각합니다.

2.3 이미지 업로드

article_img

이미지는 크기 조절과 정렬이 가능하고, 비율 유지 여부도 설정할 수 있습니다. 여러 플러그인 코드들을 보고 좋은 기능들만 가져와 합쳐서 커스텀했습니다. 사진 파일을 직접 ctrl+v로 입력하면 webp로 압축하고 원본과 비교하여 용량이 작은 파일을 백엔드 서버로 업로드하여 URL을 받아오는 방식입니다. 기존 이미지의 URL을 통해 업로드도 가능합니다. 이미지도 블럭 단위로 복사가 가능하며, 외부 사이트의 이미지를 URL로 올릴 수 있지만, CSP 정책 설정으로 이 사이트에서는 해당 이미지를 로드할 수 없습니다.

2.4 기본 텍스트 편집 기능

우선 화질은 죄송합니다 GIF를 Webp로 만들어도 용량이 커 GIF들은 최대한 색상을 손실시켜 압축했습니다.

article_img

기본 텍스트 기능은 대부분 Tiptap 플러그인에서 가져왔고 일부 기능들은 커스텀 했습니다. 예를 들면 Color Palette 중 1개는 커스텀 기능입니다. 모든 기능을 직접 구현했다면 아직도 완성하지 못했을 거라 생각합니다. 많은 기본 텍스트 기능을 제공해준 Tiptap 에디터 관계자 분들에게 감사합니다.

기능 구현에 에너지를 다 써버려서 디자인이 엉망입니다. 디자인은 기능과 사용성 개선과 함께 천천히 개선할 예정입니다.

2.5 테이블 기능

사진1 MPTI 프로젝트

사진2 콘스텔링크 프로젝트

사진3 럭퀴즈 프로젝트

쉽게 온라인 PT를 받을 수 있는 플랫폼
WebRTC로 P2P 화상 채팅 기능

살아 생전 처음 했던 프로젝트라.. 아키텍처조차 없다 ㅜㅜ

누구도 기부 내역과 사용처를 조작하지 못하는

블록체인 기반 치료비 모금 플랫폼

누구나 만들고 하기 쉬운

실시간 대규모 퀴즈 게임 플랫폼

콘스텔링크 아키텍처

럭퀴즈 아키텍처

기본 브라우저의 테이블 태그를 이용하여 만들어졌습니다. 모바일에서는 table 태그가 Width나 Height, 정렬이 제대로 적용되지 않습니다. 그래서 넘치는 경우에는 좌우 스크롤로 볼 수 있도록 개선했습니다. 셀 추가/병합/분리가 가능하고. 1행에 2개의 열을 갖는 것도 가능합니다. 사진 1 아래의 셀이 합쳐진 셀입니다.

2.6 TOC

기존 Heading 플러그인을 수정하여 사용자 정의 Table of Contents(TOC) 기능을 만들기 위해 입력 중, h태그 생성 시 전체 h태그를 찾은 뒤, 계산하여 자동으로 번호가 html id와 data set으로 할당되도록 만들었습니다. 처음에는 간단할 것 같았지만, 실제로 구현해보니 head 태그가 문서 내에서 몇 번째 태그 인지를 전체 문서를 뒤지며 정확하게 계산하는 부분이 까다로웠고, Text 또는 JSON 형식으로 내용을 추출한 후 다시 에디터에서 불러왔을 때 동일한 구조로 나타나기 위해서 HTML로 렌더링하고 파싱하는 부분도 신경써서 수정해야 했습니다.

파싱에서 끝나는 것이 아니라 Client의 화면에서는 스크롤바 쪽에 위치한 TOC를 클릭 시 Head별 위치 값을 계산해 이동할 수 있어야 합니다. 단순히 scrollIntoView를 쓰면 된다고 생각했지만, Sticky 컴포넌트의 존재로 계산을 제대로 못하는 문제가 발생했습니다. 왜냐하면 브라우저 scrollIntoView 함수나 Anchor를 사용하여 컴포넌트로 이동할 때 브라우저 화면 기준 컴포넌트 Y좌표와 + scrollY로 계산하는데 Sticky한 컴포넌트는 이 계산식이 경우에 따라 틀린 계산이 되기 때문입니다.

또 다른 이슈는 이미지의 로딩 속도에 따라 이동할 위치와 의도한 위치가 달라지는 문제입니다. 이 문제는 Debouncing과 ResizeObserver를 사용하여 해결했습니다. Debouncing은 사이즈 변경이 자주 일어나도 1번의 위치 계산만 하기 위함이고, ResizeObserver는 레이아웃 변화 감지를 위해 사용했습니다. 아래는 완성된 TOC 기능 사용 예시입니다.

article_img

3. 기타 기능

기타 기능은 지속적으로 업데이트해 나갈 예정입니다. 물론 지금 이 글을 쓰는 순간에도 소개했던 기능들의 디테일한 부분 업데이트를 많이 했습니다. 시간이 날 때마다 반영하겠습니다.

3.1 Iframe (CodePen)

블로그 개선을 위해 블로그를 탐방하다가 필요해보여 추가한 기능입니다. Iframe 때문에 CSP 설정이 조금 약화 됐습니다. Iframe 기능 구현이 어려운 것은 아닙니다. 다만 보안쪽 관련해서 고민이 많은 필요합니다. Iframe을 추가 하기 전에도 느낀거지만 third party 홈페이지를 사용할 때 어떻게 하면 안전하게 사용할 수 있을지 항상 고민이 됩니다. 예를 들면 CSP 설정에서 "반드시 안전하게 사용하려면 이것을 추가해라!!" 라고 하지만 만약 그 설정을 반영하면 사용할 수 있는 라이브러리도 적어집니다. 저의 홈페이지도 안전을 위해 COEP 설정을 안전하게 해뒀는데, third party 사이트를 Iframe에 띄우면서 이 설정을 없애야 했습니다. 저의 백엔드 서버라면 COEP 설정을 원하는대로 할 수 있겠지만 third party 사이트에서 헤더를 맞춰줄 이유가 없기 때문에 저가 맞춰야 했습니다. 이런 부분을 어떻게 해결해야 보안에 문제가 없는지 고민을 만들어준 기능입니다.