개발 기술/개발 이야기

[vscode] 컬러 변수 뷰어 만들기(1) - webview API 사용법

by GicoMomg 2024. 3. 16.

0. 들어가기 전에…

  • 팀에 컬러 시스템이 정착되기 전, 프로젝트에서 전역 컬러 변수를 바꾸거나 추가 및 제거하는 과정이 있었다. 그 당시에는 scss 파일에서 탐색하고 수정하는 게 번거롭다고 느끼지 않았다.
  • 그러다 어느 날 문득, “컴포넌트는 Storybook으로 관리하는데 컬러 변수는 어떻게 관리하지?”라는 생각이 들었다. 컬러 변수도 Storybook 형태로 한 눈에 파악하고 관리할 수는 없을까?
  • 이렇게 이어지다 보니, vscode 사이드바(하단 핑크영역)에서 컬러 변수를 확인하고 검색할 수 있는 extension이 있으면 좋겠다는 생각이 들었다.

  • 그래서 전역 컬러 변수를 보여주고, 복사는 물론 검색까지 되는 extension을 만들어보기로 했다.
  • 이번 포스팅은 시리즈로 작성했는데, 첫 번째는 “[vscode] 컬러 변수 뷰어 만들기(1) - webview API 사용법”,
  • 두 번째는 “[vscode] 컬러 변수 뷰어 만들기(2) - extension 구현하기” 로 나누었다.
  • 이번 시간에는 시리즈의 첫 번째로, 커스텀 extension의 구조와 Webview API 사용법을 알아보았다.



1. 커스텀 extension 구조 살펴보기

🙂 webivew API를 살펴보기 전, 커스텀 extension(확장)의 기본 구조를 해보자.

1) 명령어로 환경 설정하기

  • 먼저, 아래 명령어로 프로젝트 환경 설정을 한다.
npx --package yo --package generator-code -- yo code

 

2) 폴더 구조는?

  • 그러면 폴더가 자동 생성되는데, 이 중 src/extension.ts, package.json이 중요하다.
  • src/extension.ts는 커스텀 extension의 기능을 정의할 때 사용하며,
  • package.json은 커스텀 extension 이름, 버전 그리고 뷰 형태, 명령어 등을 정의한다.

 

(1) src/extension.ts 살펴보기

  • extension.ts은 vscode extension의 생명주기별 함수를 제공한다.
  • activate()는 extension이 활성화될 때 실행되는 함수로, 기능을 초기화하고 등록한다.
  • deactivate()는 extension이 비활성화될 때 실행되며, 리소스를 정리하는 역할을 한다.
import * as vscode from 'vscode';

export function activate(context: vscode.ExtensionContext) { ... }

export function deactivate() { ... }

 

(2) package.json 살펴보기

  • package.json에는 크게 2가지 설정을 한다.
  • 첫 번째 설정은 우리가 대부분 알고 있듯이 프로젝트의 이름, 버전 등이다.
{
  "name": "simplewebview",
  "displayName": "simplewebview",
  "description": "view webview simple example",
  "version": "0.0.1",
  "engines": {
    "vscode": "^1.85.0"
  },
  "categories": [ "Other" ],
  ...
}

 

  • 두 번째 설정은 커스텀 extension를 위한 설정이다.
  • 커스텀 extension을 어떤 방식으로 표시할지, 그리고 사용자와 상호작용할 명령어 등을 정의한다.
{
  ...
  "activationEvents": [],        // (a)
  "main": "./out/extension.js",
  "contributes": {
    "viewsContainers": {},       // (b)
    "views": {}                  // (c)
    "commands": []               // (d)
  },
  ...
}
번호 속성 설명
(a) activationEvents 커스텀 extension을 활성화시키는 이벤트 정의
(b) viewsContainers 커스텀 extension을 어떤 방식으로 표시할 지 정의(ex. 사이드바, 활동표시줄 등)
(c) views viewsContainers에 대한 상세 설정(ex. 사이드바를 사용한다면, 사이드바 버튼의 이미지 지정하기 등)
(d) commands 커스텀 extenstion과 사용자와의 상호작용을 위한 명령어 정의




2. vscode Webview API 사용법

1) Webview API란?

  • Webview API는 여러 형태의 UI를 vscode에서 띄우는 기능으로, 복잡한 사용자 인터페이스를 구현할 때 유용하다.
  • 이 API를 사용하면 vscode 내에 HTML 콘텐츠를 렌더링할 수 있어, 버튼이나 검색창을 추가하여 사용자와 상호작용할 수 있다.
  • 아래 예시는, webview API를 사용해 HTML 형식으로 버튼과 검색창을 렌더링한 모습이다.
  • 이렇게 추가한 요소에 이벤트를 정의하면 메시지를 띄우거나 컨텐츠를 업데이트할 수도 있다.

 

😆 Webview를 사용하면 원하는 UI를 추가할 수도 있고, 이벤트까지 정의할 수 있다니! 너무 좋은데??
그럼 extension에서 복잡한 계산이나 더 큰 형태의 UI도 추가해도 될까?

  • 복잡한 계산이나 큰 형태의 UI는 지양하는 게 좋다..ㅜ
  • 왜냐하면, vscode 공식문서에서도 언급했듯이 Webview는 많은 리소스를 소모한다.
  • 그래서 아래 3가지 사항을 체크한 뒤, 사용을 고려해보는 게 좋다 😢



2) webview API 동작 살펴보기

📌 앞서, 커스텀 extension 구조와 역할 그리고 webview API의 기본 개념도 알아 보았다.
이제 vscode 사이드바에 “할일 목록을 추가하는 기능”을 추가하면서 webview API 동작을 살펴보자!

(1) 폴더 구조

  • 구현한 코드의 프로젝트 구조는 아래와 같다.

  • icon.svg은 extension 메뉴 아이콘이며, reset.css은 view 컨텐츠에서 사용할 스타일이다.
  • extension.ts은 커스텀 extension을 활성화할 때 쓰인다.
  • TodoProvider.ts은 커스텀 extension(메모 기능)의 기능 정의하고, package.json은 커스텀 extension의 view 형태, 아이콘 등을 설정한다.

 

(2) extension 기본 설정하기(package.json)

  • package.json에서 커스텀 extension의 View 형태, 아이콘 등을 설정한다.
{
  "name": "todoView",
  "displayName": "todoView",
  "description": "view webview simple example",
  "version": "0.0.1",
  "engines": {
    "vscode": "^1.87.0"
  },
  "categories": [ "Other" ],
  "activationEvents": [ "onView:todoView" ],  // (a)
  "main": "./out/extension.js",
  "contributes": {
    "viewsContainers": {
      "activitybar": [                        // (b)
        {
          "id": "todoView",
          "title": "Todo",
          "icon": "media/icon.svg"
        }
      ]
    },
    "views": {
      "todoView": [                            // (c)
        {
          "type": "webview",
          "id": "todoView",
          "name": "Todo Viewer",
          "contextualTitle": "CVT"
        }
      ]
    },
  },
  ...
}
번호 설명
(a) - extension이 활성화되는 이벤트를 지정한다.
- onView:todoView 라고 지정하면, "todoView"라는 뷰가 나타날 때 확장을 활성화한다.
(b) - 추가하려는 뷰 컨테이너(예: Activity Bar)를 정의한다.
- 사이드바 영역에 커스텀 View를 추가하기위해, "activitybar"에 "todoView"라는 뷰를 추가한다.
(c) - (b)에서 선언한 뷰("id": "todoView")에 대한 상세 정의를 한다.
- todoView 뷰의 타입을 webview로 설정해 html 형식으로 커스텀이 가능토록 한다.

 

(3) 메모 기능 구현하기(TodoProvider.ts)

  • TodoProvider.tsTodoProvider 클래스에서 메모 기능과 뷰 화면을 구현해보자!
  • 클래스 기본 형태는 아래와 같다.
import * as vscode from "vscode";

export class TodoProvider implements vscode.WebviewViewProvider {
  _view?: vscode.WebviewView;
  _docTextList: string[] = [];

  constructor(private readonly _extensionUri: vscode.Uri) {}
  ...

}

 

  • 웹뷰(Webview)를 만들기 위해 revive(), resolveWebviewView()을 정의한다.
export class TodoProvider implements vscode.WebviewViewProvider {
  ...
  // (d)
  revive(panel: vscode.WebviewView) {
    this._view = panel;
  }
  // (e)
  resolveWebviewView(webviewView: vscode.WebviewView) {
    this._view = webviewView;

    webviewView.webview.options = { enableScripts: true };

      // (f)
    webviewView.webview.html = this._getHtmlForWebview(webviewView.webview); 
    // (g) 
    webviewView.webview.onDidReceiveMessage(this.onReceiveMessage.bind(this)); 
  }
}
번호 설명
(d) revive()는 웹뷰가 다시 로드될 때 TodoProvider를 재활성화한다.
(e) resolveWebviewView()에서 웹뷰의 HTML을 설정하고, 메시지 수신을 처리할 콜백 함수를 등록한다.
(f) _getHtmlForWebview의 리턴값을 웹뷰의 HTML으로 설정한다.
(g) onReceiveMessage에서 선언한 메시지와 동작을 수신한다.

 

export class TodoProvider implements vscode.WebviewViewProvider {
    ...
    // (h)
  private async onReceiveMessage(data: { type: string; text: string }) {
    switch (data.type) {
      case "addTodo":
        this._addTodo(data.text);
        return;
      case "update":
        this._updateWebview();
        return;
      default:
        return;
    }
  }
  // (i)
  private _addTodo(text: string) {
    this._docTextList.push(text);
  }

  // (j)
  private async _updateWebview() {
    if (this._view) {
      this._view.webview.html = this._getHtmlForWebview(this._view.webview);
    }
  }
    ...
}
번호 설명
(h) onReceiveMessage()vscode.postMessage()로 송신한 메시지를 수신하는 역할을 한다.
(i) 송신된 메시지 타입이 addTodo라면, 메모 리스트에 메모를 추가한다.
(j) 송신된 메시지 타입이 update라면, HTML을 재렌더링한다.

 

export class TodoProvider implements vscode.WebviewViewProvider {
  ...

  // (k)
  private _getHtmlForWebview(webview: vscode.Webview) {

        // (l)
    const styleResetUri = webview.asWebviewUri(
      vscode.Uri.joinPath(this._extensionUri, "media", "reset.css")
    );
    const styleVSCodeUri = webview.asWebviewUri(
      vscode.Uri.joinPath(this._extensionUri, "media", "vscode.css")
    );

    // (m)
    const listHtml = this._docTextList.map((text) => `<li>${text}</li>`).join("");
    ...
  }
}
번호 설명
(k) _getHtmlForWebview은 웹뷰에 띄워줄 콘텐츠를 리턴한다.
(l) 웹뷰에 css를 적용하기 위해, extension 프로젝트에 선언된 css 파일의 파일 위치를 가져온다.
(m) listHtml는 웹뷰에 할일 목록 리스트를 뿌려주는 역할을 한다.

 

export class TodoProvider implements vscode.WebviewViewProvider {
  ...

  private _getHtmlForWebview(webview: vscode.Webview) {
      ...
    return `<!DOCTYPE html>
      <html lang="en">
      <head>
          <!-- (o) -->
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <link rel='stylesheet' href='${styleResetUri}' />
        <link rel='stylesheet' href='${styleVSCodeUri}' />
        <title>Todo</title>
      </head>
      <body>
        <h1>Todo</h1>

        <!-- (p) -->
        <form id="todo-form">
          <input id="todo-input" type="text" />
          <button id="add-todo">Add</button>
        </form>

        <!-- (q) -->
        <ul id="todo-list">
          ${ listHtml }
        </ul>

        <script>
           // (r)
          (function() {
              // (s)
            const vscode = acquireVsCodeApi();
            const btnAdd = document.getElementById('add-todo');

                        // (t)
            btnAdd.addEventListener('click', () => {
              const input = document.getElementById('todo-input');
              vscode.postMessage({ type: 'addTodo', text: input.value });
              vscode.postMessage({ type: 'update' });

              input.value = '';
            });
          }())
      </script>
      </body>
      </html>`;
  }
}
번호 설명
(o) 스타일 경로를 헤더에 등록해준다.
(p) 할 일 목록을 작성하고 저장하기 위해, 버튼과 입력창을 추가한다.
(q) 할 일 목록을 HTML 형태로 보여준다.
(r) 스크립트에 즉시실행함수 형태로 선언하여, 이벤트 동작이 가능하도록 설정한다.
(s) acquireVsCodeApi()를 호출해, VSCode의 기능에 접근할 수 있는 변수를 선언한다.
이 변수를 통해, vscode 내장 함수나 기능을 사용할 수 있다.
(t) add 버튼 클릭시 동작이다.
postMessage({ type: 'addTodo', text: input.value })로 입력창의 정보를 전송한다.
그 다음, 업데이트된 데이터를 화면에 렌더링하기 위해 HTML을 리렌더링하는 메세지를 전송한다. (type: 'update')

 

(5) 기능 활성화시키기 (extension.ts)

  • 마지막으로 vscode가 활성화될 때, 할일 목록 화면을 렌더링해야한다.
import * as vscode from 'vscode';
import { TodoProvider } from './TodoProvider';

export function activate(context: vscode.ExtensionContext) { 
    const todo = new TodoProvider(context.extensionUri);

    // (u) 
    context.subscriptions.push( 
        // (v)
        vscode.window.registerWebviewViewProvider( 
            'todoView',
             new TodoProvider(context.extensionUri
         ))
    );
    );
}

export function deactivate() {}
번호 설명
(u) registerWebviewViewProvidertodoView 뷰가 열릴 때, 할일 목록 화면을 랜더링한다.
(v) context.subscriptions.push()은 일회용 리소스를 관리하고 메모리 누수를 방지한다.
이 함수에 할일 목록 화면 렌더링 작업(u)을 넘겨, 리소스를 관리해준다.

 

📌 앞선 과정을 거치면, 아래와 같이 할일 목록을 추가할 수 있는 WebView를 만들 수 있다. (최종 코드)




3. 마치며…

  • 이번 시간에는 “[vscode] 컬러 변수 뷰어 만들기” 시리즈의 첫 번째인 “webview API 사용법”을 알아보았다. vscode에는 extension 커스텀이 가능하도록 여러 API를 제공하고 있었으며, 이 중 webview API는 UI 커스텀이 자유롭다는 장점이 있었다. 다만 아쉬운 점은 HTML 방식으로 커스텀이 가능하나, 많은 리소스를 필요로 하는 단점이 있었다.
  • 다음 시간에는 앞선 webview API 사용법을 토대로, 컬러 변수 뷰어를 구현하면서 여러 기능을 추가로 알아보겠다.

 

반응형

댓글