PWA에 도입될 예정인 새로운 브라우저 기능 살펴보기: From Fugu With Love

1. 시작하기 전에

프로그레시브 웹 앱 (PWA)은 HTML, CSS, JavaScript를 비롯한 일반적인 웹 기술을 사용하여 빌드되고 웹을 통해 제공되는 애플리케이션 소프트웨어의 한 유형입니다. 표준을 준수하는 브라우저를 사용하는 모든 플랫폼에서 작동하도록 설계되었습니다.

이 Codelab에서는 기준 PWA로 시작하여 PWA에 초능력을 부여하는 새로운 브라우저 기능을 살펴봅니다.

이러한 새로운 브라우저 기능은 아직 개발 중이며 표준화되고 있으므로 이러한 기능을 사용하려면 브라우저 플래그를 설정해야 하는 경우가 있습니다.

기본 요건

이 Codelab에서는 최신 JavaScript, 특히 프라미스와 비동기/대기에 익숙해야 합니다. Codelab의 모든 단계가 모든 플랫폼에서 지원되는 것은 아니므로 코드를 수정하는 기기와 다른 운영체제를 사용하는 Android 휴대전화나 노트북과 같은 기기를 추가로 준비해 두면 테스트에 도움이 됩니다. 실제 기기 대신 현재 기기에서 테스트할 수 있는 Android 시뮬레이터나 BrowserStack과 같은 온라인 서비스를 사용해 볼 수 있습니다. 그렇지 않으면 단계를 건너뛰어도 됩니다. 단계는 서로 종속되지 않습니다.

빌드할 항목

인사말 카드 웹 앱을 빌드하고, 새롭고 곧 출시될 브라우저 기능이 특정 브라우저에서 고급 환경을 제공하도록 앱을 개선하는 방법을 알아봅니다 (모든 최신 브라우저에서 유용하게 유지됨).

파일 시스템 액세스, 시스템 클립보드 액세스, 연락처 검색, 주기적인 백그라운드 동기화, 화면 절전 모드 해제 잠금, 공유 기능 등 지원 기능을 추가하는 방법을 알아봅니다.

Codelab을 완료하면 호환되지 않는 브라우저를 사용하는 일부 사용자에게 다운로드 부담을 주지 않으면서, 가장 중요한 점으로 이들을 앱에서 아예 제외하지 않으면서 새로운 브라우저 기능으로 웹 앱을 점진적으로 개선하는 방법을 확실하게 이해하게 됩니다.

필요한 항목

현재 완전히 지원되는 브라우저는 다음과 같습니다.

특정 개발자 채널을 사용하는 것이 좋습니다.

2. Project Fugu

프로그레시브 웹 앱 (PWA)은 최신 API로 빌드 및 개선되어 웹에서 전 세계 어디에서나 모든 유형의 기기를 사용하는 사용자에게 도달하면서 향상된 기능, 안정성, 설치 가능성을 제공합니다.

이러한 API 중 일부는 매우 강력하며 잘못 처리하면 문제가 발생할 수 있습니다. 복어와 마찬가지로 제대로 자르면 진미가 되지만 잘못 자르면 치명적일 수 있습니다 (하지만 이 Codelab에서는 실제로 깨지는 것은 없으니 걱정하지 마세요).

이러한 이유로 관련 회사가 이러한 새로운 API를 개발하는 웹 기능 프로젝트의 내부 코드 이름은 Project Fugu입니다.

웹 기능은 이미 오늘날 대기업과 중소기업이 순수 브라우저 기반 솔루션을 기반으로 빌드할 수 있도록 지원하며, 플랫폼별 경로를 선택하는 것보다 개발 비용이 저렴하고 배포 속도가 빠른 경우가 많습니다.

3. 시작하기

브라우저를 다운로드한 다음 Chrome과 Edge 모두에서 작동하는 about://flags로 이동하여 다음 런타임 플래그 🚩를 설정합니다.

  • #enable-experimental-web-platform-features

사용 설정한 후 브라우저를 다시 시작합니다.

PWA를 호스팅할 수 있고 괜찮은 편집기가 있는 플랫폼인 Glitch를 사용합니다. Glitch는 GitHub로 가져오기 및 내보내기도 지원하므로 공급업체 종속이 없습니다. fugu-paint.glitch.me로 이동하여 애플리케이션을 사용해 보세요. Codelab에서 개선할 기본 그리기 앱 🎨입니다.

'Google'이라는 단어가 그려진 큰 캔버스가 있는 Fugu Greetings 기준 PWA

애플리케이션을 사용해 본 후 앱을 리믹스하여 수정할 수 있는 나만의 사본을 만드세요. 리믹스의 URL은 glitch.com/edit/#!/bouncy-candytuft와 같이 표시됩니다 ('bouncy-candytuft'는 다른 이름으로 표시될 수 있음). 이 리믹스는 전 세계에서 직접 액세스할 수 있습니다. Glitch에서 기존 계정에 로그인하거나 새 계정을 만들어 작업을 저장하세요. '🕶 표시' 버튼을 클릭하면 앱이 표시되며, 호스팅된 앱의 URL은 bouncy-candytuft.glitch.me와 같습니다 (최상위 도메인이 .com가 아닌 .me임).

이제 앱을 수정하고 개선할 수 있습니다. 변경할 때마다 앱이 새로고침되어 변경사항이 바로 표시됩니다.

HTML 문서 수정이 표시된 Glitch IDE

다음 작업은 순서대로 완료하는 것이 좋지만 위에서 설명한 대로 호환되는 기기에 액세스할 수 없는 경우 언제든지 단계를 건너뛸 수 있습니다. 각 작업은 무해한 담수어인 🐟 또는 '주의해서 다루어야 하는' 복어인 🐡로 표시되어 기능이 얼마나 실험적인지 알려줍니다.

DevTools의 콘솔을 확인하여 현재 기기에서 API가 지원되는지 확인합니다. 또한 Glitch를 사용하여 휴대폰과 데스크톱 컴퓨터 등 다양한 기기에서 동일한 앱을 쉽게 확인할 수 있습니다.

API 호환성이 DevTools의 콘솔에 로깅됩니다.

4. 🐟 Web Share API 지원 추가

아무도 감상해 주지 않는다면 멋진 그림을 그리는 것은 지루합니다. 사용자가 그림을 인사말 카드 형태로 전 세계와 공유할 수 있는 기능을 추가합니다.

Web Share API는 파일 공유를 지원하며, File는 특정 종류의 Blob일 뿐입니다. 따라서 share.mjs 파일에서 공유 버튼과 캔버스 콘텐츠를 blob으로 변환하는 편의 함수 toBlob()를 가져오고 아래 코드에 따라 공유 기능을 추가합니다.

이 기능을 구현했지만 버튼이 표시되지 않는다면 브라우저에서 Web Share API를 구현하지 않기 때문입니다.

import { shareButton, toBlob } from './script.mjs';

const share = async (title, text, blob) => {
  const data = {
    files: [
      new File([blob], 'fugu-greeting.png', {
        type: blob.type,
      }),
    ],
    title: title,
    text: text,
  };
  try {
    if (!navigator.canShare(data)) {
      throw new Error("Can't share data.", data);
    }
    await navigator.share(data);
  } catch (err) {
    console.error(err.name, err.message);
  }
};

shareButton.style.display = 'block';
shareButton.addEventListener('click', async () => {
  return share('Fugu Greetings', 'From Fugu With Love', await toBlob());
});

5. 🐟 Web Share Target API 지원 추가

이제 사용자는 앱을 사용하여 만든 인사말 카드를 공유할 수 있지만 사용자가 이미지를 앱에 공유하여 인사말 카드로 변환하도록 허용할 수도 있습니다. 이를 위해 Web Share Target API를 사용할 수 있습니다.

웹 애플리케이션 매니페스트에서 앱이 수락할 수 있는 파일의 종류와 하나 이상의 파일이 공유될 때 브라우저가 호출해야 하는 URL을 앱에 알려야 합니다. manifest.webmanifest 파일의 아래 발췌문에서 이를 확인할 수 있습니다.

{
  "share_target": {
    "action": "./share-target/",
    "method": "POST",
    "enctype": "multipart/form-data",
    "params": {
      "files": [
        {
          "name": "image",
          "accept": ["image/jpeg", "image/png", "image/webp", "image/gif"]
        }
      ]
    }
  }
}

그러면 서비스 워커가 수신된 파일을 처리합니다. URL ./share-target/는 실제로 존재하지 않으며, 앱은 fetch 핸들러에서 이 URL에 따라 작동하고 ?share-target 쿼리 매개변수를 추가하여 요청을 루트 URL로 리디렉션합니다.

self.addEventListener('fetch', (fetchEvent) => {
  /* 🐡 Start Web Share Target */
  if (
    fetchEvent.request.url.endsWith('/share-target/') &&
    fetchEvent.request.method === 'POST'
  ) {
    return fetchEvent.respondWith(
      (async () => {
        const formData = await fetchEvent.request.formData();
        const image = formData.get('image');
        const keys = await caches.keys();
        const mediaCache = await caches.open(
          keys.filter((key) => key.startsWith('media'))[0],
        );
        await mediaCache.put('shared-image', new Response(image));
        return Response.redirect('./?share-target', 303);
      })(),
    );
  }
  /* 🐡 End Web Share Target */

  /* ... */
});

앱이 로드되면 이 쿼리 매개변수가 설정되어 있는지 확인하고, 설정되어 있으면 공유 이미지를 캔버스에 그리고 캐시에서 삭제합니다. 이 모든 작업은 script.mjs에서 이루어집니다.

const restoreImageFromShare = async () => {
  const mediaCache = await getMediaCache();
  const image = await mediaCache.match('shared-image');
  if (image) {
    const blob = await image.blob();
    await drawBlob(blob);
    await mediaCache.delete('shared-image');
  }
};

이 함수는 앱이 초기화될 때 사용됩니다.

if (location.search.includes('share-target')) {
  restoreImageFromShare();
} else {
  drawDefaultImage();
}

6. 🐟 이미지 가져오기 지원 추가

처음부터 모든 것을 그리는 것은 어렵습니다. 사용자가 기기에서 앱으로 로컬 이미지를 업로드할 수 있는 기능을 추가합니다.

먼저 캔버스의 drawImage() 함수를 읽어 보세요. 다음으로 <​input
type=file>
요소를 숙지합니다.

이 정보를 바탕으로 import_image_legacy.mjs 파일을 수정하고 다음 스니펫을 추가합니다. 파일 상단에서 가져오기 버튼과 캔버스에 블롭을 그릴 수 있는 편의 함수 drawBlob()를 가져옵니다.

import { importButton, drawBlob } from './script.mjs';

const importImage = async () => {
  return new Promise((resolve) => {
    const input = document.createElement('input');
    input.type = 'file';
    input.accept = 'image/png, image/jpeg, image/*';
    input.addEventListener('change', () => {
      const file = input.files[0];
      input.remove();
      return resolve(file);
    });
    input.click();
  });
};

importButton.style.display = 'block';
importButton.addEventListener('click', async () => {
  const file = await importImage();
  if (file) {
    await drawBlob(file);
  }
});

7. 🐟 이미지 내보내기 지원 추가

사용자가 앱에서 만든 파일을 기기에 저장하려면 어떻게 해야 하나요? 기존에는 <​a
download>
요소를 사용하여 이 작업을 수행했습니다.

export_image_legacy.mjs 파일에 아래와 같이 콘텐츠를 추가합니다. 내보내기 버튼과 캔버스 콘텐츠를 blob으로 변환하는 toBlob() 편의 함수를 가져옵니다.

import { exportButton, toBlob } from './script.mjs';

export const exportImage = async (blob) => {
  const a = document.createElement('a');
  a.download = 'fugu-greeting.png';
  a.href = URL.createObjectURL(blob);
  a.addEventListener('click', (e) => {
    a.remove();
    setTimeout(() => URL.revokeObjectURL(a.href), 30 * 1000);
  });
  setTimeout(() => a.click(), 0);
};

exportButton.style.display = 'block';
exportButton.addEventListener('click', async () => {
  exportImage(await toBlob());
});

8. 🐟 File System Access API 지원 추가

공유는 좋은 일이지만 사용자는 자신의 기기에 최고의 작업을 저장하고 싶어 할 것입니다. 사용자가 그림을 저장하고 다시 열 수 있는 기능을 추가합니다.

이전에는 파일을 가져올 때 <​input type=file> 기존 방식을 사용하고 파일을 내보낼 때 <​a download> 기존 방식을 사용했습니다. 이제 File System Access API를 사용하여 환경을 개선합니다.

이 API를 사용하면 운영체제의 파일 시스템에서 파일을 열고 저장할 수 있습니다. 아래 콘텐츠를 추가하여 import_image.mjsexport_image.mjs 파일을 각각 수정합니다. 이러한 파일이 로드되도록 하려면 script.mjs에서 🐡 이모티콘을 삭제하세요.

이 줄을 다음과 같이 바꿉니다.

// Remove all the emojis for this feature test to succeed.
if ('show🐡Open🐡File🐡Picker' in window) {
  /* ... */
}

다음 줄로 바꿉니다.

if ('showOpenFilePicker' in window) {
  /* ... */
}

import_image.mjs에서:

import { importButton, drawBlob } from './script.mjs';

const importImage = async () => {
  try {
    const [handle] = await window.showOpenFilePicker({
      types: [
        {
          description: 'Image files',
          accept: {
            'image/*': ['.png', '.jpg', '.jpeg', '.avif', '.webp', '.svg'],
          },
        },
      ],
    });
    return await handle.getFile();
  } catch (err) {
    console.error(err.name, err.message);
  }
};

importButton.style.display = 'block';
importButton.addEventListener('click', async () => {
  const file = await importImage();
  if (file) {
    await drawBlob(file);
  }
});

export_image.mjs에서:

import { exportButton, toBlob } from './script.mjs';

const exportImage = async () => {
  try {
    const handle = await window.showSaveFilePicker({
      suggestedName: 'fugu-greetings.png',
      types: [
        {
          description: 'Image file',
          accept: {
            'image/png': ['.png'],
          },
        },
      ],
    });
    const blob = await toBlob();
    const writable = await handle.createWritable();
    await writable.write(blob);
    await writable.close();
  } catch (err) {
    console.error(err.name, err.message);
  }
};

exportButton.style.display = 'block';
exportButton.addEventListener('click', async () => {
  await exportImage();
});

9. 🐟 연락처 선택기 API 지원 추가

사용자가 인사말 카드에 메시지를 추가하고 개인적으로 누군가에게 인사하고 싶어 할 수 있습니다. 사용자가 로컬 연락처를 하나 이상 선택하고 공유 메시지에 이름을 추가할 수 있는 기능을 추가합니다.

Android 또는 iOS 기기에서 연락처 선택기 API를 사용하면 기기의 연락처 관리자 앱에서 연락처를 선택하여 애플리케이션으로 반환할 수 있습니다. contacts.mjs 파일을 수정하고 아래 코드를 추가합니다.

import { contactsButton, ctx, canvas } from './script.mjs';

const getContacts = async () => {
  const properties = ['name'];
  const options = { multiple: true };
  try {
    return await navigator.contacts.select(properties, options);
  } catch (err) {
    console.error(err.name, err.message);
  }
};

contactsButton.style.display = 'block';
contactsButton.addEventListener('click', async () => {
  const contacts = await getContacts();
  if (contacts) {
    ctx.font = '1em Comic Sans MS';
    contacts.forEach((contact, index) => {
      ctx.fillText(contact.name.join(), 20, 16 * ++index, canvas.width);
    });
  }
});

10. 🐟 비동기 클립보드 API 지원 추가

사용자가 다른 앱의 사진을 앱에 붙여넣거나 앱의 그림을 다른 앱에 복사할 수 있습니다. 사용자가 앱 안팎으로 이미지를 복사하여 붙여넣을 수 있는 기능을 추가하세요. Async Clipboard API는 PNG 이미지를 지원하므로 이제 이미지 데이터를 클립보드에 읽고 쓸 수 있습니다.

clipboard.mjs 파일을 찾아 다음을 추가합니다.

import { copyButton, pasteButton, toBlob, drawImage } from './script.mjs';

const copy = async (blob) => {
  try {
    await navigator.clipboard.write([
      /* global ClipboardItem */
      new ClipboardItem({
        [blob.type]: blob,
      }),
    ]);
  } catch (err) {
    console.error(err.name, err.message);
  }
};

const paste = async () => {
  try {
    const clipboardItems = await navigator.clipboard.read();
    for (const clipboardItem of clipboardItems) {
      try {
        for (const type of clipboardItem.types) {
          const blob = await clipboardItem.getType(type);
          return blob;
        }
      } catch (err) {
        console.error(err.name, err.message);
      }
    }
  } catch (err) {
    console.error(err.name, err.message);
  }
};

copyButton.style.display = 'block';
copyButton.addEventListener('click', async () => {
  await copy(await toBlob());
});

pasteButton.style.display = 'block';
pasteButton.addEventListener('click', async () => {
  const image = new Image();
  image.addEventListener('load', () => {
    drawImage(image);
  });
  image.src = URL.createObjectURL(await paste());
});

11. 🐟 배지 API 지원 추가

사용자가 앱을 설치하면 홈 화면에 아이콘이 표시됩니다. 이 아이콘을 사용하여 특정 그림에 사용된 브러시 획수와 같은 재미있는 정보를 전달할 수 있습니다.

사용자가 새 브러시 획을 만들 때마다 배지를 카운트하는 기능을 추가합니다. 배지 API를 사용하면 앱 아이콘에 숫자 배지를 설정할 수 있습니다. pointerdown 이벤트가 발생할 때마다 (즉, 브러시 스트로크가 발생할 때) 배지를 업데이트하고 캔버스가 지워지면 배지를 재설정할 수 있습니다.

다음 코드를 badge.mjs 파일에 넣습니다.

import { canvas, clearButton } from './script.mjs';

let strokes = 0;

canvas.addEventListener('pointerdown', () => {
  navigator.setAppBadge(++strokes);
});

clearButton.addEventListener('click', () => {
  strokes = 0;
  navigator.setAppBadge(strokes);
});

12. 🐟 화면 절전 모드 해제 잠금 API 지원 추가

때로는 사용자가 영감을 얻을 수 있도록 그림을 잠시 동안 응시해야 할 수도 있습니다. 화면을 깨어 있는 상태로 유지하고 화면 보호기가 시작되지 않도록 하는 기능을 추가합니다. 화면 절전 모드 해제 잠금 API는 사용자의 화면이 절전 모드로 전환되지 않도록 합니다. 페이지 공개 상태에 정의된 공개 상태 변경 이벤트가 발생하면 절전 모드 해제 잠금이 자동으로 해제됩니다. 따라서 페이지가 다시 표시될 때 절전 모드 해제 잠금을 다시 획득해야 합니다.

wake_lock.mjs 파일을 찾아 아래 콘텐츠를 추가합니다. 이 기능이 작동하는지 테스트하려면 1분 후에 화면 보호기가 표시되도록 구성하세요.

import { wakeLockInput, wakeLockLabel } from './script.mjs';

let wakeLock = null;

const requestWakeLock = async () => {
  try {
    wakeLock = await navigator.wakeLock.request('screen');
    wakeLock.addEventListener('release', () => {
      console.log('Wake Lock was released');
    });
    console.log('Wake Lock is active');
  } catch (err) {
    console.error(err.name, err.message);
  }
};

const handleVisibilityChange = () => {
  if (wakeLock !== null && document.visibilityState === 'visible') {
    requestWakeLock();
  }
};

document.addEventListener('visibilitychange', handleVisibilityChange);

wakeLockInput.style.display = 'block';
wakeLockLabel.style.display = 'block';
wakeLockInput.addEventListener('change', async () => {
  if (wakeLockInput.checked) {
    await requestWakeLock();
  } else {
    wakeLock.release();
  }
});

13. 🐟 주기적 백그라운드 동기화 API 지원 추가

빈 캔버스로 시작하면 지루할 수 있습니다. 주기적 백그라운드 동기화 API를 사용하여 매일 새로운 이미지(예: Unsplash의 일일 복어 사진)로 사용자의 캔버스를 초기화할 수 있습니다.

이를 위해서는 주기적 백그라운드 동기화를 등록하는 파일 periodic_background_sync.mjs와 오늘의 이미지를 다운로드하는 파일을 처리하는 또 다른 파일 image_of_the_day.mjs 등 두 개의 파일이 필요합니다.

periodic_background_sync.mjs에서:

import { periodicBackgroundSyncButton, drawBlob } from './script.mjs';

const getPermission = async () => {
  const status = await navigator.permissions.query({
    name: 'periodic-background-sync',
  });
  return status.state === 'granted';
};

const registerPeriodicBackgroundSync = async () => {
  const registration = await navigator.serviceWorker.ready;
  try {
    registration.periodicSync.register('image-of-the-day-sync', {
      // An interval of one day.
      minInterval: 24 * 60 * 60 * 1000,
    });
  } catch (err) {
    console.error(err.name, err.message);
  }
};

navigator.serviceWorker.addEventListener('message', async (event) => {
  const fakeURL = event.data.image;
  const mediaCache = await getMediaCache();
  const response = await mediaCache.match(fakeURL);
  drawBlob(await response.blob());
});

const getMediaCache = async () => {
  const keys = await caches.keys();
  return await caches.open(keys.filter((key) => key.startsWith('media'))[0]);
};

periodicBackgroundSyncButton.style.display = 'block';
periodicBackgroundSyncButton.addEventListener('click', async () => {
  if (await getPermission()) {
    await registerPeriodicBackgroundSync();
  }
  const mediaCache = await getMediaCache();
  let blob = await mediaCache.match('./assets/background.jpg');
  if (!blob) {
    blob = await mediaCache.match('./assets/fugu_greeting_card.jpg');
  }
  drawBlob(await blob.blob());
});

image_of_the_day.mjs에서:

const getImageOfTheDay = async () => {
  try {
    const fishes = ['blowfish', 'pufferfish', 'fugu'];
    const fish = fishes[Math.floor(fishes.length * Math.random())];
    const response = await fetch(`https://source.unsplash.com/daily?${fish}`);
    if (!response.ok) {
      throw new Error('Response was', response.status, response.statusText);
    }
    return await response.blob();
  } catch (err) {
    console.error(err.name, err.message);
  }
};

const getMediaCache = async () => {
  const keys = await caches.keys();
  return await caches.open(keys.filter((key) => key.startsWith('media'))[0]);
};

self.addEventListener('periodicsync', (syncEvent) => {
  if (syncEvent.tag === 'image-of-the-day-sync') {
    syncEvent.waitUntil(
      (async () => {
        try {
          const blob = await getImageOfTheDay();
          const mediaCache = await getMediaCache();
          const fakeURL = './assets/background.jpg';
          await mediaCache.put(fakeURL, new Response(blob));
          const clients = await self.clients.matchAll();
          clients.forEach((client) => {
            client.postMessage({
              image: fakeURL,
            });
          });
        } catch (err) {
          console.error(err.name, err.message);
        }
      })(),
    );
  }
});

14. 🐟 Shape Detection API 지원 추가

사용자의 그림이나 사용된 배경 이미지에 바코드와 같은 유용한 정보가 포함될 수 있습니다. 모양 감지 API, 특히 바코드 감지 API를 사용하면 이 정보를 추출할 수 있습니다. 사용자의 그림에서 바코드를 감지하려고 시도하는 기능을 추가합니다. barcode.mjs 파일을 찾아 아래 콘텐츠를 추가합니다. 이 기능을 테스트하려면 바코드가 있는 이미지를 캔버스에 로드하거나 붙여넣으면 됩니다. QR 코드 이미지 검색에서 예시 바코드를 복사할 수 있습니다.

/* global BarcodeDetector */
import {
  scanButton,
  clearButton,
  canvas,
  ctx,
  CANVAS_BACKGROUND,
  CANVAS_COLOR,
  floor,
} from './script.mjs';

const barcodeDetector = new BarcodeDetector();

const detectBarcodes = async (canvas) => {
  return await barcodeDetector.detect(canvas);
};

scanButton.style.display = 'block';
let seenBarcodes = [];
clearButton.addEventListener('click', () => {
  seenBarcodes = [];
});
scanButton.addEventListener('click', async () => {
  const barcodes = await detectBarcodes(canvas);
  if (barcodes.length) {
    barcodes.forEach((barcode) => {
      const rawValue = barcode.rawValue;
      if (seenBarcodes.includes(rawValue)) {
        return;
      }
      seenBarcodes.push(rawValue);
      ctx.font = '1em Comic Sans MS';
      ctx.textAlign = 'center';
      ctx.fillStyle = CANVAS_BACKGROUND;
      const boundingBox = barcode.boundingBox;
      const left = boundingBox.left;
      const top = boundingBox.top;
      const height = boundingBox.height;
      const oneThirdHeight = floor(height / 3);
      const width = boundingBox.width;
      ctx.fillRect(left, top + oneThirdHeight, width, oneThirdHeight);
      ctx.fillStyle = CANVAS_COLOR;
      ctx.fillText(
        rawValue,
        left + floor(width / 2),
        top + floor(height / 2),
        width,
      );
    });
  }
});

15. 🐡 Idle Detection API 지원 추가

앱이 키오스크와 유사한 설정에서 실행된다고 가정하면 유용한 기능은 일정 시간 동안 비활성 상태가 된 후 캔버스를 재설정하는 것입니다. 유휴 감지 API를 사용하면 사용자가 더 이상 기기와 상호작용하지 않는 시점을 감지할 수 있습니다.

idle_detection.mjs 파일을 찾아 아래 콘텐츠를 붙여넣습니다.

import { ephemeralInput, ephemeralLabel, clearCanvas } from './script.mjs';

let controller;

ephemeralInput.style.display = 'block';
ephemeralLabel.style.display = 'block';

ephemeralInput.addEventListener('change', async () => {
  if (ephemeralInput.checked) {
    const state = await IdleDetector.requestPermission();
    if (state !== 'granted') {
      ephemeralInput.checked = false;
      return alert('Idle detection permission must be granted!');
    }
    try {
      controller = new AbortController();
      const idleDetector = new IdleDetector();
      idleDetector.addEventListener('change', (e) => {
        const { userState, screenState } = e.target;
        console.log(`idle change: ${userState}, ${screenState}`);
        if (userState === 'idle') {
          clearCanvas();
        }
      });
      idleDetector.start({
        threshold: 60000,
        signal: controller.signal,
      });
    } catch (err) {
      console.error(err.name, err.message);
    }
  } else {
    console.log('Idle detection stopped.');
    controller.abort();
  }
});

16. 🐡 File Handling API 지원 추가

사용자가 이미지 파일을 더블클릭하면 앱이 팝업된다면 어떨까요? File Handling API를 사용하면 이 작업을 할 수 있습니다.

PWA를 이미지의 파일 핸들러로 등록해야 합니다. 이는 웹 애플리케이션 매니페스트에서 발생하며, 아래의 manifest.webmanifest 파일 발췌문에서 이를 확인할 수 있습니다. (이미 매니페스트에 포함되어 있으므로 직접 추가할 필요가 없습니다.)

{
  "file_handlers": [
    {
      "action": "./",
      "accept": {
        "image/*": [".jpg", ".jpeg", ".png", ".webp", ".svg"]
      }
    }
  ]
}

실제로 열린 파일을 처리하려면 아래 코드를 file-handling.mjs 파일에 추가하세요.

import { drawBlob } from './script.mjs';

const handleLaunchFiles = () => {
  window.launchQueue.setConsumer((launchParams) => {
    if (!launchParams.files.length) {
      return;
    }
    launchParams.files.forEach(async (handle) => {
      const file = await handle.getFile();
      drawBlob(file);
    });
  });
};

handleLaunchFiles();

17. 축하합니다

🎉 축하합니다!

Project Fugu 🐡의 맥락에서 개발되는 흥미로운 브라우저 API가 너무 많아서 이 Codelab에서는 간단하게만 다룹니다.

자세히 알아보려면 web.dev 사이트에서 Google의 게시물을 확인하세요.

사이트 web.dev의 &#39;기능&#39; 섹션 방문 페이지

하지만 여기서 끝이 아닙니다. 아직 공개되지 않은 업데이트의 경우 출시되었거나, 오리진 트라이얼 또는 개발자 트라이얼 중이거나, 작업이 시작되었거나, 고려 중이지만 아직 시작되지 않은 모든 제안서로 연결되는 링크가 포함된 Fugu API 추적기를 참고하세요.

Fugu API 추적기 웹사이트

이 Codelab은 Thomas Steiner (@tomayac)가 작성했습니다. 질문에 답변해 드리고 의견을 기다리겠습니다. 이 Codelab을 만드는 데 도움을 주신 Hemanth H.M (@GNUmanth), Christian Liebel (@christianliebel), Sven May (@Svenmay), Lars Knudsen (@larsgk), Jackie Han (@hanguokai)에게 특별히 감사드립니다.