IndexedDB와 Ollama를 활용한 로컬 우선(Local-First) Chrome 확장 프로그램 구축하기
요약
Ollama와 IndexedDB를 활용하여 개인정보를 보호하는 로컬 우선(Local-First) Chrome 확장 프로그램을 구축하는 방법을 소개합니다. 클라우드 전송 없이 브라우저 내에서 웹 페이지를 Markdown으로 인덱싱하고 로컬 LLM과 채팅할 수 있는 아키텍처를 다룹니다.
핵심 포인트
- Ollama를 통한 로컬 LLM 직접 연결으로 데이터 프라이버시 확보
- 대용량 데이터 저장을 위해 LocalStorage 대신 IndexedDB 활용
- Manifest V3 기반의 바닐라 JS 아키텍처로 가벼운 확장 프로그램 구현
- 웹 페이지를 Markdown으로 변환하여 로컬 인덱싱 및 저장
대부분의 현대적인 브라우저 AI 독서 보조 도구들은 클라우드에서 작동합니다. 질문을 하거나 페이지를 읽을 때마다, 사용자의 브라우징 기록, 활성 탭, 그리고 질의 내용이 독점적인 백엔드 데이터베이스로 전송됩니다.
저는 제 컴퓨터에서 100% 로컬로 실행되는, 개인정보가 보호되고 구독료가 없는 독서 동반자를 원했습니다.
이를 해결하기 위해 저는 ContextBridge를 구축했습니다. 이 Chrome 확장 프로그램은 사이드바에 위치하며, 웹 페이지를 Markdown으로 인덱싱하고, 이를 로컬에 저장하며, Ollama를 통한 로컬 LLM(Large Language Models)을 사용하여 오프라인에서 해당 페이지들과 채팅할 수 있게 해줍니다.
이 글에서는 Chrome의 Manifest V3, IndexedDB, 그리고 Ollama를 사용하여 로컬 우선(Local-first) 확장 프로그램을 구축할 때의 아키텍처 선택 과정을 살펴보겠습니다.
아키텍처: 왜 브라우저에서 로컬 우선(Local-First)인가?
브라우저 샌드박스 내에서 로컬 우선 시스템을 구축하는 것은 독특한 도전 과제와 이점을 동시에 제공합니다:
[ Active Webpage ] ──(Ctrl+Shift+E)──> [ DOM-to-Markdown Extractor ]
│
▼
[ Ollama (localhost) ] <───(Direct API)─── [ Extension Sidebar ] <───> [ IndexedDB (Local Store) ]
백엔드 중개자 없음: 사이드바는 사용자의 로컬 Ollama 포트에 직접 연결됩니다. 클라우드 폴백(fallback, 예: Claude 또는 Gemini)을 위한 API 키는 엄격하게 chrome.storage.local에 저장됩니다.
LocalStorage 대신 IndexedDB 사용: 더 큰 문서 인덱스, 청크(chunked) 텍스트 및 메타데이터를 저장하기에는 localStorage는 너무 제한적입니다(5MB 제한 및 동기식 방식). IndexedDB는 비동기식의 대용량 구조화된 저장 공간을 제공합니다.
번들러 없음, 불필요한 요소 없음: 이 확장 프로그램은 외부 의존성 없이 바닐라 JS(vanilla JS)로 구축되어 로딩 시간이 즉각적입니다.
💾 웹 저장하기: IndexedDB 구현
대형 NPM 래퍼(wrapper)를 추가하지 않고 IndexedDB를 관리할 수 있는 깔끔한 유틸리티가 필요합니다. 여기 우리의 페이지들을 관리하는 간단하고 견고한 바닐라 자바스크립트(vanilla Javascript) DB 래퍼가 있습니다:
javascript
class ContextStore {
constructor() {
this.dbName = 'ContextBridgeDB';
this.dbVersion = 1;
this.db = null;
}
async init() {
return new Promise((resolve, reject) => {
const request = indexedDB.open(this.dbName, this.dbVersion);
request.onerror = () => reject(request.error);
request.onsuccess = () => {
this.db = request.result;
resolve(this);
};
request.onupgradeneeded = (event) => {
const db = event.target.result;
if (!db.objectStoreNames.contains('pages')) {
const store = db.createObjectStore('pages', { keyPath: 'url' });
store.createIndex('domain', 'meta.domain', { unique: false });
store.createIndex('indexedAt', 'indexedAt', { unique: false });
}
};
});
}
async savePage(pageData) {
return new Promise((resolve, reject) => {
const transaction = this.db.transaction(['pages'], 'readwrite');
const store = transaction.objectStore('pages');
const request = store.put({
...pageData,
indexedAt: Date.now()
});
request.onsuccess = () => resolve();
request.onerror = () => reject(request.error);
});
}
}
🤖 Ollama를 활용한 사이드바 직접 통합 (Direct Sidebar Integration)
Ollama는 로컬에서 아름다운 HTTP API를 노출합니다 (기본적으로 http://localhost:11434). 크롬 확장 프로그램의 사이드바에서 localhost로 통신할 때 발생하는 주요 문제는 CORS(Cross-Origin Resource Sharing)입니다.
다행히 Manifest V3에서는 백그라운드 스크립트와 사이드바 패널이 특별한 네트워크 권한을 가집니다. 확장 프로그램이 manifest.json에 적절한 호스트 권한(host permissions)을 요청하기만 하면, 표준 브라우저 CORS 제한을 우회하여 localhost에 직접 접근할 수 있습니다:
json
{
"permissions": [
"activeTab",
"storage",
"sidePanel"
],
"host_permissions": [
"http://localhost:11434/*"
]
}
여기서는 활성 페이지의 경계 컨텍스트(bounding context)를 가지고 로컬 모델(예: llama3 또는 mistral)로부터 응답을 스트리밍하는 데 사이드바에서 사용하는 정확한 fetch 로직이 있습니다:
javascript
async function* askLocalOllama(modelName, systemPrompt, userMessage) {
const response = await fetch('http://localhost:11434/api/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
model: modelName,
messages: [
{ role: 'system', content: systemPrompt },
{ role: 'user', content: userMessage }
],
stream: true
})
});
if (!response.ok) throw new Error('Ollama 연결 실패.');
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n');
buffer = lines.pop(); // 버퍼에 부분 라인 유지
for (const line of lines) {
if (!line.trim()) continue;
const json = JSON.parse(line);
if (json.message && json.message.content) {
yield json.message.content;
}
}
}
}
Lessons Learned
가능한 한 Vector-less로 유지하세요: 단일 페이지 채팅의 경우, 전체 벡터 임베딩을 브라우저에서 로드하는 것은 과도합니다. 의미적으로 청크된 텍스트를 LLM 컨텍스트 창에 직접 전달하는 것이 훨씬 빠르고 완전히 신뢰할 수 있습니다.
오프라인 우선(Offline-First)은 UX의 슈퍼파워입니다: 로딩 스피너나 오버레이 없이 확장 프로그램을 즉시 로드하면 매우 프리미엄하게 느껴집니다.
전체 오픈 소스 코드베이스는 여기에서 확인하세요: github.com/sujalmeena7/ContextBridge
이 내용이 유용했다면, 레포지토리에 별점을 주거나 아래 댓글에 질문을 남겨주세요!
[ContextBridge – Local RAG Companion - Chrome Web Store](https://chromewebstore.google.com/detail/contextbridge-%E2%80%93-local-rag/jokgmcedjecppdfnbicfonbmgjpbglko?authuser=0&%3Bhl=en-GB
웹 페이지 콘텐츠를 로컬에서 저장, 검색 및 채팅하세요. AI 채팅 (BYOK), 전체 텍스트 검색 (Full-text search), 마크다운 (Markdown) 내보내기, 그리고 선택 가능한 RAG 엔드포인트를 제공합니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기