개발/ETC

Angular에서 Monaco Editor를 사용할 때 커스텀 언어 워커 사용하기

bitofsky 2022. 6. 25. 14:32

Angular (여기선 13.0)를 사용할 때 Monaco Editor를 쓰는 방법이 몇가지 있는데 편의를 위해 ngx-monaco-editor 와 같은 모나코 래핑 컴포넌트를 사용하면 편한데, 여기에 더해 커스텀 언어용 워커를 사용하고자 한다면 의외로 꽤 고생할만한 포인트가 있어 정리해둡니다.

저의 경우 monaco yaml 에디터에 JSONSchema를 사용할 수 있게 해주는 monaco-yaml 워커를 활성화 시켰으므로 이 기준으로 설명합니다. 다른 언어 워커를 사용하는 경우 monaco-yaml 부분만 교체하면 대동소이 합니다.

monaco를 amd 번들링으로 로드하는 ngx-monaco-editor를 그냥 쓰거나 별도로 vs/min 경로나 CDN등에서 monaco-editor를 가져오는 경우 ESM monaco-editor를 사용하는 언어 워커 프로젝트와 monaco 객체에 차이가 생기면서 정상동작 하지 않습니다.

이때문에 monaco-editor와 worker를 모두 ESM으로 import 해 사용해야 합니다.

일반적으로는 모나코팀에서 만든 webpack용 monaco-editor-webpack-plugin을 사용하는데 Angular에서는 이것을 쓰기 힘듭니다. Angular 빌드시 사용되는 Webpack 기본 설정들이 위 플러그인의 출력물과 특히 WebWorker에서 동작해야 하는 JS 번들링 파일을 손상시켜 Worker가 정상 동작 하지 않습니다.

때문에 여기서는 webpack plugin을 사용하지 않고 별도의 Webpack Config 파일을 만들어 WebWorker용 Assets를 번들링 하는 과정을 추가했습니다. [참고]

요구 작업자 수준

  • Angular  / Angular Build / Webpack에 대한 기본이상의 지식
  • Monaco Editor에 대한 기본 지식

준비물

  1. Angular
  2. package.json dependencies에 의존성 모듈 추가
    1. monaco-editor
    2. monaco-yaml
    3. ngx-monaco-editor
    4. webpack & webpack-cli
"monaco-editor": "^0.33.0",
"monaco-yaml": "^4.0.0",
"ngx-monaco-editor": "^12.0.0",
"webpack": "^5.73.0",
"webpack-cli": "^4.10.0"

 

monaco-worker-webpack.config.js 파일 생성

  • entry
    • key: ESM을 번들링해 단일 워커파일로 만드는데 그 파일명을 입력합니다.
    • value: 사용할 워커들 경로를 node_modules/[의존성경로]/[워커파일명].js 로 합니다.
const path = require('path');

module.exports = {
  mode: 'development',
  entry: {
    'editor.worker': 'monaco-editor/esm/vs/editor/editor.worker.js',
    "json.worker": 'monaco-editor/esm/vs/language/json/json.worker.js',
    "css.worker": 'monaco-editor/esm/vs/language/css/css.worker.js',
    "html.worker": 'monaco-editor/esm/vs/language/html/html.worker.js',
    "ts.worker": 'monaco-editor/esm/vs/language/typescript/ts.worker.js',
    "yaml.worker": 'monaco-yaml/yaml.worker.js',
  },
  output: {
    globalObject: 'self',
    filename: '[name].js',
    path: path.resolve(__dirname, 'dist/assets/monaco-editor/worker'),
  },
  module: {
    rules: [
      {
        test: /\.css$/,
        use: ['style-loader', 'css-loader']
      },
      {
        test: /\.ttf$/,
        use: ['file-loader']
      }
    ]
  }
};

 

package.json 편집

npm run gen-monaco-workers-dev 하면 Worker 번들 파일을 webpack으로 빌드해 dist/assets/monaco-editor/worker 경로에 넣습니다.

"scripts": {
	...
	"serve" : "npm run gen-monaco-workers-dev && ng serve --project [Project]",
	"build" : "npm run gen-monaco-workers && ng build --project [Project]",
	"gen-monaco-workers-dev": "webpack -c ./monaco-worker-webpack.config.js --progress --mode development",
	"gen-monaco-workers": "webpack -c ./monaco-worker-webpack.config.js --progress --mode production"
},

 

angular.json 편집

  • [your app].architect.build.options.assets에 추가
    • input: 위에서 만든 Worker 번들 파일 경로
    • output: assets 경로 (아래에서 사용)
"assets": [
    ...
    {
        "glob": "**/*",
        "input": "dist/assets/monaco-editor",
        "output": "./assets/monaco-editor"
    }
],

 

index.html 편집

원래는 MonacoWebpackPlugin과 함께 css-loader, style-loader를 추가해 monaco-editor의 esm css / font를 빌드시에 번들링 해야합니다만, 현재는 이렇게 뭉치는 경우 Angular 기본 Webpack 설정의 postcss와 충돌이 있는 것 같습니다. 아직 해결법을 잘 모르겠어서 우선 monaco css를 cdn으로부터 받도록 해둡니다.

<link href="https://cdn.jsdelivr.net/npm/monaco-editor@0.33.0/min/vs/editor/editor.main.css" rel="stylesheet" data-name="vs/editor/editor.main" />

 

Monaco Editor Component 작성

현재 ngx-monaco-editor 12.0.0 기준으로 esm 로드 방식의 monaco 초기화에서 정상동작 하지 않습니다.
따라서 부득이 ngx-monaco-editor의 EditorComponent를 상속해 override해서 일부 기능을 수정합니다.

import { Component, ElementRef, forwardRef } from '@angular/core';
import { NG_VALUE_ACCESSOR } from '@angular/forms';
import { EditorComponent } from 'ngx-monaco-editor'
import * as monaco from 'monaco-editor';

// global.monaco 추가
(<any>window).monaco = monaco;

// MonacoEnvironment.getWorkerUrl 추가
(<any>window).MonacoEnvironment = {
  getWorkerUrl(moduleId, label) {
  
    // angular.json에 추가한 assets output 경로로 맞춘다.
  	const OUTPUT = '/assets/monaco-editor/worker/';
  
    switch(label){
        case 'javascript':
        case 'typecript': return OUTPUT + 'ts.worker.js';
        case 'html': return OUTPUT + 'html.worker.js';
        case 'css': return OUTPUT + 'css.worker.js';
        case 'json': return OUTPUT + 'json.worker.js';
        case 'yaml': return OUTPUT + 'yaml.worker.js';
        default: return OUTPUT + 'editor.worker.js';
    }
  }
};




@Component({
  selector: 'monaco-editor',
  template: '<div class="editor-container" #editorContainer></div>',
  styles: [`
      :host {
          display: block;
          height: 200px;
      }
      .editor-container {
          width: 100%;
          height: 98%;
      }
  `],
  providers: [{
    provide: NG_VALUE_ACCESSOR,
    useExisting: forwardRef(() => MonacoEditorComponent),
    multi: true
  }]
})
export class MonacoEditorComponent extends EditorComponent {
  /** ngx-monaco-editor는 monaco esm import 되는 케이스에 대한 옵션이 없어 override해 바로 initMonaco를 호출시키도록 한다. */
  ngAfterViewInit(): void {
    this.initMonaco(this._options);
  }
}

 

Monaco Editor Compoment /  ngx-monaco-editor 모듈 등록

import { MonacoEditorModule } from 'ngx-monaco-editor';
import { MonacoEditorComponent } from './monaco-editor/monaco-editor.component';

@NgModule({
  imports: [
    MonacoEditorModule.forRoot(), // <- ngx-monaco-editor Module import 추가
  ],
  declarations: [
    MonacoEditorComponent, // <- 만든 컴포넌트 추가
  ],
  exports: [
    MonacoEditorComponent, // <- 만든 컴포넌트 추가
  ],
 });

 

Monaco-Yaml Use case / with JSONSchema

import { Component, OnInit, Input } from '@angular/core';
import { setDiagnosticsOptions } from 'monaco-yaml';
import { editor, Uri } from 'monaco-editor';

const modelUri = Uri.parse('a://b/foo.yaml');

setDiagnosticsOptions({
  enableSchemaRequest: false,
  hover: true,
  completion: true,
  validate: true,
  format: true,
  schemas: [
    {
      uri: 'http://myserver/foo-schema.json',
      fileMatch: [String(modelUri)],
      schema: {
        type: 'object',
        properties: {
          p1: {
            enum: ['v1', 'v2'],
          },
        },
      },
    },
  ],
});

@Component({
  selector: 'monaco-editor-test',
  template: `<monaco-editor [options]="editorOptions" [(ngModel)]="code" class="code-editor" />`,
  styles: [`.code-editor { height: 300px; } `],
})
export class MonacoEditorTestComponent implements OnInit {

  editorOptions = {
    theme: 'vs-dark',
    language: 'yaml',
    model: editor.createModel('p1: \n', 'yaml', modelUri)
  };
  
  code: string = 'p1 = ';
  
}

추가한 yaml language worker가 JSONSchema와 함께 잘 동작하는것을 확인할 수 있습니다.