Monaco Editor에 Shiki 하이라이팅 적용하기

현우
웹에서 코드 편집기를 만들 때 가장 많이 사용하는 선택지 중 하나는 Monaco Editor 입니다. VSCode의 편집기 엔진을 기반으로 하여 자동 완성, 진단, 다중 커서 같은 편집 경험들을 쉽게 구현할 수 있습니다.
다만 기본 문법 강조의 경우, VSCode에서 보이는 결과와 완전히 같지 않을 수 있습니다. Monaco Editor의 기본 토크나이저가 모든 TextMate 문법을 그대로 처리하지 않기 때문입니다.
이때 Shiki를 연결하면 VS Code 계열의 TextMate 문법과 테마를 사용하여 더 정확한 색상 표현을 할 수 있습니다. Shiki는 Monaco 용도의 공식 어댑터 @shikijs/monaco 를 제공합니다. 해당 어댑터를 통해 직접 토큰화 결과를 Monaco에 주입하는 방식보다 이 패키지를 사용하여 적용하는 편이 훨씬 더 간단합니다.

패키지 설치하기

Monaco , shiki, @shikijs/monaco 패키지를 설치합니다.
npm install monaco-editor shiki @shikijs/monaco

적용해보기

1.
createHighlighter 로 필요한 언어와 테마를 로드합니다.
2.
사용할 언어의 ID를 monaco.languages.register() 을 통해 등록합니다.
3.
shikiToMonaco() 를 통해 Shiki 문법과 테마를 Monaco에 연결합니다.
이때 지원되는 themes (에디터 테마)와 langs (언어 설정) shiki 공식 홈페이지에서 확인이 가능합니다.
import * as monaco from 'monaco-editor'
import { createHighlighter } from 'shiki'
import { shikiToMonaco } from '@shikijs/monaco'

async function createEditor() {
  const highlighter = await createHighlighter({
    themes: ['github-dark', 'github-light'],
    langs: ['typescript', 'javascript', 'json'],
  })

  // Shiki로 강조할 언어는 Monaco에도 등록해야 한다.
  for (const id of ['typescript', 'javascript', 'json']) {
    monaco.languages.register({ id })
  }

  // Shiki 테마와 문법 강조 규칙을 Monaco에 등록한다.
  shikiToMonaco(highlighter, monaco)

  monaco.editor.create(document.getElementById('editor')!, {
    value: `const greeting: string = 'Hello, Shiki!'`,
    language: 'typescript',
    theme: 'github-dark',
    automaticLayout: true,
  })
}

void createEditor()

다크모드와 같이 테마를 동적으로 변경하고 싶다면

Shiki에 여러 테마를 미리 등록했다면 Monaco 테마로 바로 전환이 가능합니다.
function applyTheme(isDark: boolean) {
monaco.editor.setTheme(isDark ? 'github-dark' : 'github-light');
}
중요한 점은 monaco.editor.defineTheme() 를 별도로 호출할 필요가 없으며, shikiToMonaco() 가 Shiki에서 로드한 테마를 Moaco 테마로 자동 등록해줍니다.

커스텀 언어셋을 나만의 언어로 하이라이팅 하고싶다면

만약 사내에서 커스텀 언어 (ex. 보안 위협 탐지룰 등) 으로 기본 언어 목록에 없는 문법으로 강조를 해야한다면, TextMate Grammer 파일, 즉 .tmLanguage.json 을 추가하여 로드해야합니다.
import myDslGrammar from './my-dsl.tmLanguage.json'
import * as monaco from 'monaco-editor'
import { createHighlighter } from 'shiki'
import { shikiToMonaco } from '@shikijs/monaco'

const highlighter = await createHighlighter({
  themes: ['github-dark'],
  langs: [myDslGrammar],
})

monaco.languages.register({ id: 'my-dsl' })
shikiToMonaco(highlighter, monaco)
그리고 이후 모델 또는 에디터의 언어를 my-dsl 로 지정해주면 설정이 완료됩니다.
editor.setModelLanguage(editor.getModel()!, 'my-dsl')
tmLanguage.json 은 색상 값을 담는 파일이 아니라, 어떤 문자열을 키워드, 문자열, 주석, 함수 명으로 볼지 정의하는 정규식 기반의 문법 파일입니다.
반면 실제 색상은 Shiki 하이라이팅의 테마가 결정합니다.
{
  "name": 'my-dsl',
  "scopeName": "source.my-dsl",
  "patterns": [
    {
      "name": "keyword.control.my-dsl",
      "match": "\\b(task|when|end)\\b"
    },
    {
      "name": "string.quoted.double.my-dsl",
      "begin": "\"",
      "end": "\""
    }
  ]
}
문법을 처음부터 만드는 일은 생각보다 유지보수 비용이 많이 듭니다. 기존 VSCode 확장에 같은 언어의 TextMate Grammer가 있다면 그것을 재사용하는 것이 기회 비용을 줄일 수 있는 방법 중 하나이니 참고하면 좋을 것 같습니다.
悠悠自適
유유자적한 블로그 주인장 김현우입니다.
오늘도 안녕하신가요?
Subscribe
👍