글 목록

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

2026년 5월 26일 6분 읽기
Transformers.jsAI

사이드 프로젝트에 AI 리뷰 기능을 붙이고 싶었습니다. 변환된 코드를 보고 모바일에서 깨질 만한 부분을 한국어로 알려주는 기능이었습니다. 자연스럽게 외부 API부터 떠올렸지만, 호출 비용과 코드 전송이 부담스러웠습니다.

그러다 Transformers.js를 발견했습니다. 모델 파일을 브라우저로 내려받아 사용자 기기에서 추론할 수 있다는 설명이 눈에 들어왔습니다. 직접 실행해보니 가능성은 보였지만, 제품에 붙이려면 모델 다운로드부터 실패 처리까지 새로 책임져야 했습니다.

서버 호출을 없애면 비용이 어디로 갈까

브라우저 추론은 학습된 가중치와 모델 구조를 실행 런타임으로 계산하는 방식입니다. Transformers.js는 전처리와 후처리, 모델 로딩 같은 과정을 pipeline API로 묶고, ONNX Runtime을 통해 모델을 실행합니다. 자바스크립트로 수억 개의 숫자를 직접 반복 계산한다는 뜻은 아닙니다.

서버 추론과 브라우저 추론에서 모델, 입력, 연산이 놓이는 위치를 비교한 도식

서버 추론에서는 요청마다 입력이 서버로 가고 결과가 돌아옵니다. 브라우저 추론에서는 모델과 런타임을 먼저 받은 뒤 기기 안에서 입력을 처리할 수 있습니다. API 호출 비용은 줄일 수 있지만 다운로드 시간, 저장 공간, 메모리와 배터리 사용은 사용자의 기기로 이동합니다.

모델 파일을 한 번 받는 것과 오프라인에서 항상 실행되는 것도 다릅니다. 런타임 파일·토크나이저·모델이 모두 준비돼 있어야 하고, 저장 공간 부족이나 브라우저 정리 정책으로 캐시가 사라질 수도 있습니다. 민감한 코드가 밖으로 나가지 않는지 확인하려면 추론뿐 아니라 앱의 분석 이벤트와 오류 로그도 살펴봐야 합니다.

모델 이름을 바꾸는 것만으로는 실행되지 않았다

파이썬 Transformers와 Transformers.js의 pipeline API는 비슷하지만, 같은 모델 ID를 그대로 넣을 수 있다고 보장되지는 않습니다. 브라우저에서 사용하는 런타임에 맞는 ONNX 파일과 모델 아키텍처 지원이 필요합니다.

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

const generator = await pipeline(
  "text-generation",
  "onnx-community/Qwen2.5-0.5B-Instruct",
  { device: "webgpu", dtype: "q4" },
);

const result = await generator(
  [{ role: "user", content: "다음 코드의 잠재적인 문제를 설명해 주세요: ..." }],
  { max_new_tokens: 100, do_sample: false },
);
console.log(result);

이것은 ONNX 변환본을 사용하는 구성 예입니다. 설치한 Transformers.js 버전과 해당 저장소가 제공하는 파일 조합을 확인해야 합니다. 원본 파이썬 모델 저장소와 ONNX 변환 저장소를 구분하는 것부터 시작하면 파일을 못 찾는 오류를 줄일 수 있습니다. 예제 모델 저장소

q4는 낮은 정밀도로 저장·계산하는 양자화 선택입니다. 파일을 줄일 수 있지만 같은 파라미터 수라도 정밀도와 부가 파일에 따라 다운로드 크기가 달라집니다. 실제 추론 메모리에는 가중치뿐 아니라 중간 계산과 생성 문맥도 들어갑니다. 350MB 파일을 받았다고 메모리도 350MB만 필요하다고 보면 안 됩니다.

WebGPU가 있다고 모든 모델이 빠른 것은 아니다

WebGPU를 선택하면 GPU 연산을 사용할 수 있습니다. 하지만 브라우저 지원, GPU 드라이버, 모델의 연산 구성, 입력 길이에 따라 실행 가능 여부와 성능이 달라집니다. 같은 모델과 입력도 샘플링 설정·양자화·런타임의 수치 계산 차이에 따라 출력이 달라질 수 있습니다.

특히 WebGPU 선택이 실패했을 때 자동으로 원하는 WASM 구성으로 바뀐다고 가정해서는 안 됩니다. CPU 실행을 제공할지, 더 작은 모델을 제안할지, 해당 기능을 사용 불가로 표시할지 정책을 정해야 합니다. 단순히 모든 오류를 잡아 다시 로드하면 잘못된 모델 ID나 다운로드 실패도 GPU 문제로 보이게 됩니다. WebGPU 설정 가이드

처음 실험할 때는 한 모델과 한 실행 장치 조합부터 고정하는 편이 편합니다. 이후 같은 입력으로 WASM과 WebGPU의 첫 로딩, 첫 토큰까지의 시간, 전체 생성 시간을 따로 측정합니다. 다운로드가 빠른 재방문과 처음 방문을 섞어 평균을 내면 실제 사용자가 기다릴 시간을 놓치기 쉽습니다.

다운로드 대기와 메인 스레드 점유도 구분해야 했다

네트워크 다운로드 자체는 비동기이므로 파일이 크다는 이유만으로 자바스크립트가 그 시간 내내 화면을 막는 것은 아닙니다. 다만 모델 초기화, 전처리, CPU 추론 등에서 메인 스레드를 오래 쓰면 입력과 렌더링이 지연될 수 있습니다.

Worker는 이런 연산을 UI와 분리하는 수단입니다. 메모리 부족을 해결하거나 같은 계산을 무조건 빠르게 만드는 도구는 아닙니다. 메시지를 주고받고 런타임을 초기화하는 비용도 있습니다. 이 구분을 하고 나니 로딩 중 화면이 멈췄다는 증상에서 다운로드와 연산을 따로 확인할 수 있었습니다.

모델 준비가 끝나기 전에 생성 요청을 보내면 안 된다

Worker를 붙일 때는 로딩과 생성의 순서를 명시해야 합니다. 아래는 한 번에 한 요청만 처리하고, 로딩 완료 후 생성하도록 만든 축약 예제입니다. 예제의 오류 이벤트는 사용자에게 실패를 알리고 재시도할 수 있게 연결해야 합니다.

js
// worker.js
import { pipeline } from "@huggingface/transformers";

let generator = null;
let busy = false;

self.onmessage = async ({ data }) => {
  if (busy) {
    self.postMessage({ type: "ERROR", message: "이전 작업이 진행 중입니다." });
    return;
  }
  busy = true;
  try {
    if (data.type === "LOAD") {
      generator ??= await pipeline(
        "text-generation",
        "onnx-community/Qwen2.5-0.5B-Instruct",
        {
          device: "webgpu",
          dtype: "q4",
          progress_callback: (progress) =>
            self.postMessage({ type: "PROGRESS", progress }),
        },
      );
      self.postMessage({ type: "READY" });
    } else if (data.type === "GENERATE") {
      if (!generator) throw new Error("모델 준비가 필요합니다.");
      const result = await generator(
        [{ role: "user", content: data.prompt }],
        { max_new_tokens: 100, do_sample: false },
      );
      self.postMessage({ type: "RESULT", result });
    }
  } catch (error) {
    self.postMessage({ type: "ERROR", message: String(error) });
  } finally {
    busy = false;
  }
};
js
// 클라이언트에서 실행하는 연결 코드
const worker = new Worker(new URL("./worker.js", import.meta.url), {
  type: "module",
});
let ready = false;
let generating = false;

worker.onmessage = ({ data }) => {
  if (data.type === "READY") ready = true;
  if (data.type === "RESULT" || data.type === "ERROR") generating = false;
  console.log(data); // 실제 화면에서는 진행률·결과·오류 UI에 연결
};
worker.onerror = (event) => {
  ready = false;
  generating = false;
  console.error(event.message);
};
worker.postMessage({ type: "LOAD" });

function review(prompt) {
  if (!ready || generating) return;
  generating = true;
  worker.postMessage({ type: "GENERATE", prompt });
}

// 화면을 떠나거나 작업을 강제로 취소할 때 worker.terminate()

진행률 콜백은 여러 파일에 대해 발생할 수 있습니다. 한 파일의 100%를 전체 준비 완료로 표시하지 말고, 모델 초기화가 끝난 READY를 별도로 보여주는 편이 정확합니다. 네트워크 실패 후 재시도와 실행 중 취소도 로딩 UI의 일부입니다.

기본 브라우저 모델 캐시는 Cache API를 사용합니다. IndexedDB에 언제나 저장된다고 단정하면 디버깅할 저장소부터 잘못 찾을 수 있습니다. 실제 설정의 useBrowserCache와 커스텀 캐시 사용 여부를 확인해야 합니다. 캐시 설정 문서

코드 리뷰라면 실행 속도보다 놓친 문제를 먼저 본다

작은 모델이 한국어 문장을 만든다는 사실과 코드의 문제를 정확히 찾는다는 사실은 다릅니다. 한국어 지원 표기만으로 우리 코드의 리뷰 품질을 판단할 수 없습니다. 모델 카드의 라이선스와 사용 조건도 별도로 확인해야 합니다.

검증 입력에는 정상 코드와 실제로 문제가 있는 코드를 함께 넣습니다. 존재하지 않는 오류를 얼마나 지적하는지, 재현 가능한 버그를 얼마나 놓치는지, 긴 코드에서 중요한 조건을 유지하는지를 봅니다. 생성 결과는 보조 의견으로 보여주고 테스트나 실제 동작 확인으로 이어지게 하는 편이 맞습니다.

성능에서는 최소 지원 기기의 첫 로딩 시간, 첫 토큰 지연, 생성 중 입력 반응성, 메모리 부족 실패율을 확인하고 싶습니다. 이 글에는 그 환경별 벤치마크 결과가 없으므로 특정 모델 크기를 모바일의 보편적인 상한으로 제시하지 않겠습니다.

서버 추론과 브라우저 추론은 비용과 책임을 놓는 위치가 다릅니다. 코드가 기기 안에 머물러야 하거나 반복해서 쓰는 작은 작업이라면 브라우저 실행을 실험해볼 가치가 있습니다. 다만 모델을 실행해본 다음에는, 가장 느린 기기에서 처음 사용하는 사람이 무엇을 기다려야 하는지도 함께 보면 좋겠습니다.