개요
VSCode의 GUI를 통한 Git 명령어는 기본적으로 속도가 느린 편이다. 레포지토리 크기가 커질수록 이 문제는 더욱 심해지는데, 이는 모노레포가 갖는 단점 중 하나이다.
현재 회사의 코드는 25년 이상 유지되고 있고, 메인 레포지토리는 10GB 이상의 용량과 약 29만 개의 파일로 구성되어 있다. 이렇게 많은 파일이 존재하다 보니 PC 성능에 따라 VSCode GUI를 통한 Git 명령어 실행 시간이 크게 지연되는 문제가 발생하게 되었다.
내가 사용하는 PC는 회사에서 가장 성능이 낮은 편이라, 전체 빌드시 생성되는 수만 개의 파일을 UI를 통해 git reset하는 데에만 약 3분이 걸렸으며 GUI명령을 통해 git reset을 실행시에 최대 9999개의 파일만 reset 가능하기에 여러번 실행해야하여 처리 과정이 번거롭고 시간이 오래 걸리며, 작업이 완료된 뒤에도 UI에 즉시 반영되지 않아 추가적인 문제가 발생할 가능성이 존재하였다.
이런 문제를 해결하기 위해 CLI 명령어를 통해 편하게 git 명령어를 실행할 수 있는 개인용 Extension을 개발했고, 실 사용 결과 사용성이 꽤 좋아서 회사 개발팀 전체에 배포하게 되었다.
개발
처음에는 vscode gui를 사용하지 않고 단순하게 cli를 통해 git 명령어를 입력하는식으로 문제를 해결하였다.
예를들면 reset을 원할 때에는 아래 명령어를 사용하였으며
git reset --hard
git clean -fd
reset이후 자동으로 remote branch를 fetch하고 checkout까지 하기 위해 아래 명령어를 사용하였다.
git reset --hard
git clean -fd
git fetch --all
git checkout [브랜치 이름]
이렇게 사용하는 것도 썩 나쁘지는 않았다. 복잡한 작업을 해야할 때 깃 명령어를 직접 공부할 수 있었으며 속도또한 기존 3분 이상 걸리는 작업들이 30초 내에 끝났기 때문이다. 다만 명령어에 익숙해질때쯔음 이런 명령어를 매번 입력하는 것이 귀찮다는 생각을 하게되었다.
bat 파일 개발
귀찮다는 생각이 들었을 때 내 드라이브에 폴더를 따로 만들어 위 명령어를 한 번의 명령으로 압축시켜 실행시킬 수 있도록 .bat 파일을 만들었다. 예를들면 아래와 같다.
@REM reset_fetch_checkout.bat
@REM Reset the local repository, fetch the latest changes from the remote repository and checkout the specified branch
@echo off
set /p BRANCH="Input branch name: "
call "%~dp0reset.bat"
git fetch --all
git checkout %BRANCH%
이런 작업을 통해 나에게 필요한 git 명령어를 직접 커스텀하여 bat 파일로 만들어 해당 파일 실행만으로 내가 원하는 작업을 진행할 수 있도록 만들었다. 또한 git 명령어 외에도 kill_port와 같이 일일히 기억할 필요가 없는 기본 명령어 또한 bat파일로 만들어 사용하였다.
처음에는 방식또한 나쁘지는 않았다. 하지만 이 또한 결국에는 해당 파일을 찾거나 해당 파일의 경로를 찾아 cli를 통해 실행해야하는 작업에 실증이났으며 귀찮아졌다. 개선이 필요한 시점이였다. 내가 원하는 작업을 더욱 편하게 하기 위해 확장 프로그램을 개발하였다.
확장 프로그램 개발
목표는 단 하나였다. 사용하기 편할 것
먼저 내가 정의한 bat 파일들을 extension을 통해 트리구조로 보여주는 로직을 개발하였다.
function extractRemComment(filePath: string): string | null {
const fileContent = fs.readFileSync(filePath, "utf-8");
const lines = fileContent.split(/\\r?\\n/);
for (const line of lines) {
if (line.trim().startsWith("@REM")) {
return line.trim().replace("@REM", "").trim();
}
}
return "";
}
class GitCommandItem extends vscode.TreeItem {
constructor(
public readonly label: string,
public readonly commandName: string,
public readonly tooltip: string,
public readonly description: string,
public readonly collapsibleState: vscode.TreeItemCollapsibleState = vscode
.TreeItemCollapsibleState.None
) {
super(label);
this.tooltip = `${this.label} - ${this.description}`;
this.command = {
command: "extension.addFilePathToTerminal",
title: this.label,
arguments: [this.commandName],
};
this.contextValue = "gitCommandItem";
this.resourceUri = vscode.Uri.file(
path.join(__dirname, "..", "scripts", `${commandName}.bat`)
);
}
}
class GitCommandProvider implements vscode.TreeDataProvider<GitCommandItem> {
private _onDidChangeTreeData: vscode.EventEmitter<
GitCommandItem | undefined | void
> = new vscode.EventEmitter<GitCommandItem | undefined | void>();
readonly onDidChangeTreeData: vscode.Event<
GitCommandItem | undefined | void
> = this._onDidChangeTreeData.event;
getTreeItem(element: GitCommandItem): vscode.TreeItem {
return element;
}
getChildren(): GitCommandItem[] {
const scriptDir = path.join(__dirname, "..", "scripts");
const items: GitCommandItem[] = [];
try {
const files = fs.readdirSync(scriptDir);
files.forEach((file) => {
if (file.endsWith(".bat")) {
const description = extractRemComment(path.join(scriptDir, file));
const commandName = path.basename(file, ".bat");
items.push(
new GitCommandItem(
commandName
.replace(/_/g, " ")
.replace(/\\b\\w/g, (char) => char.toUpperCase()),
commandName,
`Path for ${commandName} script`,
description || commandName
)
);
}
});
} catch (error) {
vscode.window.showErrorMessage(`Failed to load commands: ${error}`);
}
return items;
}
}
다음으로는 해당 커맨드를 눌렀을 때 커맨드가 바로 실행될것인지, 실행할 수 있는 환경을 추가할것인지를 고민하였는데 reset과 같은 명령어는 매우 민감한 명령어이기에 후자를 선택하였다. 사용자가 실수로 reset을 클릭했는데 바로 모든 git change 내역을 reset hard 해버리면 안되기 떄문이다.
아래는 해당 코드이다.
function addFilePathToTerminal(commandName: string) {
return new Promise<void>(async (resolve, reject) => {
const batFilePath = path.join(
__dirname,
"..",
"scripts",
`${commandName}.bat`
);
try {
let terminal = vscode.window.activeTerminal;
if (!terminal || terminal.name !== "PowerShell") {
terminal = vscode.window.createTerminal({
name: "PowerShell",
shellPath: "powershell.exe", // Ensure PowerShell is used
});
}
terminal.show();
terminal.sendText(batFilePath, false);
vscode.window.showInformationMessage(
`Command file path added to terminal: ${commandName}.bat`
);
resolve();
} catch (error) {
vscode.window.showErrorMessage(`Failed to add to terminal: ${error}`);
reject(error);
}
});
}
export function activate(context: vscode.ExtensionContext) {
const gitCommandProvider = new GitCommandProvider();
vscode.window.registerTreeDataProvider("gitCommands", gitCommandProvider);
// Existing command registration
let addFilePathCommand = vscode.commands.registerCommand(
"extension.addFilePathToTerminal",
(commandName: string) => {
addFilePathToTerminal(commandName);
}
);
context.subscriptions.push(addFilePathCommand);
}
위 로직을 통해 사용자가 원하는 bat 파일을 클릭하였을 때 이파일을 실행할 수 있는 명령어를 terminal에 추가해주는 작업을 할 수 있다. 사용자는 한 번 더 고민해보고 엔터를 누르기만 하면 원하는 명령어를 실행할 수 있게 된다.
이후 피드백을 받아 키보드 액션 또한 추가해주었다. vscode 터미널을 통해 실행할 수 있었으면 좋겠다는 피드백이 있었다. activate 함수에 아래와 같이 executeBatchCommand를 추가해주었다.
export function activate(context: vscode.ExtensionContext) {
const gitCommandProvider = new GitCommandProvider();
vscode.window.registerTreeDataProvider("gitCommands", gitCommandProvider);
// Existing command registration
let addFilePathCommand = vscode.commands.registerCommand(
"extension.addFilePathToTerminal",
(commandName: string) => {
addFilePathToTerminal(commandName);
}
);
context.subscriptions.push(addFilePathCommand);
// Register command to show batch commands in QuickPick
let executeBatchCommand = vscode.commands.registerCommand(
"extension.executeBatchCommand",
async () => {
const items = gitCommandProvider.getChildren();
const selected = await vscode.window.showQuickPick(
items.map((item) => ({
label: item.label,
description: item.description,
commandName: item.commandName,
})),
{
placeHolder: "Select a batch command to execute",
matchOnDescription: true,
}
);
if (selected) {
addFilePathToTerminal(selected.commandName);
}
}
);
context.subscriptions.push(executeBatchCommand);
}
이로써 모든 작업이 완료되었다. 아래는 extension.ts전체 코드이다
import * as vscode from "vscode";
import * as path from "path";
import * as fs from "fs";
function extractRemComment(filePath: string): string | null {
const fileContent = fs.readFileSync(filePath, "utf-8");
const lines = fileContent.split(/\\r?\\n/);
for (const line of lines) {
if (line.trim().startsWith("@REM")) {
return line.trim().replace("@REM", "").trim();
}
}
return "";
}
function addFilePathToTerminal(commandName: string) {
return new Promise<void>(async (resolve, reject) => {
const batFilePath = path.join(
__dirname,
"..",
"scripts",
`${commandName}.bat`
);
try {
let terminal = vscode.window.activeTerminal;
if (!terminal || terminal.name !== "PowerShell") {
terminal = vscode.window.createTerminal({
name: "PowerShell",
shellPath: "powershell.exe", // Ensure PowerShell is used
});
}
terminal.show();
terminal.sendText(batFilePath, false);
vscode.window.showInformationMessage(
`Command file path added to terminal: ${commandName}.bat`
);
resolve();
} catch (error) {
vscode.window.showErrorMessage(`Failed to add to terminal: ${error}`);
reject(error);
}
});
}
class GitCommandItem extends vscode.TreeItem {
constructor(
public readonly label: string,
public readonly commandName: string,
public readonly tooltip: string,
public readonly description: string,
public readonly collapsibleState: vscode.TreeItemCollapsibleState = vscode
.TreeItemCollapsibleState.None
) {
super(label);
this.tooltip = `${this.label} - ${this.description}`;
this.command = {
command: "extension.addFilePathToTerminal",
title: this.label,
arguments: [this.commandName],
};
this.contextValue = "gitCommandItem";
this.resourceUri = vscode.Uri.file(
path.join(__dirname, "..", "scripts", `${commandName}.bat`)
);
}
}
class GitCommandProvider implements vscode.TreeDataProvider<GitCommandItem> {
private _onDidChangeTreeData: vscode.EventEmitter<
GitCommandItem | undefined | void
> = new vscode.EventEmitter<GitCommandItem | undefined | void>();
readonly onDidChangeTreeData: vscode.Event<
GitCommandItem | undefined | void
> = this._onDidChangeTreeData.event;
getTreeItem(element: GitCommandItem): vscode.TreeItem {
return element;
}
getChildren(): GitCommandItem[] {
const scriptDir = path.join(__dirname, "..", "scripts");
const items: GitCommandItem[] = [];
try {
const files = fs.readdirSync(scriptDir);
files.forEach((file) => {
if (file.endsWith(".bat")) {
const description = extractRemComment(path.join(scriptDir, file));
const commandName = path.basename(file, ".bat");
items.push(
new GitCommandItem(
commandName
.replace(/_/g, " ")
.replace(/\\b\\w/g, (char) => char.toUpperCase()),
commandName,
`Path for ${commandName} script`,
description || commandName
)
);
}
});
} catch (error) {
vscode.window.showErrorMessage(`Failed to load commands: ${error}`);
}
return items;
}
}
export function activate(context: vscode.ExtensionContext) {
const gitCommandProvider = new GitCommandProvider();
vscode.window.registerTreeDataProvider("gitCommands", gitCommandProvider);
// Existing command registration
let addFilePathCommand = vscode.commands.registerCommand(
"extension.addFilePathToTerminal",
(commandName: string) => {
addFilePathToTerminal(commandName);
}
);
context.subscriptions.push(addFilePathCommand);
// Register command to show batch commands in QuickPick
let executeBatchCommand = vscode.commands.registerCommand(
"extension.executeBatchCommand",
async () => {
const items = gitCommandProvider.getChildren();
const selected = await vscode.window.showQuickPick(
items.map((item) => ({
label: item.label,
description: item.description,
commandName: item.commandName,
})),
{
placeHolder: "Select a batch command to execute",
matchOnDescription: true,
}
);
if (selected) {
addFilePathToTerminal(selected.commandName);
}
}
);
context.subscriptions.push(executeBatchCommand);
}
extension의 command 개발이 완료되었으니 이제 이를 vscode에 올리기 위한 코드를 작성해주었다. 아래는 package.json 전체 코드이다.
{
"name": "git-batch-commands",
"displayName": "Git Batch Commands",
"description": "A Visual Studio Code extension that provides batch commands for Git.",
"version": "1.1.0-alpha.2",
"engines": {
"vscode": "^1.50.0"
},
"categories": [
"Other"
],
"repository": {
"type": "gitlab",
"url": ""
},
"icon": "assets/icon.jfif",
"license": "SEE LICENSE IN LICENSE",
"publisher": "jongsik",
"main": "./out/extension.js",
"contributes": {
"viewsContainers": {
"activitybar": [
{
"id": "git-batch-command",
"title": "Git Batch Commands",
"icon": "resources/git.svg"
}
]
},
"views": {
"git-batch-command": [
{
"id": "gitCommands",
"name": "Git Batch Commands"
}
]
},
"commands": [
{
"command": "extension.addFilePathToTerminal",
"title": "Add File Path to Terminal"
},
{
"command": "extension.executeBatchCommand",
"title": "Execute Batch Command"
}
]
},
"activationEvents": [
"onCommand:extension.executeBatchCommand",
"onView:gitCommands"
],
"scripts": {
"vscode:prepublish": "npm run compile",
"compile": "tsc -p ./",
"watch": "tsc -watch -p ./",
"pretest": "npm run compile && npm run lint",
"lint": "eslint src --ext ts",
"package": "vsce package"
},
"devDependencies": {
"@types/mocha": "^10.0.9",
"@types/node": "^12.11.7",
"@types/vscode": "^1.50.0",
"eslint": "^7.8.1",
"typescript": "^4.0.3",
"vscode-test": "^1.4.0"
}
}
결론
이제 vscode 좌측 extension의 tree에서 원하는 batch 파일을 클릭하면 원하는 명령어를 아주 손쉽게 실행할 수 있는 구조가 완성되었다. 아래는 실제 동작 순서이다.
1. 클릭
2. 명령어 확인 및 엔터
3. 실행 완료
추후 윈도우만 동작하는 것이 아니라 맥 환경에서도 동작하도록 개선하여 나만 사용하는 것이 아니라 extension으로 배포해볼까라는 생각도 해보고 조사도 해보았는데 이미 유사한 작업을 하는 extension이 엄청나게 많기에 따로 배포는 하지 않기로 결정하였다. 대신 개발본부에 사용법, 다운받는법 등을 공유하여 나와 비슷한 상황인 개발자들이 사용할 수 있도록 하였으며 피드백을 받아 주기적으로 수정하는 정도로 프로젝트를 마무리하였다.
'etc' 카테고리의 다른 글
당근(당근마켓) 최종 면접 회고 (0) | 2025.01.14 |
---|---|
(언제 막힐지 모르는)Bun을 사용한 크롤링 방지 우회 방법과 원리(2) (0) | 2024.11.28 |
(언제 막힐지 모르는)Bun을 사용한 크롤링 방지 우회 방법과 원리(1) (0) | 2024.11.07 |
기가막힌 디자인/개발 툴 소개(v0.dev) (3) | 2024.10.06 |
기술 블로그와 시니어 개발자(나의 생각) (7) | 2024.09.04 |