지난 몇 주 동안 브라우저 안에서만 돌아가는 AI 도구를 연달아 만들었습니다. 웹캠으로 3D 아바타를 움직이는 트래커, AI 이미지 업스케일러, 영상 변환기까지. 셋의 공통점은 서버가 없다는 점입니다. 추론 엔진은 방문자의 브라우저 그 자체이고, 카메라 영상이든 사진이든 문서든 데이터가 기기 밖으로 나가지 않습니다.
장점은 분명합니다. 설치도 가입도 없이 링크 하나로 바로 쓸 수 있고, 연산은 사용자 기기가 하니 트래픽이 늘어도 GPU 비용은 늘지 않습니다. 결과물이 정적 파일이라 CDN에 올려 두면 확장은 저절로 되고, 모델이 한 번 캐시된 뒤에는 인터넷 없이도 동작합니다. 문제는 이 장점들 뒤에 서버 기반 AI와는 전혀 다른 종류의 함정이 숨어 있다는 것입니다. 이 글은 그 함정들을 하나씩 밟으며 정리한 플레이북입니다. 시간 순이 아니라, 비싸게 배운 교훈 순으로 씁니다.
먼저, 무엇 위에서 도는가
브라우저 AI의 바닥에는 세 가지 기술이 있습니다. C++이나 Rust로 짠 추론 엔진을 네이티브에 가까운 속도로 돌려 주는 WebAssembly(WASM), GPU 가속을 맡는 WebGPU와 WebGL, 그리고 벡터 연산을 가속하는 WASM SIMD와 SharedArrayBuffer 기반 멀티스레딩입니다. 웹 AI의 대부분은 결국 WASM 위에서 돌고, WebGPU는 가장 새롭고 강력하지만 지원하지 않는 환경에서는 WebGL이나 WASM으로 내려와야 합니다. SIMD와 스레드는 페이지가 “cross-origin isolated” 상태일 때만 열리는데, 이 조건이 뒤에서 이야기할 두 번째 교훈의 주인공입니다.
그 위에 얹는 런타임은 작업에 따라 갈립니다.
- 얼굴·손·자세·세그멘테이션 같은 비전 작업 — MediaPipe(Tasks Vision). 모델과 전처리가 한 덩어리로 패키징되어 있어 연결이 가장 빠릅니다.
- 범용 ONNX 모델 — ONNX Runtime Web. PyTorch로 학습하고 ONNX로 내보내 브라우저에서 추론하는 흐름입니다.
- 커스텀·실험적인 모델 — TensorFlow.js. 브라우저 안에서 학습까지 가능합니다.
- LLM과 트랜스포머 — Transformers.js(Hugging Face), WebLLM. 작은 모델은 이제 브라우저에서도 돌아갑니다.
교훈 1. 정확도보다 다운로드 크기를 먼저 보라
서버에서는 모델 크기가 디스크 용량 문제일 뿐이지만, 브라우저에서는 사용자가 그 파일을 매번 내려받습니다. 첫 로딩 시간이 곧 이탈률입니다. 그래서 모델을 고를 때 벤치마크 점수보다 int8/fp16 양자화, 프루닝, 더 작은 변형 모델부터 살피게 됐습니다. 정확도를 조금 내주고 수십 MB를 줄이는 쪽이 실제 사용감에서는 대체로 이득이었습니다.
모델을 고르기 전에 숫자 세 개를 기록해 두는 습관도 여기서 생겼습니다. 압축된 모델 다운로드 크기, 추론 중 피크 메모리, 첫 유의미한 결과까지 걸리는 시간. 이 셋은 개발용 워크스테이션이 아니라 캐시를 끈 중급 사양의 폰이나 노트북에서 재야 의미가 있습니다.
가져오는 방식도 로딩의 일부입니다. jsDelivr 같은 CDN은 편하지만 교차 출처 제약이 따라오고, 직접 호스팅은 더 빠르고 안정적이며 CORS도 단순해집니다. 모델 파일은 Cache API나 Service Worker로 한 번만 받게 만들고, 큰 모델은 방문자가 실제로 AI 기능을 켜기 전에는 내려받지 않는 것이 좋습니다. 그리고 어떤 경우든 진행 표시는 필수입니다. 수십 MB를 받는 동안 빈 화면만 보이면 사용자는 떠납니다. 실제 진행률과 함께 취소 버튼까지 제공해야 합니다.
교훈 2. 웹의 보안 모델과 싸우지 말 것
두 번째 교훈은 브라우저의 보안 규칙이 기능 명세보다 먼저라는 것입니다. 카메라와 마이크(getUserMedia)를 비롯한 여러 API는 보안 컨텍스트, 즉 HTTPS나 localhost에서만 동작합니다. HTML 파일을 file://로 열면 카메라는 물론 모듈과 WASM 로딩까지 전부 막힙니다. 로컬 개발에도 localhost 서버가 필요하고, 배포는 반드시 HTTPS여야 합니다.
멀티스레딩을 쓰려면 한 겹 더 있습니다. SharedArrayBuffer를 열려면 페이지가 cross-origin isolated 상태여야 하고, 그러려면 응답 헤더 두 개를 켜야 합니다.
Cross-Origin-Opener-Policy: same-originCross-Origin-Embedder-Policy: require-corp
함정은 그다음입니다. require-corp를 켜는 순간 페이지가 불러오는 모든 외부 리소스가 CORP나 CORS 조건을 만족해야 하고, 아니면 그냥 차단됩니다. 그래서 CDN이 아래 두 헤더를 보내 주는지부터 확인해야 합니다(jsDelivr는 보냅니다).
Access-Control-Allow-Origin: *Cross-Origin-Resource-Policy: cross-origin
iframe 임베드에서도 같은 규칙이 다시 나타납니다. cross-origin isolated 상태인 부모 페이지에 도구를 iframe으로 넣으려면 자식 문서도 같은 COEP를 써야 하고, 아니면 “refused to connect”와 함께 막힙니다. 저는 완성한 도구를 WordPress 같은 콘텐츠 사이트에 iframe으로 얹는 방식을 쓰는데, 모델 파일을 같은 도메인에 두면 캐싱과 CORS가 함께 단순해집니다. 웹 AI 도구는 결국 정적 파일 묶음이라 정적 호스팅이면 충분합니다.
교훈 3. 화면을 멈추지 말고, 조용한 실패를 의심하라
추론은 무겁습니다. UI 스레드에서 돌리면 화면이 얼어붙습니다. 무거운 런타임은 Web Worker로 옮기고 결과만 postMessage로 받아야 합니다. Worker는 페이지의 메인 UI 스레드 밖에서 코드를 돌리지만 DOM은 직접 만질 수 없으므로, 화면 갱신은 메인 스레드 몫으로 남습니다. 카메라 같은 실시간 입력은 requestAnimationFrame 루프에 태우고, 처리 속도가 밀리면 프레임을 버리는 쪽이 옳습니다.
메모리도 같은 이야기입니다. 고해상도 이미지를 한 번에 GPU로 밀면 메모리가 터집니다. 이미지를 타일로 쪼개 타일 단위로 추론하고 결과를 이어 붙여야 하는데, 업스케일러가 정확히 이렇게 동작합니다. 텐서는 다 쓰는 즉시 해제해야 하고, 이것은 모바일에서 가장 크게 체감됩니다. 입력 크기와 동시 실행 개수에도 상한을 둬야 합니다. 상한이 없으면 지나치게 큰 파일 하나가 탭 전체를 얼릴 수 있습니다.
폴백은 선택이 아니라 기본 구조입니다. 사용자 환경은 상상 이상으로 제각각이라 WebGPU → WASM(SIMD) → 단일 스레드 순으로 층을 쌓아야 합니다. WebGPU 지원 여부를 페이지 전체의 예/아니오 관문으로 삼지 말고, 기능 감지로 그 기기에서 가능한 최선의 경로를 고르는 사다리를 설계하는 쪽이 맞습니다. 두 경로 모두 어려운 환경에는 수동 대안이나 서버 보조 대안을 명확히 안내해야 합니다. MDN의 WebGPU 문서도 WebGPU가 보안 컨텍스트에서만 쓸 수 있고 널리 쓰이는 브라우저들에서 균일하게 지원되지는 않는다고 명시합니다. ONNX Runtime Web은 실행 프로바이더 배열만 넘기면 지원 여부에 따라 자동으로 한 단계씩 내려갑니다.
ort.InferenceSession.create(modelUrl, { executionProviders: ['webgpu', 'wasm'] })
브라우저에서 고를 수 있는 실행 경로는 ONNX Runtime Web 공식 튜토리얼이 잘 정리하고 있습니다.
그리고 가장 찾기 어려웠던 버그는 조용한 버그였습니다. 입력 정규화, 채널 순서(NCHW/NHWC), 색 공간 중 하나만 틀려도 에러 없이 결과만 슬그머니 망가집니다. 결과가 이상하면 모델보다 전처리를 먼저 의심하는 편이 빠릅니다.
출시 전 검증 — 데모와 제품의 차이
브라우저 데모는 금방 만들어지지만, 제품이 되려면 검증 절차가 필요합니다. 프라이버시부터 말이 아니라 목록으로 확인합니다. 사용자가 파일을 고르거나 카메라를 켠 뒤 발생하는 네트워크 요청을 전부 나열해 보고, 그 목록에 입력에서 나온 바이트나 프레임, 파생 특징이 섞여 있지 않은지 브라우저 네트워크 패널로 확인합니다. 모델과 라이브러리 다운로드는 별개의 네트워크 요청이므로 따로 설명해야 하고, 파일 이름·이미지 크기·생성된 임베딩 같은 것도 방문자가 알고 동의하지 않는 한 로깅하지 않는 것이 원칙입니다.
릴리스 전에 돌리는 수용 테스트는 이렇게 굳어졌습니다.
- 데스크톱과 모바일에서 콜드 캐시로 기능을 실행한다.
- 미지원 브라우저, 오프라인 재시도, 손상된 파일, 상한을 넘는 입력을 각각 시험한다.
- 취소·초기화·반복 실행이 메모리를 제대로 해제하는지 확인한다.
- 네트워크 패널로 프라이버시 안내문이 사실인지 검증한다.
- 결과물이 중요하거나 시간에 민감한 경우를 위해 AI를 거치지 않는 경로를 남겨 둔다.
구체적인 사례가 필요하면 이 사이트의 도구들을 참고할 수 있습니다. GPU 추론과 CPU 폴백을 실제로 어떻게 엮었는지는 WebGPU AI 이미지 업스케일러 글에, 실시간 카메라 입력이 성능 예산을 어떻게 바꾸는지는 웹캠 3D 아바타 모션 캡처 글에 정리했습니다. 진행률 표시와 입력 상한 같은 미디어 처리 패턴은 로컬 GIF·영상 변환기가 또 하나의 참고가 됩니다.
브라우저가 답이 아닐 때
- 수 GB짜리 모델 — 방문할 때마다 내려받게 하는 것은 비현실적입니다.
- 학습과 파인튜닝 — 서버나 전용 하드웨어의 일입니다.
- 모델 가중치를 숨겨야 하거나 일관성·감사가 중요한 경우 — 서버 추론이 안전합니다.
결국 브라우저 AI의 본질은 사용자의 기기를 추론 서버로 쓰는 것입니다. 비용과 프라이버시에서 강력하지만, 보안 컨텍스트·격리·메모리·로딩이라는 웹 고유의 제약을 처음부터 설계에 넣어야 합니다. 시작은 작게 하면 됩니다. 작은 모델과 WASM 폴백으로 일단 돌아가게 만들고, 그다음에 WebGPU와 멀티스레딩으로 올라가는 순서입니다.
자주 묻는 질문
모델 파일은 방문할 때마다 다시 내려받아야 하나요?
아닙니다. Cache API나 Service Worker로 캐시하면 첫 방문에만 내려받고 이후에는 캐시에서 바로 읽습니다. 모델이 한 번 캐시된 뒤에는 인터넷 연결 없이도 동작합니다.
WebGPU를 지원하지 않는 브라우저에서는 어떻게 되나요?
WebGPU, WASM(SIMD), 단일 스레드 순으로 폴백을 층층이 쌓으면 됩니다. ONNX Runtime Web은 executionProviders 배열에 [‘webgpu’, ‘wasm’]을 넘기면 지원 여부에 따라 자동으로 한 단계씩 내려갑니다.
어떤 작업은 브라우저 AI에 맞지 않나요?
수 GB짜리 모델, 학습과 파인튜닝, 그리고 모델 가중치를 숨기거나 일관성과 감사가 필요한 작업은 서버 쪽이 맞습니다. 브라우저는 작은 모델의 추론에 가장 잘 맞습니다.