새로운 마크다운 파서가 필요한 이유


최근 개발자들이 가장 선호하는 문서 형식을 하나 꼽으라면 단연 마크다운일 것이다. 마크다운은 깃허브(GitHub), 깃랩(GitLab), Bitbucket등 업무나 이슈 관리를 지원하는 대부분의 서비스에서 기본 문서 형식으로 사용되고 있다. 또한 IntelliJ, VSCode, Vim, Emacs등 거의 모든 텍스트 편집 도구에서도 플러그인을 통해 마크다운 문서의 구문 강조나 미리보기 기능을 사용할 수 있다.

TOAST UI Editor는 여기서 한 걸음 더 나아가 마크다운 에디터와 위지윅 에디터를 통합한 형태의 인터페이스를 제공한다. 위지윅 에디터를 사용하면 테이블 등의 복잡한 문법을 더 직관적이고 편리하게 편집할 수 있으며, 마크다운에 익숙하지 않은 사용자도 마크다운 기반의 문서를 손쉽게 편집할 수 있어 개발자와 비 개발자의 협업에 특히 유용하다. 이런 장점을 바탕으로 TOAST UI Editor의 사용자는 지난 몇 년간 꾸준히 증가했으며, 지난달에 깃허브 스타 10,000개라는 의미 있는 성과를 달성하기도 했다.

TOAST UI Editor는 마크다운 에디터라는 정체성을 유지하면서 더 나은 사용자 경험을 제공하기 위해 계속해서 노력하고 있다. 최근에 릴리스한 2.0 버전은 기존에 사용하던 마크다운 파서인 markdown-it을 제거하고 commonmark.js를 기반으로 한 새로운 마크다운 파서를 구현했는데, 이는 기존 버전이 갖고 있던 많은 문제점을 해결할 뿐만 아니라 앞으로의 더 큰 발전을 위한 초석이 될 중요한 변화이다.

이 글에서는 TOAST UI Editor를 비롯한 기존 마크다운 에디터들이 가진 문제점과 한계를 짚어보고, 2.0에서 새롭게 구현한 마크다운 파서가 이러한 문제를 어떻게 해결할 수 있는지를 살펴볼 것이다. 또한 이 새로운 파서를 활용해서 마크다운 편집의 사용성을 얼마나 더 발전시킬 수 있는지도 함께 살펴보도록 하겠다.

마크다운 에디터의 기능과 문제점

TOAST UI Editor는 마크다운 에디터와 위지윅 에디터를 둘 다 지원하지만, 마크다운 에디터만으로도 다른 에디터에 못지않은 다양한 기능을 제공한다. 그중 구문 강조, 실시간 미리 보기, 도구 모음 버튼 등은 마크다운 에디터라면 필수적으로 갖춰야 할 기능이라 할 수 있다. 하지만 TOAST UI Editor뿐만 아니라 다른 에디터들도 기술적 어려움으로 인해 이 기능들을 완벽하게 구현하지는 못하고 있다. 이 단락에서는 각 기능에 대한 설명과 함께 문제점과 원인에 대해서도 자세히 알아보도록 하겠다.

구문 강조(syntax highlighting)

마크다운 문서를 편집할 때 가장 먼저 필요한 것은 바로 구문 강조 기능이다. 개발자라면 당연히 알고 있겠지만, 구문 강조는 마크다운 문서를 분석하여 의미 있는 문법이 적용된 텍스트를 시각적으로 쉽게 구분할 수 있도록 스타일을 적용하는 것이다. 구문 강조를 지원하는 에디터에서는 실제 렌더링 된 화면을 보지 않아도 현재 작성하고 있는 문서에 마크다운 문법이 제대로 적용되었는지 확신할 수 있다. 반면 깃허브의 이슈 등록 폼과 같이 구문 강조를 지원하지 않는 환경에서는 글을 작성하면서 미리 보기를 자주 확인해야만 할 것이다.

하지만 마크다운의 구문 강조를 완벽하게 지원하는 에디터는 거의 없다. 에디터마다 구문 강조를 위해 사용하는 모듈의 사용법이 달라서 기존에 존재하는 마크다운 파서를 사용할 수 없기 때문이다. 즉, 에디터마다 각자 다른 방법으로 구문 분석을 해야 하므로 실제 렌더링을 위해 사용하는 파서와 완벽하게 동일한 결과를 만들어 내기가 쉽지 않은 것이다.

예를 들어 TOAST UI Editor는 HTML 변환을 위해 markdown-it을 사용하지만, 구문 강조를 위해서는 CodeMirrorGFM 모드를 사용하고 있다. CodeMirror의 모드 시스템은 구문 분석을 위한 언어별 토크나이저(toeknizer)를 구현해서 사용하는데, 완벽한 파서가 아니기 때문에 markdown-it이 분석한 결과만큼 정확하지 않다. 결국 아래의 그림에서 볼 수 있듯이 구문 강조와 미리 보기가 다르게 표시되는 경우를 자주 볼 수 있다.

Syntax Highlighting in TOAST UI Editor

VSCode마크다운 플러그인도 마찬가지로 HTML 변환을 위해 markdown-it을 사용하고 있는데, 구문 분석을 위해서는 다른 에디터에서 많이 사용되는 TextMate 문법 형식을 사용한다. 아래 그림을 보면 CodeMirror와는 또 다른 문제점이 발생하는 것을 볼 수 있다.

Syntax Highlighting in VSCode

구문 강조의 또다른 문제는 성능이다. 에디터에서 텍스트를 변경할 때마다 매번 전체 문서를 다시 분석해서 구문 강조를 갱신해야 하므로 문서의 크기가 클수록 편집 속도가 느려지게 된다. CodeMirror의 모드 시스템은 이런 현상을 방지하기 위해 내부적으로 최적화를 하고 있어서 큰 문제가 없다. 하지만 VSCode와 같이 TextMate 문법 형식을 사용하는 에디터에서 큰 용량의 마크다운 문서를 편집하면 구문 강조로 인해 속도가 현저하게 느려지는 현상을 볼 수 있을 것이다.

실시간 미리 보기(Preview)

구문 강조만으로도 많은 도움이 되긴 하지만, 마크다운 문서가 변환된 결과를 완벽하게 예측할 수 있는 건 아니다. 특히 이미지나 테이블 같은 요소들은 눈으로 보지 않고서는 화면에 렌더링 될 결과를 정확하게 예측하기가 힘들다. 미리 보기는 글을 저장하기 전에 실제 렌더링 된 화면을 미리 확인할 수 있어 불필요한 시행착오를 줄여준다. 특히 TOAST UI Editor는 화면을 반으로 분할해서 에디터와 미리 보기 화면을 동시에 보여주기 때문에 편집과 동시에 렌더링될 결과를 확인할 수 있다. GitHub에서 이슈를 등록할 때 미리 보기 탭과 편집 탭을 전환하면서 긴 내용의 마크다운 문서를 작성해 본 사람이라면 이 기능이 얼마나 유용한지 잘 알 것이다.

하지만 실시간 미리 보기도 구문 강조와 마찬가지로 성능 문제에서 벗어날 수 없다. 에디터의 텍스트가 수정될 때마다 매번 전체 문서를 변환해서 HTML을 생성해야 하므로 문서의 크기가 커질수록 편집 속도를 느리게 만든다. 또한 이미지와 같은 외부 리소스를 매번 다시 렌더링하기 때문에 깜빡이는 현상이 생길 수 있다. 특히 플러그인 등을 통해 문법을 확장해서 스크립트를 실행하는 경우에는 해당 스크립트가 매번 실행되게 만들어 문서 편집을 방해할 수 있다.

TOAST UI Editor에서는 이를 해결하기 위해 이미 디바운스(debounce) 기법을 사용해서 일정 시간 이상 변경이 없을 때만 렌더링을 갱신하고 있는데, 이는 미리 보기의 반응 속도를 늦추어 사용성을 떨어뜨린다. 또한 변경과 관계 없는 부분까지 매번 렌더링하거나 스크립트가 재실행되는 문제는 여전히 해결하지 못한다. 아래 이미지는 TOAST UI Editor에서 미리 보기가 지연되어 갱신되는 현상과 차트 플러그인의 애니메이션이 매번 다시 실행되는 현상을 보여준다.

Preview Rerendering Issue

또 다른 해결책으로는 기존 DOM 트리와 새롭게 렌더링 된 DOM 트리를 비교하여 변경된 부분만 갱신하도록 방법이 있지만, 이 방법은 DOM 트리 비교를 위한 추가 연산이 필요하므로 또 다른 성능 문제를 만들어낼 수도 있다.

도구 모음(Toolbar)을 통한 문서 구조 변경

마크다운은 문서를 작성하기 위한 마크업 언어이기 때문에 프로그래밍 언어와는 다르게 텍스트의 일정 영역을 선택해서 스타일을 변경하는 등의 작업을 많이 하게 된다. 그래서 TOAST UI Editor와 같은 몇몇 마크다운 에디터에서는 볼드, 이탤릭, 목록 등의 스타일을 변환하거나 링크, 이미지, 테이블 등의 요소를 입력할 때 버튼 형식으로 쉽게 사용할 수 있도록 도구 모음을 제공한다. 하지만 마크다운 에디터에서 도구 모음을 구현하는 일은 다음의 두 가지 기능을 만족해야 해서 상당히 까다롭다.

먼저, 에디터의 커서 위치와 선택 영역의 범위에 따라 버튼의 상태를 동기화하는 기능이 필요하다. 예를 들면, 커서가 목록 안에 있을 때는 목록 버튼이 활성화되고, 볼드 처리된 텍스트를 선택하면 "볼드" 버튼이 활성화되는 식인데, 구글 독스나 MS의 워드 등을 사용해본 적이 있다면 아마 익숙한 기능일 것이다.

하지만 특정 위치의 텍스트에 실제 어떤 문법이 적용되었는지를 알아내는 것은 쉬운 일이 아니다. 이를 위해서는 결국 별도의 구문 분석 작업이 필요한데, 마크다운 문법의 특성상 단순히 커서가 위치한 줄만 분석해서는 안 되고, 문맥을 알아내기 위해 주변 줄까지 모두 분석해야 하기 때문이다. CodeMirror의 모드 시스템을 사용하면 구문 강조를 위해 분석했던 정보를 얻을 수 있긴 하지만, 앞서 언급했듯이 구문 강조를 위해 사용되는 분석기는 완벽하지 않기 때문에 얻을 수 있는 정보 또한 제한적이다.

다음은 버튼을 클릭했을 때 커서의 위치에 원하는 요소를 입력하거나 선택된 영역에 적용된 스타일을 변경하는 기능이다. 사실 커서 위치에 이미지나 링크 등의 문법을 입력하는 것은 별로 어려운 일이 아니다. 하지만 여러 줄에 걸친 텍스트 영역의 스타일을 변경하는 것은 상당히 까다로운데, 이때도 버튼 상태를 동기화할 때와 마찬가지로 구문 분석이 필요하기 때문이다.

예를 들어 목록 내의 특정 항목에 커서가 위치했을 때 "순서 있는 목록(Ordered List)" 버튼을 누르면, 이 목록 전체를 순서 있는 목록으로 변경하기 위해 커서 주변의 모든 줄을 순회하면서 같은 목록 내에 있는 모든 항목인지를 확인하고, 시작 기호(- 혹은 *)를 찾아서 숫자와 점으로 변경해야만 한다. TOAST UI Editor에는 이런 목록 처리를 위한 모듈이 별도로 존재하는데, 많은 양의 코드를 포함하고 있음에도 불구하고 아래 그림처럼 목록의 항목이 여러 줄에 걸쳐 있는 경우에는 제대로 처리하지 못하고 있다.

Toolbar Button Inconistency

통합 개발 환경(IDE)에서 배울 수 있는 것들

여기까지 읽었다면, 기존 마크다운 에디터의 문제점이 무엇인지 어느 정도 감이 올 것이다. 바로 마크다운 파서의 파편화이다. 즉, 구문 강조, HTML 변환, 도구 모음 등 모든 기능에서 구문 분석이 필요한데도 불구하고 각 모듈이 별도의 구문 분석을 하는 것이다. 당연한 이야기지만, 하나의 파서가 이 모든 것을 처리해 준다면 훨씬 더 정확하고 통일된 사용성을 보장할 수 있을뿐더러 기능 구현 로직도 단순해질 것이다. 그렇다면 왜 이런 낭비를 하고 있는 걸까?

그 이유는 대부분의 마크다운 파서가 주로 마크다운 문법을 HTML로 변환하기 위한 목적으로만 만들어졌기 때문이다. 구문을 강조하거나 문서 구조를 변경하기 위해서는 소스 코드와 분석된 구문 트리 사이의 맵핑 정보가 필요한데, markdown-it과 같이 현재 널리 사용되는 마크다운 파서들은 이런 기능을 제공하지 않는다. 또한, 전체 마크다운 문서를 한 번에 변환하는 용도로 만들어졌기 때문에, 에디터처럼 실시간으로 내용을 변경해야 하는 상황에서는 만족스러운 성능을 보장해주지 못한다.

그렇다면 이 문제를 해결할 방법은 없을까? 답은 가까운 곳에서 찾을 수 있다. 바로 개발자들이 사용하는 통합 개발 환경(IDE)이다.

사실 마크다운 에디터의 문제는 개발자들이 사용하는 텍스트 에디터 혹은 통합 개발 환경(IDE)에서 똑같이 겪는 문제이다. 예를 들어 VSCode, SublimeText, Eclipse 등의 많은 에디터는 구문 분석을 위해 정규식 기반의 TextMate 문법 형식을 사용하는데, 앞서 설명했듯이 이는 구문의 의미를 완벽하게 분석하지 못하기 때문에 키워드의 색을 구분하는 수준에 머물러 있다. 게다가 자동 완성, 리팩토링, 정의 바로 가기(Go To Definition) 등을 처리하기 위해서는 또 다른 분석기가 필요한데, 이 또한 각 에디터가 지원하는 언어와 API에 맞게 별도로 구현해야 하므로 에디터에 따라 언어별 지원 수준이 각기 다른 상황이다.

개발 환경에 관심이 많은 사람은 이미 알겠지만, 최근 이런 문제의 해결책으로 주목받는 기술 두 가지가 있다. 바로 트리시터(Tree-Sitter)와 언어 서버 프로토콜(Language Server Protocol: 이하 LSP)이다.

트리시터(Tree-Sitter)

트리시터는 정규식 기반 구문 분석의 한계를 벗어나 실제 전체 문서의 의미를 분석해서 구문 트리(Syntax Tree)를 만들어내는 시스템으로, Atom 에디터 내부 모듈로 시작해서 지금은 많은 에디터 환경에서 사용되고 있다. 트리시터 기반의 파서는 실시간 편집이 가능한 에디터 환경에서 사용하려는 목적으로 만들어졌기 때문에 일반적인 파서와는 다른 두 가지 특징을 갖는다. 하나는 소스 코드와의 1:1 맵핑 정보를 가진 구상 구문 트리(Concrete Syntax Tree)를 만들어 낸다는 점이고, 또 하나는 점진적 분석(Incremental Parsing)을 지원한다는 점이다.

구상 구문 트리는 추상 구문 트리(Abstract Syntax Tree)와는 다르게 소스 코드에서 생략되는 부분이 없이 모든 구문의 정보를 갖고 있다. 트리시터의 구문 트리는 여기에 더해서 구문마다 매칭되는 소스 코드의 시작 위치와 끝 위치를 저장하고 있기 때문에, 소스 코드의 특정 영역이 문법적으로 어떤 의미가 있는지를 정확하게 알 수 있다. Atom 에디터 블로그의 트리시터 소개 글에서 이 정보를 어떻게 활용하고 있는지를 잘 보여주는데, 구문 강조 기능뿐만 아니라 코드 폴딩(folding), 문법에 따른 자동 영역 선택(Syntax-aware selection) 등의 기능도 완벽하게 개선된 것을 확인할 수 있을 것이다.

점진적 분석은 전체 문서를 매번 분석하는 대신 변경된 부분만 분석해서 구문 트리를 갱신하는 기능이다. 즉 기존에 만들었던 구문 트리를 저장하고 있다가, 변경된 텍스트 정보를 받아서 필요한 부분만 다시 분석한 후 기존 트리를 일부만 업데이트하는 것이다. 이 기능은 에디터와 같이 계속해서 변경되는 문서를 실시간으로 분석해야 하는 용도에 최적화된 가장 큰 장점이라 볼 수 있다. 이 장점 덕분에 트리시터는 용량이 큰 소스 코드를 편집할 때도 성능 저하 없이 구문 트리 정보를 갱신할 수 있다.

(CodeMirror도 최근 메이저 업데이트인 CodeMirror 6을 준비하면서 동일한 고민을 했고, 트리시터에서 영감을 받은 새로운 파서인 Lezer를 만들었다. 개발자의 블로그 글에 고민의 과정과 결과가 상세하게 정리되어 있으니 관심 있는 분들은 꼭 읽어보길 권한다.)

언어 서버 프로토콜 (LSP)

LSP는 IDE 환경에서 자동 완성, 리팩토링 등의 기능을 지원하는 데 필요한 언어 서버의 스펙을 정의한 규약이다. LSP는 기존 IDE의 접근 방식과 다르게 클라이언트(에디터)에 독립적으로 사용 가능한 언어 서버를 만들어서 프로세스 간(inter-process) 통신을 하는 방식을 사용한다. 즉, 규약에 맞는 언어 서버를 구현하기만 하면 어떤 에디터에서든 동일한 IDE 기능을 사용할 수 있는 것이다. 마이크로소프트에서 처음 만들어 비주얼 스튜디오 코드 등의 에디터에서 사용하기 시작한 LSP는 현재 IntelliJ, Emacs, Vim 등 다양한 에디터에서 사용되고 있다.

사실 웹에서 사용될 마크다운 에디터를 만들 때 클라이언트에 독립적인 구조나 프로세스 간 통신 등의 개념이 중요한 것은 아니다. 그보다 중요한 것은 언어 서버라는 개념인데, 아마 가장 친숙한 예로 타입스크립트의 ts-server를 들 수 있을 것이다.

타입스크립트는 크게 tsc와 ts-server로 구성된다. tsc는 한 번에 전체 파일을 컴파일하는 용도로 주로 웹팩 등의 도구와 연결해서 번들링을 할 때 사용한다. 반면 ts-server는 에디터 내부에서 실행되어 텍스트가 변경될 때마다 실시간으로 구문 분석 결과를 갱신하며, 필요에 따라 자동 완성, 타입 에러 등의 결과를 반환한다. VSCode, IntelliJ 등의 에디터에서 타입스크립트 개발 도구에 감탄해 본 사람들은 모두 ts-server의 도움을 받고 있는 것이다.

(참고로 ts-server는 아직 LSP를 지원하지 않으며, 대신 LSP용 랩퍼 라이브러리를 사용할 수 있다)

언어 서버에서 또 하나 주목할 것은 리팩토링 기능이다. 타입스크립트를 개발할 때 "함수 이름 변경" 등의 리팩토링 기능을 사용한 적이 있을 것이다. 이는 언어 서버가 단순히 소스 코드를 분석해서 정보를 반환하는 역할을 넘어서서 직접 소스 코드를 수정하는 역할까지 수행한다는 의미이다. 즉, 내부에서 구문 분석 트리의 구조를 변경하고 변경된 구조에 맞게 소스 코드를 갱신한 후 변경된 내용을 반환하는 것이다. LSP에는 이를 위해서 CodeAction 요청이 정의되어 있는데, 이 기능은 앞서 "도구 모음을 사용한 문서 구조 변경"에서 살펴본 문제를 해결할 때 좋은 참고가 될 수 있다.

사실 언어 서버는 트리시터가 하는 대부분의 일을 수행할 수 있으며, 실제로 LSP에는 구문 강조를 위한 API도 정의되어 있다. 하지만 언어 서버는 트리시터보다 훨씬 복잡한 기능을 수행하며 점진적 분석을 지원하기도 어렵기 때문에, 실시간으로 구문 강조나 코드 폴딩 등의 작업을 수행하기에는 비교적 무겁고 느리다. 즉, 트리시터와 LSP는 구현 방식과 용도가 다르므로 상호 보완적인 역할을 한다고 볼 수 있다.

새로운 마크다운 파서: ToastMark

앞서 살펴본 내용을 모두 종합한 결과 우리는 트리시터와 언어 서버의 장점을 모두 갖춘 새로운 마크다운 파서가 기존 에디터의 문제를 해결해 줄 수 있다고 결론지었다. 하지만 새로운 마크다운 파서를 만드는 일은 어마어마한 노력이 드는 작업이기에 이미 잘 만들어진 오픈소스를 개선하기로 했고 며칠간의 분석과 논의를 거쳐 commonmark.js가 최종 선택되었다.

commonmark.js는 CommonMark 명세의 레퍼런스 구현체로서, CommonMark 명세를 완벽하게 준수하는 유일한 자바스크립트 라이브러리이다. 레퍼런스 구현체이기 때문에 코드를 분석하고 확장하기가 쉬우며, 이후에 CommonMark 명세가 변경될 때에도 변경 사항을 비교해서 적용하기가 가장 용이하다고 판단했다.

한 달여 간의 개선 작업 끝에 마침내 새로운 마크다운 파서인 ToastMark가 탄생했다. ToastMark는 다음과 같은 세 가지의 특징을 갖는다.

1. 소스 맵핑 정보를 갖는 추상 구문 트리(AST) 생성

ToastMark는 마크다운 문서의 추상 구문 트리(Abstract Syntax Tree)를 만들고, 각 노드에 매칭되는 소스 코드 시작 위치와 끝 위치를 저장한다. 저장된 소스 코드의 위치 정보는 구문 강조, 도구 모음 버튼 동기화, 실시간 미리 보기의 스크롤 동기화 등 다양한 용도로 사용된다. 트리시터와 다르게 추상 구문 트리를 선택한 이유는 이 파서가 구문 강조 외에도 다양한 용도로 사용되기 때문이다. 구문 강조를 위해서는 구상 구문 트리가 더 유리하지만, 문서의 특정 요소를 탐색하거나 문서 구조를 변경할 때에는 추상 구문 트리가 훨씬 유리하다. 또한 마크다운은 각 요소를 구성하는 토큰의 형식이 단순해서 추상 구문 트리에 몇 가지 정보를 추가하는 것만으로도 구문 강조를 문제없이 구현할 수 있다.

commonmark.js는 이미 추상 구문 트리 정보를 반환하는 API가 있고 블록 요소에 대한 소스 코드 위치 정보도 이미 갖고 있었기 때문에, 인라인 요소에 대한 소스 코드 위치 정보를 추가하는 것만으로 어렵지 않게 구현할 수 있었다.

2. 문서 변경에 따른 점진적 분석

ToastMark는 트리시터와 같은 점진적 분석을 지원한다. 즉, 기존에 분석한 구문 트리를 저장하고 있다가 변경된 내용에 따라 구문 트리를 일부만 변경하는 것이다. 에디터에서 변경된 사항이 생기면 ToastMark에 변경 소스 코드의 위치와 텍스트 정보를 전달하고, ToastMark는 구문 트리를 일부 갱신한 다음 변경된 노드의 정보를 반환한다. 사용법을 간단하게 살펴보면 다음과 같다.

const toastMark = new ToastMark('# Hello World');

// 첫 문자인 #를 -로 변경
const result = toastMark.editMarkdown([1, 1], [1, 2], '-');
const { removedNodeRange, newNodes } = result;

// 마크다운 에디터 구문 강조 갱신
refreshSyntaxHighlighting(newNodes);

// 미리보기 DOM 갱신
refreshPreview(removedNodeRange, newNodes);

변경된 노드의 정보는 구문 강조의 스타일과 실시간 미리 보기의 DOM 노드를 갱신하는 데 사용된다. 삭제된 노드의 아이디와 추가된 노드의 배열을 반환해 주기 때문에 구문 강조와 실시간 미리 보기에서도 정확히 변경된 부분만 갱신할 수 있다. 이 덕분에 큰 용량의 마크다운 문서를 편집할 때에도 성능 저하 없이 구문 강조와 미리 보기를 실시간으로 갱신하여 사용성을 크게 개선할 수 있다.

3. 구문 트리 탐색 및 변경

도구 모음 버튼의 상태를 동기화하기 위해서는 커서의 위치가 변경될 때마다 해당 위치의 소스 코드가 어떤 마크다운 요소와 매치되는지를 알아야 한다. 이를 위해서는 문서가 변경되지 않아도 위치에 따른 노드 정보를 알아낼 수 있어야 한다. 또한 미리 보기에 있는 DOM과 매치되는 노드의 정보를 가져오기 위해서는 각 노드에 아이디를 부여하고 해당 아이디를 통해서 노드를 가져올 수도 있어야 한다. 이러한 용도를 위해 ToastMark는 다음과 같은 몇 가지 메소드를 제공한다.

// 3번째 줄의 1번째 노드 반환
toastMark.findFirstNodeAtLine(3); 

// 2번째 줄의 5번째 글자에 매치되는 노드 반환
toastMark.findNodeAtPosition([2, 5]);

// 아이디가 10인 노드 반환
toastMark.findNodeById(10);

도구 모음의 버튼을 클릭했을 때는 문서의 구조를 변경해야 한다. 예를 들면 문단을 목록으로 변경하거나, 순서 없는 목록을 순서 있는 목록으로 변경하는 등의 작업이다. 이는 구문 트리를 변경한 후 및 실제 마크다운 소스 코드를 생성해서 반환하는 기능까지 포함한다. 현재는 초기 버전이라 아직 구현되어 있지 않지만, 이어지는 업데이트에서 이 기능을 위한 API도 추가될 예정이다.

TOAST UI Editor 2.0에서 개선된 점

TOAST UI Editor 2.0은 마크다운 파서를 안정적으로 변경하고, 모듈 및 번들 구조를 변경하는 것에 초점을 맞추었기 때문에 특별하게 추가된 기능은 없다. 하지만 파서를 ToastMark로 변경한 것만으로도 기존 마크다운 에디터 기능들의 정확도와 안정성이 대폭 향상되었다.

구문 강조 정확도 개선

기존에 사용하던 CodeMirror의 마크다운 모드를 제거하고, CodeMirror API를 사용해 특정 구문에 직접 스타일을 추가하는 방식으로 변경되었다. 이제 미리 보기와 구문 강조 모두 ToastMark의 구문 트리에 의존하고 있으므로, 아래와 그림과 같이 두 결과가 완벽하게 동일한 것을 확인할 수 있을 것이다.

Improved Syntax Highlighting in TOAST UI Editor

실시간 미리 보기 성능 및 스크롤 위치 동기화 개선

앞서 언급했듯이 기존의 실시간 미리 보기는 전체 문서를 파싱하고 전체 DOM을 갱신하는 데 드는 비용 때문에 입력이 일정 시간 이상 멈추었을 때만 갱신되었고, 그때마다 차트 등의 확장 플러그인이 매번 새로 실행되는 문제도 있었다. 하지만 2.0 버전부터는 ToastMark의 점진적 분석 기능을 사용해서 변경된 내용만 분석하고 갱신하기 때문에, 불필요한 지연이나 플러그인 재실행이 없이 미리보기를 글자 그대로 "실시간으로" 확인할 수 있다.

Improved Live Preview in TOAST UI Editor

그뿐만 아니라 마크다운 에디터와 실시간 미리 보기의 스크롤 위치를 동기화하는 기능도 ToastMark의 정확한 맵핑 정보를 사용해서 더 정밀하게 동작하도록 개선되었다.

커서 위치에 따른 버튼 상태 동기화 개선

도구 모음 버튼의 상태 동기화 문제는 예전부터 꾸준히 개선이 요구되는 이슈이다. 기존에도 볼드, 이탤릭, 취소선 등의 몇 가지 요소에 대해서는 동작하고 있었지만, 목록이나 테이블 등의 요소에 대해서는 제대로 동작하지 않았다. 비록 CodeMirror의 마크다운 모드에서 토크나이저가 분석한 정보를 사용할 수는 있었지만, 그 정보 자체가 완벽하지 않았기 때문에, 제대로 된 동기화를 위해서는 추가적인 분석이 필요했기 때문이다.

하지만 ToastMark에서는 커서 위치에 따른 정확한 마크다운 노드 정보를 받을 수 있어, 약간의 코드만으로 기능이 대폭 개선되었다.

Improved Toolbar Button Status

2.X에서 개선될 점

이외에도 ToastMark를 사용해서 개선할 수 있는 기능은 많다. 사실 기존에도 구현할 수 없는 건 아니었지만 별도의 구문 분석 작업이 필요하다는 이유로 보류된 기능들이다. 이제 정확한 구문 분석 정보를 마음껏 사용할 수 있기 때문에 다양한 기능들을 어렵지 않게 구현할 수 있게 되었다. 그래서 2.0 이후의 마이너 업데이트에서는 ToastMark로 할 수 있는 다양한 시도를 하면서 마크다운 에디터와 미리 보기의 사용성을 개선하는 데에 집중할 예정이다.

가장 먼저 할 일은 도구 모음 버튼의 사용성을 더 개선하는 일이다. 현재는 커서 위치와 버튼의 상태를 동기화하는 수준의 개선에서 그쳤지만, 더 나은 사용성을 위해서는 선택 영역에 따라서 버튼의 상태를 동기화하거나, 실행 불가능한 버튼의 상태를 비활성화하는 등의 개선이 더 필요하다. 또한 실제 버튼을 클릭했을 때도 커서뿐만 아니라 선택 영역의 상태를 고려해서 동작하도록 개선할 수 있으며, 기존에 추가로 구문을 분석하던 로직을 제거하고 코드를 단순화할 수도 있다.

이 외에도 섹션별 코드 접기(folding), 코드 포매팅, 테이블 열/행 추가, 미리 보기의 변경된 영역 강조 등 다양한 기능을 추가할 수 있다. 이런 기능들은 아직 가능성으로만 남아있지만, 추후 사용성이나 우선순위를 고려해서 꾸준히 추가할 예정이다. 만약 제안할 만한 더 좋은 기능이 있다면 이슈로 남겨주기 바란다.

ToastMark의 미래

ToastMark는 이제 막 첫발을 내디뎠기 때문에 아직 개선될 여지가 많이 남아 있다. 또한 TOAST UI Editor 내부에서만 사용되는 라이브러리이기 때문에 별도의 npm 패키지로 등록하지 않았으며, API도 에디터의 요구 사항에 따라 계속해서 변경될 예정이다. 즉, 아직 외부에서 사용할 용도로는 적합하지 않다는 의미이다.

하지만 TOAST UI Editor의 발전과 함께 ToastMark도 계속해서 발전할 것이며, 충분한 안정성을 확보한 후에 외부에 정식으로 배포, 공개할 계획이다. 깃허브 저장소에 ToastMark의 특징과 몇 가지 API를 정리해 두었으니 관심 있는 개발자들은 꼭 살펴보길 바라며, 좋은 의견이 있다면 꼭 깃허브 이슈로 남겨주길 바란다.

TOAST UI Editor의 미래

지금까지 새로운 마크다운 파서인 ToastMark를 만들게 된 이유와 이를 사용해서 개선된 점들을 모두 살펴보았다. ToastMark는 마크다운 에디터 사용성을 개선하는 데 큰 도움을 주었으며, 앞으로도 다양한 개선을 이끌 잠재력을 갖고 있다. 하지만 ToastMark의 역할은 여기서 끝이 아니다. 사실 ToastMark는 마크다운 에디터와 위지윅 에디터를 진정으로 통합할 수 있는 길을 열어줄 중요한 출발점이다.

현재 마크다운 에디터의 구조에서는 CodeMirror와 ToastMark가 각각 텍스트 정보를 관리하며 변경 정보를 서로 동기화하며 상호작용하고 있다. 여기서 한발 더 나아가면 ToastMark에서만 텍스트 정보를 관리하고, 에디터에서는 단순히 커서의 움직임과 이벤트 처리만 담당할 수도 있다. 마치 리덕스와 리액트의 관계처럼 ToastMark가 상태 관리자가 되고, 에디터는 단순히 뷰의 역할을 하게 되는 것이다. 그러면 CodeMirror의 의존성이 없이 더 가볍고 통일된 구조의 마크다운 에디터를 만들 수 있을 것이다.

여기서 또 한발 더 나아가면, 위지윅 에디터에도 동일한 구조를 적용할 수 있다. 즉, ToastMark가 하나의 상태 관리자가 되고, 마크다운 에디터와 위지윅 에디터 모두 뷰의 역할만 담당하게 되는 것이다. 이게 가능하다면 현재 위지윅 에디터 구현을 위해 사용하는 Squire의 의존성도 제거할 수 있고, 결과적으로 더 가볍고 통일성 있는 위지윅 에디터를 만들 수 있다.

예를 들어, 기존 구조에서는 위지윅 에디터가 HTML(DOM)을 편집하고, 마크다운 에디터는 마크다운 텍스트를 편집하기 때문에 에디터 간 전환이 일어날 때마다 전체 데이터를 마크다운→HTML, HTML→마크다운으로 변환하는 작업이 필요했고, 이 과정에서 기존 데이터가 의도치 않게 변경되는 문제가 자주 발생했다. 하지만 새로운 구조에서는 두 에디터가 ToastMark가 관리하는 AST 하나에 의존하기 때문에, 불필요한 데이터 변환 작업으로 인한 문제가 모두 사라지고 에디터 간 전환도 더 매끄러워진다.

물론 현재 단계에서는 고려해야 할 기술적인 난관들이 많이 남아있기 때문에, 실제로 적용하기 전까지 많은 연구와 프로토타이핑이 필요하다. 더 자세한 이야기는 이후 다른 글에서 상세하게 다루도록 하겠다. 지금은 먼 미래보다는 새롭게 릴리즈된 2.0 버전에 많은 관심을 두길 바라며, 업데이트를 통해 꾸준히 개선되는 마크다운 에디터의 사용성을 지켜봐 주기 바란다.

참고 링크

TextMate 문법

Language Server Protocol

트리시터(Tree-Sitter)