vscode 익스텐션을 만들어보자 !

Jongco·2023년 6월 28일
4
post-thumbnail

Intro

프로젝트를 진행하다 환경변수 오타로 인해서 에러가 발생한 경우가 몇번 있었다.
인턴을 할 때에는 api 주소를 환경변수에 넣고 사용했었는데, 여러가지 환경이 존재하다 보니, 잘못입력한 환경변수를 찾으려면 각각의 파일을 전부 돌면서 하나하나씩 확인해야 했다.

왜 환경변수는 다른 변수처럼 auto-complete가 안 될까..

그렇게 만든 Env Complete 익스텐션 ! 이 것을 만드는 과정을 글로 남겨보고자 한다.

1. env.d.ts에 설정하면 되잖아

사실 env를 타입으로 받아오려면 env.d.ts파일을 생성해서 이 안에 아래와 같이 선언해주면 된다.
설정하는 것도 env에 있는 값들을 싹 다 복사 후에 선택해서 아래와 같은 형식으로 바꿔주면 끝이다.

declare namespace NodeJS {
  interface ProcessEnv {
    REACT_APP_SERVICE_URL: string
  }
}

물론 간단하지만 추후에 환경변수가 추가되거나, 삭제된다면 다시 이 파일로 돌아와서 고쳐주어야 하는 불편함이 있다.
두번째로, 여전히 환경변수에 오타가 있을 때 파일들을 전부 돌면서 찾아야 한다는 것이다.

2. 그럼 vscode extension을 만들자 !

파일을 건드려야 하는 것이 아닌, vscode에서 바로바로 환경변수를 불러올 수 있도록 할 수 있지 않을까?
Auto Import 익스텐션도 다른 파일에 접근해서 해당 모듈을 불러올 수 있는 것이 아닌가?

위와 같은 생각을 갖고 vscode extension에 대한 정보를 찾기 시작했다.
vscode는 typescript로 만들어진 오픈소스이기 때문에 언어 또한 원래 사용하던 것으로 사용할 수 있었다.
셋팅 또한 그렇게 어렵지 않았다.
vscode에서 지원해주는 api가 종류가 굉장히 많아서 하나하나 살펴보면서 하기에는 무리라고 생각했고,
내가 만드는 익스텐션에 필요한 기능만 추려서 찾아보는 방식으로 진행했다.

2-1. vscode extension 셋팅

셋팅 방식은 간단하다. 아래와 같이 yogenerator-code를 전역적으로 설치한다.

npm install -g yo generator-code

그 후 아래 코드를 실행하면 된다.

yo code

그럼 간단하게
언어는 어떤 것을 사용할 건지(typescript, javascript 중)
이름은 어떻게 할 건지(추후에 바꿀 수 있으니 생각한 게 없다면 대충 입력해도 된다) 등을
결정한 후에 초기 셋팅이 완료된다.

2-1-1.package.json

package.json에 있는 중요한 요소들은 아래와 같다.

  • name: 확장 프로그램의 이름. 고유해야하며, npm 등록 규칙을 따라야 함 (고유한 id값이라고 생각하면 됨)
  • displayName: vscode에 등록 되는 확장프로그램의 이름 (검색하면 확인할 수 있는 이름)
  • description: vscode에 검색하면 바로 아래 나오는 설명
  • version: 확장 프로그램의 버전 지정
  • publisher: 배포자 또는 개발자의 이름 식별자
  • engines: vscode 엔진 버전
  • activationEvents: 확장 프로그램이 활성화 되는 이벤트 지정 ("*"을 넣으면 모든 경우에 활성화됨)
  • license: 확장프로그램에 적용되는 라이센스 지정

2-1-2. src/extension.ts

앞으로 프로그램을 만들어 나갈 공간이다.

  • activate: 확장프로그램이 활성화 될 때 호출하는 함수
  • deactivate: 확장프로그램이 비활성화 될 때 호출하는 함수

기능을 만들기 위해서는 일단 activate 함수에 집중하면 된다.

2-2. 내가 만들 기능 추리기

익스텐션에 들어가는 기능은 아래와 같다.

  • 스캔한 파일을 자동 완성으로 보여주는 기능
  • 자동완성된 item의 설명에 환경변수의 값을 보여주는 기능

2-3. 기능 구현

내가 구상한 방식은 아래와 같다.

  1. 사용자의 workspace에 접근해서 루트 디렉터리에 존재하는 .env로 시작하는 파일명을 찾는다.
    (보통 프론트에서 환경변수는 루트 디렉터리에 존재하기 때문에)
  2. 각각의 파일을 읽어온다.
  3. 환경변수의 key값을 auto-complete에 넣고, 해당 환경변수 설명에 파일 위치와, 환경변수 전체를 넣어준다.

2-3-1. 사용자의 workspace에 접근

첫번째로 사용자의 workspace에 접근한다.
처음에는 node환경에 있는 path와 fs를 사용해 파일을 읽으려고 했으나, 해당 익스텐션폴더에 접근하는 것이 아니라 익스텐션 사용자의 폴더에 접근해야 했기에 이러한 방식으로는 접근이 불가능했고, vscode에서 지원하는 api를 사용했다.

import * as vscode from "vscode";

// workspace를 읽어오는 함수
function readWorkspaceFolder() {
  const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
  if (!workspaceFolder) {
    throw new Error("No workspace folders are open");
  }
  return workspaceFolder;
}

// directory를 읽어오는 함수
async function readDir(uri: vscode.Uri) {
  const filesAndDirectories = await vscode.workspace.fs.readDirectory(uri);
  return filesAndDirectories;
}

// 파일을 필터링 하는 함수
const filterEnvFileList = (fileList: [string, vscode.FileType][]) => {
  const regExp = /^\.env/g;
  return fileList
    .map(([name, type]) => name)
    .filter((filename) => filename.match(regExp));
};

async function getEnvFilelist() {
  const workspaceFolder = readWorkspaceFolder();
  const filesAndDirectories = await readDir(workspaceFolder.uri);
  const filteredEnvFileList = filterEnvFileList(filesAndDirectories);

  return filteredEnvFileList;
}

2-3-2. 각각의 파일을 읽어온다.

각각의 파일에 있는 환경변수를 읽어온다.

//파일을 읽어오는 함수
async function readFile(filename: string) {
  const workspaceFolder = readWorkspaceFolder();
  const uri = vscode.Uri.joinPath(workspaceFolder.uri, filename);
  try {
    const data = await vscode.workspace.fs.readFile(uri);
    const text = new TextDecoder().decode(data);
    return text;
  } catch (err) {
    console.error(err);
  }
}
//환경변수를 [key, value] 형태로 리턴하는 함수
const getEnvVariable = async (env: string) => {
  const data = await readFile(env);
  if (!data) {
    return;
  }

  const envVariable = data.split(/\n/g).filter((variable) => variable !== "");

  return envVariable.map((v) => v.split("="));
};

2-3-3. 환경변수의 key값을 auto-complete에 넣는다.

auto-complete에 넣어주기 위해서는 vscode에서 지원해주는 registerCompletionItemProvider를 이용해야 한다.

일단 completionItem먼저 만들어보자. 아래와 같은 코드로 한개의 자동완성을 만들 수 있다.

function makeCompletionItem(item: string, desc: string[]) {
  //vscode에서 제공되는 자동완성을 만드는 api
  const completionItem = new vscode.CompletionItem(
    //어떤 이름으로 자동완성을 보여줄 것인지
    item,
    //자동완성 가장 좌측에 나올 아이콘
    vscode.CompletionItemKind.EnumMember
  );
  // 해당 자동완성을 이용하면 어떻게 입력시킬지
  completionItem.insertText = new vscode.SnippetString(`process.env.${item}`); 
  // 설명 제일 위에 제목처럼 나온다.
  completionItem.detail = "env-type";
  // 설명의 내용(여기에 환경변수의 내용을 넣어줄 거다)
  completionItem.documentation = new vscode.MarkdownString(desc.join(`\n`));

  return completionItem;
}

그 후 activate 함수 안에 차례대로 넣어주면 된다.

export function activate(context: vscode.ExtensionContext) {
  let disposable = vscode.languages.registerCompletionItemProvider(
    //어떤 파일에서 사용할 건지(typescript -> .ts, typescriptreact -> .tsx)
    [
      { language: "typescript", scheme: "file" },
      { language: "typescriptreact", scheme: "file" },
    ],
    {
      //어떤 아이템을 제공할건지
      async provideCompletionItems(
        document: vscode.TextDocument,
        position: vscode.Position
      ) {
        const variable: { [key in string]: string[] } = {};
        const completionItemList: vscode.CompletionItem[] = [];

        const envFilelist = await getEnvFilelist();
        if (!envFilelist) {
          return;
        }

        for (let file of envFilelist) {
          const envVariable = await getEnvVariable(file);
          if (!envVariable) {
            continue;
          }
          envVariable.forEach(([key, value]) => {
            if (variable[key]) {
              variable[key].push(`${file}: \n${value}\n`);
            } else {
              variable[key] = [`${file}: \n${value}\n`];
            }
          });
        }

        Object.entries(variable).forEach(([key, value]) => {
          const completionItem = makeCompletionItem(key, value);
          completionItemList.push(completionItem);
        });

        // 자동완성 아이템이 담긴 리스트
        return completionItemList;
      },
    }
  );

  context.subscriptions.push(disposable);
}

3. 배포까지 !

이렇게 익스텐션을 만들었다면 배포하기 전에 패키징 작업을 해줘야 한다. 아래 명령어로 vsce를 전역적으로 설치해준다.

npm install -g vsce

3-1. package.json 설정

위에 각각의 기능은 설명했으나, 여기에서 정해줘야 하는 것이 있다.
activationEvents인데, 이걸 "*"로 넣어주면 패키징을 할 때 이 방법은 좋지 않다고 소리친다..
그럼 여기에 언제 실행할지 설정을 추가해 줘야 하는데(필수는 아님) 나는 아래와 같이 설정했다.

  "activationEvents": [
    "workspaceContains:**/.env*", // workspace에 env가 있을 때
    "onLanguage:typescript", // 파일이 ts
    "onLanguage:typescriptreact" //파일이 tsx
  ],

3-2. 패키징

아래 명령어를 실행해준다.

vsce package

그러면 프로젝트 최상단에 .vsix 파일이 생기는데 이걸 마켓에다가 올리면 배포가 되는 것이다 !
여기 링크에 로그인을 해준다.

우측 상단에 있는 Publish extensions 으로 이동 후 New extension 클릭

여기에 방금 생성된 vsix파일을 업로드 하고 조금만 기다리면 배포가 완료된다!
vscode extension탭에서 내가 만든 익스텐션을 다운로드 받을 수 있다.

마켓 링크 바로가기

마무리

이렇게 처음 만든 익스텐션이 성공적으로 마켓에 배포되었다! 프론트엔드 작업과 조금은 다르지만 또 다른 느낌대로 재밌었다. 그리고 간단한 익스텐션이었지만, 처음으로 내가 직면한 문제를 프로그래밍적으로 해결했기에 뿌듯함을 느낀다 😊

0개의 댓글