Posts

브라우저에서 AI를 돌릴 수 있다고요?

2026-05-26

"AI를 서버 없이 돌린다고요?"

사이드 프로젝트를 만들다가 AI 리뷰 기능을 붙이고 싶어졌어요. 변환된 코드를 보고 "여기 모바일에서 깨질 수 있어요" 같은 한국어 코멘트를 자동으로 달아주는 기능이요. 자연스럽게 OpenAI나 Anthropic API를 떠올렸죠.

그런데 사이드 프로젝트이니 최대한 비용이 안 나가는 방법을 찾고 싶었어요.

대안을 찾다가 Transformers.js라는 라이브러리를 발견했습니다. 처음 봤을 때 한 줄 설명이 브라우저에서 Hugging Face 모델을 서버 없이 실행이었어요. 이게 무슨 소리야 싶었죠. AI 모델을 브라우저가 어떻게 돌려?

처음 데모를 직접 띄워봤을 때 신기하게도 350MB짜리 모델을 사용자 브라우저에 한 번 다운받아 두면, 그 다음부터는 인터넷도 안 끊기면 서버 없이 돈다는 거예요.

AI 모델이란 ?

여기서 한번 AI 모델을 간략하게 설명하고 넘어가겠습니다.

LLM(Large Language Model, 우리가 흔히 말하는 AI)을 프로그램이라고 생각하시면 헷갈려요. 이건 엄청나게 큰 숫자 묶음이에요. 수십억 개의 숫자(가중치라고 불러요)가 일정한 구조로 배치된 거예요.

이 숫자 묶음에 "안녕하세요" 같은 입력을 흘려보내면, 일련의 계산이 일어나서 "반갑습니다" 같은 출력이 나와요. 프로그램이 아니라 계산기에 가까운 거죠.

그래서 AI 모델 파일은 보통 수백 MB ~ 수십 GB예요. 코드가 아니라 학습된 숫자들이거든요.

이 사실이 중요한 이유는, 누가 돌려도 결과가 같다는 거예요. OpenAI 서버가 돌리든, 내 노트북이 돌리든, 같은 모델에 같은 입력을 넣으면 같은 출력이 나와요.

왜 보통 AI를 서버에서 돌릴까

이유는 단순해요. 그 수십억 개의 숫자 계산이 무거우니까요.

GPT-4 같은 거대 모델은 수천억 개의 숫자를 갖고 있고, 답변 한 번 만들려면 GPU 여러 장이 협력해야 해요. 내 노트북에서는 돌릴 수가 없어요. 그래서 OpenAI가 자기네 데이터센터에서 돌리고, 우리는 API로 결과만 받아오죠.

근데 모델이 작으면 사정이 달라져요. 5억 개(0.5B) 정도 되는 작은 모델은 요즘 노트북·핸드폰에서도 돌아가요. 답변 품질이 GPT-4만큼은 아니지만, 특정 작업에는 충분히 쓸 만한 수준이에요.

이런 작은 모델들을 SLM(Small Language Model)이라고 불러요. 그리고 이런 SLM을 브라우저에서 돌리게 해주는 도구가 Transformers.js예요.

Transformers.js의 정체

원래 Hugging Face라는 회사가 파이썬용 transformers라는 라이브러리를 만들었어요. AI 모델을 쉽게 가져다 쓸 수 있게 해주는 도구인데, 사실상 업계 표준이에요. 모델 하나 쓰려면 코드 세 줄이면 끝나요.

Transformers.js는 같은 걸 자바스크립트로 옮긴 거예요. 파이썬 코드랑 거의 똑같은 API로 브라우저에서 모델을 돌릴 수 있어요. 파이썬 코드는 이렇게 생겼는데:

from transformers import pipeline
generator = pipeline("text-generation", "Qwen/Qwen2.5-0.5B-Instruct")
result = generator("안녕하세요")

자바스크립트도 거의 똑같아요:

import { pipeline } from "@huggingface/transformers";
const generator = await pipeline(
  "text-generation",
  "Qwen/Qwen2.5-0.5B-Instruct",
);
const result = await generator("안녕하세요");

이게 파이썬으로 익숙한 사람이 자바스크립트로 넘어갈 때도 좋고, 모델 비교를 같은 코드로 양쪽에서 할 수 있다는 점도 좋아요.

모델 파일은 어디서 오는가

신기한 부분이에요. pipeline("text-generation", "Qwen/Qwen2.5-0.5B-Instruct") 이 코드를 실행하면 그 순간 모델 파일이 어디선가 다운받아져요. 어디서 올까요?

Hugging Face Hub라는 곳에서요. 깃허브 같은 건데, 코드가 아니라 AI 모델을 호스팅하는 사이트예요. 누구나 모델을 올릴 수 있고, 다운받을 수 있어요. 무료고요.

다운받은 모델은 브라우저 IndexedDB(브라우저 안의 작은 데이터베이스)에 저장돼요. 첫 방문 때만 350MB를 받고, 그 다음부터는 즉시 로드예요.

저는 이 동작을 처음 봤을 때 npm install 같은 느낌이라고 생각했어요. 라이브러리를 한 번 받아두면 그 다음부터는 import만으로 쓸 수 있는 것처럼, 모델도 한 번 받아두면 그 다음부터는 즉시 쓸 수 있는 거죠. 다른 점은 npm처럼 미리 받는 게 아니라, 실행 시점에 받는다는 거예요.

WebGPU — 이게 진짜 돌아가는 이유

여기서 한 가지 의문이 들 거예요. 노트북에서 돌릴 정도로 작은 모델이라고 해도, 5억 개 숫자 계산은 여전히 무거워요. 자바스크립트가 그걸 어떻게 빠르게 처리하죠?

여기서 등장하는 게 WebGPU예요.

WebGPU는 브라우저가 GPU(그래픽카드)를 직접 쓰게 해주는 표준이에요. 원래 브라우저는 안전을 위해 시스템 자원에 직접 접근 못 하게 막혀 있는데, GPU만큼은 예외적으로 쓸 수 있게 열어준 거예요. 게임이나 3D 그래픽 때문에 시작된 표준인데, GPU가 잘하는 게 결국 대량의 숫자 계산이라 AI랑 궁합이 좋아요.

Transformers.js v3에 WebGPU 지원이 들어왔는데, WebGPU 지원 덕분에 모델 로딩 시 device: 'webgpu' 옵션 하나로 GPU 가속을 켤 수 있고, WASM(CPU 실행) 대비 최대 100배까지 빨라질 수 있어요.

이게 작은 모델이 브라우저에서 실용적인 속도로 동작하는 결정적 이유예요. GPU 없이 CPU만으로 돌리면 답변 한 번에 30초씩 걸리지만, WebGPU 켜면 1~2초로 떨어져요.

실제로 어떻게 쓰는가

설치

npm install @huggingface/transformers

이게 전부예요. 모델 파일은 아직 안 받아요. 라이브러리만 받아져요.

가장 간단한 실행

import { pipeline } from "@huggingface/transformers";

// 1. 모델 로드 (첫 호출 시 다운로드 시작)
const generator = await pipeline(
  "text-generation",
  "Qwen/Qwen2.5-0.5B-Instruct",
  { device: "webgpu", dtype: "q4" },
);

// 2. 추론
const output = await generator(
  "변환된 코드를 보고 한국어로 한 줄 코멘트:\n" + convertedCode,
  { max_new_tokens: 100 },
);

console.log(output[0].generated_text);

여기 두 가지 옵션이 핵심이에요.

  • device: "webgpu" — GPU 가속. 없으면 자동으로 CPU(WASM)로 떨어져요.
  • dtype: "q4" — 양자화(quantization)예요. 모델 파일의 숫자 정밀도를 낮춰서 크기를 줄이고 속도를 올리는 기법. q4면 4비트, q8이면 8비트. 정밀도가 낮을수록 가볍지만 답변 품질이 살짝 떨어져요. 작은 모델일수록 q8이 안전하고, 큰 모델은 q4도 충분할 때가 많아요.

모델 다운로드 진행률 보여주기

위 코드의 문제는 첫 실행 때 사용자가 350MB를 기다린다는 거예요. 이걸 해결하려면 진행률 콜백을 받아야 해요.

const generator = await pipeline(
  "text-generation",
  "Qwen/Qwen2.5-0.5B-Instruct",
  {
    device: "webgpu",
    dtype: "q4",
    progress_callback: (progress) => {
      // status: 'initiate' | 'download' | 'progress' | 'done' | 'ready'
      // file: 다운로드 중인 파일명 (모델은 보통 여러 파일로 나뉘어요)
      // progress: 0 ~ 100
      console.log(progress.status, progress.file, progress.progress);
    },
  },
);

이걸 UI 상태에 연결해서 진행률 바로 보여주면 사용자가 멍하니 기다리지 않아요. "어디서 멈춰 있나"가 아니라 "지금 X% 받는 중이구나"로 인식이 바뀌어요.

WebGPU 없는 환경 대비

WebGPU는 2026년 기준으로도 모든 브라우저에서 다 되지는 않아요. Safari는 최근에야 켜졌고, 모바일 브라우저는 더 늦어요. 그래서 WebGPU가 안 되면 자동으로 WASM(CPU)으로 떨어지는 코드가 필요합니다.

async function loadModel(modelName: string) {
  try {
    // 먼저 WebGPU 시도
    return await pipeline("text-generation", modelName, {
      device: "webgpu",
      dtype: "q4",
    });
  } catch (e) {
    // 실패하면 WASM (CPU)
    console.warn("WebGPU 안 됨, CPU로 떨어집니다", e);
    return await pipeline("text-generation", modelName, {
      device: "wasm",
    });
  }
}

Web Worker로 분리 — 사실상 필수

여기까지 봤으면 코드가 잘 돌아가긴 할 거예요. 근데 한 가지 더 짚고 가야 해요.

위 코드를 그냥 React 컴포넌트에 박으면 모델 다운로드 350MB 동안 화면이 얼어붙어요. 클릭도 안 먹고, 스크롤도 안 되고, 탭이 죽은 줄 알게 돼요. 자바스크립트는 한 번에 한 가지 일만 하니까요.

이걸 해결하려면 Web Worker라는 걸 써야 해요. 별도 스레드에서 모델을 돌리고, 메인 스레드는 UI만 담당하는 구조예요.

// worker.ts (백그라운드에서 도는 코드)
import { pipeline } from "@huggingface/transformers";

let generator = null;

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

  if (type === "LOAD") {
    generator = await pipeline("text-generation", payload.modelName, {
      device: "webgpu",
      dtype: "q4",
      progress_callback: (p) =>
        self.postMessage({ type: "PROGRESS", payload: p }),
    });
    self.postMessage({ type: "READY" });
  }

  if (type === "GENERATE") {
    const output = await generator(payload.prompt, { max_new_tokens: 200 });
    self.postMessage({ type: "RESULT", payload: output });
  }
});
// 메인 컴포넌트
const worker = new Worker(new URL("./worker.ts", import.meta.url), {
  type: "module",
});

worker.addEventListener("message", (event) => {
  const { type, payload } = event.data;
  if (type === "PROGRESS") setProgress(payload.progress);
  if (type === "READY") setReady(true);
  if (type === "RESULT") setReview(payload[0].generated_text);
});

// 사용
worker.postMessage({
  type: "LOAD",
  payload: { modelName: "Qwen/Qwen2.5-0.5B-Instruct" },
});
worker.postMessage({
  type: "GENERATE",
  payload: { prompt: "변환된 코드: ..." },
});

코드가 좀 늘어났지만, 이렇게 분리하면 모델 다운로드 중에도 UI가 살아 있어요. 진행률 바도 부드럽게 움직이고, 사용자가 다른 동작도 할 수 있어요. 무거운 일을 다루는 라이브러리를 쓸 때는 거의 패턴이에요.

모델은 어디서 고르나

마지막으로 어떤 모델을 쓰느냐가 결과 품질을 좌우해요. Hugging Face Hub에서 Transformers.js 호환 모델만 따로 모아둔 페이지가 있어요. ONNX 형식으로 변환된 것만 돌아가거든요.

huggingface.co/models?library=transformers.js 이 페이지에서 필터 걸어서 찾을 수 있어요.

선택할 때 고려할 점:

  • 모델 크기 — 0.5B(약 350MB) / 1B(약 700MB) / 1.5B(약 1GB) 정도가 브라우저에서 실용적인 상한이에요. 그 이상은 모바일에서 메모리 부족으로 죽어요.
  • 한국어 지원 — 모델 카드(README)에 학습 데이터 언어가 적혀 있어요. 한국어가 명시된 모델을 골라야 한국어 출력이 자연스러워요. 영어만 학습한 모델은 한국어로 추론하면 횡설수설해요.
  • Instruction-tuned인가 — 모델 이름 끝에 -Instruct-Chat이 붙은 게 대화·지시 따르기에 맞춰 학습된 거예요. 베이스 모델은 문장 이어쓰기만 하니까 우리가 원하는 답변을 안 줘요.

이 세 가지 기준으로 보면 2026년 기준 Qwen2.5 시리즈나 Gemma3 시리즈가 무난해요. 둘 다 한국어가 되고, 0.5B ~ 3B 크기로 여러 옵션이 있어요.

정리하면

처음에는 AI는 OpenAI나 Anthropic API로 부르는 것이라고 생각했습니다. 근데 사이드 프로젝트 하면서 알게 된 건, 프로젝트 성격에 따라 SLM을 직접 돌리는 게 더 맞는 경우가 있다는 거예요.

특히:

  • 코드를 외부로 보내면 안 되는 도구
  • 오프라인에서도 돌아야 하는 도구
  • 호출 비용을 0으로 만들고 싶은 도구
  • 프라이버시가 핵심 가치인 도구

이런 경우엔 Transformers.js사용하는걸 추천드립니다.

참고 자료