⚠️

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

이미지 로딩 중...

실전 프로젝트 음성 메모 앱 완벽 가이드 - 슬라이드 1/7
A

AI Generated

2025. 11. 27. · 0 Views

실전 프로젝트 음성 메모 앱 완벽 가이드

브라우저에서 마이크로 녹음하고, 실시간 음성 인식을 통해 텍스트로 변환하며, IndexedDB를 활용해 오프라인에서도 동작하는 음성 메모 앱을 만들어봅니다. 웹 API의 다양한 기능을 실전 프로젝트에 적용하는 방법을 배웁니다.


목차

  1. 마이크_녹음_구현
  2. 실시간_음성_인식
  3. 메모_저장_기능
  4. 검색_기능_추가
  5. IndexedDB_활용
  6. 오프라인_동작

1. 마이크 녹음 구현

김개발 씨는 평소 회의 중에 중요한 내용을 놓치는 일이 잦았습니다. "아, 그때 팀장님이 뭐라고 하셨더라..." 메모를 하자니 타이핑 소리가 거슬리고, 손으로 쓰자니 내용을 다 담기 어려웠습니다.

그래서 문득 떠올린 아이디어가 바로 "음성 메모 앱"이었습니다.

MediaRecorder API는 브라우저에서 마이크 입력을 녹음할 수 있게 해주는 웹 표준 기술입니다. 마치 스마트폰의 녹음기 앱처럼, 웹 페이지에서도 사용자의 음성을 캡처하고 저장할 수 있습니다.

이 API를 활용하면 별도의 플러그인 없이 순수 JavaScript만으로 녹음 기능을 구현할 수 있습니다.

다음 코드를 살펴봅시다.

// 마이크 권한 요청 및 녹음 설정
async function startRecording() {
  // 사용자에게 마이크 접근 권한을 요청합니다
  const stream = await navigator.mediaDevices.getUserMedia({ audio: true });

  // MediaRecorder 인스턴스 생성
  const mediaRecorder = new MediaRecorder(stream);
  const audioChunks = [];

  // 녹음 데이터가 들어올 때마다 저장
  mediaRecorder.ondataavailable = (event) => {
    audioChunks.push(event.data);
  };

  // 녹음 종료 시 Blob으로 변환
  mediaRecorder.onstop = () => {
    const audioBlob = new Blob(audioChunks, { type: 'audio/webm' });
    const audioUrl = URL.createObjectURL(audioBlob);
    console.log('녹음 완료:', audioUrl);
  };

  mediaRecorder.start();
  return mediaRecorder;
}

김개발 씨는 입사 6개월 차 주니어 개발자입니다. 회의 중 놓치는 내용이 아쉬워 음성 메모 앱을 만들기로 결심했습니다.

하지만 브라우저에서 마이크를 어떻게 사용하는지 전혀 감이 오지 않았습니다. 선배 개발자 박시니어 씨에게 물어보니 웃으며 대답했습니다.

"걱정 마, 요즘 브라우저는 정말 똑똑해. MediaRecorder API라는 게 있어서 별도 라이브러리 없이도 녹음할 수 있어." 그렇다면 MediaRecorder API란 정확히 무엇일까요?

쉽게 비유하자면, MediaRecorder는 마치 카세트 녹음기와 같습니다. 예전에 라디오에서 좋아하는 노래가 나오면 녹음 버튼을 눌러 테이프에 담았던 것처럼, MediaRecorder도 마이크로 들어오는 소리를 디지털 데이터로 차곡차곡 담아둡니다.

녹음이 끝나면 그 데이터를 하나의 파일로 만들어주는 것입니다. 녹음을 시작하려면 먼저 사용자의 허락을 받아야 합니다.

아무 웹사이트나 마음대로 마이크를 사용한다면 심각한 개인정보 문제가 생기겠죠? 바로 이때 navigator.mediaDevices.getUserMedia가 등장합니다.

이 메서드를 호출하면 브라우저가 사용자에게 "이 사이트가 마이크를 사용하려고 합니다. 허용하시겠습니까?"라고 물어봅니다.

사용자가 허용하면 마이크 스트림을 얻을 수 있습니다. 위의 코드를 한 줄씩 살펴보겠습니다.

먼저 **getUserMedia({ audio: true })**를 호출합니다. 이 부분이 마이크 권한을 요청하는 핵심입니다.

반환된 stream은 마이크에서 들어오는 실시간 오디오 데이터입니다. 다음으로 이 스트림을 **new MediaRecorder(stream)**에 전달하여 녹음기 인스턴스를 만듭니다.

이제 녹음할 준비가 된 것입니다. ondataavailable 이벤트 핸들러가 중요합니다.

녹음 중에 주기적으로 데이터 조각이 생기는데, 이것을 배열에 차곡차곡 쌓아둡니다. 마치 퍼즐 조각을 모으는 것과 같습니다.

녹음이 끝나면 onstop 이벤트가 발생합니다. 이때 모아둔 조각들을 Blob으로 합칩니다.

Blob은 Binary Large Object의 약자로, 이미지나 오디오 같은 큰 이진 데이터를 다루는 객체입니다. URL.createObjectURL을 사용하면 이 Blob을 재생 가능한 URL로 변환할 수 있습니다.

이 URL을 audio 태그의 src에 넣으면 바로 재생이 됩니다. 실제 현업에서는 어떻게 활용할까요?

고객 상담 시스템을 예로 들어보겠습니다. 상담 내용을 녹음해두면 나중에 분쟁이 생겼을 때 증거로 활용할 수 있습니다.

또한 신입 상담원 교육용 자료로도 사용할 수 있습니다. 많은 콜센터 시스템이 이런 방식으로 녹음 기능을 구현하고 있습니다.

하지만 주의할 점도 있습니다. 초보 개발자들이 흔히 하는 실수 중 하나는 녹음 종료 후 스트림을 정리하지 않는 것입니다.

**stream.getTracks().forEach(track => track.stop())**을 호출하여 마이크 사용을 명시적으로 종료해야 합니다. 그렇지 않으면 브라우저 탭에 마이크 아이콘이 계속 표시됩니다.

다시 김개발 씨의 이야기로 돌아가 봅시다. 박시니어 씨의 설명을 들은 김개발 씨는 눈을 빛내며 말했습니다.

"생각보다 간단하네요! 바로 해볼게요!"

실전 팁

💡 - HTTPS 환경에서만 마이크 권한 요청이 가능합니다 (localhost는 예외)

  • 녹음 종료 시 반드시 스트림의 모든 트랙을 stop()으로 정리하세요

2. 실시간 음성 인식

녹음 기능을 완성한 김개발 씨는 한 가지 아쉬움이 있었습니다. 나중에 녹음 파일을 일일이 다시 들어야 한다는 점이었습니다.

"녹음하면서 바로 텍스트로 변환되면 얼마나 좋을까?" 그래서 음성 인식 기능을 찾아보기 시작했습니다.

Web Speech APISpeechRecognition은 브라우저에서 실시간으로 음성을 텍스트로 변환해주는 기술입니다. 마치 통역사가 옆에서 실시간으로 말을 받아 적어주는 것과 같습니다.

Google의 음성 인식 기술을 기반으로 하여 상당히 높은 인식률을 자랑하며, 한국어도 지원합니다.

다음 코드를 살펴봅시다.

// 음성 인식 설정 및 시작
function setupSpeechRecognition() {
  // 브라우저 호환성 처리
  const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
  const recognition = new SpeechRecognition();

  // 한국어로 설정
  recognition.lang = 'ko-KR';
  // 실시간으로 중간 결과도 받기
  recognition.interimResults = true;
  // 연속 인식 모드
  recognition.continuous = true;

  recognition.onresult = (event) => {
    let finalTranscript = '';
    let interimTranscript = '';

    for (let i = event.resultIndex; i < event.results.length; i++) {
      const transcript = event.results[i][0].transcript;
      if (event.results[i].isFinal) {
        finalTranscript += transcript;
      } else {
        interimTranscript += transcript;
      }
    }
    console.log('확정:', finalTranscript, '임시:', interimTranscript);
  };

  return recognition;
}

김개발 씨는 음성 인식이라고 하면 복잡한 AI 모델을 서버에 구축해야 한다고 생각했습니다. 비용도 많이 들고 구현도 어려울 것 같았습니다.

하지만 박시니어 씨는 고개를 저었습니다. "브라우저에 이미 Web Speech API가 내장되어 있어.

별도 서버 없이도 음성 인식이 가능하다고." 그렇다면 SpeechRecognition이란 정확히 무엇일까요? 쉽게 비유하자면, SpeechRecognition은 마치 속기사와 같습니다.

법정이나 국회에서 속기사가 말하는 내용을 실시간으로 받아 적는 것처럼, SpeechRecognition도 마이크로 들어오는 음성을 즉시 텍스트로 변환합니다. 심지어 아직 문장이 끝나지 않았을 때도 중간 결과를 보여줍니다.

과거에는 음성 인식을 구현하려면 어떻게 해야 했을까요? 전용 음성 인식 서버를 구축하고, 오디오 데이터를 스트리밍으로 전송하고, 결과를 다시 받아오는 복잡한 과정이 필요했습니다.

비용도 만만치 않았습니다. 하지만 이제는 브라우저가 이 모든 것을 처리해줍니다.

위의 코드를 자세히 살펴보겠습니다. 먼저 window.SpeechRecognition || window.webkitSpeechRecognition으로 브라우저 호환성을 처리합니다.

Chrome 등 일부 브라우저는 아직 webkit 접두사를 사용하기 때문입니다. **recognition.lang = 'ko-KR'**은 인식할 언어를 한국어로 설정합니다.

영어라면 'en-US', 일본어라면 'ja-JP'를 사용합니다. 이 설정이 인식률에 큰 영향을 미칩니다.

interimResults = true가 중요한 설정입니다. 이것을 켜면 아직 확정되지 않은 중간 결과도 받아볼 수 있습니다.

사용자가 말하는 도중에도 화면에 텍스트가 나타나므로 반응성이 좋아집니다. continuous = true는 연속 인식 모드입니다.

이 설정이 없으면 한 문장을 인식하고 자동으로 멈춥니다. 메모 앱처럼 오래 녹음해야 하는 경우에는 반드시 켜야 합니다.

onresult 이벤트 핸들러에서 인식 결과를 처리합니다. event.results는 지금까지의 모든 인식 결과를 담고 있고, isFinal 속성으로 확정된 결과인지 아닌지 구분합니다.

실제 현업에서는 다양하게 활용됩니다. 화상 회의 시스템에서 실시간 자막을 생성하거나, 접근성이 필요한 사용자를 위한 음성 입력 기능, 또는 AI 비서 서비스에서 사용자 명령을 인식하는 데 활용됩니다.

최근에는 의료 기록 작성이나 법률 문서 작성에도 활용 범위가 넓어지고 있습니다. 하지만 주의할 점도 있습니다.

Web Speech API는 현재 Chrome 계열 브라우저에서 가장 잘 지원됩니다. Firefox나 Safari에서는 지원이 제한적이거나 없을 수 있습니다.

따라서 SpeechRecognition이 undefined인지 먼저 확인하고, 지원하지 않는 브라우저에서는 대체 UI를 보여주는 것이 좋습니다. 김개발 씨는 코드를 실행해보고 감탄했습니다.

자신이 말하는 내용이 실시간으로 화면에 나타나는 것이 마법 같았습니다. "이제 회의록 작성이 훨씬 쉬워지겠어요!"

실전 팁

💡 - 인터넷 연결이 필요합니다 (음성 데이터가 서버로 전송되어 처리됨)

  • onerror 이벤트로 에러 처리를 반드시 구현하세요

3. 메모 저장 기능

녹음도 되고 텍스트 변환도 되니, 이제 이 데이터를 저장해야 했습니다. 김개발 씨는 처음에 단순히 localStorage를 생각했습니다.

하지만 오디오 파일은 용량이 크고, 여러 개의 메모를 체계적으로 관리하려면 더 나은 방법이 필요했습니다.

음성 메모 앱에서 저장 기능은 오디오 데이터텍스트 데이터를 함께 관리해야 합니다. 마치 도서관에서 책과 함께 색인 카드를 보관하는 것처럼, 오디오 원본과 변환된 텍스트, 그리고 메타데이터(제목, 날짜 등)를 하나의 단위로 묶어 저장합니다.

이를 위한 데이터 구조 설계가 핵심입니다.

다음 코드를 살펴봅시다.

// 음성 메모 데이터 구조 정의
class VoiceMemo {
  constructor(audioBlob, transcript) {
    this.id = Date.now().toString();
    this.createdAt = new Date().toISOString();
    this.audioBlob = audioBlob;
    this.transcript = transcript;
    this.title = this.generateTitle();
    this.duration = 0;
  }

  // 첫 문장에서 제목 자동 생성
  generateTitle() {
    const firstSentence = this.transcript.split(/[.!?]/)[0];
    return firstSentence.slice(0, 30) || '제목 없음';
  }

  // 저장용 객체로 변환
  toStorageObject() {
    return {
      id: this.id,
      createdAt: this.createdAt,
      audioBlob: this.audioBlob,
      transcript: this.transcript,
      title: this.title,
      duration: this.duration
    };
  }
}

김개발 씨는 데이터를 어떤 형태로 저장해야 할지 고민에 빠졌습니다. 오디오 파일만 저장하면 나중에 찾기 어렵고, 텍스트만 저장하면 원본 음성을 확인할 수 없었습니다.

박시니어 씨가 조언했습니다. "데이터 모델링부터 제대로 해야 해.

지금 귀찮다고 대충 하면 나중에 후회한다고." 그렇다면 음성 메모의 데이터 구조는 어떻게 설계해야 할까요? 쉽게 비유하자면, 음성 메모는 마치 앨범 속 사진과 같습니다.

사진 자체도 중요하지만, 언제 어디서 찍었는지, 누가 찍었는지 같은 정보도 함께 기록해두어야 나중에 찾기 쉽습니다. 이런 부가 정보를 메타데이터라고 합니다.

음성 메모에 필요한 요소들을 정리해봅시다. 첫째, 오디오 데이터 자체입니다.

Blob 형태로 저장되며, 실제 녹음 내용을 담고 있습니다. 둘째, 변환된 텍스트입니다.

음성 인식 결과물로, 검색이나 미리보기에 활용됩니다. 셋째, 메타데이터입니다.

생성 일시, 제목, 녹음 시간 등의 정보입니다. 위의 코드에서 VoiceMemo 클래스는 이 세 가지를 하나로 묶어줍니다.

id는 각 메모를 구분하는 고유 식별자입니다. Date.now()를 사용하면 밀리초 단위의 타임스탬프를 얻을 수 있어 중복될 일이 거의 없습니다.

createdAt은 생성 시각을 ISO 형식으로 저장합니다. ISO 형식은 "2024-01-15T09:30:00.000Z"처럼 국제 표준을 따르므로, 정렬이나 비교가 쉽습니다.

generateTitle 메서드가 재미있습니다. 변환된 텍스트의 첫 문장을 자동으로 제목으로 사용합니다.

물론 나중에 사용자가 직접 수정할 수도 있습니다. 이렇게 하면 "제목을 입력하세요"라는 귀찮은 과정을 줄일 수 있습니다.

toStorageObject 메서드는 저장소에 저장할 형태로 변환합니다. 클래스 인스턴스를 그대로 저장하면 복원할 때 문제가 생길 수 있으므로, 순수한 객체 형태로 바꿔주는 것입니다.

실제 현업에서는 이런 데이터 모델링이 정말 중요합니다. 노션이나 에버노트 같은 메모 앱도 내부적으로 비슷한 구조를 사용합니다.

콘텐츠 본문, 메타데이터, 첨부 파일을 분리하되 하나의 단위로 관리합니다. 이렇게 해야 나중에 기능을 확장하기 쉽습니다.

하지만 주의할 점도 있습니다. audioBlob은 용량이 클 수 있습니다.

1분 녹음에 약 1MB 정도가 됩니다. 메모가 많아지면 저장소 용량을 신경 써야 합니다.

또한 Blob 자체는 JSON으로 직렬화되지 않으므로, 특별한 처리가 필요합니다. 김개발 씨는 고개를 끄덕였습니다.

"데이터 구조를 먼저 잘 설계해놓으면 나중에 편하겠네요. 검색 기능이나 태그 기능도 추가하기 쉬울 것 같아요!"

실전 팁

💡 - 제목 자동 생성 로직은 사용자 경험을 크게 향상시킵니다

  • duration은 오디오 로드 후 audio.duration으로 구할 수 있습니다

4. 검색 기능 추가

메모가 쌓이기 시작하자 김개발 씨는 새로운 문제에 직면했습니다. "저번 주에 녹음한 회의 내용이 어디 있더라?" 스크롤을 내려가며 하나씩 확인하는 것은 너무 비효율적이었습니다.

검색 기능이 절실해졌습니다.

음성 메모 앱의 검색은 변환된 텍스트를 기반으로 합니다. 마치 책의 색인을 뒤져 원하는 페이지를 찾는 것처럼, 텍스트에서 키워드를 검색하여 해당 메모를 빠르게 찾아냅니다.

초성 검색, 부분 일치 등 다양한 검색 방식을 구현할 수 있습니다.

다음 코드를 살펴봅시다.

// 음성 메모 검색 기능
class MemoSearchEngine {
  constructor(memos) {
    this.memos = memos;
  }

  // 기본 키워드 검색
  search(keyword) {
    const lowerKeyword = keyword.toLowerCase();
    return this.memos.filter(memo => {
      const content = memo.transcript.toLowerCase();
      const title = memo.title.toLowerCase();
      return content.includes(lowerKeyword) || title.includes(lowerKeyword);
    });
  }

  // 날짜 범위 검색
  searchByDateRange(startDate, endDate) {
    return this.memos.filter(memo => {
      const memoDate = new Date(memo.createdAt);
      return memoDate >= startDate && memoDate <= endDate;
    });
  }

  // 검색 결과 하이라이트
  highlightKeyword(text, keyword) {
    const regex = new RegExp(`(${keyword})`, 'gi');
    return text.replace(regex, '<mark>$1</mark>');
  }
}

김개발 씨의 메모가 50개를 넘어가자, 원하는 메모를 찾는 데 시간이 너무 오래 걸렸습니다. 제목만 봐서는 내용을 알 수 없는 경우도 많았습니다.

박시니어 씨가 말했습니다. "음성 메모의 장점이 뭐야?

바로 텍스트로 변환된다는 거지. 그 텍스트를 검색하면 돼." 그렇다면 메모 검색은 어떻게 구현할까요?

쉽게 비유하자면, 검색 기능은 마치 도서관의 사서와 같습니다. "경제 관련 책 어디 있어요?"라고 물으면 사서가 데이터베이스를 뒤져 해당 책들의 위치를 알려주는 것처럼, 검색 엔진도 키워드가 포함된 메모를 찾아줍니다.

위의 코드에서 MemoSearchEngine 클래스는 검색 기능을 담당합니다. search 메서드는 가장 기본적인 키워드 검색입니다.

입력받은 키워드를 소문자로 변환하고, 각 메모의 제목과 내용에서 해당 키워드가 있는지 확인합니다. **toLowerCase()**를 사용하는 이유는 대소문자를 구분하지 않고 검색하기 위함입니다.

filter 메서드는 조건에 맞는 요소만 걸러내는 배열 메서드입니다. 원본 배열은 변경하지 않고 새 배열을 반환합니다.

검색 기능에 딱 맞는 메서드입니다. searchByDateRange는 날짜 범위로 검색합니다.

"지난주 회의 내용 찾아줘"와 같은 요청에 유용합니다. Date 객체끼리는 비교 연산자로 직접 비교할 수 있어 구현이 간단합니다.

highlightKeyword는 검색 결과에서 키워드를 강조 표시합니다. 정규표현식의 gi 플래그는 대소문자 구분 없이(i) 전역으로(g) 모든 일치를 찾습니다.

mark 태그로 감싸면 브라우저가 자동으로 노란색 배경을 적용합니다. 실제 현업에서는 검색 기능이 서비스의 핵심입니다.

Gmail이나 Slack을 생각해보세요. 검색 기능이 없다면 과거 메시지를 찾는 것이 거의 불가능합니다.

사용자들은 좋은 검색 기능을 당연하게 여기지만, 그것이 없으면 바로 불편함을 느낍니다. 하지만 주의할 점도 있습니다.

메모가 수천 개로 늘어나면 filter를 매번 돌리는 것은 비효율적입니다. 이 경우 **역색인(inverted index)**을 구축하거나, IndexedDB의 인덱스 기능을 활용해야 합니다.

하지만 수백 개 수준에서는 이 정도로 충분합니다. 김개발 씨는 검색 기능을 추가하고 테스트해보았습니다.

"회의"라고 검색하자 관련 메모가 순식간에 나왔습니다. "이제야 진짜 쓸 만한 앱이 되는 것 같아요!"

실전 팁

💡 - 초성 검색을 구현하면 한글 사용자 경험이 크게 향상됩니다

  • 최근 검색어를 저장해두면 사용자 편의성이 높아집니다

5. IndexedDB 활용

localStorage에 데이터를 저장하던 김개발 씨는 곧 한계에 부딪혔습니다. 용량 제한 때문에 녹음 파일 몇 개만 저장해도 꽉 찼고, Blob 데이터는 저장 자체가 안 되었습니다.

더 강력한 저장소가 필요했습니다.

IndexedDB는 브라우저에 내장된 대용량 NoSQL 데이터베이스입니다. 마치 컴퓨터 안에 작은 MongoDB가 들어 있는 것과 같습니다.

Blob, 파일, 대용량 데이터를 저장할 수 있고, 인덱스를 통한 빠른 검색도 지원합니다. 음성 메모처럼 용량이 큰 데이터를 다루기에 최적의 선택입니다.

다음 코드를 살펴봅시다.

// IndexedDB 초기화 및 기본 연산
class MemoDatabase {
  constructor() {
    this.dbName = 'VoiceMemoApp';
    this.storeName = 'memos';
    this.db = null;
  }

  // 데이터베이스 열기
  async open() {
    return new Promise((resolve, reject) => {
      const request = indexedDB.open(this.dbName, 1);

      request.onerror = () => reject(request.error);
      request.onsuccess = () => {
        this.db = request.result;
        resolve(this.db);
      };

      // 스키마 설정 (최초 생성 또는 버전 업그레이드 시)
      request.onupgradeneeded = (event) => {
        const db = event.target.result;
        const store = db.createObjectStore(this.storeName, { keyPath: 'id' });
        store.createIndex('createdAt', 'createdAt', { unique: false });
      };
    });
  }

  // 메모 저장
  async save(memo) {
    const tx = this.db.transaction(this.storeName, 'readwrite');
    tx.objectStore(this.storeName).put(memo);
    return tx.complete;
  }
}

김개발 씨는 localStorage의 5MB 제한에 걸려 더 이상 녹음을 저장할 수 없게 되었습니다. 게다가 Blob 데이터는 JSON.stringify로 변환할 수도 없었습니다.

박시니어 씨가 새로운 방법을 알려주었습니다. "브라우저에 IndexedDB라는 진짜 데이터베이스가 있어.

용량도 훨씬 크고, Blob도 바로 저장할 수 있지." 그렇다면 IndexedDB란 정확히 무엇일까요? 쉽게 비유하자면, localStorage가 작은 메모장이라면 IndexedDB는 대형 창고입니다.

메모장에는 간단한 메모만 적을 수 있지만, 창고에는 가구든 박스든 뭐든 보관할 수 있습니다. 게다가 라벨을 붙여두면 나중에 쉽게 찾을 수도 있습니다.

IndexedDB와 localStorage의 차이점을 정리해봅시다. 용량 면에서 localStorage는 약 5MB로 제한되지만, IndexedDB는 디스크 용량의 상당 부분까지 사용할 수 있습니다.

데이터 타입 면에서 localStorage는 문자열만 저장하지만, IndexedDB는 Blob, ArrayBuffer, 객체 등 다양한 타입을 직접 저장합니다. API 방식 면에서 localStorage는 동기식이지만, IndexedDB는 비동기식입니다.

위의 코드를 살펴보겠습니다. indexedDB.open으로 데이터베이스를 엽니다.

두 번째 인자인 1은 버전 번호입니다. 버전이 바뀌면 onupgradeneeded가 호출되어 스키마를 수정할 수 있습니다.

createObjectStore는 테이블을 만드는 것과 비슷합니다. **keyPath: 'id'**는 각 객체의 id 속성을 기본 키로 사용한다는 의미입니다.

createIndex로 인덱스를 생성합니다. createdAt에 인덱스를 걸어두면 날짜순 정렬이나 날짜 범위 검색이 빨라집니다.

transaction은 데이터 조작의 단위입니다. 'readwrite'는 읽기와 쓰기를 모두 허용한다는 의미입니다.

put은 새 데이터를 추가하거나 기존 데이터를 업데이트합니다. 실제 현업에서는 IndexedDB가 광범위하게 사용됩니다.

Google Docs는 오프라인에서 작성한 문서를 IndexedDB에 저장해두었다가 온라인이 되면 동기화합니다. Spotify 웹 플레이어는 캐시된 음악을 IndexedDB에 저장합니다.

대부분의 PWA가 IndexedDB를 활용합니다. 하지만 주의할 점도 있습니다.

IndexedDB의 API는 꽤 복잡합니다. 비동기 콜백 지옥에 빠지기 쉬우므로, 위의 코드처럼 Promise로 감싸서 사용하는 것이 좋습니다.

또는 Dexie.js 같은 라이브러리를 사용하면 훨씬 편리합니다. 김개발 씨는 IndexedDB로 마이그레이션한 후 녹음을 마음껏 저장할 수 있게 되었습니다.

"이제 용량 걱정 없이 몇 시간이고 녹음할 수 있겠네요!"

실전 팁

💡 - 개발 중에는 브라우저 개발자 도구의 Application 탭에서 IndexedDB 내용을 확인할 수 있습니다

  • Dexie.js 라이브러리를 사용하면 IndexedDB를 훨씬 쉽게 다룰 수 있습니다

6. 오프라인 동작

김개발 씨는 지하철에서 앱을 사용하려다 인터넷이 끊기는 바람에 녹음한 내용을 잃어버린 적이 있습니다. "오프라인에서도 동작하게 만들 수는 없을까?" 웹 앱의 가장 큰 약점이라고 생각했던 오프라인 지원, 과연 가능할까요?

Service WorkerCache API를 활용하면 웹 앱도 오프라인에서 동작할 수 있습니다. 마치 캠핑 갈 때 음식을 미리 아이스박스에 챙겨가는 것처럼, 필요한 파일들을 브라우저에 캐시해두면 네트워크 없이도 앱이 작동합니다.

이것이 바로 **PWA(Progressive Web App)**의 핵심입니다.

다음 코드를 살펴봅시다.

// service-worker.js
const CACHE_NAME = 'voice-memo-v1';
const urlsToCache = [
  '/',
  '/index.html',
  '/styles.css',
  '/app.js'
];

// 설치 시 필요한 파일 캐시
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 => {
        // 캐시에 있으면 캐시에서, 없으면 네트워크에서
        return response || fetch(event.request);
      })
  );
});

// 메인 페이지에서 서비스 워커 등록
if ('serviceWorker' in navigator) {
  navigator.serviceWorker.register('/service-worker.js');
}

김개발 씨는 네이티브 앱을 부러워했습니다. "카카오톡은 지하철에서도 잘 되는데, 왜 웹은 인터넷 없으면 아무것도 안 되지?" 박시니어 씨가 웃으며 말했습니다.

"요즘 웹도 오프라인 지원돼. Service Worker라는 기술이 있거든.

알고 보면 꽤 강력해." 그렇다면 Service Worker란 정확히 무엇일까요? 쉽게 비유하자면, Service Worker는 마치 충성스러운 집사와 같습니다.

주인이 "물 한 잔 줘"라고 하면, 집사는 먼저 집 안에 있는 물을 가져다줍니다. 만약 집에 물이 없다면 그때 마트에 가서 사옵니다.

Service Worker도 마찬가지로, 먼저 캐시를 확인하고 없을 때만 네트워크로 요청합니다. Service Worker의 동작 원리를 단계별로 살펴보겠습니다.

첫째, 등록 단계입니다. 메인 페이지에서 navigator.serviceWorker.register를 호출하면 브라우저가 Service Worker 파일을 다운로드하고 설치합니다.

둘째, 설치 단계입니다. install 이벤트에서 필요한 파일들을 미리 캐시에 저장합니다.

urlsToCache 배열에 포함된 파일들이 브라우저 캐시에 담깁니다. 셋째, 활성화 단계입니다.

설치가 완료되면 Service Worker가 페이지의 네트워크 요청을 제어할 수 있게 됩니다. 넷째, 요청 처리 단계입니다.

fetch 이벤트에서 모든 네트워크 요청을 가로챕니다. caches.match로 캐시를 확인하고, 있으면 캐시에서 반환하고 없으면 실제 네트워크 요청을 보냅니다.

위의 코드에서 핵심적인 부분을 살펴보겠습니다. event.waitUntil은 비동기 작업이 완료될 때까지 이벤트를 기다리게 합니다.

캐시 저장이 끝나기 전에 설치가 완료되면 안 되기 때문입니다. caches.open으로 캐시 저장소를 열고, cache.addAll로 여러 URL을 한 번에 캐시합니다.

배열의 모든 파일이 성공적으로 캐시되어야 설치가 완료됩니다. event.respondWith는 원래 응답 대신 우리가 제공하는 응답을 사용하겠다는 의미입니다.

이것이 네트워크 요청을 가로채는 핵심입니다. 실제 현업에서는 PWA가 점점 보편화되고 있습니다.

Twitter Lite, Pinterest, Starbucks 등 많은 기업이 PWA를 채택하고 있습니다. 오프라인 지원뿐만 아니라 빠른 로딩, 푸시 알림, 홈 화면 설치 등 네이티브 앱에 가까운 경험을 제공할 수 있기 때문입니다.

하지만 주의할 점도 있습니다. 음성 인식은 서버를 거쳐야 하므로 오프라인에서는 불가능합니다.

따라서 오프라인 상태에서는 녹음만 하고, 온라인이 되면 텍스트 변환을 진행하는 식으로 설계해야 합니다. 또한 캐시 버전 관리를 잘 해야 새 버전 배포 시 문제가 없습니다.

김개발 씨는 Service Worker를 적용한 후 비행기 모드에서도 앱이 열리는 것을 확인하고 감탄했습니다. "이제 진짜 어디서든 사용할 수 있는 메모 앱이 됐어요!"

실전 팁

💡 - Service Worker는 HTTPS에서만 동작합니다 (localhost는 예외)

  • 캐시 업데이트 전략에 따라 사용자 경험이 크게 달라집니다

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

#JavaScript#WebAPI#MediaRecorder#SpeechRecognition#IndexedDB

댓글 (0)

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