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

1. 시작하기 전에

프로그레시브 웹 애플리케이션 (PWA)은 웹을 통해 제공되는 애플리케이션 소프트웨어로서 HTML, CSS, 자바스크립트 등의 일반적인 웹 기술을 사용하여 개발됩니다. 표준 준수 브라우저를 사용하는 모든 플랫폼에서 작동하도록 만들어졌습니다.

이 Codelab에서는 기본 PWA부터 시작한 다음, 궁극적으로 PWA 초능력을 발휘할 새로운 브라우저 기능을 살펴봅니다. 🦸

새로운 브라우저 기능은 대부분 아직 표준화 단계에 있습니다. 따라서 이러한 기능을 사용하기 위해 브라우저 플래그를 설정해야 하는 경우도 있습니다.

기본 요건

이 Codelab을 위해 최신 자바스크립트, 특히 프라미스와 async/await에 익숙해야 합니다. Codelab의 모든 단계가 모든 플랫폼에서 지원되는 것은 아니므로 코드를 수정하는 기기가 아닌 다른 운영체제를 사용하는 Android 휴대전화 또는 노트북과 같은 기기를 추가로 가지고 있는지 테스트하는 데 도움이 됩니다. 실제 기기 대신 Android 시뮬레이터와 같은 시뮬레이터를 사용하거나 현재 기기에서 테스트할 수 있는 BrowserStack과 같은 온라인 서비스를 사용해 보세요. 또는 어떤 단계든지 건너뛰어도 되며, 서로 의존하지 않아도 됩니다.

빌드할 항목

인사말 카드 웹 앱을 빌드하고, 신규 및 향후 브라우저 기능이 앱을 향상시켜 특정 브라우저에서 고급 환경을 제공하지만 모든 최신 브라우저에서 유용한 것은 무엇인지 알아보세요.

파일 시스템 액세스, 시스템 클립보드 액세스, 연락처 가져오기, 주기적 백그라운드 동기화, 화면 wake lock, 공유 기능 등 지원 기능을 추가하는 방법을 알아봅니다.

Codelab을 작업한 후에는 호환되지 않는 브라우저에 있는 일부 사용자에게 다운로드 부담을 주지 않으면서도 앱을 제외하지 않으면서 새로운 브라우저 기능을 사용하여 웹 앱을 점진적으로 개선하는 방법을 확실하게 이해하게 됩니다.

필요한 항목

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

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

2. 프로젝트 구구

프로그레시브 웹 앱 (PWA)은 최신 API로 빌드 및 개선되어 향상된 기능, 안정성, 설치 가능성을 제공하는 동시에 전 세계 어디에 있든지 모든 유형의 기기에 액세스하게 됩니다.

이러한 API 중 일부는 매우 강력하므로 잘못 처리하면 문제가 발생할 수 있습니다. fugu 물고기와 마찬가지로 🐡: 잘라내면 먹음직스러움을 줄이지만, 자르면 죽을 수 있습니다 (하지만 걱정하지 마세요). 이 Codelab에서는 실제로 아무것도 깨지지 않습니다.

그래서 웹 기능 프로젝트의 내부 코드명 (관련 회사에서 이러한 새 API를 개발하고 있음)은 프로젝트 Fugu입니다.

오늘날 이미 사용 중인 웹 기능은 대기업과 중소기업에서 순수 브라우저 기반 솔루션을 기반으로 빌드할 수 있는 경우가 많기 때문에 가끔 플랫폼별 경로를 사용하는 것보다 개발 비용을 낮추면서 빠르게 배포할 수 있습니다.

3. 시작하기

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

  • #enable-experimental-web-platform-features

사용 설정 후 브라우저를 다시 시작하세요.

우수한 편집기가 포함되어 있으므로 PWA를 호스팅할 수 있기 때문에 플랫폼 Glitch를 사용합니다. 또한 Glitch는 GitHub로 가져오기와 내보내기를 지원하므로 공급업체에 종속되지 않습니다. fugu-paint.glitch.me로 이동하여 애플리케이션을 사용해 봅니다. 이 앱은 Codelab을 진행하는 동안 개선할 기본 그리기 앱 🎨입니다.

Fugu Greetings의 기준: PWA에 '“Google”라는 큰 캔버스가 그려져 있습니다.

애플리케이션을 사용한 후 앱을 리믹스하여 수정 가능한 사본을 직접 만듭니다. 리믹스의 URL은 glitch.com/edit/"><//bouncy-candytuft ("bouncy-candytuft"; forURL만 같음)와 같은 형태입니다. 전 세계 사용자들이 리믹스를 바로 이용할 수 있습니다. 기존 계정에 로그인하거나 Glitch에서 새 계정을 만들어 작업을 저장하세요. '🕶 Show" 버튼을 클릭하면 앱을 볼 수 있으며, 호스팅된 앱의 URL은 bouncy-candytuft.glitch.me와 같은 형식이 됩니다. .com 대신 최상위 도메인으로 .me를 기록해 둡니다.

이제 앱을 수정하고 개선할 준비가 되었습니다. 변경할 때마다 앱이 새로고침되고 변경사항이 직접 표시됩니다.

HTML 문서 편집을 보여주는 Glitch IDE

다음 작업은 순서대로 완료하는 것이 좋지만 위에서 설명한 것처럼 호환 기기에 액세스할 수 없는 경우 언제든지 단계를 건너뛸 수 있습니다. 각 작업은 '해롭지 않은 담수 물고기' 또는 🐡 & fut fish의 🐡이라는 이름으로 표시되어 기능이 실험적인지 여부를 알려주는 역할을 합니다.

DevTools의 콘솔을 확인하여 현재 기기에서 API를 지원하는지 확인하세요. Google은 휴대전화 및 데스크톱 컴퓨터 등 다양한 기기에서 동일한 앱을 간편하게 확인할 수 있도록 Glitch도 사용합니다.

DevTools에서 콘솔에 기록된 API 호환성

4. 🚰 웹 공유 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"]
        }
      ]
    }
  }
}

그러면 서비스 워커에서 수신된 파일을 처리합니다. ./share-target/ URL은 실제로 존재하지 않습니다. 앱은 fetch 핸들러에서 작업을 하고 쿼리 매개변수 ?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()에 캔버스에 blob을 그릴 수 있습니다.

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. ✝ 파일 시스템 액세스 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 기기의 경우 Contact Picker 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 지원 추가

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

사용자가 새 브러시를 만들 때마다 배지를 계산하는 기능을 추가합니다. Badging 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. 광범위한 화면 Wake Lock API 지원

사용자가 그림을 바라보는 것만으로도 충분히 영감을 얻기 위해 잠시 시간을 내어 기다려야 할 수 있습니다. 화면을 켠 상태로 유지하고 화면 보호기가 시작되지 않도록 하는 기능을 추가합니다. Screen Wake Lock API를 사용하면 사용자의 화면이 절전 모드로 전환되는 것을 방지할 수 있습니다. wake lock은 페이지 공개 상태에 정의된 대로 공개 상태 변경 이벤트가 발생하면 자동으로 해제됩니다. 따라서 페이지가 다시 표시될 때 wake lock을 다시 확보해야 합니다.

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 지원 추가

빈 캔버스로 시작하는 것은 지루할 수 있습니다. Periodic Background Sync API를 사용하면 매일 새로운 이미지로 사용자 캔버스를 초기화할 수 있습니다. 예를 들어 Unreal fugu 사진입니다.

여기에는 두 가지 파일이 필요합니다. 하나는 주기적 백그라운드 동기화를 등록하는 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 지원 추가

때로는 사용자가 그림 또는 중고 배경 이미지에 바코드와 같은 유용한 정보를 포함할 수 있습니다. Shape Detection 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. 🐡 유휴 상태 감지 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. 🐡 파일 처리 API 지원 추가

사용자가 이미지 파일을 doubleclick만 만들면 앱이 팝업으로 표시되면 어떻게 해야 할까요? 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 사이트의 간행물을 팔로우하세요.

web.dev 사이트에 있는 &ldquo;기능&rdquo; 섹션의 방문 페이지입니다.

하지만 여기서 끝나지 않을 거예요. 아직 공개되지 않은 업데이트의 경우 발송된 모든 제안서, 원본 시험 또는 개발자 체험판, 작업이 시작된 모든 제안서 및 고려 중인 모든 제안서의 링크가 포함된 Fugu API 추적기에 액세스할 수 있습니다.

Fugu API 추적기 웹사이트

이 Codelab은 토마스 스타이너 (@tomayac)가 작성했으며, 질문에 기쁜 마음으로 답변해 드립니다. 여러분의 피드백을 기대하겠습니다. 이 Codelab을 설계하는 데 도움을 주신 헤먼스 H.M (@GNUmanth), 크리스천 리벨 (@christianliebel), 스벤 메이 (@Svenmay), 라스 크누센 (@larsgk), 재키 한 (@hanguokai)님께 감사드립니다.