본문으로 건너뛰기
2025. 2. 2
© WONKOOK LEE

Ollama과 Self-hosting DeepSeek으로 간단한 VS Code Extention 만들기

설 연휴를 떠들썩하게 만든 DeepSeek 논란

DeepSeek는 중국의 AI 스타트업이 개발한 오픈소스 기반의 대규모 언어모델(LLM)입니다. GPT-4와 유사한 기능을 제공하면서도 훨씬 낮은 비용으로 개발되었다는 점에서 큰 주목을 받고 있습니다.

한편 DeepSeek는 개인정보 유출 우려로 여러 국가에셔 경계의 대상이 되고 있습니다. DeepSeek의 개인정보 보호 정책에 따르면, 사용자 데이터는 중국 내 서버에 저장되며, 이는 중국 법률의 적용을 받습니다.

이러한 데이터 처리 방식으로 인해 사용자 정보가 중국 정부에 유출될 수 있다는 우려가 제기되고 있습니다.

그렇다면 로컬 환경에서 DeepSeek를 사용해볼까?

DeepSeek를 로컬 환경에서 실행하면 데이터 유출 위험은 최소화 할 수 있습니다. 로컬 실행은 데이터를 외부 서버로 전송하지 않고, 모든 처리를 사용자의 컴퓨터에서 수행하기 때문에 데이터 프라이버시와 보안 문제는 걱정이 없겠죠.

이번 예제를 통해 만든 결과물
이번 예제를 통해 만든 결과물

fireship youtube 영상을 따라 만든 DeepSeek 챗봇 VS Code Extension

마침 제가 즐겨보는 fireship이라는 개발 관련 컨텐츠 유투버가 해당 예제를 올리길래 재빠르게 따라해보았습니다.

VS Code Extension 인터페이스에서 로컬 AI 서버 API와 통신하는 예제를 따라하며 Ollama, VS Code Extension API에 대해 간단히 배워보세요.

원본 링크 https://youtu.be/clJCDHml2cA


Ollama, 로컬에서 LLM을 실행하기 위한 툴

Ollama는 로컬 환경에서 대규모 언어 모델(LLM)을 실행할 수 있도록 설계된 플랫폼 또는 툴입니다. 주로 개인 사용자가 자신의 하드웨어에서 언어 모델을 실행하여 데이터 프라이버시를 보장하고, 클라우드 의존도를 줄이는 데 초점이 맞춰져 있습니다.

Ollama의 주요 특징

  • 데이터 프라이버시: 모든 작업이 로컬에서 이루어지므로 민감한 데이터가 외부로 유출되지 않습니다.
  • 비용 절감: 클라우드 비용 없이 로컬 자원을 활용.
  • 성능 최적화: 로컬 하드웨어에 맞게 최적화된 모델 제공.
  • 오픈소스 친화적: 다양한 오픈소스 모델과 호환 가능.

Ollama 설치

세상에 너무 귀엽잖아?
세상에 너무 귀엽잖아?

https://ollama.com/download

링크에 들어가 본인의 운영 체제에 맞는 설치 파일을 받으시면 됩니다.
설치하게 되면 자동으로 ollama CLI 툴을 설치하게 됩니다.

이제 원하는 AI 모델을 pull 받아서 실행할 수 있게 됩니다.

Ollama로 원하는 AI 모델 Pull 받기

https://ollama.com/search

위 페이지에서 마치 도커 이미지처럼 원하는 모델을 찾습니다.

저는 deepseek-r1:7b 모델을 선택했습니다.

특정 태그를 선택하면 우측에 CLI 명령어를 복사할 수 있는 UI가 보이는데, 이걸 로컬 TTY 콘솔에서 실행합니다.

$ ollama run deepseek-r1

명령어를 입력하면 해당 모델을 다운로드 받은 후 CLI에서 프롬프트를 직접 띄워줍니다.

여기서 직접 질문, 답변을 할 수도 있지만 우리의 목적은 VS Code Extension을 호스트로 해서 통신을 주고받기 위함이므로 다음 스텝으로 넘어갑니다.




VS Code Extension 프로젝트 개발을 위한 초기 환경 설정(스캐폴딩)

Yeoman(yo)는 프로젝트 템플릿을 자동으로 생성해주는 도구입니다.

VS Code 확장 프로그램을 만들 때, generator-code 템플릿을 사용하면 필요한 파일과 폴더 구조를 자동으로 생성할 수 있습니다.

npx --package yo --package generator-code -- yo code

위 명령어를 실행하면, VS Code 확장 개발을 위한 기본 프로젝트가 자동으로 생성되며, 확장 유형과 설정을 선택할 수 있는 마법사가 실행됩니다.

이를 통해 확장 개발을 빠르게 시작할 수 있습니다.

위와 같이 대화형 프롬프트가 나타나 어떤 익스텐션을 만들 것인지 차례대로 설정할 수 있습니다.

셋업이 완료되면 우리가 흔히 보던 npm 패키지 프로젝트가 생성됩니다. extension.ts가 VS Code Extension을 실행했을때 제일 처음 도달하는 진입점이 됩니다.


확장 프로그램 소스 코드 구조의 주요 부분

1. vscode 모듈

import * as vscode from "vscode";
  • VS Code Extension API를 가져오는 모듈입니다.
  • 이 API는 명령 등록, UI 알림 표시, 파일 시스템 접근 등 VS Code와 상호작용할 수 있는 기능들을 제공합니다.
  • 여기서 vscode라는 이름(namespace)으로 모듈을 참조합니다.

2. activate 함수

export function activate(context: vscode.ExtensionContext) {
console.log(
'Congratulations, your extension "wonkooklee-ext" is now active!'
);
}
  • 확장이 활성화될 때 호출되는 함수입니다.
  • 사용자가 확장과 관련된 명령을 실행하거나, 특정 조건에서 자동 활성화될 때 호출됩니다.
  • 매개변수:
    • context: 확장의 실행 컨텍스트를 나타내는 객체로, 확장 수명 동안 사용되는 리소스를 관리합니다.
    • context.subscriptions: 확장 종료 시 함께 정리되어야 할 리소스를 등록하는 배열입니다.

3. vscode.commands.registerCommand

const disposable = vscode.commands.registerCommand(
"wonkooklee-ext.helloWorld",
() => {
vscode.window.showInformationMessage("Hello World from wonkooklee-ext!");
}
);
  • 사용자 명령을 등록하는 메서드입니다. 등록된 명령은 VS Code 명령 팔레트(Ctrl+Shift+P)에서 실행할 수 있습니다.
  • 매개변수:
    • 'wonkooklee-ext.helloWorld': 명령 ID입니다. package.json의 commands 섹션에 정의된 명령과 동일해야 합니다.
    • () => : 명령이 실행될 때 호출되는 콜백 함수입니다.
    • 여기서는 vscode.window.showInformationMessage()로 사용자에게 알림을 표시합니다.

4. vscode.window.showInformationMessage

vscode.window.showInformationMessage("Hello World from wonkooklee-ext!");
  • VS Code 알림 메시지를 표시하는 메서드입니다.
  • 메시지는 편집기 상단에 나타나며, 정보성 메시지를 전달할 때 유용합니다.

5. context.subscriptions.push(disposable)

context.subscriptions.push(disposable);
  • 등록된 리소스(disposable)를 확장의 수명 동안 관리합니다.
  • 확장이 비활성화될 때(deactivate 호출 시), 자동으로 리소스를 정리해 메모리 누수를 방지합니다.

6. deactivate 함수

export function deactivate() {}
  • 확장이 비활성화될 때 호출되는 함수입니다.
  • 여기서 필요한 정리 작업(예: 리소스 해제)을 수행할 수 있습니다.
  • 현재는 특별한 동작 없이 빈 함수로 작성되어 있습니다.



Extention Host를 사용한 VS Code Extension 동작 확인

VS Code는 확장을 개발하고 디버깅할 수 있도록 Extension Host라는 별도의 실행 환경을 제공합니다.
이 환경은 확장을 격리된 상태에서 실행하며, 디버거를 통해 확장의 코드를 단계별로 추적할 수 있게 합니다.

디버깅과 launch.json의 관계

launch.json은 VS Code 디버거의 설정 파일로, 확장 디버깅을 어떻게 실행할지 정의합니다.
이 설정 파일 덕분에, 개발 중인 확장을 디버깅할 수 있습니다.

// A launch configuration that compiles the extension and then opens it inside a new window
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
{
"version": "0.2.0",
"configurations": [
{
"name": "Run Extension",
"type": "extensionHost",
"request": "launch",
"args": ["--extensionDevelopmentPath=${workspaceFolder}"],
"outFiles": ["${workspaceFolder}/out/**/*.js"],
"preLaunchTask": "${defaultBuildTask}"
}
]
}

VS Code Extension 작동 간단하게 체험하기

export function activate(context: vscode.ExtensionContext) {
console.log("축하합니다! 당신의 첫 VS Code Extension");

const disposable = vscode.commands.registerCommand(
"wonkooklee-ext.helloWorld",
() => {
vscode.window.showErrorMessage(
"안녕하세요, VSCode Extension 처음 띄워보아요!"
);
}
);

context.subscriptions.push(disposable);
}

registerCommand를 사용하여 임의의 명령어 helloWorld를 입력하면 두 번째 인자로 전달된 콜백이 실행되도록 만들어봅니다.

커맨드 팔레트를 열어 Debug: Start Debugging을 통해 새로운 실행 환경을 열어줍니다. 그럼 아래와 같이 새로운 VS Code 실행 환경이 만들어집니다.

새로운 실행 환경에서 Command + Shift + P 단축키를 사용하여 커맨드 팔레트를 열어준 뒤 임의로 설정했던 Exntesion Commands를 입력해줍니다.

커맨드 팔레트에 커스텀 명령을 쳐봅니다.
커맨드 팔레트에 커스텀 명령을 쳐봅니다.

showErrorMessage는 이렇게 동작하는군요.




ollama JavaScript SDK를 사용해서 로컬 호스트 DeepSeek과 API 연결

아 진짜 귀엽네..
아 진짜 귀엽네..

ollama에서는 파이썬과 자바스크립트로 ollama와 통할할 수 있는 SDK를 제공합니다.

VS Code Extension이 실행되었을 때 ollama와 통신할 수 있도록 SDK를 설치해줍니다.

$ npm install ollama

이제 registerCommand 콜백 함수 내부 내용을 용도에 맞춰 수정해봅니다.

export function activate(context: vscode.ExtensionContext) {
console.log(
'Congratulations, your extension "wonkooklee-ext" is now active!'
);

const disposable = vscode.commands.registerCommand(
"wonkooklee-ext.helloWorld",
() => {
// 1
const panel = vscode.window.createWebviewPanel(
"deepChat",
"Deep Seek Chat",
vscode.ViewColumn.One,
{ enableScripts: true }
);

// 2
panel.webview.html = getWebviewContent();

panel.webview.onDidReceiveMessage(async (message: any) => {
if (message.command === "chat") {
const userPrompt = message.text;
let responseText = "";

try {
const streamResponse = await ollama.chat({
model: "deepseek-r1:7b",
messages: [{ role: "user", content: userPrompt }],
stream: true,
});

for await (const part of streamResponse) {
responseText += part.message.content.replace(/\<\/?think\>/g, "");
panel.webview.postMessage({
command: "chatResponse",
text: responseText,
});
}
} catch (error) {
panel.webview.postMessage({
command: "chatResponse",
text: `Error: ${String(error)}`,
});
}
}
});
}
);

context.subscriptions.push(disposable);
}

이 코드는 사용자 명령(wonkooklee-ext.helloWorld) 실행 시 WebView 창을 띄우고, 사용자가 입력한 내용을 기반으로 Ollama의 deepseek-r1:7b 모델과 상호작용하여 응답을 표시하는 기능을 제공합니다. 주요 부분을 단계별로 설명하겠습니다.


1. WebView 생성 (vscode.window.createWebviewPanel)

const panel = vscode.window.createWebviewPanel(
"deepChat",
"Deep Seek Chat",
vscode.ViewColumn.One,
{ enableScripts: true }
);
  • 새로운 WebView 창을 생성합니다.
  • "deepChat"은 WebView의 내부 식별자, "Deep Seek Chat"은 창 제목입니다.
  • vscode.ViewColumn.One은 WebView가 VS Code 창의 첫 번째 열에 표시되도록 지정합니다.
  • { enableScripts: true }로 WebView에서 JavaScript 실행을 허용합니다.

2. WebView의 HTML 설정

panel.webview.html = getWebviewContent();
  • getWebviewContent 함수에서 생성된 HTML 콘텐츠를 WebView에 렌더링합니다.
  • WebView의 UI 및 사용자 상호작용을 정의합니다.

3. WebView 메시지 수신 처리

panel.webview.onDidReceiveMessage(async (message: any) => { ... });
  • WebView에서 보내는 메시지를 수신하는 이벤트 리스너를 설정합니다.
  • 메시지가 "chat" 명령일 경우, 사용자 입력(message.text)을 처리하여 Ollama 모델과 상호작용합니다.

4. Ollama 모델과 상호작용

const streamResponse = await ollama.chat({
model: "deepseek-r1:7b",
messages: [{ role: "user", content: userPrompt }],
stream: true,
});
  • Ollama의 deepseek-r1:7b 모델과 상호작용하여 사용자 입력(userPrompt)에 대한 응답을 스트리밍 방식으로 받습니다.
  • messages 배열에는 사용자 입력 내용과 관련된 대화가 포함됩니다.

5. 스트리밍 응답 처리

for await (const part of streamResponse) {
responseText += part.message.content.replace(/\<\/?think\>/g, "");
panel.webview.postMessage({
command: "chatResponse",
text: responseText,
});
}
  • 스트리밍 방식으로 응답을 받으며, 응답에서 <think></think> 태그를 제거한 후 responseText에 추가합니다.
  • WebView로 처리된 응답을 메시지(chatResponse 커맨드)로 전송하여 화면에 표시합니다.

6. 확장 리소스 등록

context.subscriptions.push(disposable);
  • disposable 객체를 context.subscriptions에 추가하여, 확장이 비활성화될 때 리소스를 자동으로 정리하도록 합니다.

7. 웹뷰 화면 등록

사용자와 상호작용을 하기 위한 웹뷰 화면을 간단하게 인라인 HTML로 마크업 합니다.

각 요소에 상호작용 이벤트를 바인딩하는 작업도 스크립트 태그 안에 넣어줍니다.

function getWebviewContent(): string {
return /*html*/ `
<!DOCTYPE html>
<html>
<meta charset="UTF-8" />
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
#container {
width: 100%;
display: flex;
flex-direction: column;
padding: 24px;
}
h2 {
margin: 8px 0 16px;
}
#prompt {
padding: 16px;
border-radius: 12px;
}
#askBtn {
margin-top: 4px;
height: 48px;
font-size: 18px;
border-radius: 12px;
border: none;
background-color: #1a73e8;
color: #fff;
cursor: pointer;
}
#askBtn:hover {
background-color: #174ea6;
}
#response {
padding-top: 24px;
line-height: 140%;
font-size: 18px;
word-break: keep-all;
}
</style>
<body>
<div id="container">
<h2>Local Deepseek VS Code Extension</h2>
<textarea id="prompt" rows="5" placeholder="Ask something..."></textarea
><br />
<button id="askBtn">Ask</button>
<div id="response"></div>
</div>
<script>
const vscode = acquireVsCodeApi();

document.getElementById('askBtn').addEventListener('click', () => {
const text = document.getElementById('prompt').value;
vscode.postMessage({ command: 'chat', text });
})

window.addEventListener('message', event => {
const {command, text} = event.data;
if (command === 'chatResponse') {
document.getElementById('response').innerTextㅎ = text;
}
})
</script>
</body>
</html>
`;
}

Extension이 정상적으로 실행되면 아래와 같은 화면이 보입니다.

비록 로컬 환경이지만 VS Code Extension 창을 띄워 LLM 모델과 대화를 할 수 있습니다.

좋은 사람들과 재미있는 일을 하며 열정적이고 즐겁게 살고 싶은 개발자