웹사이트에 AI 기능 3분 만에 추가하기 자바스크립트 머신러닝 라이브러리 실전 가이드

 

웹사이트에 AI 기능 3분 만에 추가하기 자바스크립트 머신러닝 라이브러리 실전 가이드

프론트엔드 개발자라면 누구나 한 번쯤 상상해본 적이 있을 겁니다. 내가 만든 웹사이트가 사용자 얼굴을 인식하고, 업로드한 이미지에서 물체를 찾아내고, 텍스트의 감정까지 분석한다면 얼마나 멋질까요. 하지만 AI는 파이썬 전문가나 데이터 과학자의 영역이라고 생각하며 포기했을 겁니다.

그런데 말입니다. 이제 그런 시대는 정말 끝났습니다. ML5.js와 Brain.js 같은 자바스크립트 머신러닝 라이브러리를 쓰면 복잡한 수학 공식 하나 몰라도, 단 몇 줄의 코드만으로 웹사이트에 AI 기능을 넣을 수 있거든요. 사전 학습된 모델을 불러와 웹캠에서 실시간으로 포즈를 추적하거나, COCO-SSD로 80가지 물체를 자동 인식하는 기능을 3분 안에 구현할 수 있습니다.

진짜 놀라운 건 이 모든 처리가 브라우저 안에서 이루어진다는 거죠. 사용자 데이터가 서버로 전송되지 않으니까 보안 걱정도 덜고요. 이 글에서는 프론트엔드 개발자가 즉시 적용할 수 있는 실용적인 자바스크립트 AI 라이브러리와 실제 작동하는 코드 예제를 통해 웹사이트에 마법 같은 AI 경험을 선사하는 방법을 안내합니다.


프론트엔드 개발자여 이제 AI로 영역을 확장할 시간

웹 개발의 패러다임이 완전히 바뀌고 있습니다. 예전에는 백엔드 서버에서 AI API를 호출하는 게 유일한 방법이었죠. 그런데 이제는 브라우저가 직접 AI 엔진 역할을 합니다. TensorFlow.js가 2018년 등장한 이후로 자바스크립트 머신러닝 생태계는 정말 미친 듯이 성장했거든요.

WebGL과 WebGPU 같은 브라우저 기술이 GPU 가속을 지원하면서, 클라이언트 사이드에서도 실시간 AI 추론이 가능해졌습니다. 프론트엔드 개발자에게 이건 엄청난 기회예요. 기존 HTML, CSS, 자바스크립트 지식만 있으면 얼굴 인식, 객체 탐지, 자세 추정 같은 첨단 AI 기능을 구현할 수 있게 됐으니까요.

온디바이스 AI라고 불리는 클라이언트 사이드 AI의 장점은 확실합니다. 첫째로 개인정보 보호가 강화됩니다. 사용자의 얼굴 이미지나 음성 데이터가 서버로 전송되지 않고 브라우저 안에서만 처리되니까, 데이터 유출 위험이 원천적으로 차단되는 거죠. 과거 아마존 알렉사나 애플 시리에서 사용자 음성이 외부로 유출된 사례들을 생각해보면, 이 장점이 얼마나 중요한지 알 수 있습니다.

둘째는 응답 속도입니다. 네트워크 지연이 없고 브라우저에서 즉시 처리되니까 실시간 인터랙션이 가능해요. 셋째로 서버 비용이 확 줄어듭니다. AI 연산을 사용자 기기로 오프로드하면 서버 부담이 줄고 API 키도 필요 없거든요.

프론트엔드 프레임워크와의 통합도 정말 매끄럽습니다. React, Vue, Angular 같은 현대적인 프레임워크에서 TensorFlow.js나 ML5.js를 npm으로 설치해서 바로 쓸 수 있어요. React 컴포넌트 안에서 머신러닝 모델을 로드하고, 상태 관리를 통해 추론 결과를 UI에 반영하는 게 자연스럽죠. 예를 들어 useEffect 훅에서 모델을 로드하고, useState로 예측 결과를 저장하며, 컴포넌트가 언마운트될 때 메모리를 정리하는 패턴이 일반적입니다.

시작하기도 정말 쉬워요. 복잡한 개발 환경 설정이나 특별한 도구 없이 HTML 파일에 CDN 스크립트 태그 한 줄만 추가하면 됩니다. ML5.js의 경우 <script src="https://unpkg.com/ml5@latest/dist/ml5.min.js"></script>를 추가하는 것만으로 준비가 끝나거든요. p5.js와 함께 사용하면 비주얼 코딩 경험이 더욱 좋아져서, 초보자도 몇 시간 만에 작동하는 AI 웹앱을 만들 수 있습니다.

클라이언트 AI 장점 구체적인 설명 실제 활용 시나리오 주의사항
개인정보 보호 강화 데이터가 브라우저 밖으로 나가지 않음 서버 저장 불필요 얼굴 인식 로그인 의료 데이터 분석 금융 정보 처리 GDPR 준수 명시 필요
빠른 응답 속도 네트워크 지연 제로 즉각 처리 가능 라이브 비디오 필터 실시간 번역 게임 AI 기기 성능 편차 존재
서버 비용 절감 연산을 클라이언트로 완전 오프로드 대규모 사용자 서비스 스타트업 초기 단계 초기 로딩 시간 증가
오프라인 작동 인터넷 없이도 AI 기능 사용 가능 PWA 모바일 앱 오지 지역 서비스 모델 캐싱 필수
API 키 불필요 서버 API 없이 완전 독립 실행 오픈소스 프로젝트 교육용 앱 모델 업데이트 관리
개발자 친화적 기존 웹 기술 스택 그대로 활용 프론트엔드 개발자 빠른 프로토타이핑 학습 곡선 존재
확장성 우수 사용자 증가해도 서버 부담 없음 바이럴 서비스 이벤트 페이지 브라우저 호환성 체크

세상에서 제일 쉬운 ML5.js로 시작하는 AI 웹 개발

ML5.js는 코딩 교육과 크리에이티브 프로젝트를 위해 만들어진 자바스크립트 머신러닝 라이브러리로, 초보자도 10분 안에 AI를 체험할 수 있게 설계됐습니다. 뉴욕대학교 ITP에서 개발했고 내부적으로는 TensorFlow.js를 사용하지만, API를 극도로 단순화해서 복잡한 수학이나 머신러닝 지식 없이도 바로 쓸 수 있어요.

이미지 분류, 포즈 추정, 얼굴 인식, 사운드 분류 등 다양한 사전 학습 모델이 준비되어 있어서 함수 몇 개만 호출하면 즉시 작동합니다. p5.js와 완벽하게 통합되어 있어 비주얼 코딩과 인터랙티브 아트에 최적화되어 있고요.

웹캠으로 포즈 추적하기는 ML5.js의 대표적인 예제입니다. PoseNet 모델을 사용하면 사람의 관절 17개 포인트를 실시간으로 추적할 수 있거든요. 코드는 놀랄 만큼 간단합니다. 먼저 비디오 캡처를 생성하고, ml5.poseNet으로 모델을 초기화한 뒤, 'pose' 이벤트를 리스닝하면 끝이에요.

이벤트 콜백 함수에서 코, 눈, 어깨, 팔꿈치, 손목 등의 좌표를 받아올 수 있고, 이걸 캔버스에 그려서 사용자 몸 위에 시각적 효과를 더할 수 있습니다. 예를 들어 머리 위에 천사 고리를 띄우거나, 손 위치를 추적해서 가상 악기를 연주하는 인터랙티브 앱을 만들 수 있어요.

손 마디 인식은 HandPose 모델로 가능합니다. 손가락 각 마디의 21개 포인트를 정확히 추적해서 제스처 인식이나 수화 번역 앱을 만들 수 있죠. 설정 방법은 PoseNet과 거의 동일하고, ml5.handpose 함수로 모델을 로드하고 이벤트 리스너를 등록하면 손 좌표가 실시간으로 들어옵니다. 웹캠 없이도 업로드한 이미지에서 손을 인식할 수 있어서 정적 이미지 분석에도 활용 가능하고요.

실시간 인간 행동 인식은 ML5.js의 고급 활용 사례입니다. PoseNet으로 추출한 키포인트를 Neural Network 모델에 입력해서, 사용자가 걷는지, 앉는지, 점프하는지, 서 있는지를 분류할 수 있어요. 이걸 위해서는 먼저 각 동작별 데이터를 수집하는 단계가 필요합니다.

동작명을 입력하고 자료 수집 버튼을 누르면 일정 시간 동안 포즈 데이터를 기록하고, 이걸 JSON 파일로 저장합니다. 그 다음 train 단계에서 수집된 데이터로 신경망을 훈련시키고, 학습된 모델을 저장하죠. 마지막 inference 단계에서는 실시간 웹캠 입력으로 행동을 예측합니다. 전체 과정이 브라우저에서만 이루어지고, 서버가 필요 없어요.

// ML5.js PoseNet 완전 실전 예제
let video;
let poseNet;
let poses = [];
let skeleton;

function setup() {
  createCanvas(640, 480);
  video = createCapture(VIDEO);
  video.size(width, height);
  video.hide();
  
  // PoseNet 모델 로드 및 초기화
  const options = {
    imageScaleFactor: 0.3,
    outputStride: 16,
    flipHorizontal: false,
    minConfidence: 0.5,
    maxPoseDetections: 2,
    scoreThreshold: 0.5,
    nmsRadius: 20,
    detectionType: 'single',
    multiplier: 0.75,
  };
  
  poseNet = ml5.poseNet(video, options, modelLoaded);
  
  // pose 이벤트 리스너 등록
  poseNet.on('pose', function(results) {
    poses = results;
  });
}

function modelLoaded() {
  console.log('PoseNet 모델 로드 완료!');
  select('#status').html('모델 준비 완료 - 포즈를 취해보세요!');
}

function draw() {
  // 비디오 이미지 그리기
  image(video, 0, 0, width, height);
  
  // 감지된 포즈 그리기
  drawKeypoints();
  drawSkeleton();
}

function drawKeypoints() {
  // 모든 감지된 포즈에 대해 반복
  for (let i = 0; i < poses.length; i++) {
    let pose = poses[i].pose;
    
    // 각 키포인트에 대해 반복
    for (let j = 0; j < pose.keypoints.length; j++) {
      let keypoint = pose.keypoints[j];
      
      // 신뢰도가 0.2 이상인 키포인트만 그리기
      if (keypoint.score > 0.2) {
        fill(255, 0, 0);
        noStroke();
        ellipse(keypoint.position.x, keypoint.position.y, 10, 10);
        
        // 키포인트 이름 표시
        fill(255);
        textSize(10);
        text(keypoint.part, keypoint.position.x + 5, keypoint.position.y);
      }
    }
  }
}

function drawSkeleton() {
  // 모든 감지된 포즈에 대해 반복
  for (let i = 0; i < poses.length; i++) {
    let skeleton = poses[i].skeleton;
    
    // 스켈레톤 선 그리기
    for (let j = 0; j < skeleton.length; j++) {
      let partA = skeleton[j][0];
      let partB = skeleton[j][1];
      
      stroke(255, 0, 0);
      strokeWeight(2);
      line(
        partA.position.x, partA.position.y,
        partB.position.x, partB.position.y
      );
    }
  }
}

// 특정 부위 좌표 가져오기 함수
function getNosePosition() {
  if (poses.length > 0) {
    let nose = poses[0].pose.nose;
    return {x: nose.x, y: nose.y, confidence: nose.confidence};
  }
  return null;
}

// 포즈 데이터 저장 함수
function savePoseData() {
  if (poses.length > 0) {
    let poseData = poses[0].pose.keypoints.map(kp => ({
      part: kp.part,
      x: kp.position.x,
      y: kp.position.y,
      score: kp.score
    }));
    
    saveJSON(poseData, 'pose_data.json');
    console.log('포즈 데이터 저장 완료!');
  }
}
ML5.js 주요 모델 기능 설명 코드 난이도 실전 활용 예시 모델 크기 처리 속도
imageClassifier MobileNet 기반 1000개 물체 분류 매우 쉬움 1~2줄 물체 인식 앱 검색 필터 재고 관리 약 16MB 실시간 가능
PoseNet 17개 관절 포즈 추정 2D 좌표 쉬움 10줄 내외 운동 코칭 댄스 게임 자세 교정 약 12MB 초당 30프레임
HandPose 21개 손 마디 정밀 추적 쉬움 제스처 컨트롤 수화 번역 가상 피아노 약 10MB 초당 15프레임
BodyPix 사람 배경 픽셀 단위 분리 중간 15줄 가상 배경 AR 필터 프라이버시 블러 약 24MB 초당 10프레임
faceApi 얼굴 68점 감정 7가지 분석 중간 출석 시스템 감정 기반 UI 보안 인증 약 6MB 실시간 가능
soundClassifier 18개 일상 소리 분류 쉬움 음성 명령 환경음 감지 알람 시스템 약 8MB 실시간 가능
featureExtractor 전이 학습 커스텀 모델 제작 중간 20줄 특화된 분류기 품질 검사 맞춤 인식 가변적 학습 필요
StyleTransfer 예술 스타일 이미지 변환 중간 사진 필터 앱 예술 작품 생성 약 15MB 5~10초/이미지
CharRNN 텍스트 생성 시퀀스 예측 어려움 자동 작곡 시 생성 텍스트 완성 약 3MB 실시간 가능
UNET 이미지 세그멘테이션 중간 의료 영상 분석 객체 분리 약 20MB 3~5초/이미지

ML5.js의 강력한 점은 전이 학습 기능입니다. 사전 학습된 모델을 기반으로 자신만의 데이터를 추가해서 커스텀 분류기를 만들 수 있어요. 예를 들어 고양이와 강아지를 구분하는 분류기를 만들려면 각각의 이미지를 몇 장씩 웹캠으로 찍어서 학습시키기만 하면 됩니다.

수천 장이 아니라 수십 장만으로도 충분한 정확도를 얻을 수 있는 이유는 MobileNet이 이미 일반적인 특징 추출 능력을 갖추고 있기 때문이죠. 교실에서 학생들의 얼굴을 학습시켜 출석 체크 시스템을 만들거나, 회사에서 자사 제품 이미지를 학습시켜 품질 검사 도구를 만드는 등 실용적인 프로젝트가 가능합니다.

// ML5.js 전이 학습 커스텀 분류기 예제
let featureExtractor;
let classifier;
let video;
let loss;
let catButton, dogButton, trainButton, saveButton, loadButton;
let catImages = 0;
let dogImages = 0;

function setup() {
  createCanvas(640, 480);
  video = createCapture(VIDEO);
  video.hide();
  
  // MobileNet 기반 특징 추출기 생성
  featureExtractor = ml5.featureExtractor('MobileNet', modelReady);
  classifier = featureExtractor.classification(video);
  
  // UI 버튼 생성
  catButton = createButton('고양이 추가');
  catButton.mousePressed(function() {
    classifier.addImage('고양이');
    catImages++;
    console.log('고양이 이미지: ' + catImages);
  });
  
  dogButton = createButton('강아지 추가');
  dogButton.mousePressed(function() {
    classifier.addImage('강아지');
    dogImages++;
    console.log('강아지 이미지: ' + dogImages);
  });
  
  trainButton = createButton('학습 시작');
  trainButton.mousePressed(function() {
    classifier.train(function(lossValue) {
      if (lossValue) {
        loss = lossValue;
        console.log('Loss: ' + loss);
      } else {
        console.log('학습 완료!');
        classifier.classify(gotResults);
      }
    });
  });
  
  saveButton = createButton('모델 저장');
  saveButton.mousePressed(function() {
    classifier.save();
  });
  
  loadButton = createButton('모델 불러오기');
  loadButton.mousePressed(function() {
    classifier.load('model.json', function() {
      console.log('모델 로드 완료!');
      classifier.classify(gotResults);
    });
  });
}

function modelReady() {
  console.log('MobileNet 준비 완료!');
}

function gotResults(error, results) {
  if (error) {
    console.error(error);
    return;
  }
  
  // 결과 표시
  fill(255);
  textSize(32);
  text(results[0].label, 10, height - 100);
  text(nf(results[0].confidence * 100, 2, 2) + '%', 10, height - 50);
  
  // 계속 분류 진행
  classifier.classify(gotResults);
}

function draw() {
  image(video, 0, 0, width, height);
  
  // 학습 정보 표시
  fill(255);
  textSize(16);
  text('고양이 이미지: ' + catImages, 10, 30);
  text('강아지 이미지: ' + dogImages, 10, 50);
  
  if (loss) {
    text('Loss: ' + nf(loss, 1, 4), 10, 70);
  }
}

3줄 코드로 완성하는 COCO-SSD 객체 탐지 마법

COCO-SSD는 TensorFlow.js 기반의 사전 학습된 객체 탐지 모델로, 80가지 일상 물체를 실시간으로 인식합니다. 사람, 자동차, 고양이, 의자, 노트북 등 COCO 데이터셋에 포함된 객체를 이미지나 비디오에서 찾아내고, 각 객체의 위치를 바운딩 박스로 표시하죠.

모델 로드부터 객체 탐지까지 단 3~4줄의 코드로 구현할 수 있어서, 웹 개발자가 가장 쉽게 접근할 수 있는 AI 기능 중 하나예요. 구글 Codelabs에서 공식 튜토리얼을 제공해서 초보자도 30분 안에 작동하는 데모를 만들 수 있습니다.

COCO-SSD 사용법은 놀라울 정도로 간단해요. 먼저 HTML에서 TensorFlow.js와 COCO-SSD 모델 스크립트를 로드합니다. 그 다음 자바스크립트에서 cocoSsd.load()로 모델을 초기화하고, 모델이 로드되면 이미지나 비디오 엘리먼트를 model.detect()에 전달하면 끝입니다.

반환된 결과에는 각 객체의 클래스명, 신뢰도 점수, 바운딩 박스 좌표가 포함되어 있어요. 이걸 캔버스에 그리면 객체 주위에 박스와 레이블이 표시되는 인상적인 시각 효과를 얻을 수 있죠. 웹캠 스트림에 적용하면 실시간 객체 탐지가 구현됩니다.

// COCO-SSD 완전 실전 가이드
let model;
let video;
let canvas;
let ctx;
let detectInterval;
let isDetecting = false;

// 페이지 로드 시 초기화
window.addEventListener('load', async () => {
  await setupWebcam();
  await loadModel();
  startDetection();
});

async function setupWebcam() {
  video = document.getElementById('webcam');
  canvas = document.getElementById('canvas');
  ctx = canvas.getContext('2d');
  
  // 웹캠 스트림 가져오기
  const stream = await navigator.mediaDevices.getUserMedia({
    video: { 
      width: 640, 
      height: 480,
      facingMode: 'user'
    }
  });
  
  video.srcObject = stream;
  
  return new Promise((resolve) => {
    video.onloadedmetadata = () => {
      video.play();
      canvas.width = video.videoWidth;
      canvas.height = video.videoHeight;
      console.log('웹캠 준비 완료!');
      resolve();
    };
  });
}

async function loadModel() {
  try {
    // 로딩 상태 표시
    updateStatus('모델 로딩 중...');
    
    // COCO-SSD 모델 로드
    model = await cocoSsd.load({
      base: 'mobilenet_v2' // 또는 'lite_mobilenet_v2'
    });
    
    updateStatus('모델 로드 완료!');
    console.log('COCO-SSD 모델 준비 완료');
  } catch (error) {
    console.error('모델 로드 실패:', error);
    updateStatus('모델 로드 실패. 페이지를 새로고침하세요.');
  }
}

function startDetection() {
  if (!isDetecting) {
    isDetecting = true;
    detectFrame();
  }
}

function stopDetection() {
  isDetecting = false;
  if (detectInterval) {
    clearTimeout(detectInterval);
  }
}

async function detectFrame() {
  if (!isDetecting) return;
  
  try {
    // 비디오에서 객체 탐지
    const predictions = await model.detect(video);
    
    // 결과 렌더링
    renderPredictions(predictions);
    
    // 통계 업데이트
    updateStats(predictions);
    
    // 다음 프레임 처리 (약 100ms 간격)
    detectInterval = setTimeout(() => detectFrame(), 100);
  } catch (error) {
    console.error('탐지 오류:', error);
    detectInterval = setTimeout(() => detectFrame(), 100);
  }
}

function renderPredictions(predictions) {
  // 캔버스 초기화
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  
  // 비디오 프레임 그리기
  ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
  
  // 각 객체에 대해 박스와 레이블 그리기
  predictions.forEach(prediction => {
    const [x, y, width, height] = prediction.bbox;
    const label = prediction.class;
    const score = (prediction.score * 100).toFixed(1);
    
    // 신뢰도에 따라 색상 변경
    const color = getColorByScore(prediction.score);
    
    // 바운딩 박스
    ctx.strokeStyle = color;
    ctx.lineWidth = 3;
    ctx.strokeRect(x, y, width, height);
    
    // 레이블 배경
    const labelText = `${label} ${score}%`;
    ctx.font = 'bold 16px Arial';
    const textWidth = ctx.measureText(labelText).width;
    const textHeight = 20;
    
    ctx.fillStyle = color;
    ctx.fillRect(x, y > textHeight ? y - textHeight : y, textWidth + 10, textHeight);
    
    // 레이블 텍스트
    ctx.fillStyle = '#FFFFFF';
    ctx.fillText(labelText, x + 5, y > textHeight ? y - 5 : y + 15);
  });
}

function getColorByScore(score) {
  if (score > 0.8) return '#00FF00'; // 높은 신뢰도: 녹색
  if (score > 0.5) return '#FFFF00'; // 중간 신뢰도: 노란색
  return '#FF6600'; // 낮은 신뢰도: 주황색
}

function updateStats(predictions) {
  const statsDiv = document.getElementById('stats');
  const objectCounts = {};
  
  predictions.forEach(pred => {
    objectCounts[pred.class] = (objectCounts[pred.class] || 0) + 1;
  });
  
  let statsHTML = '<h3>감지된 객체</h3><ul>';
  for (const [obj, count] of Object.entries(objectCounts)) {
    statsHTML += `<li>${obj}: ${count}개</li>`;
  }
  statsHTML += '</ul>';
  
  statsDiv.innerHTML = statsHTML;
}

function updateStatus(message) {
  document.getElementById('status').innerText = message;
}

// 이미지 파일 업로드 처리
async function detectFromImage(imageFile) {
  const img = new Image();
  const reader = new FileReader();
  
  reader.onload = async (e) => {
    img.src = e.target.result;
    img.onload = async () => {
      // 캔버스 크기 조정
      canvas.width = img.width;
      canvas.height = img.height;
      
      // 이미지 그리기
      ctx.drawImage(img, 0, 0);
      
      // 객체 탐지
      const predictions = await model.detect(img);
      
      // 결과 그리기
      renderPredictionsOnImage(predictions);
    };
  };
  
  reader.readAsDataURL(imageFile);
}

function renderPredictionsOnImage(predictions) {
  predictions.forEach(prediction => {
    const [x, y, width, height] = prediction.bbox;
    const label = `${prediction.class} ${(prediction.score * 100).toFixed(1)}%`;
    
    ctx.strokeStyle = '#00FF00';
    ctx.lineWidth = 2;
    ctx.strokeRect(x, y, width, height);
    
    ctx.fillStyle = '#00FF00';
    ctx.font = '18px Arial';
    ctx.fillText(label, x, y > 20 ? y - 5 : y + 20);
  });
}

// 특정 객체만 필터링
function filterPredictions(predictions, targetClasses) {
  return predictions.filter(pred => 
    targetClasses.includes(pred.class)
  );
}

// 사용 예시
// const peopleOnly = filterPredictions(predictions, ['person']);
// const vehiclesOnly = filterPredictions(predictions, ['car', 'truck', 'bus', 'motorcycle']);
COCO-SSD 지원 객체 카테고리 포함 객체 예시 활용 분야 평균 정확도
사람 관련 person 보안 감시 인원 카운팅 출입 관리 90% 이상
차량 car truck bus motorcycle bicycle 주차 관리 교통 분석 차량 인식 85% 이상
동물 cat dog horse sheep cow elephant bear 반려동물 관리 야생 동물 모니터링 80% 이상
가구 chair couch bed dining table 인테리어 AR 부동산 자동 태깅 75% 이상
전자기기 laptop cell phone tv keyboard mouse 재고 관리 분실물 찾기 85% 이상
주방용품 bottle cup fork knife spoon bowl 식당 자동화 음식 인식 70% 이상
스포츠용품 sports ball tennis racket baseball bat 스포츠 분석 운동 기록 75% 이상
교통 신호 traffic light stop sign parking meter 자율주행 교통 관리 80% 이상

실전 활용 사례는 정말 무궁무진합니다. 온라인 쇼핑몰에서 사용자가 업로드한 사진에서 제품을 자동으로 태그하거나, 부동산 사이트에서 매물 사진 속 가구를 자동 인식해서 설명을 생성할 수 있어요. 교육 사이트에서는 학생이 제출한 과제 사진에서 필요한 재료가 모두 포함되어 있는지 자동 검증하는 기능을 만들 수 있고요.

보안 분야에서는 웹캠으로 특정 물체의 출입을 감지하는 모니터링 시스템을 구축할 수도 있습니다. 모델이 이미 학습되어 있어서 추가 학습 없이 바로 사용할 수 있다는 점이 큰 장점이죠.

성능 최적화도 중요합니다. COCO-SSD 모델은 약 25MB 크기이고, 처음 로드될 때 몇 초가 걸릴 수 있어요. 사용자 경험을 위해 로딩 인디케이터를 표시하고, 모델이 준비되면 UI를 활성화하는 패턴을 사용해야 합니다. 브라우저 캐시를 활용하면 재방문 시에는 즉시 로드되고요.

WebGL 백엔드가 자동으로 활성화되어 GPU 가속을 받지만, 구형 기기에서는 CPU 백엔드로 폴백될 수 있어서 속도가 느려질 수 있습니다. navigator.hardwareConcurrency로 기기 성능을 감지해서 저사양 기기에서는 프레임 레이트를 낮추는 등의 대응이 필요하죠.


자연어 처리도 자바스크립트로 텍스트 분석 라이브러리 총정리

자연어 처리는 AI의 또 다른 핵심 분야이고, 자바스크립트에도 훌륭한 NLP 도구들이 많습니다. Natural은 Node.js 환경에서 작동하는 종합 NLP 툴킷으로, 토큰화, 스테밍, 감성 분석, TF-IDF, 베이즈 분류기 등을 제공해요. 영어뿐 아니라 한국어 형태소 분석도 지원하고, 챗봇 백엔드를 구축하거나 스팸 필터를 만드는 데 활용됩니다.

compromise는 브라우저와 Node.js 모두에서 작동하는 경량 라이브러리로, 영어 문법 파싱에 특화되어 있습니다. 명사, 동사, 형용사를 추출하고 시제를 변환하는 등의 작업을 빠르게 처리하죠.

최근 주목받는 Transformers.js는 Hugging Face의 트랜스포머 모델을 자바스크립트로 포팅한 라이브러리입니다. BERT, GPT, T5 같은 최신 모델을 브라우저에서 실행해서 텍스트 생성, 요약, 번역, 질의응답 같은 고급 NLP 작업을 수행할 수 있어요. 모델이 수백 MB에 달하고 추론 속도도 느리지만, 서버 없이 클라이언트에서 GPT 수준의 기능을 제공한다는 건 정말 혁신적이죠.

감성 분석은 웹사이트에서 가장 많이 활용되는 NLP 기능입니다. sentiment 패키지를 사용하면 몇 줄로 구현 가능하고, 사용자 리뷰나 SNS 댓글의 긍정/부정을 자동 판단할 수 있어요. 고객 피드백을 자동 분류하거나, 브랜드 모니터링 대시보드를 만들거나, 실시간 여론 분석 도구를 구축하는 데 활용되죠.

// Natural 라이브러리 종합 활용 예제
const natural = require('natural');
const tokenizer = new natural.WordTokenizer();
const TfIdf = natural.TfIdf;
const tfidf = new TfIdf();

// 1. 텍스트 토큰화
function tokenizeText(text) {
  return tokenizer.tokenize(text);
}

// 2. 스테밍 (어간 추출)
function stemWords(words) {
  const stemmer = natural.PorterStemmer;
  return words.map(word => stemmer.stem(word));
}

// 3. 문장 유사도 계산
function calculateSimilarity(sentence1, sentence2) {
  const distance = natural.JaroWinklerDistance(sentence1, sentence2);
  return distance;
}

// 4. TF-IDF 계산
function analyzeTfIdf(documents) {
  documents.forEach(doc => {
    tfidf.addDocument(doc);
  });
  
  tfidf.listTerms(0).forEach(item => {
    console.log(item.term + ': ' + item.tfidf);
  });
}

// 5. 베이즈 분류기 (스팸 필터)
const classifier = new natural.BayesClassifier();

function trainSpamFilter() {
  // 스팸 학습 데이터
  classifier.addDocument('무료 대출 상담 지금 바로', 'spam');
  classifier.addDocument('이벤트 당첨 축하 링크 클릭', 'spam');
  classifier.addDocument('긴급 계정 확인 필요', 'spam');
  
  // 정상 메일 학습 데이터
  classifier.addDocument('다음 주 회의 일정 공유합니다', 'normal');
  classifier.addDocument('프로젝트 진행 상황 보고', 'normal');
  classifier.addDocument('문의하신 내용 답변드립니다', 'normal');
  
  classifier.train();
  
  console.log('스팸 필터 학습 완료!');
}

function classifyMessage(message) {
  return classifier.classify(message);
}

// 6. 한국어 형태소 분석 (별도 라이브러리 필요)
// npm install kuromoji

// 실제 사용 예시
const sampleText = "자연어 처리는 정말 흥미로운 분야입니다. 다양한 활용이 가능합니다.";
const tokens = tokenizeText(sampleText);
console.log('토큰:', tokens);

const stems = stemWords(tokens);
console.log('어간:', stems);

trainSpamFilter();
console.log('메시지 분류:', classifyMessage('무료 이벤트 참여하세요'));
// Sentiment 감성 분석 실전 활용
const Sentiment = require('sentiment');
const sentiment = new Sentiment();

// 기본 감성 분석
function analyzeSentiment(text) {
  const result = sentiment.analyze(text);
  
  return {
    score: result.score,
    comparative: result.comparative,
    sentiment: getSentimentLabel(result.score),
    positive: result.positive,
    negative: result.negative,
    tokens: result.tokens
  };
}

function getSentimentLabel(score) {
  if (score > 2) return '매우 긍정';
  if (score > 0) return '긍정';
  if (score === 0) return '중립';
  if (score > -2) return '부정';
  return '매우 부정';
}

// 리뷰 데이터 분석
function analyzeReviews(reviews) {
  const results = reviews.map(review => ({
    text: review,
    analysis: analyzeSentiment(review)
  }));
  
  // 통계 계산
  const stats = {
    total: results.length,
    positive: results.filter(r => r.analysis.score > 0).length,
    negative: results.filter(r => r.analysis.score < 0).length,
    neutral: results.filter(r => r.analysis.score === 0).length,
    avgScore: results.reduce((sum, r) => sum + r.analysis.score, 0) / results.length
  };
  
  return { results, stats };
}

// 사용 예시
const reviews = [
  "이 제품은 정말 훌륭합니다! 강력 추천합니다.",
  "최악입니다. 완전 실망했어요.",
  "그냥 그래요. 보통 수준입니다.",
  "품질이 우수하고 배송도 빨라요. 만족합니다!",
  "불량품이 와서 환불했습니다. 화가 나네요."
];

const analysis = analyzeReviews(reviews);
console.log('리뷰 분석 결과:', analysis);

// 실시간 감성 모니터링
function monitorSentiment(texts, threshold) {
  const alerts = [];
  
  texts.forEach((text, index) => {
    const result = analyzeSentiment(text);
    
    if (result.score < threshold) {
      alerts.push({
        index: index,
        text: text,
        score: result.score,
        sentiment: result.sentiment,
        negativeWords: result.negative
      });
    }
  });
  
  return alerts;
}

// 부정 리뷰 자동 감지
const negativeThreshold = -1;
const alerts = monitorSentiment(reviews, negativeThreshold);
console.log('부정 리뷰 알림:', alerts);
NLP 라이브러리 비교 주요 기능 실행 환경 모델 크기 처리 속도 적합한 용도 학습 난이도
Natural 토큰화 스테밍 분류 TF-IDF Node.js 소형 5MB 빠름 실시간 챗봇 백엔드 스팸 필터 문서 분석 중간
compromise 문법 파싱 품사 태깅 시제 변환 브라우저 Node.js 200KB 매우 빠름 문법 검사 텍스트 분석 자동 수정 쉬움
Transformers.js BERT GPT T5 최신 모델 브라우저 Node.js 100~500MB 느림 수초 텍스트 생성 요약 번역 QA 어려움
sentiment 감성 분석 전용 브라우저 Node.js 매우 소형 빠름 즉시 리뷰 분석 피드백 분류 여론 조사 매우 쉬움
franc 75개 언어 자동 감지 브라우저 Node.js 소형 3MB 빠름 다국어 자동 처리 번역 전처리 쉬움
wink-nlp 한국어 포함 다국어 Node.js 중형 15MB 보통 다국어 챗봇 글로벌 서비스 중간
nlp.js 30개 언어 NLU 엔진 Node.js 중형 20MB 보통 의도 분석 엔티티 추출 대화 AI 중간

클라이언트 사이드 AI 개발 시 반드시 알아야 할 주의사항

클라이언트 사이드 AI는 많은 장점이 있지만 주의할 점도 분명 있습니다. 첫 번째는 모델 다운로드 크기예요. TensorFlow.js 모델은 수십 MB에서 수백 MB에 달하니까, 사용자가 처음 페이지를 로드할 때 시간이 오래 걸릴 수 있거든요. 특히 모바일 네트워크에서는 다운로드 시간이 사용자 이탈로 직결될 수 있습니다.

해결책은 모델을 Cache API로 명시적으로 캐싱해서 재방문 시 즉시 로드되게 만드는 거예요. 또한 Service Worker를 활용하면 오프라인 환경에서도 AI 기능이 작동합니다.

// Service Worker를 이용한 모델 캐싱
// service-worker.js
const CACHE_NAME = 'ai-model-cache-v1';
const MODEL_URLS = [
  'https://storage.googleapis.com/tfjs-models/tfjs/mobilenet_v1_0.25_224/model.json',
  'https://storage.googleapis.com/tfjs-models/tfjs/mobilenet_v1_0.25_224/group1-shard1of1.bin'
];

self.addEventListener('install', (event) => {
  event.waitUntil(
    caches.open(CACHE_NAME).then((cache) => {
      console.log('모델 캐싱 시작');
      return cache.addAll(MODEL_URLS);
    })
  );
});

self.addEventListener('fetch', (event) => {
  // 모델 요청인 경우 캐시 우선
  if (event.request.url.includes('tfjs-models')) {
    event.respondWith(
      caches.match(event.request).then((response) => {
        return response || fetch(event.request);
      })
    );
  }
});

// main.js에서 Service Worker 등록
if ('serviceWorker' in navigator) {
  navigator.serviceWorker.register('/service-worker.js')
    .then(() => console.log('Service Worker 등록 완료'))
    .catch((err) => console.error('Service Worker 등록 실패:', err));
}

두 번째는 기기 성능 차이입니다. 고성능 데스크탑에서는 실시간 추론이 가능하지만, 저사양 모바일 기기에서는 느려지거나 배터리를 빠르게 소모할 수 있어요. Navigator.hardwareConcurrency와 Navigator.deviceMemory로 기기 성능을 감지해서, 저사양 기기에서는 가벼운 모델로 전환하거나 프레임 레이트를 낮추는 등의 적응형 전략을 사용해야 합니다.

// 기기 성능 감지 및 적응형 모델 선택
function selectModelBasedOnDevice() {
  const cores = navigator.hardwareConcurrency || 2;
  const memory = navigator.deviceMemory || 4; // GB
  
  console.log(`CPU 코어: ${cores}, 메모리: ${memory}GB`);
  
  // 고성능 기기 (8코어 이상, 8GB 이상 메모리)
  if (cores >= 8 && memory >= 8) {
    return {
      modelUrl: 'mobilenet_v2_1.0_224',
      imageSize: 224,
      frameRate: 30,
      quality: 'high'
    };
  }
  
  // 중간 성능 기기 (4코어 이상, 4GB 이상 메모리)
  if (cores >= 4 && memory >= 4) {
    return {
      modelUrl: 'mobilenet_v2_0.5_224',
      imageSize: 224,
      frameRate: 15,
      quality: 'medium'
    };
  }
  
  // 저성능 기기
  return {
    modelUrl: 'mobilenet_v2_0.25_224',
    imageSize: 192,
    frameRate: 10,
    quality: 'low'
  };
}

// Compute Pressure API를 활용한 동적 조절
if ('ComputePressure' in window) {
  const observer = new ComputePressure({
    cpuUtilizationThresholds: [0.5, 0.75, 0.9],
    cpuSpeedThresholds: [0.5]
  });
  
  observer.observe('cpu', (update) => {
    console.log('CPU 압력:', update);
    
    // CPU 부하가 높으면 프레임 레이트 감소
    if (update.pressure === 'critical') {
      reduceFrameRate();
      console.log('프레임 레이트 감소로 CPU 부하 완화');
    }
  });
}

function reduceFrameRate() {
  // 실시간 처리 간격 증가
  detectionInterval = 200; // 100ms에서 200ms로 증가
}

세 번째는 사용자 경험 설계입니다. 모델 로딩과 추론에 시간이 걸리니까, 적절한 로딩 인디케이터와 진행 상황 표시가 필수예요. 스켈레톤 UI나 프로그레스 바를 통해 사용자가 기다리는 동안 무슨 일이 일어나고 있는지 알려줘야 하죠.

// 프로그레스 바를 활용한 모델 로딩 UX
async function loadModelWithProgress() {
  const progressBar = document.getElementById('progress-bar');
  const statusText = document.getElementById('status-text');
  
  try {
    statusText.innerText = '모델 다운로드 중...';
    progressBar.value = 0;
    
    // 커스텀 로딩 핸들러
    const model = await tf.loadLayersModel(
      'path/to/model.json',
      {
        onProgress: (fraction) => {
          progressBar.value = fraction * 100;
          statusText.innerText = `모델 로딩: ${Math.round(fraction * 100)}%`;
        }
      }
    );
    
    statusText.innerText = '모델 준비 완료!';
    progressBar.value = 100;
    
    // 로딩 UI 숨기기
    setTimeout(() => {
      document.getElementById('loading-overlay').style.display = 'none';
    }, 500);
    
    return model;
  } catch (error) {
    statusText.innerText = '모델 로딩 실패. 다시 시도해주세요.';
    console.error('로딩 오류:', error);
  }
}

// 추론 대기 시간 동안 애니메이션
function showThinkingAnimation() {
  const thinkingDiv = document.getElementById('thinking');
  thinkingDiv.innerHTML = `
    <div class="spinner"></div>
    <p>AI가 분석 중입니다...</p>
  `;
  thinkingDiv.style.display = 'block';
}

function hideThinkingAnimation() {
  document.getElementById('thinking').style.display = 'none';
}

네 번째는 브라우저 호환성이에요. WebGL은 대부분의 모던 브라우저에서 지원되지만, 일부 구형 브라우저나 특정 모바일 기기에서는 작동하지 않을 수 있거든요. TensorFlow.js는 자동으로 백엔드를 감지해서 WebGL, WebAssembly, CPU 중 사용 가능한 걸 선택하지만, 성능 차이가 크니까 테스트가 필요합니다.

주의사항 항목 구체적인 문제점 해결 방법 구현 난이도 효과
모델 크기 초기 로딩 시간 10~30초 소요 Cache API Service Worker 캐싱 중간 재방문 시 즉시 로드
기기 성능 편차 저사양 기기 느린 추론 속도 성능 감지 적응형 모델 선택 중간 모든 기기 최적화
배터리 소모 지속 AI 연산 시 배터리 빠름 추론 빈도 조절 유휴 시 중지 쉬움 배터리 수명 2배 향상
브라우저 호환성 구형 브라우저 WebGL 미지원 백엔드 자동 감지 폴백 제공 쉬움 호환성 95% 이상
메모리 누수 텐서 미해제 시 메모리 증가 dispose 메서드 명시적 호출 어려움 장시간 안정 작동
네트워크 의존성 오프라인 시 모델 로드 불가 Service Worker 오프라인 캐싱 중간 완전 오프라인 작동
사용자 경험 로딩 중 이탈률 30% 프로그레스 바 스켈레톤 UI 쉬움 이탈률 50% 감소
모바일 최적화 작은 화면 터치 인터페이스 반응형 디자인 제스처 지원 중간 모바일 사용성 향상

React와 Vue에서 머신러닝 라이브러리 완벽 통합하기

React에서 TensorFlow.js나 ML5.js를 사용하는 건 일반적인 npm 패키지를 추가하는 것과 똑같습니다. npm install @tensorflow/tfjs ml5로 설치하고, 컴포넌트에서 import해서 사용하면 돼요. 주요 패턴은 useEffect 훅에서 모델을 로드하고, useState로 추론 결과를 관리하며, 컴포넌트 언마운트 시 메모리를 정리하는 거죠.

// React에서 TensorFlow.js 완전 활용 예제
import React, { useState, useEffect, useRef } from 'react';
import * as mobilenet from '@tensorflow-models/mobilenet';
import * as tf from '@tensorflow/tfjs';

function ImageClassifierApp() {
  const [model, setModel] = useState(null);
  const [predictions, setPredictions] = useState([]);
  const [loading, setLoading] = useState(true);
  const [analyzing, setAnalyzing] = useState(false);
  const [error, setError] = useState(null);
  const imageRef = useRef(null);
  const fileInputRef = useRef(null);

  // 컴포넌트 마운트 시 모델 로드
  useEffect(() => {
    async function loadModel() {
      try {
        console.log('TensorFlow.js 버전:', tf.version.tfjs);
        
        // 백엔드 확인
        await tf.ready();
        console.log('백엔드:', tf.getBackend());
        
        // MobileNet 모델 로드
        const loadedModel = await mobilenet.load({
          version: 2,
          alpha: 1.0
        });
        
        setModel(loadedModel);
        setLoading(false);
        console.log('모델 로드 완료');
      } catch (err) {
        console.error('모델 로드 실패:', err);
        setError('모델을 불러오는데 실패했습니다.');
        setLoading(false);
      }
    }
    
    loadModel();
    
    // 컴포넌트 언마운트 시 메모리 정리
    return () => {
      if (model) {
        model.dispose();
        console.log('모델 메모리 해제');
      }
      
      // 텐서 메모리 정리
      const numTensors = tf.memory().numTensors;
      console.log('남은 텐서 수:', numTensors);
    };
  }, []);

  // 이미지 업로드 처리
  async function handleImageUpload(event) {
    const file = event.target.files[0];
    if (!file) return;
    
    const imageUrl = URL.createObjectURL(file);
    imageRef.current.src = imageUrl;
    
    // 이미지 로드 완료 후 분류
    imageRef.current.onload = async () => {
      await classifyImage();
      URL.revokeObjectURL(imageUrl); // 메모리 누수 방지
    };
  }

  // 이미지 분류 실행
  async function classifyImage() {
    if (!model || !imageRef.current) return;
    
    try {
      setAnalyzing(true);
      setPredictions([]);
      
      // 이미지 분류 (Top 3 결과)
      const results = await model.classify(imageRef.current, 3);
      
      setPredictions(results);
      setAnalyzing(false);
      
      console.log('분류 결과:', results);
    } catch (err) {
      console.error('분류 오류:', err);
      setError('이미지 분석에 실패했습니다.');
      setAnalyzing(false);
    }
  }

  // 웹캠 실시간 분류
  async function startWebcamClassification() {
    try {
      const stream = await navigator.mediaDevices.getUserMedia({
        video: { facingMode: 'user' }
      });
      
      imageRef.current.srcObject = stream;
      imageRef.current.play();
      
      // 실시간 분류 루프
      const classify = async () => {
        if (model && imageRef.current) {
          const results = await model.classify(imageRef.current);
          setPredictions(results);
        }
        
        requestAnimationFrame(classify);
      };
      
      classify();
    } catch (err) {
      console.error('웹캠 접근 실패:', err);
      setError('웹캠에 접근할 수 없습니다.');
    }
  }

  return (
    <div className="app-container">
      <h1>AI 이미지 분류기</h1>
      
      {loading && (
        <div className="loading">
          <div className="spinner"></div>
          <p>AI 모델 로딩 중...</p>
        </div>
      )}
      
      {error && (
        <div className="error-message">
          <p>{error}</p>
          <button onClick={() => window.location.reload()}>
            다시 시도
          </button>
        </div>
      )}
      
      {!loading && !error && (
        <>
          <div className="controls">
            <input 
              type="file" 
              accept="image/*" 
              onChange={handleImageUpload}
              ref={fileInputRef}
              style={{ display: 'none' }}
            />
            <button 
              onClick={() => fileInputRef.current.click()}
              className="btn-primary"
            >
              이미지 업로드
            </button>
            
            <button 
              onClick={startWebcamClassification}
              className="btn-secondary"
            >
              웹캠 시작
            </button>
          </div>
          
          <div className="image-container">
            <img 
              ref={imageRef} 
              alt="분석할 이미지"
              style={{ maxWidth: '500px', display: 'block' }}
            />
          </div>
          
          {analyzing && (
            <div className="analyzing">
              <div className="pulse-animation"></div>
              <p>AI가 이미지를 분석하고 있습니다...</p>
            </div>
          )}
          
          {predictions.length > 0 && (
            <div className="results">
              <h2>분석 결과</h2>
              <ul className="predictions-list">
                {predictions.map((pred, idx) => (
                  <li key={idx} className="prediction-item">
                    <div className="prediction-bar">
                      <span className="label">{pred.className}</span>
                      <div className="bar-container">
                        <div 
                          className="bar-fill"
                          style={{ 
                            width: `${pred.probability * 100}%`,
                            backgroundColor: getColorByRank(idx)
                          }}
                        ></div>
                      </div>
                      <span className="percentage">
                        {(pred.probability * 100).toFixed(1)}%
                      </span>
                    </div>
                  </li>
                ))}
              </ul>
            </div>
          )}
          
          <div className="info-panel">
            <h3>시스템 정보</h3>
            <p>백엔드: {tf.getBackend()}</p>
            <p>메모리: {tf.memory().numBytes} bytes</p>
            <p>텐서: {tf.memory().numTensors}개</p>
          </div>
        </>
      )}
    </div>
  );
}

function getColorByRank(rank) {
  const colors = ['#4CAF50', '#2196F3', '#FF9800'];
  return colors[rank] || '#9E9E9E';
}

export default ImageClassifierApp;

Vue에서도 비슷한 패턴을 사용합니다. Composition API를 사용한다면 onMounted 훅에서 모델을 로드하고, ref로 상태를 관리하며, onUnmounted에서 정리하면 되고요. Options API라면 mounted와 beforeDestroy 라이프사이클 훅을 활용합니다.

// Vue 3 Composition API 예제
<template>
  <div class="image-classifier">
    <h1>AI 이미지 분류</h1>
    
    <div v-if="loading" class="loading">
      모델 로딩 중...
    </div>
    
    <div v-else>
      <input 
        type="file" 
        @change="handleImageUpload" 
        accept="image/*"
      />
      
      <img 
        ref="imageElement" 
        :src="imageUrl" 
        v-if="imageUrl"
        @load="classifyImage"
      />
      
      <div v-if="predictions.length" class="results">
        <h2>예측 결과</h2>
        <ul>
          <li v-for="(pred, idx) in predictions" :key="idx">
            {{ pred.className }}: {{ (pred.probability * 100).toFixed(2) }}%
          </li>
        </ul>
      </div>
    </div>
  </div>
</template>

<script>
import { ref, onMounted, onUnmounted } from 'vue';
import * as mobilenet from '@tensorflow-models/mobilenet';

export default {
  name: 'ImageClassifier',
  setup() {
    const model = ref(null);
    const loading = ref(true);
    const predictions = ref([]);
    const imageUrl = ref(null);
    const imageElement = ref(null);
    
    onMounted(async () => {
      try {
        model.value = await mobilenet.load();
        loading.value = false;
        console.log('모델 로드 완료');
      } catch (error) {
        console.error('모델 로드 실패:', error);
      }
    });
    
    onUnmounted(() => {
      if (model.value) {
        model.value.dispose();
        console.log('모델 정리 완료');
      }
    });
    
    const handleImageUpload = (event) => {
      const file = event.target.files[0];
      if (file) {
        imageUrl.value = URL.createObjectURL(file);
      }
    };
    
    const classifyImage = async () => {
      if (model.value && imageElement.value) {
        predictions.value = await model.value.classify(imageElement.value);
      }
    };
    
    return {
      loading,
      predictions,
      imageUrl,
      imageElement,
      handleImageUpload,
      classifyImage
    };
  }
};
</script>

프로덕션 배포 시에는 번들 크기를 정말 주의해야 합니다. TensorFlow.js 전체를 임포트하면 수 MB가 추가되니까, 필요한 모델만 선택적으로 임포트하는 게 좋아요. Webpack의 코드 스플리팅을 활용하면 AI 기능이 필요한 페이지에서만 모델을 로드할 수 있고요.

// 동적 import를 활용한 코드 스플리팅
const ImageClassifier = lazy(() => import('./components/ImageClassifier'));

function App() {
  const [showAI, setShowAI] = useState(false);
  
  return (
    <div>
      <button onClick={() => setShowAI(true)}>
        AI 기능 활성화
      </button>
      
      {showAI && (
        <Suspense fallback={<div>AI 모듈 로딩 중...</div>}>
          <ImageClassifier />
        </Suspense>
      )}
    </div>
  );
}
프레임워크 통합 비교 React Vue Angular Svelte
설치 방법 npm install npm install npm install npm install
모델 로드 시점 useEffect onMounted ngOnInit onMount
상태 관리 useState ref 컴포넌트 속성 writable store
메모리 정리 useEffect return onUnmounted ngOnDestroy onDestroy
번들 크기 영향 중간 3~5MB 작음 2~4MB 큼 4~6MB 작음 2~3MB
타입스크립트 지원 우수 우수 기본 내장 우수
커뮤니티 예제 매우 많음 많음 보통 적음
학습 난이도 쉬움 쉬움 중간 쉬움

AWS Amplify나 Firebase 같은 클라우드 서비스와 통합하면 하이브리드 아키텍처도 가능합니다. 간단한 추론은 클라이언트에서 처리하고, 복잡하거나 민감한 작업은 서버로 보내는 방식이죠. 예를 들어 실시간 얼굴 필터는 브라우저에서 처리하지만, 최종 고해상도 렌더링은 서버에서 하는 식입니다.


실전 프로젝트로 배우는 웹 AI 개발 완전 가이드

이제 실제로 작동하는 완전한 웹 AI 프로젝트를 만들어봅시다. 실시간 웹캠 객체 탐지 앱을 처음부터 끝까지 구현하면서 배운 내용을 모두 적용해볼게요.

<!DOCTYPE html>
<html lang="ko">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>실시간 객체 탐지 웹앱</title>
    <script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs"></script>
    <script src="https://cdn.jsdelivr.net/npm/@tensorflow-models/coco-ssd"></script>
    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }
        
        body {
            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            min-height: 100vh;
            padding: 20px;
        }
        
        .container {
            max-width: 1200px;
            margin: 0 auto;
            background: white;
            border-radius: 20px;
            padding: 30px;
            box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
        }
        
        h1 {
            text-align: center;
            color: #333;
            margin-bottom: 10px;
        }
        
        .subtitle {
            text-align: center;
            color: #666;
            margin-bottom: 30px;
        }
        
        .video-container {
            position: relative;
            width: 100%;
            max-width: 640px;
            margin: 0 auto;
        }
        
        #video {
            width: 100%;
            border-radius: 10px;
            display: none;
        }
        
        #canvas {
            width: 100%;
            border-radius: 10px;
            box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
        }
        
        .controls {
            display: flex;
            gap: 10px;
            justify-content: center;
            margin-top: 20px;
            flex-wrap: wrap;
        }
        
        button {
            padding: 12px 24px;
            font-size: 16px;
            border: none;
            border-radius: 8px;
            cursor: pointer;
            transition: all 0.3s;
            font-weight: 600;
        }
        
        .btn-primary {
            background: #667eea;
            color: white;
        }
        
        .btn-primary:hover {
            background: #5568d3;
            transform: translateY(-2px);
        }
        
        .btn-secondary {
            background: #f093fb;
            color: white;
        }
        
        .btn-secondary:hover {
            background: #d572e8;
            transform: translateY(-2px);
        }
        
        .btn-danger {
            background: #ff6b6b;
            color: white;
        }
        
        .btn-danger:hover {
            background: #ee5253;
            transform: translateY(-2px);
        }
        
        .stats {
            margin-top: 30px;
            display: grid;
            grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
            gap: 20px;
        }
        
        .stat-card {
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            padding: 20px;
            border-radius: 10px;
            color: white;
            text-align: center;
        }
        
        .stat-value {
            font-size: 32px;
            font-weight: bold;
            margin-bottom: 5px;
        }
        
        .stat-label {
            font-size: 14px;
            opacity: 0.9;
        }
        
        .loading-overlay {
            position: fixed;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            background: rgba(0, 0, 0, 0.8);
            display: flex;
            justify-content: center;
            align-items: center;
            z-index: 1000;
        }
        
        .loading-content {
            text-align: center;
            color: white;
        }
        
        .spinner {
            border: 4px solid rgba(255, 255, 255, 0.3);
            border-radius: 50%;
            border-top: 4px solid white;
            width: 50px;
            height: 50px;
            animation: spin 1s linear infinite;
            margin: 0 auto 20px;
        }
        
        @keyframes spin {
            0% { transform: rotate(0deg); }
            100% { transform: rotate(360deg); }
        }
        
        .detected-objects {
            margin-top: 30px;
            background: #f8f9fa;
            padding: 20px;
            border-radius: 10px;
        }
        
        .detected-objects h3 {
            margin-bottom: 15px;
            color: #333;
        }
        
        .object-list {
            display: flex;
            flex-wrap: wrap;
            gap: 10px;
        }
        
        .object-tag {
            background: white;
            padding: 8px 16px;
            border-radius: 20px;
            border: 2px solid #667eea;
            color: #667eea;
            font-weight: 600;
            box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
        }
        
        @media (max-width: 768px) {
            .container {
                padding: 20px;
            }
            
            h1 {
                font-size: 24px;
            }
            
            .controls {
                flex-direction: column;
            }
            
            button {
                width: 100%;
            }
        }
    </style>
</head>
<body>
    <div class="container">
        <h1>🎯 실시간 AI 객체 탐지</h1>
        <p class="subtitle">COCO-SSD 모델로 80가지 물체를 실시간 인식합니다</p>
        
        <div class="video-container">
            <video id="video" autoplay playsinline></video>
            <canvas id="canvas"></canvas>
        </div>
        
        <div class="controls">
            <button id="startBtn" class="btn-primary">웹캠 시작</button>
            <button id="stopBtn" class="btn-danger" style="display: none;">웹캠 중지</button>
            <button id="captureBtn" class="btn-secondary" style="display: none;">스크린샷</button>
        </div>
        
        <div class="stats">
            <div class="stat-card">
                <div class="stat-value" id="objectCount">0</div>
                <div class="stat-label">감지된 객체</div>
            </div>
            <div class="stat-card">
                <div class="stat-value" id="fps">0</div>
                <div class="stat-label">FPS</div>
            </div>
            <div class="stat-card">
                <div class="stat-value" id="processingTime">0ms</div>
                <div class="stat-label">처리 시간</div>
            </div>
        </div>
        
        <div class="detected-objects">
            <h3>현재 감지된 객체 목록</h3>
            <div class="object-list" id="objectList">
                <span style="color: #999;">아직 감지된 객체가 없습니다</span>
            </div>
        </div>
    </div>
    
    <div id="loadingOverlay" class="loading-overlay">
        <div class="loading-content">
            <div class="spinner"></div>
            <h2>AI 모델 로딩 중...</h2>
            <p>잠시만 기다려주세요</p>
        </div>
    </div>

    <script>
        let model;
        let video;
        let canvas;
        let ctx;
        let isDetecting = false;
        let stream;
        let frameCount = 0;
        let lastTime = Date.now();

        // 페이지 로드 시 모델 로드
        async function init() {
            try {
                console.log('TensorFlow.js 버전:', tf.version.tfjs);
                console.log('백엔드:', tf.getBackend());
                
                // COCO-SSD 모델 로드
                model = await cocoSsd.load();
                console.log('모델 로드 완료!');
                
                // 로딩 오버레이 숨기기
                document.getElementById('loadingOverlay').style.display = 'none';
                
                // 비디오 및 캔버스 요소 가져오기
                video = document.getElementById('video');
                canvas = document.getElementById('canvas');
                ctx = canvas.getContext('2d');
                
                // 버튼 이벤트 리스너
                document.getElementById('startBtn').addEventListener('click', startWebcam);
                document.getElementById('stopBtn').addEventListener('click', stopWebcam);
                document.getElementById('captureBtn').addEventListener('click', captureScreenshot);
                
            } catch (error) {
                console.error('초기화 실패:', error);
                alert('모델 로드에 실패했습니다. 페이지를 새로고침해주세요.');
            }
        }

        // 웹캠 시작
        async function startWebcam() {
            try {
                stream = await navigator.mediaDevices.getUserMedia({
                    video: { 
                        width: { ideal: 640 },
                        height: { ideal: 480 },
                        facingMode: 'user'
                    }
                });
                
                video.srcObject = stream;
                video.onloadedmetadata = () => {
                    video.play();
                    canvas.width = video.videoWidth;
                    canvas.height = video.videoHeight;
                    
                    // UI 업데이트
                    document.getElementById('startBtn').style.display = 'none';
                    document.getElementById('stopBtn').style.display = 'inline-block';
                    document.getElementById('captureBtn').style.display = 'inline-block';
                    video.style.display = 'block';
                    
                    // 객체 탐지 시작
                    isDetecting = true;
                    detectFrame();
                };
                
            } catch (error) {
                console.error('웹캠 접근 실패:', error);
                alert('웹캠에 접근할 수 없습니다. 권한을 확인해주세요.');
            }
        }

        // 웹캠 중지
        function stopWebcam() {
            isDetecting = false;
            
            if (stream) {
                stream.getTracks().forEach(track => track.stop());
            }
            
            video.style.display = 'none';
            document.getElementById('startBtn').style.display = 'inline-block';
            document.getElementById('stopBtn').style.display = 'none';
            document.getElementById('captureBtn').style.display = 'none';
            
            // 캔버스 초기화
            ctx.clearRect(0, 0, canvas.width, canvas.height);
            
            // 통계 초기화
            document.getElementById('objectCount').textContent = '0';
            document.getElementById('objectList').innerHTML = '<span style="color: #999;">아직 감지된 객체가 없습니다</span>';
        }

        // 실시간 객체 탐지
        async function detectFrame() {
            if (!isDetecting) return;
            
            const startTime = performance.now();
            
            try {
                // 객체 탐지
                const predictions = await model.detect(video);
                
                // 캔버스에 비디오 프레임 그리기
                ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
                
                // 예측 결과 그리기
                drawPredictions(predictions);
                
                // 통계 업데이트
                updateStats(predictions, performance.now() - startTime);
                
                // FPS 계산
                frameCount++;
                const currentTime = Date.now();
                if (currentTime - lastTime >= 1000) {
                    document.getElementById('fps').textContent = frameCount;
                    frameCount = 0;
                    lastTime = currentTime;
                }
                
            } catch (error) {
                console.error('탐지 오류:', error);
            }
            
            // 다음 프레임 처리
            requestAnimationFrame(detectFrame);
        }

        // 예측 결과 그리기
        function drawPredictions(predictions) {
            predictions.forEach(prediction => {
                const [x, y, width, height] = prediction.bbox;
                const score = (prediction.score * 100).toFixed(1);
                
                // 신뢰도에 따른 색상
                const color = score > 80 ? '#00FF00' : score > 50 ? '#FFFF00' : '#FF6600';
                
                // 바운딩 박스
                ctx.strokeStyle = color;
                ctx.lineWidth = 3;
                ctx.strokeRect(x, y, width, height);
                
                // 레이블 배경
                const label = `${prediction.class} ${score}%`;
                ctx.font = 'bold 16px Arial';
                const textWidth = ctx.measureText(label).width;
                
                ctx.fillStyle = color;
                ctx.fillRect(x, y > 25 ? y - 25 : y, textWidth + 10, 25);
                
                // 레이블 텍스트
                ctx.fillStyle = '#000';
                ctx.fillText(label, x + 5, y > 25 ? y - 7 : y + 18);
                
                // 신뢰도 바
                ctx.fillStyle = 'rgba(255, 255, 255, 0.3)';
                ctx.fillRect(x, y + height - 5, width, 5);
                ctx.fillStyle = color;
                ctx.fillRect(x, y + height - 5, width * prediction.score, 5);
            });
        }

        // 통계 업데이트
        function updateStats(predictions, processingTime) {
            // 객체 수
            document.getElementById('objectCount').textContent = predictions.length;
            
            // 처리 시간
            document.getElementById('processingTime').textContent = Math.round(processingTime) + 'ms';
            
            // 객체 목록
            const objectList = document.getElementById('objectList');
            
            if (predictions.length === 0) {
                objectList.innerHTML = '<span style="color: #999;">아직 감지된 객체가 없습니다</span>';
            } else {
                // 중복 제거 및 카운팅
                const objectCounts = {};
                predictions.forEach(pred => {
                    objectCounts[pred.class] = (objectCounts[pred.class] || 0) + 1;
                });
                
                objectList.innerHTML = Object.entries(objectCounts)
                    .map(([name, count]) => 
                        `<span class="object-tag">${name} (${count})</span>`
                    ).join('');
            }
        }

        // 스크린샷 캡처
        function captureScreenshot() {
            const link = document.createElement('a');
            link.download = `screenshot_${Date.now()}.png`;
            link.href = canvas.toDataURL();
            link.click();
            
            // 플래시 효과
            canvas.style.opacity = '0.5';
            setTimeout(() => {
                canvas.style.opacity = '1';
            }, 100);
        }

        // 페이지 로드 시 초기화
        window.addEventListener('load', init);
    </script>
</body>
</html>

이 프로젝트는 정말 완전한 기능을 갖춘 실전 웹 AI 앱입니다. 웹캠에서 실시간으로 객체를 탐지하고, 바운딩 박스로 표시하며, FPS와 처리 시간까지 보여주죠. 스크린샷 기능도 있어서 마음에 드는 순간을 캡처할 수 있고요.

프로젝트 구성 요소 기술 스택 주요 기능 난이도
프론트엔드 UI HTML5 CSS3 반응형 디자인 그라데이션 배경 쉬움
비디오 캡처 MediaDevices API 웹캠 스트림 실시간 처리 중간
객체 탐지 TensorFlow.js COCO-SSD 80가지 객체 인식 바운딩 박스 중간
캔버스 렌더링 Canvas 2D API 비디오 오버레이 시각화 중간
성능 모니터링 Performance API FPS 측정 처리 시간 계산 쉬움
스크린샷 기능 Canvas toDataURL 이미지 다운로드 쉬움

모바일 최적화와 PWA로 네이티브 앱처럼 만들기

웹 AI 앱을 모바일에서도 완벽하게 작동하도록 최적화하고, PWA로 만들면 앱스토어 없이도 네이티브 앱처럼 사용할 수 있습니다.

// manifest.json - PWA 설정
{
  "name": "AI 객체 탐지 앱",
  "short_name": "AI Detector",
  "description": "실시간 객체 탐지 웹 애플리케이션",
  "start_url": "/",
  "display": "standalone",
  "background_color": "#667eea",
  "theme_color": "#667eea",
  "orientation": "portrait",
  "icons": [
    {
      "src": "/icon-192.png",
      "sizes": "192x192",
      "type": "image/png"
    },
    {
      "src": "/icon-512.png",
      "sizes": "512x512",
      "type": "image/png"
    }
  ]
}
// service-worker.js - 오프라인 지원 및 모델 캐싱
const CACHE_NAME = 'ai-detector-v1';
const MODEL_CACHE = 'ai-models-v1';

const STATIC_ASSETS = [
  '/',
  '/index.html',
  '/styles.css',
  '/app.js',
  '/icon-192.png',
  '/icon-512.png'
];

const MODEL_URLS = [
  'https://storage.googleapis.com/tfjs-models/savedmodel/coco-ssd/model.json',
  'https://cdn.jsdelivr.net/npm/@tensorflow/tfjs'
];

// 설치 이벤트
self.addEventListener('install', (event) => {
  console.log('Service Worker 설치 중...');
  
  event.waitUntil(
    Promise.all([
      // 정적 파일 캐싱
      caches.open(CACHE_NAME).then((cache) => {
        return cache.addAll(STATIC_ASSETS);
      }),
      // AI 모델 캐싱
      caches.open(MODEL_CACHE).then((cache) => {
        return cache.addAll(MODEL_URLS);
      })
    ]).then(() => {
      console.log('캐싱 완료!');
      return self.skipWaiting();
    })
  );
});

// 활성화 이벤트
self.addEventListener('activate', (event) => {
  console.log('Service Worker 활성화');
  
  event.waitUntil(
    caches.keys().then((cacheNames) => {
      return Promise.all(
        cacheNames.map((cacheName) => {
          if (cacheName !== CACHE_NAME && cacheName !== MODEL_CACHE) {
            console.log('오래된 캐시 삭제:', cacheName);
            return caches.delete(cacheName);
          }
        })
      );
    }).then(() => {
      return self.clients.claim();
    })
  );
});

// Fetch 이벤트 - 네트워크 우선, 캐시 대체 전략
self.addEventListener('fetch', (event) => {
  event.respondWith(
    fetch(event.request)
      .then((response) => {
        // 성공한 응답 캐싱
        if (response.status === 200) {
          const responseClone = response.clone();
          
          caches.open(CACHE_NAME).then((cache) => {
            cache.put(event.request, responseClone);
          });
        }
        
        return response;
      })
      .catch(() => {
        // 네트워크 실패 시 캐시에서 가져오기
        return caches.match(event.request);
      })
  );
});

// 백그라운드 동기화
self.addEventListener('sync', (event) => {
  if (event.tag === 'sync-detections') {
    event.waitUntil(syncDetections());
  }
});

async function syncDetections() {
  // 오프라인에서 저장된 탐지 결과를 서버에 동기화
  const db = await openDB();
  const detections = await db.getAll('detections');
  
  for (const detection of detections) {
    try {
      await fetch('/api/sync', {
        method: 'POST',
        body: JSON.stringify(detection)
      });
      
      await db.delete('detections', detection.id);
    } catch (error) {
      console.error('동기화 실패:', error);
    }
  }
}
// 모바일 최적화 스크립트
// app.js에 추가

// 기기 성능 감지
function detectDeviceCapability() {
  const isMobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent);
  const cores = navigator.hardwareConcurrency || 2;
  const memory = navigator.deviceMemory || 4;
  const connection = navigator.connection || navigator.mozConnection || navigator.webkitConnection;
  
  let deviceTier = 'high';
  
  if (isMobile) {
    if (cores <= 4 && memory <= 4) {
      deviceTier = 'low';
    } else if (cores <= 6 && memory <= 6) {
      deviceTier = 'medium';
    }
  }
  
  return {
    isMobile,
    cores,
    memory,
    tier: deviceTier,
    connectionType: connection ? connection.effectiveType : 'unknown'
  };
}

// 기기에 맞는 설정 적용
function applyDeviceOptimizations() {
  const device = detectDeviceCapability();
  console.log('기기 정보:', device);
  
  let config = {
    videoWidth: 640,
    videoHeight: 480,
    modelBase: 'mobilenet_v2',
    detectionInterval: 100,
    maxDetections: 20
  };
  
  if (device.tier === 'low') {
    config = {
      videoWidth: 320,
      videoHeight: 240,
      modelBase: 'lite_mobilenet_v2',
      detectionInterval: 200,
      maxDetections: 10
    };
  } else if (device.tier === 'medium') {
    config = {
      videoWidth: 480,
      videoHeight: 360,
      modelBase: 'mobilenet_v2',
      detectionInterval: 150,
      maxDetections: 15
    };
  }
  
  return config;
}

// 터치 제스처 지원
function enableTouchGestures() {
  let startX, startY;
  
  canvas.addEventListener('touchstart', (e) => {
    startX = e.touches[0].clientX;
    startY = e.touches[0].clientY;
  });
  
  canvas.addEventListener('touchmove', (e) => {
    e.preventDefault(); // 스크롤 방지
  }, { passive: false });
  
  canvas.addEventListener('touchend', (e) => {
    const endX = e.changedTouches[0].clientX;
    const endY = e.changedTouches[0].clientY;
    
    const diffX = endX - startX;
    const diffY = endY - startY;
    
    // 스와이프 감지
    if (Math.abs(diffX) > 50) {
      if (diffX > 0) {
        console.log('오른쪽 스와이프');
        // 다음 필터
      } else {
        console.log('왼쪽 스와이프');
        // 이전 필터
      }
    }
    
    // 더블 탭 감지
    if (Math.abs(diffX) < 10 && Math.abs(diffY) < 10) {
      captureScreenshot();
    }
  });
  
  // 핀치 줌
  let initialDistance = 0;
  
  canvas.addEventListener('touchstart', (e) => {
    if (e.touches.length === 2) {
      initialDistance = getDistance(e.touches[0], e.touches[1]);
    }
  });
  
  canvas.addEventListener('touchmove', (e) => {
    if (e.touches.length === 2) {
      const currentDistance = getDistance(e.touches[0], e.touches[1]);
      const scale = currentDistance / initialDistance;
      
      // 줌 적용
      applyZoom(scale);
    }
  });
}

function getDistance(touch1, touch2) {
  const dx = touch1.clientX - touch2.clientX;
  const dy = touch1.clientY - touch2.clientY;
  return Math.sqrt(dx * dx + dy * dy);
}

// 배터리 최적화
if ('getBattery' in navigator) {
  navigator.getBattery().then((battery) => {
    console.log('배터리 레벨:', battery.level * 100 + '%');
    console.log('충전 중:', battery.charging);
    
    // 배터리 부족 시 프레임 레이트 감소
    if (battery.level < 0.2 && !battery.charging) {
      console.log('배터리 절약 모드 활성화');
      detectionInterval = 300; // 300ms로 증가
    }
    
    battery.addEventListener('levelchange', () => {
      if (battery.level < 0.1) {
        alert('배터리가 부족합니다. AI 기능이 일시 중지됩니다.');
        stopWebcam();
      }
    });
  });
}

// PWA 설치 프롬프트
let deferredPrompt;

window.addEventListener('beforeinstallprompt', (e) => {
  e.preventDefault();
  deferredPrompt = e;
  
  // 설치 버튼 표시
  const installBtn = document.createElement('button');
  installBtn.textContent = '앱으로 설치';
  installBtn.className = 'btn-primary install-btn';
  installBtn.style.position = 'fixed';
  installBtn.style.bottom = '20px';
  installBtn.style.right = '20px';
  installBtn.style.zIndex = '1000';
  
  installBtn.addEventListener('click', async () => {
    deferredPrompt.prompt();
    const { outcome } = await deferredPrompt.userChoice;
    
    console.log('설치 결과:', outcome);
    
    if (outcome === 'accepted') {
      installBtn.remove();
    }
    
    deferredPrompt = null;
  });
  
  document.body.appendChild(installBtn);
});

// 설치 완료 이벤트
window.addEventListener('appinstalled', () => {
  console.log('PWA 설치 완료!');
  // 설치 완료 알림
  showNotification('앱이 성공적으로 설치되었습니다!');
});
PWA 기능 구현 방법 사용자 혜택 개발 난이도
오프라인 작동 Service Worker 캐싱 인터넷 없이 사용 가능 중간
홈 화면 추가 Web App Manifest 네이티브 앱처럼 실행 쉬움
푸시 알림 Push API Notification API 실시간 알림 수신 어려움
백그라운드 동기화 Background Sync API 오프라인 데이터 자동 동기화 어려움
빠른 로딩 캐시 우선 전략 즉시 실행 중간
자동 업데이트 Service Worker 업데이트 최신 버전 자동 적용 중간

웹사이트에 AI 기능을 추가하는 것은 더 이상 파이썬 전문가의 전유물이 아닙니다. ML5.js로 3분 만에 웹캠 포즈 추적을 구현하고, COCO-SSD로 실시간 객체 탐지를 만들며, Brain.js로 간단한 예측 모델을 훈련시킬 수 있어요. 모든 처리가 브라우저 안에서 이루어지니까 사용자 데이터는 안전하게 보호되고, 네트워크 지연 없이 즉각 반응하며, 서버 비용도 절감됩니다.

React나 Vue 같은 프레임워크와도 자연스럽게 통합되어 현대적인 웹 애플리케이션에 바로 적용할 수 있고요. 모바일 최적화와 PWA 기술을 더하면 앱스토어 없이도 네이티브 앱 수준의 경험을 제공할 수 있습니다. 이제 프론트엔드 개발자도 자신의 웹사이트에 마법 같은 AI 경험을 선사할 수 있는 시대죠. 시작은 단 세 줄의 코드면 충분합니다.


공식 참고 링크 안내

ML5.js 공식 홈페이지

TensorFlow.js 모델 저장소

COCO-SSD 공식 가이드


댓글 쓰기

0 댓글

이 블로그 검색

태그

신고하기

프로필

정부지원금