⚠️

본 콘텐츠의 이미지 및 내용을 무단으로 복제, 배포, 수정하여 사용할 경우 저작권법에 의해 법적 제재를 받을 수 있습니다.

이미지 로딩 중...

실전 프로젝트 이미지 분석 앱 만들기 - 슬라이드 1/7
A

AI Generated

2025. 11. 27. · 0 Views

실전 프로젝트 이미지 분석 앱 만들기

드래그앤드롭으로 이미지를 업로드하고, AI 모델로 분석한 결과를 시각화하여 내보내는 완성형 웹 앱을 만들어봅니다. 초급 개발자도 따라할 수 있도록 단계별로 설명합니다.


목차

  1. 드래그앤드롭_구현
  2. 이미지_미리보기
  3. 다중_모델_분석
  4. 분석_결과_시각화
  5. 결과_내보내기
  6. PWA로_배포

1. 드래그앤드롭 구현

어느 날 김개발 씨는 회사에서 새로운 프로젝트를 맡게 되었습니다. "이미지를 업로드해서 AI로 분석하는 웹 앱을 만들어 주세요." 기획팀의 요청이었습니다.

김개발 씨는 파일 선택 버튼만으로는 뭔가 부족하다고 느꼈습니다. 요즘 트렌드는 드래그앤드롭 아닌가요?

드래그앤드롭은 사용자가 파일을 마우스로 끌어다 놓는 것만으로 업로드할 수 있게 해주는 기능입니다. 마치 책상 위의 서류를 서류함에 넣는 것처럼 직관적입니다.

이 기능을 구현하면 사용자 경험이 크게 향상되고, 여러 파일을 한 번에 처리하기도 쉬워집니다.

다음 코드를 살펴봅시다.

// 드롭 영역 HTML 요소 가져오기
const dropZone = document.getElementById('drop-zone');

// 드래그 중인 파일이 영역 위에 있을 때
dropZone.addEventListener('dragover', (e) => {
  e.preventDefault(); // 기본 동작 방지 필수
  dropZone.classList.add('drag-active');
});

// 드래그가 영역을 벗어났을 때
dropZone.addEventListener('dragleave', () => {
  dropZone.classList.remove('drag-active');
});

// 파일을 놓았을 때 핵심 처리
dropZone.addEventListener('drop', (e) => {
  e.preventDefault();
  const files = Array.from(e.dataTransfer.files);
  const imageFiles = files.filter(file => file.type.startsWith('image/'));
  handleImages(imageFiles); // 이미지 처리 함수 호출
});

김개발 씨는 입사 6개월 차 주니어 개발자입니다. 이번이 첫 번째 실전 프로젝트라 긴장이 됩니다.

일단 파일 업로드부터 구현해보기로 했습니다. 그런데 선배 박시니어 씨가 지나가다 한마디 합니다.

"요즘 파일 선택 버튼만 덜렁 있으면 좀 구식으로 보여요. 드래그앤드롭 넣어보는 게 어때요?" 그렇다면 드래그앤드롭이란 정확히 어떻게 동작하는 걸까요?

쉽게 비유하자면, 드래그앤드롭은 마치 우체통에 편지를 넣는 것과 같습니다. 편지를 들고 우체통 앞에 서면 우체통이 알아차리고, 편지를 넣으면 우체통이 받아서 처리합니다.

웹에서도 마찬가지입니다. 파일을 들고 드롭 영역 위에 오면 영역이 이를 감지하고, 파일을 놓으면 받아서 처리합니다.

드래그앤드롭을 구현하려면 세 가지 이벤트를 알아야 합니다. 첫 번째는 dragover 이벤트입니다.

파일을 끌어서 드롭 영역 위에 올려놓고 있을 때 계속 발생합니다. 여기서 중요한 것은 반드시 **preventDefault()**를 호출해야 한다는 점입니다.

그렇지 않으면 브라우저가 파일을 직접 열어버립니다. 두 번째는 dragleave 이벤트입니다.

파일을 끌다가 드롭 영역을 벗어났을 때 발생합니다. 이때 시각적 피드백을 원래대로 되돌려주면 사용자가 "아, 여기에 놓으면 안 되는구나"를 알 수 있습니다.

세 번째는 drop 이벤트입니다. 파일을 실제로 놓았을 때 발생합니다.

여기서도 preventDefault()를 호출하고, e.dataTransfer.files를 통해 드롭된 파일들에 접근할 수 있습니다. 위 코드를 자세히 살펴보겠습니다.

3번째 줄에서 dragover 이벤트를 처리합니다. preventDefault()로 기본 동작을 막고, 드롭 영역에 'drag-active' 클래스를 추가해 시각적 피드백을 줍니다.

9번째 줄의 dragleave에서는 이 클래스를 제거합니다. 핵심은 14번째 줄부터입니다.

drop 이벤트에서 e.dataTransfer.files로 파일 목록을 가져옵니다. 이것은 FileList 객체인데, 배열처럼 다루려면 **Array.from()**으로 변환해야 합니다.

그다음 filter로 이미지 파일만 골라냅니다. 실제 현업에서는 어떻게 활용할까요?

쇼핑몰 관리자 페이지를 만든다고 가정해봅시다. 상품 이미지를 여러 장 등록해야 하는데, 매번 파일 선택 버튼을 클릭하는 것은 번거롭습니다.

드래그앤드롭으로 여러 이미지를 한 번에 끌어다 놓으면 업무 효율이 크게 올라갑니다. 하지만 주의할 점도 있습니다.

초보 개발자들이 흔히 하는 실수는 preventDefault()를 빼먹는 것입니다. 이러면 파일을 드롭했을 때 브라우저가 이미지를 새 탭에서 열어버립니다.

반드시 dragover와 drop 모두에서 preventDefault()를 호출해야 합니다. 다시 김개발 씨의 이야기로 돌아가 봅시다.

드래그앤드롭을 구현하고 나니 앱이 한결 세련되어 보입니다. 박시니어 씨도 엄지를 치켜세웁니다.

"오, 제법인데요?"

실전 팁

💡 - dragover와 drop 둘 다 preventDefault() 필수

  • 드래그 중일 때 시각적 피드백(테두리 색상 변경 등)을 주면 UX가 좋아집니다
  • file.type.startsWith('image/')로 이미지 파일만 필터링하세요

2. 이미지 미리보기

드래그앤드롭까지 구현한 김개발 씨. 그런데 기획팀에서 추가 요청이 들어왔습니다.

"파일을 올리면 바로 미리보기가 보였으면 좋겠어요." 당연한 요청이었습니다. 어떤 이미지를 올렸는지 확인도 못하고 분석 버튼을 누르라니, 사용자 입장에서 불안하지 않겠습니까?

FileReader API는 브라우저에서 파일의 내용을 읽을 수 있게 해주는 도구입니다. 마치 사진관에서 필름을 현상하는 것처럼, 파일 객체를 실제로 화면에 표시할 수 있는 형태로 변환해줍니다.

이를 활용하면 서버에 업로드하기 전에 클라이언트에서 미리보기를 제공할 수 있습니다.

다음 코드를 살펴봅시다.

// 이미지 파일들을 받아 미리보기 생성
function handleImages(files) {
  const previewContainer = document.getElementById('preview');
  previewContainer.innerHTML = ''; // 기존 미리보기 초기화

  files.forEach((file, index) => {
    const reader = new FileReader();

    // 파일 읽기가 완료되면 실행
    reader.onload = (e) => {
      const img = document.createElement('img');
      img.src = e.target.result; // Base64 인코딩된 이미지
      img.className = 'preview-image';
      img.dataset.index = index; // 나중에 분석할 때 사용
      previewContainer.appendChild(img);
    };

    reader.readAsDataURL(file); // 파일을 Data URL로 읽기
  });
}

김개발 씨는 고민에 빠졌습니다. 파일은 받았는데, 이걸 어떻게 화면에 보여주지?

서버에 먼저 올려야 하나? 아니, 그러면 불필요한 서버 요청이 발생하잖아.

박시니어 씨가 힌트를 줍니다. "FileReader라는 게 있어요.

브라우저에서 바로 파일 내용을 읽을 수 있어요." FileReader란 무엇일까요? 쉽게 비유하자면, FileReader는 마치 즉석 사진 현상기와 같습니다.

필름(파일)을 넣으면 바로 사진(미리보기)을 뽑아줍니다. 사진관(서버)에 가지 않아도 됩니다.

이처럼 FileReader를 사용하면 서버 없이 클라이언트에서 직접 파일 내용을 읽을 수 있습니다. FileReader에는 여러 메서드가 있지만, 이미지 미리보기에는 readAsDataURL을 사용합니다.

이 메서드는 파일을 Data URL 형식으로 변환합니다. Data URL은 "data:image/png;base64,iVBORw0KGgo..." 같은 형태의 문자열입니다.

이 문자열을 img 태그의 src 속성에 넣으면 브라우저가 이미지로 렌더링합니다. 위 코드를 단계별로 살펴보겠습니다.

7번째 줄에서 **new FileReader()**로 리더 객체를 생성합니다. 10번째 줄의 reader.onload는 파일 읽기가 완료되었을 때 실행되는 콜백입니다.

파일 읽기는 비동기로 진행되기 때문에, 완료 시점을 이벤트로 알려줍니다. 12번째 줄이 핵심입니다.

e.target.result에 읽어들인 Data URL이 담겨 있습니다. 이걸 새로 만든 img 태그의 src에 할당하면 미리보기 완성입니다.

18번째 줄의 **readAsDataURL(file)**이 실제로 파일 읽기를 시작하는 부분입니다. 이 메서드를 호출해야 onload가 나중에 실행됩니다.

실무에서 자주 쓰이는 패턴이 있습니다. 여러 이미지를 업로드할 때는 각 이미지에 고유 식별자를 부여해두면 좋습니다.

위 코드에서 dataset.index를 사용한 이유입니다. 나중에 특정 이미지만 삭제하거나, 특정 이미지의 분석 결과를 매칭할 때 유용합니다.

주의할 점도 있습니다. FileReader는 비동기로 동작합니다.

즉, readAsDataURL을 호출한 직후에 result를 읽으면 아직 undefined입니다. 반드시 onload 콜백 안에서 결과를 사용해야 합니다.

또한 매우 큰 파일을 읽을 때는 메모리 사용량에 주의해야 합니다. 김개발 씨는 미리보기 기능을 추가하고 테스트해봤습니다.

이미지를 드래그해서 놓으니 바로 썸네일이 나타납니다. "오, 이제 좀 앱 같아 보이네요!"

실전 팁

💡 - readAsDataURL은 비동기이므로 반드시 onload에서 결과를 처리하세요

  • 큰 이미지는 미리보기용으로 리사이즈하면 성능이 좋아집니다
  • URL.createObjectURL을 사용하면 메모리 효율이 더 좋습니다

3. 다중 모델 분석

미리보기까지 완성한 김개발 씨. 이제 본격적으로 AI 분석 기능을 넣을 차례입니다.

그런데 기획팀의 요구사항이 조금 특이합니다. "이미지 분류도 하고, 객체 탐지도 하고, 텍스트 인식도 했으면 좋겠어요." 하나의 이미지를 여러 모델로 분석해야 한다니, 어떻게 구조를 잡아야 할까요?

다중 모델 분석은 하나의 입력 데이터를 여러 AI 모델로 처리하여 다양한 결과를 얻는 기법입니다. 마치 건강검진에서 혈압, 혈당, 시력을 각각 다른 장비로 측정하는 것과 같습니다.

TensorFlow.js를 활용하면 브라우저에서 직접 여러 모델을 실행할 수 있어 서버 비용도 절감할 수 있습니다.

다음 코드를 살펴봅시다.

// 여러 AI 모델을 로드하고 분석 실행
async function analyzeWithMultipleModels(imageElement) {
  // 모델들을 병렬로 로드 (처음 한 번만)
  const [classifyModel, detectModel] = await Promise.all([
    mobilenet.load(), // 이미지 분류 모델
    cocoSsd.load()    // 객체 탐지 모델
  ]);

  // 각 모델로 분석 실행
  const [classifications, detections] = await Promise.all([
    classifyModel.classify(imageElement),
    detectModel.detect(imageElement)
  ]);

  return {
    labels: classifications.map(c => ({ name: c.className, score: c.probability })),
    objects: detections.map(d => ({ name: d.class, box: d.bbox, score: d.score }))
  };
}

김개발 씨는 처음에 간단하게 생각했습니다. 모델 하나 로드하고, 분석하고, 결과 보여주면 되지 않나?

그런데 요구사항을 다시 읽어보니 여러 종류의 분석이 필요했습니다. 박시니어 씨가 조언합니다.

"여러 모델을 순차적으로 실행하면 시간이 오래 걸려요. Promise.all로 병렬 처리하는 게 좋아요." 다중 모델 분석이란 무엇일까요?

건강검진을 떠올려보세요. 병원에 가면 혈압 측정, 혈액 검사, X선 촬영 등 여러 검사를 받습니다.

각 검사는 서로 다른 장비로 진행되고, 서로 다른 정보를 알려줍니다. 이미지 분석도 마찬가지입니다.

분류 모델은 "이 사진은 고양이입니다"를 알려주고, 객체 탐지 모델은 "고양이가 여기에 있습니다"를 알려줍니다. 왜 병렬 처리가 중요할까요?

만약 모델 로드에 2초, 분석에 1초씩 걸린다고 가정합시다. 두 모델을 순차 처리하면 (2+1) + (2+1) = 6초입니다.

하지만 병렬 처리하면 로드 2초 + 분석 1초 = 3초면 됩니다. 사용자 경험이 두 배 좋아집니다.

코드를 자세히 살펴보겠습니다. 4번째 줄에서 Promise.all로 두 모델을 동시에 로드합니다.

mobilenet은 이미지 분류 모델이고, cocoSsd는 객체 탐지 모델입니다. 둘 다 TensorFlow.js에서 제공하는 사전 학습 모델입니다.

10번째 줄에서 다시 Promise.all로 두 분석을 동시에 실행합니다. classify는 이미지가 무엇인지 분류하고, detect는 이미지 안의 객체들을 찾아 위치까지 알려줍니다.

15번째 줄부터는 결과를 정리합니다. 각 모델의 결과 형식이 다르므로, 통일된 형태로 변환해서 반환합니다.

labels는 분류 결과, objects는 탐지된 객체 정보입니다. 실무에서는 모델 로딩을 최적화해야 합니다.

매번 분석할 때마다 모델을 새로 로드하면 비효율적입니다. 앱이 시작될 때 한 번만 로드하고 변수에 저장해두세요.

위 코드는 이해를 위해 함수 안에서 로드했지만, 실제로는 전역 변수나 싱글톤 패턴을 사용합니다. 주의할 점이 있습니다.

TensorFlow.js 모델은 크기가 큽니다. MobileNet만 해도 수 MB입니다.

모바일 환경에서는 로딩 시간을 고려해야 합니다. 로딩 중에는 반드시 로딩 인디케이터를 보여주세요.

또한 WebGL을 지원하지 않는 구형 브라우저에서는 성능이 매우 느릴 수 있습니다. 김개발 씨는 분석 버튼을 누르고 결과를 기다렸습니다.

잠시 후 화면에 "고양이 94%, 위치: 좌표 (120, 80)"라는 결과가 나타났습니다. "와, 브라우저에서 AI가 돌아가다니!"

실전 팁

💡 - 모델 로드는 앱 시작 시 한 번만 하고 캐싱하세요

  • 분석 중에는 로딩 UI를 보여주세요
  • 모바일에서는 더 가벼운 모델(TinyModel)을 고려하세요

4. 분석 결과 시각화

AI 분석까지 성공한 김개발 씨. 그런데 결과가 콘솔에만 찍히고 있었습니다.

기획팀에서 당연히 한마디 합니다. "사용자가 콘솔 열어볼 리가 없잖아요.

결과를 예쁘게 보여줘야죠!" 맞는 말입니다. 특히 객체 탐지 결과는 이미지 위에 박스를 그려주면 훨씬 직관적일 텐데요.

Canvas API를 활용하면 이미지 위에 분석 결과를 시각적으로 표시할 수 있습니다. 마치 사진 위에 스티커를 붙이거나 펜으로 표시하는 것처럼, 탐지된 객체 주변에 박스를 그리고 라벨을 표시할 수 있습니다.

이렇게 하면 사용자가 분석 결과를 한눈에 이해할 수 있습니다.

다음 코드를 살펴봅시다.

// 분석 결과를 이미지 위에 시각화
function visualizeResults(imageElement, results) {
  const canvas = document.createElement('canvas');
  const ctx = canvas.getContext('2d');

  // 캔버스 크기를 이미지에 맞춤
  canvas.width = imageElement.naturalWidth;
  canvas.height = imageElement.naturalHeight;
  ctx.drawImage(imageElement, 0, 0);

  // 탐지된 객체마다 박스 그리기
  results.objects.forEach(obj => {
    const [x, y, width, height] = obj.box;
    ctx.strokeStyle = '#00FF00';
    ctx.lineWidth = 3;
    ctx.strokeRect(x, y, width, height);

    // 라벨 표시
    ctx.fillStyle = '#00FF00';
    ctx.font = '16px Arial';
    ctx.fillText(`${obj.name} ${Math.round(obj.score * 100)}%`, x, y - 5);
  });

  return canvas;
}

김개발 씨는 분석 결과를 어떻게 보여줄지 고민했습니다. 분류 결과야 텍스트로 보여주면 되지만, 객체 탐지 결과는 좌표값만 보여주면 사용자가 이해하기 어렵습니다.

박시니어 씨가 아이디어를 줍니다. "Canvas로 이미지 위에 직접 그리면 돼요.

영상에서 얼굴 인식할 때 박스 그려지는 거 본 적 있죠? 그거예요." Canvas API란 무엇일까요?

Canvas는 웹 페이지 위의 도화지라고 생각하면 됩니다. 일반 img 태그는 이미지를 그냥 보여주기만 합니다.

하지만 Canvas는 그 위에 선을 긋고, 도형을 그리고, 텍스트를 쓸 수 있습니다. 마치 투명 필름을 사진 위에 올려놓고 마음껏 그림을 그리는 것과 같습니다.

시각화가 왜 중요할까요? "객체: 고양이, 좌표: (120, 80, 200, 180)"이라는 텍스트만 보여주면 사용자는 "그래서 어디에 있다는 거지?"라고 생각합니다.

하지만 이미지 위에 초록색 박스를 그려주면 단번에 이해할 수 있습니다. 시각화는 데이터를 직관적으로 전달하는 가장 효과적인 방법입니다.

코드를 단계별로 살펴보겠습니다. 3번째 줄에서 canvas 요소를 생성하고, 4번째 줄에서 2D 컨텍스트를 얻습니다.

컨텍스트는 실제로 그림을 그리는 도구 모음이라고 보면 됩니다. 7-9번째 줄이 중요합니다.

캔버스 크기를 이미지와 동일하게 맞추고, drawImage로 원본 이미지를 캔버스에 그립니다. 이제 이 위에 추가로 그림을 그릴 수 있습니다.

12번째 줄부터 탐지된 객체마다 반복합니다. obj.box에는 [x, y, width, height] 형태의 좌표가 들어있습니다.

14-16번째 줄에서 초록색 테두리 박스를 그립니다. 19-21번째 줄에서 라벨을 표시합니다.

객체 이름과 확률을 박스 위쪽에 텍스트로 씁니다. fillText의 세 번째 인자에서 y - 5를 한 이유는 텍스트가 박스 바로 위에 오도록 하기 위해서입니다.

실무에서 자주 사용하는 개선 포인트가 있습니다. 여러 종류의 객체를 탐지할 때는 각각 다른 색상을 사용하면 좋습니다.

"사람은 빨간색, 자동차는 파란색" 식으로요. 또한 박스가 겹칠 때를 대비해 투명도를 조절하거나, 라벨 배경을 추가하면 가독성이 좋아집니다.

주의할 점도 있습니다. 이미지와 캔버스의 크기가 다르면 좌표가 어긋납니다.

특히 CSS로 이미지 크기를 조절했다면, naturalWidth와 clientWidth가 다를 수 있습니다. 좌표 계산 시 이 차이를 보정해야 합니다.

김개발 씨는 결과물을 보고 뿌듯해졌습니다. 고양이 사진을 분석하니 고양이 주변에 초록색 박스가 그려지고, 위에 "cat 94%"라고 표시됩니다.

"이제 진짜 AI 앱 같아 보이네요!"

실전 팁

💡 - 객체 종류별로 다른 색상을 사용하면 구분이 쉽습니다

  • 라벨에 반투명 배경을 추가하면 가독성이 좋아집니다
  • 이미지 리사이즈 시 좌표 비율도 함께 조정하세요

5. 결과 내보내기

시각화까지 완성한 김개발 씨. 기획팀에서 또 연락이 왔습니다.

"분석 결과를 저장하거나 공유할 수 있으면 좋겠어요." 당연한 요구입니다. 멋지게 분석한 결과를 스크린샷으로만 저장하라고 할 수는 없으니까요.

이미지 파일로도, JSON 데이터로도 내보낼 수 있으면 좋겠습니다.

결과 내보내기는 분석 결과를 파일로 저장하는 기능입니다. 마치 문서 작업 후 저장 버튼을 누르는 것처럼, 시각화된 이미지나 분석 데이터를 다운로드할 수 있게 해줍니다.

Canvas의 toDataURL과 Blob을 활용하면 서버 없이 클라이언트에서 직접 파일을 생성할 수 있습니다.

다음 코드를 살펴봅시다.

// 결과를 다양한 형식으로 내보내기
function exportResults(canvas, analysisData) {
  // 이미지로 내보내기
  const exportImage = () => {
    const link = document.createElement('a');
    link.download = 'analysis-result.png';
    link.href = canvas.toDataURL('image/png');
    link.click();
  };

  // JSON으로 내보내기
  const exportJSON = () => {
    const dataStr = JSON.stringify(analysisData, null, 2);
    const blob = new Blob([dataStr], { type: 'application/json' });
    const url = URL.createObjectURL(blob);
    const link = document.createElement('a');
    link.download = 'analysis-data.json';
    link.href = url;
    link.click();
    URL.revokeObjectURL(url); // 메모리 해제
  };

  return { exportImage, exportJSON };
}

김개발 씨는 내보내기 기능을 어떻게 구현할지 고민했습니다. 서버에 파일을 저장하고 다운로드 링크를 받아와야 하나?

그건 너무 복잡하잖아. 박시니어 씨가 알려줍니다.

"브라우저에서 바로 파일을 만들 수 있어요. Blob이라는 게 있거든요." Blob이란 무엇일까요?

Blob은 Binary Large Object의 약자로, 파일과 같은 형태의 데이터 덩어리입니다. 쉽게 비유하면, Blob은 메모리에 있는 가상의 파일입니다.

실제 디스크에 저장되지 않았을 뿐, 파일처럼 다룰 수 있습니다. 이걸 다운로드하면 진짜 파일이 됩니다.

이미지 내보내기부터 살펴보겠습니다. Canvas에는 toDataURL이라는 편리한 메서드가 있습니다.

캔버스의 내용을 Data URL 문자열로 변환해줍니다. 이 문자열을 a 태그의 href에 넣고 download 속성을 지정하면, 클릭했을 때 파일로 다운로드됩니다.

6번째 줄에서 가상의 a 태그를 만들고, 7번째 줄에서 다운로드될 파일명을 지정합니다. 8번째 줄에서 캔버스를 PNG 이미지로 변환한 URL을 넣고, 9번째 줄에서 프로그래밍적으로 클릭합니다.

그러면 다운로드가 시작됩니다. JSON 내보내기는 조금 다릅니다.

JSON은 텍스트 데이터이므로 Data URL보다는 Blob이 적합합니다. 14번째 줄에서 JSON 문자열을 만들고, 15번째 줄에서 이를 Blob으로 감쌉니다.

URL.createObjectURL로 이 Blob에 접근할 수 있는 임시 URL을 만듭니다. 중요한 것은 21번째 줄의 URL.revokeObjectURL입니다.

createObjectURL로 만든 URL은 메모리를 차지합니다. 다운로드가 끝나면 이 URL을 해제해야 메모리 누수를 방지할 수 있습니다.

실무에서는 여러 형식을 지원하는 경우가 많습니다. CSV, PDF, Excel 등 다양한 형식으로 내보내기를 요구받을 수 있습니다.

PDF는 jsPDF, Excel은 SheetJS 같은 라이브러리를 활용합니다. 기본 원리는 같습니다.

데이터를 원하는 형식으로 변환하고, Blob으로 만들어 다운로드합니다. 주의할 점이 있습니다.

모바일 브라우저에서는 프로그래밍적 다운로드가 제한될 수 있습니다. 특히 iOS Safari에서는 다르게 동작하는 경우가 있습니다.

실제 배포 전에 다양한 환경에서 테스트해보세요. 또한 큰 파일을 생성할 때는 성능에 주의해야 합니다.

김개발 씨는 내보내기 버튼을 누르고 파일을 확인했습니다. PNG 이미지에는 분석 박스가 그려져 있고, JSON 파일에는 모든 분석 데이터가 깔끔하게 정리되어 있습니다.

"이제 결과를 공유할 수 있겠네요!"

실전 팁

💡 - URL.createObjectURL 사용 후 revokeObjectURL로 메모리를 해제하세요

  • 파일명에 타임스탬프를 넣으면 덮어쓰기를 방지할 수 있습니다
  • 모바일 환경에서는 다운로드 동작을 별도로 테스트하세요

6. PWA로 배포

모든 기능이 완성되었습니다. 김개발 씨는 뿌듯한 마음으로 기획팀에 데모를 보여줬습니다.

그런데 예상치 못한 질문이 들어왔습니다. "이거 앱스토어에서 다운받을 수 있나요?" 웹 앱인데 앱스토어라니?

하지만 방법이 있습니다. PWA로 만들면 됩니다.

**PWA(Progressive Web App)**는 웹 앱을 네이티브 앱처럼 사용할 수 있게 해주는 기술입니다. 마치 웹사이트에 앱의 옷을 입히는 것과 같습니다.

홈 화면에 아이콘을 추가하고, 오프라인에서도 동작하며, 푸시 알림도 보낼 수 있습니다. 앱스토어 심사 없이 바로 배포할 수 있다는 것도 큰 장점입니다.

다음 코드를 살펴봅시다.

// service-worker.js - 오프라인 지원의 핵심
const CACHE_NAME = 'image-analyzer-v1';
const urlsToCache = [
  '/',
  '/index.html',
  '/styles.css',
  '/app.js',
  '/models/mobilenet.json' // AI 모델도 캐시
];

// 설치 시 필요한 파일 캐시
self.addEventListener('install', (event) => {
  event.waitUntil(
    caches.open(CACHE_NAME)
      .then(cache => cache.addAll(urlsToCache))
  );
});

// 네트워크 요청 가로채기
self.addEventListener('fetch', (event) => {
  event.respondWith(
    caches.match(event.request)
      .then(response => response || fetch(event.request))
  );
});

김개발 씨는 PWA라는 단어를 들어본 적은 있지만 직접 만들어본 적은 없었습니다. 박시니어 씨에게 물어봤습니다.

"PWA가 뭔가요? 웹인데 앱이라니 좀 헷갈려요." **PWA(Progressive Web App)**란 무엇일까요?

쉽게 비유하면, PWA는 웹사이트에 앱의 기능을 추가한 것입니다. 마치 평범한 자전거에 전기 모터를 달아 전기 자전거로 만드는 것과 같습니다.

본질은 자전거(웹)이지만, 모터(PWA 기능) 덕분에 더 강력해집니다. PWA의 핵심 세 가지가 있습니다.

첫째, Service Worker입니다. 브라우저와 네트워크 사이에서 중개자 역할을 합니다.

파일을 캐시해두고, 오프라인일 때 캐시된 파일을 제공합니다. 둘째, manifest.json입니다.

앱의 이름, 아이콘, 시작 URL, 화면 방향 등을 정의합니다. 이 파일이 있어야 "홈 화면에 추가" 기능이 동작합니다.

셋째, HTTPS입니다. PWA는 보안상의 이유로 반드시 HTTPS에서만 동작합니다.

코드를 살펴보겠습니다. 2번째 줄에서 캐시 이름을 정의합니다.

버전 관리를 위해 'v1' 같은 접미사를 붙입니다. 3-8번째 줄에서 캐시할 파일 목록을 정의합니다.

중요한 것은 AI 모델 파일도 캐시한다는 점입니다. 이렇게 하면 오프라인에서도 이미지 분석이 가능합니다.

12번째 줄의 install 이벤트는 Service Worker가 처음 설치될 때 발생합니다. 이때 필요한 파일들을 미리 캐시해둡니다.

20번째 줄의 fetch 이벤트는 브라우저가 네트워크 요청을 할 때마다 발생합니다. 캐시에 있으면 캐시된 파일을, 없으면 네트워크에서 가져옵니다.

manifest.json도 필요합니다. json { "name": "이미지 분석기", "short_name": "이미지AI", "start_url": "/", "display": "standalone", "icons": [{"src": "/icon-192.png", "sizes": "192x192", "type": "image/png"}] } HTML에서 이 파일을 링크하면 됩니다.

주의할 점이 있습니다. Service Worker의 업데이트는 까다롭습니다.

캐시 이름을 변경하지 않으면 사용자에게 새 버전이 반영되지 않습니다. 버전을 올릴 때는 반드시 CACHE_NAME을 변경하고, 오래된 캐시를 삭제하는 로직을 추가하세요.

또한 개발 중에는 Service Worker가 방해가 될 수 있습니다. 크롬 개발자 도구에서 "Update on reload"를 켜두거나, 개발 서버에서는 Service Worker를 비활성화하세요.

김개발 씨는 PWA 설정을 마치고 스마트폰으로 접속해봤습니다. "홈 화면에 추가" 버튼을 누르니 앱 아이콘이 생겼습니다.

비행기 모드로 바꿔도 앱이 동작합니다. "와, 진짜 앱 같아요!" 기획팀도 만족했습니다.

"앱스토어 심사 없이 바로 배포할 수 있다니, 좋네요!"

실전 팁

💡 - 캐시 버전 관리를 위해 CACHE_NAME에 버전 번호를 포함하세요

  • 개발 중에는 Service Worker를 비활성화하거나 항상 새로고침하도록 설정하세요
  • Lighthouse 도구로 PWA 점수를 확인하고 개선하세요

이상으로 학습을 마칩니다. 위 내용을 직접 코드로 작성해보면서 익혀보세요!

#JavaScript#ImageAnalysis#DragAndDrop#TensorFlow#PWA#AI,ML,JavaScript

댓글 (0)

댓글을 작성하려면 로그인이 필요합니다.