LLM

WebLLM 완전 가이드 — 서버 없이 브라우저에서 LLM 실행하기

cell-devlog 2026. 5. 19. 09:05
반응형

API 키 없고, 서버 없고, 토큰 비용 없습니다. Llama·Gemma·Phi가 사용자 브라우저 GPU에서 직접 돌아갑니다. 프롬프트가 외부로 나가지 않습니다. 2026년 기준 브라우저가 AI 추론 런타임이 됐습니다.

[핵심 요약]
→ WebLLM: MLC AI(Carnegie Mellon·SJTU·NVIDIA)가 만든 오픈소스 브라우저 LLM 라이브러리
→ 동작 원리: WebGPU로 GPU 직접 접근 → 네이티브 수준 추론 속도
→ OpenAI API 호환: chat.completions.create() 그대로 사용
→ 지원 모델: Llama 3.2, Phi-3.5, Gemma 2, Mistral, Qwen 등
→ 브라우저 지원: Chrome·Edge·Firefox·Safari 기본 활성화 (2025년 말부터)
→ 대안: Transformers.js (Hugging Face), Chrome Built-in AI (Gemini Nano 내장)
→ 한계: 첫 로드 500MB~2GB 다운로드, 모바일 미지원, WebGPU 필요
→ 최적 케이스: 프라이버시 필수 앱, 오프라인 PWA, Chrome 익스텐션

 


왜 지금 브라우저에서 LLM이 가능한가

2023~2024 (불가능):
→ WebGL — 그래픽 전용, 행렬 연산 불가
→ 모델 크기 수십GB → 브라우저 메모리 초과
→ 브라우저 GPU 접근 API 없음

2026 (가능):
→ WebGPU — GPGPU 연산 지원, CUDA·Metal 대신 단일 API
→ 양자화(Quantization) — 7B 모델을 2~4GB로 압축
→ 스트리밍 가중치 로딩 — 전체 다운 전 추론 시작
→ Chrome·Edge·Firefox·Safari 모두 WebGPU 기본 활성화
→ 소비자 GPU 성능 향상 — M1/M2/M3, RTX 30/40 시리즈
[3가지 브라우저 LLM 선택지]

WebLLM:
→ 최고 성능 (WebGPU 풀 활용)
→ 모델 선택 자유 (Llama, Phi, Gemma 등)
→ OpenAI API 호환
→ 500MB~2GB 첫 다운로드

Transformers.js (Hugging Face):
→ WebGPU + WebAssembly 둘 다 지원
→ Hugging Face 모델 직접 사용
→ 임베딩, 분류, 번역 등 다양한 태스크
→ 더 넓은 브라우저 호환성

Chrome Built-in AI (Gemini Nano):
→ 다운로드 없음 (Chrome에 내장)
→ Chrome 전용
→ 모델 선택 불가 (Gemini Nano 고정)
→ 가장 빠른 시작, 가장 제한적

실전 1 — WebLLM 기본 세팅

# npm 프로젝트
npm install @mlc-ai/web-llm

# CDN (빠른 프로토타이핑)
# <script type="module"> 안에서 import
// 3줄로 시작하는 WebLLM
import { CreateMLCEngine } from "@mlc-ai/web-llm";

// 모델 로드 (첫 실행 시 다운로드, 이후 IndexedDB 캐시)
const engine = await CreateMLCEngine("Llama-3.2-1B-Instruct-q4f16_1-MLC");

// OpenAI API와 완전 동일한 인터페이스
const reply = await engine.chat.completions.create({
  messages: [
    { role: "system", content: "한국어로 답변해줘." },
    { role: "user", content: "WebLLM이 뭐야?" }
  ],
});
console.log(reply.choices[0].message.content);
// 진행률 표시 + 스트리밍 응답
import { CreateMLCEngine } from "@mlc-ai/web-llm";

async function loadModel() {
  const engine = await CreateMLCEngine(
    "Llama-3.2-3B-Instruct-q4f16_1-MLC",
    {
      // 다운로드 진행률 콜백
      initProgressCallback: (progress) => {
        const pct = Math.round(progress.progress * 100);
        document.getElementById("progress").textContent =
          `모델 로딩 중... ${pct}% (${progress.text})`;
      }
    }
  );
  return engine;
}

async function streamChat(engine, userMessage) {
  const chunks = await engine.chat.completions.create({
    messages: [{ role: "user", content: userMessage }],
    stream: true,  // 스트리밍 활성화
  });

  let fullResponse = "";
  for await (const chunk of chunks) {
    const delta = chunk.choices[0]?.delta?.content ?? "";
    fullResponse += delta;
    // 실시간 UI 업데이트
    document.getElementById("output").textContent = fullResponse;
  }
  return fullResponse;
}
[모델 선택 가이드 — 2026년 기준]

빠름 + 가벼움 (저사양 GPU):
→ Llama-3.2-1B-Instruct-q4f16_1-MLC    (~700MB)
→ Phi-3.5-mini-instruct-q4f16_1-MLC    (~2.2GB)
→ 추론 속도: 15~30 tok/s (M2 MacBook 기준)

균형 (일반 GPU):
→ Llama-3.2-3B-Instruct-q4f16_1-MLC    (~1.8GB)
→ Gemma-2-2b-it-q4f16_1-MLC            (~1.4GB)
→ 추론 속도: 10~20 tok/s

고성능 (RTX 3080+ / M3 Pro+):
→ Llama-3.1-8B-Instruct-q4f32_1-MLC    (~4.5GB)
→ Mistral-7B-Instruct-v0.3-q4f16_1-MLC (~4.1GB)
→ 추론 속도: 5~15 tok/s

실전 2 — Web Worker로 UI 블로킹 방지

LLM 추론은 무겁습니다. 메인 스레드에서 돌리면 UI가 얼어붙습니다. Web Worker로 분리합니다.

// worker.js — 별도 워커 파일
import { CreateMLCEngine } from "@mlc-ai/web-llm";

let engine = null;

self.onmessage = async (event) => {
  const { type, payload } = event.data;

  if (type === "LOAD_MODEL") {
    engine = await CreateMLCEngine(payload.model, {
      initProgressCallback: (progress) => {
        // 메인 스레드로 진행률 전송
        self.postMessage({
          type: "PROGRESS",
          payload: { pct: Math.round(progress.progress * 100) }
        });
      }
    });
    self.postMessage({ type: "MODEL_READY" });
  }

  if (type === "CHAT") {
    const chunks = await engine.chat.completions.create({
      messages: payload.messages,
      stream: true,
    });

    for await (const chunk of chunks) {
      const delta = chunk.choices[0]?.delta?.content ?? "";
      if (delta) {
        self.postMessage({ type: "CHUNK", payload: { delta } });
      }
    }
    self.postMessage({ type: "DONE" });
  }
};
// main.js — 메인 스레드
const worker = new Worker(new URL("./worker.js", import.meta.url), {
  type: "module"
});

// 워커 메시지 처리
worker.onmessage = (event) => {
  const { type, payload } = event.data;

  switch (type) {
    case "PROGRESS":
      updateProgressBar(payload.pct);
      break;
    case "MODEL_READY":
      enableChatUI();
      break;
    case "CHUNK":
      appendToOutput(payload.delta);  // 실시간 스트리밍
      break;
    case "DONE":
      finalizeChatMessage();
      break;
  }
};

// 모델 로드
worker.postMessage({
  type: "LOAD_MODEL",
  model: "Llama-3.2-3B-Instruct-q4f16_1-MLC"
});

// 채팅 전송
function sendMessage(userInput) {
  worker.postMessage({
    type: "CHAT",
    payload: {
      messages: [
        { role: "system", content: "친절하게 답변해줘." },
        { role: "user", content: userInput }
      ]
    }
  });
}

실전 3 — React 앱에 통합

// useWebLLM.ts — 커스텀 훅
import { useState, useEffect, useRef } from "react";
import { CreateMLCEngine, MLCEngine } from "@mlc-ai/web-llm";

interface Message {
  role: "user" | "assistant" | "system";
  content: string;
}

export function useWebLLM(modelId: string) {
  const [engine, setEngine] = useState<MLCEngine | null>(null);
  const [loading, setLoading] = useState(false);
  const [progress, setProgress] = useState(0);
  const [streaming, setStreaming] = useState(false);

  useEffect(() => {
    setLoading(true);
    CreateMLCEngine(modelId, {
      initProgressCallback: (p) => setProgress(Math.round(p.progress * 100))
    }).then((eng) => {
      setEngine(eng);
      setLoading(false);
    });
  }, [modelId]);

  const chat = async (
    messages: Message[],
    onChunk: (delta: string) => void
  ) => {
    if (!engine) return;
    setStreaming(true);

    const chunks = await engine.chat.completions.create({
      messages,
      stream: true,
    });

    for await (const chunk of chunks) {
      const delta = chunk.choices[0]?.delta?.content ?? "";
      if (delta) onChunk(delta);
    }
    setStreaming(false);
  };

  return { engine, loading, progress, streaming, chat };
}

// ChatApp.tsx
export function ChatApp() {
  const { loading, progress, streaming, chat } = useWebLLM(
    "Phi-3.5-mini-instruct-q4f16_1-MLC"
  );
  const [messages, setMessages] = useState<Message[]>([]);
  const [input, setInput] = useState("");
  const [response, setResponse] = useState("");

  const handleSend = async () => {
    const userMsg: Message = { role: "user", content: input };
    const history = [...messages, userMsg];
    setMessages(history);
    setInput("");
    setResponse("");

    await chat(history, (delta) => {
      setResponse(prev => prev + delta);
    });

    setMessages(prev => [
      ...prev,
      { role: "assistant", content: response }
    ]);
  };

  if (loading) return <div>모델 로딩 중... {progress}%</div>;

  return (
    <div>
      <div className="messages">
        {messages.map((m, i) => (
          <div key={i} className={m.role}>{m.content}</div>
        ))}
        {streaming && <div className="assistant">{response}</div>}
      </div>
      <input value={input} onChange={e => setInput(e.target.value)} />
      <button onClick={handleSend} disabled={streaming}>전송</button>
    </div>
  );
}

실전 4 — Chrome 익스텐션 AI

WebLLM의 강력한 사용 케이스입니다. 백그라운드 서비스 워커에서 LLM을 돌리고 모든 탭이 공유합니다.

// background.js (Service Worker)
import { CreateMLCEngine } from "@mlc-ai/web-llm";

let engine = null;

// 익스텐션 설치 시 모델 로드
chrome.runtime.onInstalled.addListener(async () => {
  engine = await CreateMLCEngine("Phi-3.5-mini-instruct-q4f16_1-MLC");
  console.log("WebLLM 준비 완료");
});

// content script / popup에서 메시지 수신
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
  if (request.type === "SUMMARIZE") {
    (async () => {
      const reply = await engine.chat.completions.create({
        messages: [
          {
            role: "user",
            content: `다음 텍스트를 3줄로 요약해줘:\n\n${request.text}`
          }
        ],
      });
      sendResponse({ summary: reply.choices[0].message.content });
    })();
    return true; // 비동기 응답
  }
});
// content.js — 웹페이지에서 선택한 텍스트 요약
document.addEventListener("mouseup", async () => {
  const selected = window.getSelection()?.toString().trim();
  if (!selected || selected.length < 100) return;

  const response = await chrome.runtime.sendMessage({
    type: "SUMMARIZE",
    text: selected
  });

  // 선택 영역 옆에 요약 툴팁 표시
  showTooltip(response.summary);
});
[Chrome 익스텐션 WebLLM의 장점]
→ 사용자 텍스트 서버 전송 없음 — 완전 프라이버시
→ 모델 한 번 로드 → 모든 탭 재사용
→ 오프라인 동작
→ API 비용 0원
→ 사용 케이스: 페이지 요약, 문법 교정, 번역, 코드 설명

WebGPU 미지원 폴백 — Transformers.js

WebGPU 없는 환경 (구형 브라우저, 일부 모바일)을 위한 폴백입니다.

// webgpu-check.js — 환경 감지 + 자동 폴백
async function createAIEngine() {
  const hasWebGPU = "gpu" in navigator;

  if (hasWebGPU) {
    // WebGPU 지원 → WebLLM 사용
    const { CreateMLCEngine } = await import("@mlc-ai/web-llm");
    return await CreateMLCEngine("Llama-3.2-1B-Instruct-q4f16_1-MLC");
  } else {
    // WebGPU 미지원 → Transformers.js (WebAssembly 폴백)
    const { pipeline } = await import("@xenova/transformers");
    return await pipeline("text-generation", "Xenova/Phi-3-mini-4k-instruct");
  }
}

// Chrome Built-in AI 감지
async function checkChromeBuiltinAI() {
  if ("ai" in window && "languageModel" in window.ai) {
    const status = await window.ai.languageModel.availability();
    if (status === "available") {
      // Gemini Nano 내장 — 다운로드 없음
      return await window.ai.languageModel.create();
    }
  }
  return null;
}

마무리

✅ WebLLM 써야 하는 경우
→ 사용자 데이터가 서버로 나가면 안 되는 앱 (의료, 법률, 금융)
→ API 비용 없애고 싶은 고트래픽 서비스
→ 오프라인 동작 필수 PWA
→ Chrome 익스텐션에 AI 추가
→ 토큰 비용 없이 무제한 추론 가능한 프로토타입

❌ WebLLM이 안 맞는 경우
→ 모바일 사용자가 많은 서비스 (WebGPU 모바일 미지원)
→ 최신 GPT/Claude 수준 품질 필요 (소형 모델 한계)
→ 첫 로드 2GB 다운로드가 UX에 치명적인 경우
→ 서버사이드 RAG, 외부 도구 연결 필요한 에이전트
→ 실시간 최신 정보 필요한 서비스

관련 글

 

반응형