최신 브라우저용으로 빌드하고 2003년처럼 점진적으로 개선
2003년 3월, 닉 핀크와 스티브 챔피언은 핵심 웹페이지 콘텐츠를 먼저 로드한 다음 콘텐츠 위에 더 미묘하고 기술적으로 엄격한 프레젠테이션 및 기능 레이어를 점진적으로 추가하는 웹 디자인 전략인 점진적 개선 개념으로 웹 디자인계를 놀라게 했습니다. 2003년에는 점진적 개선이란 당시 최신 CSS 기능, 눈에 잘 띄지 않는 JavaScript, 확장형 벡터 그래픽을 사용하는 것을 의미했습니다. 2020년 이후의 프로그레시브 개선은 최신 브라우저 기능을 사용하는 것입니다.
![점진적 개선을 통해 미래를 위한 포용적인 웹 디자인 Finck 및 Champeon의 원본 프레젠테이션의 제목 슬라이드](https://web.developers.google.cn/static/articles/progressively-enhance-your-pwa/image/inclusive-web-design-the-8dd761f5f7ef6.png?authuser=6&hl=ko)
최신 JavaScript
JavaScript에 관해 말하자면 최신 핵심 ES 2015 JavaScript 기능에 대한 브라우저 지원 상황은 매우 좋습니다.
새 표준에는 약속, 모듈, 클래스, 템플릿 문자열, 화살표 함수, let
및 const
, 기본 매개변수, 생성자, 디스트럭처링 할당, 나머지 및 스프레드, Map
/Set
, WeakMap
/WeakSet
등이 포함됩니다.
모두 지원됩니다.
![모든 주요 브라우저에서 지원되는 ES6 기능에 관한 CanIUse 지원 표입니다.](https://web.developers.google.cn/static/articles/progressively-enhance-your-pwa/image/the-caniuse-support-table-fea500f90ffcf.png?authuser=6&hl=ko)
ES 2017 기능이자 제가 개인적으로 가장 좋아하는 비동기 함수는 모든 주요 브라우저에서 사용할 수 있습니다.
async
및 await
키워드를 사용하면 비동기 프라미스 기반 동작을 더 깔끔한 스타일로 작성할 수 있으므로 프라미스 체인을 명시적으로 구성할 필요가 없습니다.
![모든 주요 브라우저에서 지원되는 비동기 함수에 관한 CanIUse 지원 표](https://web.developers.google.cn/static/articles/progressively-enhance-your-pwa/image/the-caniuse-support-table-bec0941fba39c.png?authuser=6&hl=ko)
선택적 체이닝 및 nullish coalescing과 같은 최근에 추가된 ES 2020 언어도 매우 빠르게 지원을 받았습니다. 아래의 코드 샘플을 참고하세요. 핵심 JavaScript 기능에 관해서는 지금보다 더 나은 환경은 없을 것입니다.
const adventurer = {
name: 'Alice',
cat: {
name: 'Dinah',
},
};
console.log(adventurer.dog?.name);
// Expected output: undefined
console.log(0 ?? 42);
// Expected output: 0
![Windows XP의 상징적인 녹색 잔디 배경 이미지](https://web.developers.google.cn/static/articles/progressively-enhance-your-pwa/image/the-iconic-windows-xp-gre-52c72bfaafb9d.png?authuser=6&hl=ko)
샘플 앱: Fugu Greetings
이 도움말에서는 Fugu Greetings(GitHub)라는 간단한 PWA를 사용합니다. 이 앱의 이름은 웹에 Android/iOS/데스크톱 애플리케이션의 모든 기능을 제공하기 위한 노력인 Project Fugu 🐡를 기리기 위한 것입니다. 프로젝트에 관한 자세한 내용은 방문 페이지를 참고하세요.
Fugu Greetings는 가상 인사말 카드를 만들어 사랑하는 사람에게 보낼 수 있는 그림 그리기 앱입니다. PWA의 핵심 개념을 보여주는 예시입니다. 안정적이며 완전히 오프라인으로 사용 설정되어 있으므로 네트워크가 없어도 사용할 수 있습니다. 또한 기기의 홈 화면에 설치할 수 있으며 독립형 애플리케이션으로 운영체제와 원활하게 통합됩니다.
![PWA 커뮤니티 로고와 유사한 그림이 있는 Fugu Greetings PWA](https://web.developers.google.cn/static/articles/progressively-enhance-your-pwa/image/fugu-greetings-pwa-a-dra-964a41cbe2963.png?authuser=6&hl=ko)
점진적 개선
이제 점진적 개선에 대해 이야기해 보겠습니다. MDN 웹 문서 용어집에서는 이 개념을 다음과 같이 정의합니다.
점진적 개선은 최대한 많은 사용자에게 필수 콘텐츠와 기능의 기준을 제공하는 동시에 필요한 모든 코드를 실행할 수 있는 최신 브라우저 사용자에게만 최상의 환경을 제공하는 디자인 철학입니다.
기능 감지는 일반적으로 브라우저가 더 최신 기능을 처리할 수 있는지 확인하는 데 사용되는 반면, 폴리필은 자바스크립트로 누락된 기능을 추가하는 데 자주 사용됩니다.
[…]
점진적 개선은 웹 개발자가 최상의 웹사이트를 개발하는 데 집중하면서 여러 알 수 없는 사용자 에이전트에서 웹사이트가 작동하도록 하는 데 도움이 되는 유용한 기법입니다. 조용히 중단은 관련이 있지만 동일하지는 않으며 점진적 개선과는 반대 방향으로 진행되는 것으로 간주되는 경우가 많습니다. 실제로는 두 접근 방식 모두 유효하며 서로 보완할 수 있습니다.
MDN 참여자
각 인사말 카드를 처음부터 시작하는 것은 매우 번거로울 수 있습니다.
사용자가 이미지를 가져와서 시작할 수 있는 기능을 제공하면 어떨까요?
기존 접근 방식에서는 이를 위해 <input type=file>
요소를 사용했습니다.
먼저 요소를 만들고 type
를 'file'
로 설정하고 accept
속성에 MIME 유형을 추가한 다음 프로그래매틱 방식으로 '클릭'하고 변경사항을 리슨합니다.
이미지를 선택하면 캔버스에 바로 가져옵니다.
const importImage = async () => {
return new Promise((resolve) => {
const input = document.createElement('input');
input.type = 'file';
input.accept = 'image/*';
input.addEventListener('change', () => {
resolve(input.files[0]);
});
input.click();
});
};
가져오기 기능이 있는 경우 사용자가 크리스마스 카드를 로컬에 저장할 수 있는 내보내기 기능도 있어야 합니다.
파일을 저장하는 기존 방법은 download
속성과 blob URL을 href
로 사용하여 앵커 링크를 만드는 것입니다.
또한 프로그래매틱 방식으로 '클릭'하여 다운로드를 트리거하고 메모리 누수를 방지하기 위해 blob 객체 URL을 취소하는 것을 잊지 마세요.
const exportImage = async (blob) => {
const a = document.createElement('a');
a.download = 'fugu-greeting.png';
a.href = URL.createObjectURL(blob);
a.addEventListener('click', (e) => {
setTimeout(() => URL.revokeObjectURL(a.href), 30 * 1000);
});
a.click();
};
하지만 잠깐만요. 정신적으로는 인사말 카드를 '다운로드'한 것이 아니라 '저장'한 것입니다. 파일을 저장할 위치를 선택할 수 있는 '저장' 대화상자를 표시하는 대신 브라우저가 사용자 상호작용 없이 직접 인사말 카드를 다운로드하여 다운로드 폴더에 바로 저장했습니다. 좋지 않은 상황입니다.
더 나은 방법이 있다면 어떨까요? 로컬 파일을 열고 수정한 다음 새 파일이나 처음에 연 원본 파일로 다시 저장할 수 있다면 어떨까요? File System Access API를 사용하면 파일과 디렉터리를 열고 만들 수 있을 뿐만 아니라 수정하고 저장할 수도 있습니다.
API를 기능 감지하려면 어떻게 해야 하나요?
File System Access API는 새 메서드 window.chooseFileSystemEntries()
를 노출합니다.
따라서 이 메서드를 사용할 수 있는지 여부에 따라 다른 가져오기 및 내보내기 모듈을 조건부로 로드해야 합니다. 아래에서 방법을 확인하실 수 있습니다.
const loadImportAndExport = () => {
if ('chooseFileSystemEntries' in window) {
Promise.all([
import('./import_image.mjs'),
import('./export_image.mjs'),
]);
} else {
Promise.all([
import('./import_image_legacy.mjs'),
import('./export_image_legacy.mjs'),
]);
}
};
하지만 File System Access API 세부정보를 살펴보기 전에 먼저 점진적 개선 패턴을 간단히 살펴보겠습니다. 현재 File System Access API를 지원하지 않는 브라우저에서는 기존 스크립트를 로드합니다. 아래에서 Firefox 및 Safari의 네트워크 탭을 확인할 수 있습니다.
![기존 파일이 로드되는 것을 보여주는 Safari 웹 검사기](https://web.developers.google.cn/static/articles/progressively-enhance-your-pwa/image/safari-web-inspector-show-38b32feb7b4f.png?authuser=6&hl=ko)
![기존 파일이 로드되는 모습을 보여주는 Firefox 개발자 도구](https://web.developers.google.cn/static/articles/progressively-enhance-your-pwa/image/firefox-developer-tools-s-60e8cb93aaa6b.png?authuser=6&hl=ko)
그러나 API를 지원하는 브라우저인 Chrome에서는 새 스크립트만 로드됩니다.
이는 모든 최신 브라우저에서 지원하는 동적 import()
덕분에 우아하게 가능합니다.
앞서 말씀드렸듯이 요즘 잔디가 꽤 푸르네요.
![최신 파일이 로드되는 모습을 보여주는 Chrome DevTools](https://web.developers.google.cn/static/articles/progressively-enhance-your-pwa/image/chrome-devtools-showing-770ae149354f5.png?authuser=6&hl=ko)
File System Access API
이제 이 문제를 해결했으므로 File System Access API를 기반으로 하는 실제 구현을 살펴보겠습니다.
이미지를 가져오기 위해 window.chooseFileSystemEntries()
를 호출하고 이미지 파일을 원한다고 말하는 accepts
속성을 전달합니다.
파일 확장자와 MIME 유형이 모두 지원됩니다.
이렇게 하면 getFile()
를 호출하여 실제 파일을 가져올 수 있는 파일 핸들이 생성됩니다.
const importImage = async () => {
try {
const handle = await window.chooseFileSystemEntries({
accepts: [
{
description: 'Image files',
mimeTypes: ['image/*'],
extensions: ['jpg', 'jpeg', 'png', 'webp', 'svg'],
},
],
});
return handle.getFile();
} catch (err) {
console.error(err.name, err.message);
}
};
이미지 내보내기도 거의 동일하지만 이번에는 'save-file'
유형 매개변수를 chooseFileSystemEntries()
메서드에 전달해야 합니다.
여기에서 파일 저장 대화상자가 표시됩니다.
파일이 열려 있으면 'open-file'
가 기본값이므로 이 작업이 필요하지 않았습니다.
이전과 마찬가지로 accepts
매개변수를 설정했지만 이번에는 PNG 이미지로만 제한했습니다.
다시 파일 핸들을 가져오지만 이번에는 파일을 가져오는 대신 createWritable()
를 호출하여 쓰기 가능한 스트림을 만듭니다.
다음으로, 인사말 카드 이미지인 blob을 파일에 씁니다.
마지막으로 쓰기 가능한 스트림을 닫습니다.
언제든지 모든 것이 실패할 수 있습니다. 디스크에 여유 공간이 없거나 쓰기 또는 읽기 오류가 발생할 수 있으며 사용자가 파일 대화상자를 취소할 수도 있습니다.
그렇기 때문에 항상 호출을 try...catch
식으로 래핑합니다.
const exportImage = async (blob) => {
try {
const handle = await window.chooseFileSystemEntries({
type: 'save-file',
accepts: [
{
description: 'Image file',
extensions: ['png'],
mimeTypes: ['image/png'],
},
],
});
const writable = await handle.createWritable();
await writable.write(blob);
await writable.close();
} catch (err) {
console.error(err.name, err.message);
}
};
File System Access API와 함께 점진적 개선을 사용하면 이전과 같이 파일을 열 수 있습니다. 가져온 파일이 캔버스에 바로 그려집니다. 수정한 후 파일의 이름과 저장 위치를 선택할 수 있는 실제 저장 대화상자를 사용하여 저장할 수 있습니다. 이제 파일을 영구적으로 보존할 수 있습니다.
![파일 열기 대화상자가 있는 Fugu Greetings 앱](https://web.developers.google.cn/static/articles/progressively-enhance-your-pwa/image/fugu-greetings-app-a-fil-e1040300ddcaf.png?authuser=6&hl=ko)
![이제 가져온 이미지가 포함된 Fugu Greetings 앱](https://web.developers.google.cn/static/articles/progressively-enhance-your-pwa/image/fugu-greetings-app-with-56c3523778222.png?authuser=6&hl=ko)
![수정된 이미지가 포함된 Fugu Greetings 앱](https://web.developers.google.cn/static/articles/progressively-enhance-your-pwa/image/fugu-greetings-app-the-m-1a86c627405ad.png?authuser=6&hl=ko)
Web Share 및 Web Share Target API
영원히 보관하는 것 외에도 실제로 크리스마스 카드를 공유하고 싶을 수 있습니다. 이는 Web Share API 및 Web Share Target API를 사용하면 할 수 있는 작업입니다. 모바일 및 최근에는 데스크톱 운영체제에 내장된 공유 메커니즘이 추가되었습니다. 예를 들어 아래는 내 블로그의 도움말에서 트리거된 macOS의 데스크톱 Safari 공유 시트입니다. 도움말 공유 버튼을 클릭하면 macOS 메시지 앱을 통해 친구와 도움말 링크를 공유할 수 있습니다.
![도움말의 공유 버튼에서 트리거된 macOS의 데스크톱 Safari 공유 시트](https://web.developers.google.cn/static/articles/progressively-enhance-your-pwa/image/desktop-safaris-share-sh-8fbd756c55ba8.png?authuser=6&hl=ko)
이를 실행하는 코드는 매우 간단합니다. navigator.share()
를 호출하고 객체에 선택적 title
, text
, url
를 전달합니다.
하지만 이미지를 첨부하려면 어떻게 해야 하나요? Web Share API의 수준 1은 아직 이를 지원하지 않습니다.
다행히 웹 공유 수준 2에 파일 공유 기능이 추가되었습니다.
try {
await navigator.share({
title: 'Check out this article:',
text: `"${document.title}" by @tomayac:`,
url: document.querySelector('link[rel=canonical]').href,
});
} catch (err) {
console.warn(err.name, err.message);
}
Fugu 인사말 카드 애플리케이션에서 이 작업을 실행하는 방법을 보여드리겠습니다.
먼저 하나의 블롭으로 구성된 files
배열이 포함된 data
객체를 준비한 다음 title
및 text
를 준비해야 합니다. 다음으로 권장사항에 따라 이름에서 알 수 있듯이 공유하려는 data
객체를 브라우저에서 기술적으로 공유할 수 있는지 알려주는 새 navigator.canShare()
메서드를 사용합니다.
navigator.canShare()
에서 데이터를 공유할 수 있다고 알려주면 이전과 같이 navigator.share()
를 호출할 수 있습니다.
모든 것이 실패할 수 있으므로 다시 try...catch
블록을 사용합니다.
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);
}
};
이전과 마찬가지로 점진적 향상을 사용합니다.
'share'
와 'canShare'
가 모두 navigator
객체에 있는 경우에만 동적 import()
를 통해 share.mjs
를 로드합니다.
두 조건 중 하나만 충족하는 모바일 Safari와 같은 브라우저에서는 기능을 로드하지 않습니다.
const loadShare = () => {
if ('share' in navigator && 'canShare' in navigator) {
import('./share.mjs');
}
};
Fugu Greetings에서 Android의 Chrome과 같은 지원되는 브라우저에서 공유 버튼을 탭하면 내장 공유 시트가 열립니다. 예를 들어 Gmail을 선택하면 이미지가 첨부된 이메일 작성기 위젯이 표시됩니다.
![이미지를 공유할 수 있는 다양한 앱을 보여주는 OS 수준 Sharesheet](https://web.developers.google.cn/static/articles/progressively-enhance-your-pwa/image/os-level-share-sheet-show-3161a8aab13b2.png?authuser=6&hl=ko)
![이미지가 첨부된 Gmail의 이메일 작성 위젯입니다.](https://web.developers.google.cn/static/articles/progressively-enhance-your-pwa/image/gmails-email-compose-wid-fb4dcf2c7e4d4.png?authuser=6&hl=ko)
Contact Picker API
다음으로 연락처, 즉 기기의 주소록 또는 연락처 관리자 앱에 대해 이야기하겠습니다. 기념 카드를 작성할 때 누군가의 이름을 정확하게 쓰는 것이 항상 쉬운 것은 아닙니다. 예를 들어 이름을 키릴 문자로 표기하는 것을 선호하는 세르게이라는 친구가 있습니다. 독일어 QWERTZ 키보드를 사용 중이며 이름을 입력하는 방법을 모르겠습니다. 이 문제는 Contact Picker API로 해결할 수 있습니다. 휴대전화의 연락처 앱에 친구가 저장되어 있으므로 연락처 선택 도구 API를 통해 웹에서 연락처를 탭할 수 있습니다.
먼저 액세스하려는 속성 목록을 지정해야 합니다.
이 경우 이름만 필요하지만 다른 사용 사례에서는 전화번호, 이메일, 아바타 아이콘, 실제 주소에 관심이 있을 수 있습니다.
그런 다음 options
객체를 구성하고 multiple
를 true
로 설정하여 두 개 이상의 항목을 선택할 수 있도록 합니다.
마지막으로 navigator.contacts.select()
를 호출하면 사용자가 선택한 연락처의 원하는 속성이 반환됩니다.
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);
}
};
이제 패턴을 파악하셨을 겁니다. API가 실제로 지원되는 경우에만 파일을 로드합니다.
if ('contacts' in navigator) {
import('./contacts.mjs');
}
Fugu Greeting에서 연락처 버튼을 탭하고 가장 친한 친구인 Сергей Михайлович Брин과 劳伦斯·爱德华·"拉里"·佩奇를 선택하면 연락처 선택 도구가 이름만 표시하고 이메일 주소나 전화번호와 같은 다른 정보는 표시하지 않는 것을 볼 수 있습니다. 그러면 내 크리스마스 카드에 그들의 이름이 그려집니다.
![주소록에 있는 두 명의 연락처 이름을 보여주는 연락처 선택기](https://web.developers.google.cn/static/articles/progressively-enhance-your-pwa/image/contacts-picker-showing-4d7400f689224.png?authuser=6&hl=ko)
![이전에 선택한 두 명의 연락처 이름이 크리에이터가 만든 인화된 카드에 표시됩니다.](https://web.developers.google.cn/static/articles/progressively-enhance-your-pwa/image/the-names-the-previousl-58fa638399f8c.png?authuser=6&hl=ko)
비동기 Clipboard API
다음은 복사 및 붙여넣기입니다. 소프트웨어 개발자가 가장 좋아하는 작업 중 하나는 복사 및 붙여넣기입니다. 저는 크리에이터로서 때때로 같은 일을 하고 싶을 때가 있습니다. 작업 중인 인사말 카드에 이미지를 붙여넣거나 다른 곳에서 계속 수정할 수 있도록 인사말 카드를 복사하고 싶을 수 있습니다. Async Clipboard API는 텍스트와 이미지를 모두 지원합니다. Fugu Greetings 앱에 복사 및 붙여넣기 지원을 추가한 방법을 안내해 드리겠습니다.
시스템 클립보드에 항목을 복사하려면 클립보드에 써야 합니다.
navigator.clipboard.write()
메서드는 클립보드 항목 배열을 매개변수로 사용합니다.
각 클립보드 항목은 기본적으로 blob이 값이고 blob의 유형이 키인 객체입니다.
const copy = async (blob) => {
try {
await navigator.clipboard.write([
new ClipboardItem({
[blob.type]: blob,
}),
]);
} catch (err) {
console.error(err.name, err.message);
}
};
붙여넣으려면 navigator.clipboard.read()
를 호출하여 가져온 클립보드 항목을 루프 처리해야 합니다.
이는 여러 클립보드 항목이 클립보드에 서로 다른 표현으로 있을 수 있기 때문입니다.
각 클립보드 항목에는 사용 가능한 리소스의 MIME 유형을 알려주는 types
필드가 있습니다.
이전에 가져온 MIME 유형을 전달하여 클립보드 항목의 getType()
메서드를 호출합니다.
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);
}
};
이제는 말할 필요도 없을 것입니다. 지원되는 브라우저에서만 이 작업을 실행합니다.
if ('clipboard' in navigator && 'write' in navigator.clipboard) {
import('./clipboard.mjs');
}
실제로는 어떻게 작동할까요? macOS 미리보기 앱에서 이미지를 열고 클립보드에 복사합니다. 붙여넣기를 클릭하면 Fugu Greetings 앱에서 앱이 클립보드의 텍스트와 이미지를 볼 수 있도록 허용할지 묻는 메시지가 표시됩니다.
![클립보드 권한 메시지를 보여주는 Fugu Greetings 앱](https://web.developers.google.cn/static/articles/progressively-enhance-your-pwa/image/fugu-greetings-app-showin-2da915014bf66.png?authuser=6&hl=ko)
마지막으로 권한을 수락하면 이미지가 애플리케이션에 붙여넣어집니다. 반대로도 작동합니다. 클립보드에 인사말 카드를 복사하겠습니다. 그런 다음 미리보기를 열고 파일을 클릭한 다음 클립보드에서 새로 만들기를 클릭하면 새 제목 없는 이미지에 크리스마스 카드가 붙여넣어집니다.
![방금 붙여넣은 제목 없는 이미지가 있는 macOS 미리보기 앱](https://web.developers.google.cn/static/articles/progressively-enhance-your-pwa/image/the-macos-preview-app-an-9ec120ebd7ad8.png?authuser=6&hl=ko)
Badging API
또 다른 유용한 API는 Badging API입니다.
설치 가능한 PWA인 Fugu Greetings에는 물론 사용자가 앱 도크나 홈 화면에 배치할 수 있는 앱 아이콘이 있습니다.
API를 보여주는 재미있고 쉬운 방법은 Fugu Greetings에서 펜 획 카운터로 API를 (오)용하는 것입니다.
pointerdown
이벤트가 발생할 때마다 펜 획 카운터를 증가시키고 업데이트된 아이콘 배지를 설정하는 이벤트 리스너를 추가했습니다.
캔버스가 지워질 때마다 카운터가 재설정되고 배지가 삭제됩니다.
let strokes = 0;
canvas.addEventListener('pointerdown', () => {
navigator.setAppBadge(++strokes);
});
clearButton.addEventListener('click', () => {
strokes = 0;
navigator.setAppBadge(strokes);
});
이 기능은 점진적인 개선이므로 로드 로직은 평소와 같습니다.
if ('setAppBadge' in navigator) {
import('./badge.mjs');
}
이 예에서는 숫자당 하나의 펜 획을 사용하여 1부터 7까지의 숫자를 그렸습니다. 아이콘의 배지 카운터가 7이 되었습니다.
![각각 펜 획 하나로 그려진 1~7의 숫자가 크리스마스 카드에 그려져 있습니다.](https://web.developers.google.cn/static/articles/progressively-enhance-your-pwa/image/the-numbers-one-seven-d-890d712e8df6d.png?authuser=6&hl=ko)
![7이라는 숫자를 보여주는 Fugu Greetings 앱의 배지 아이콘](https://web.developers.google.cn/static/articles/progressively-enhance-your-pwa/image/badge-icon-the-fugu-gree-bc1d070282039.png?authuser=6&hl=ko)
Periodic Background Sync API
매일 새로운 기분으로 하루를 시작하고 싶으신가요? Fugu Greetings 앱의 멋진 기능은 매일 아침 새로운 배경 이미지로 인사말 카드를 시작할 수 있다는 점입니다. 앱은 Periodic Background Sync API를 사용하여 이를 실행합니다.
첫 번째 단계는 서비스 워커 등록에서 주기적 동기화 이벤트를 등록하는 것입니다.
'image-of-the-day'
라는 동기화 태그를 리슨하며 최소 간격이 1일이므로 사용자는 24시간마다 새 배경 이미지를 가져올 수 있습니다.
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);
}
};
두 번째 단계는 서비스 워커에서 periodicsync
이벤트를 수신 대기하는 것입니다.
이벤트 태그가 'image-of-the-day'
, 즉 이전에 등록된 태그인 경우 오늘의 이미지가 getImageOfTheDay()
함수를 통해 검색되고 결과가 모든 클라이언트로 전파되므로 클라이언트가 캔버스와 캐시를 업데이트할 수 있습니다.
self.addEventListener('periodicsync', (syncEvent) => {
if (syncEvent.tag === 'image-of-the-day-sync') {
syncEvent.waitUntil(
(async () => {
const blob = await getImageOfTheDay();
const clients = await self.clients.matchAll();
clients.forEach((client) => {
client.postMessage({
image: blob,
});
});
})()
);
}
});
다시 말하지만 이는 진정한 점진적 개선이므로 브라우저에서 API를 지원하는 경우에만 코드가 로드됩니다.
이는 클라이언트 코드와 서비스 워커 코드 모두에 적용됩니다.
지원되지 않는 브라우저에서는 둘 다 로드되지 않습니다.
서비스 워커에서는 동적 import()
(서비스 워커 컨텍스트에서 아직 지원되지 않음) 대신 기존 importScripts()
를 사용하는 것을 볼 수 있습니다.
// In the client:
const registration = await navigator.serviceWorker.ready;
if (registration && 'periodicSync' in registration) {
import('./periodic_background_sync.mjs');
}
// In the service worker:
if ('periodicSync' in self.registration) {
importScripts('./image_of_the_day.mjs');
}
Fugu Greetings에서 배경화면 버튼을 누르면 주기적 백그라운드 동기화 API를 통해 매일 업데이트되는 오늘의 인사말 카드 이미지가 표시됩니다.
![오늘의 새로운 인사말 카드 이미지가 포함된 Fugu Greetings 앱](https://web.developers.google.cn/static/articles/progressively-enhance-your-pwa/image/fugu-greetings-app-a-gr-d81b949b1fb1c.png?authuser=6&hl=ko)
Notification Triggers API
아이디어가 많더라도 시작된 인사 카드를 완성하려면 약간의 자극이 필요할 때가 있습니다. 이 기능은 Notification Triggers API에서 사용 설정합니다. 사용자는 카드 작성을 완료하라는 알림을 받으려는 시간을 입력할 수 있습니다. 그때가 되면 기념 카드가 대기 중이라는 알림이 전송됩니다.
타겟 시간을 묻는 메시지가 표시되면 애플리케이션은 showTrigger
로 알림을 예약합니다.
이전에 선택한 타겟 날짜가 포함된 TimestampTrigger
일 수 있습니다.
리마인더 알림은 로컬에서 트리거되므로 네트워크 또는 서버 측은 필요하지 않습니다.
const targetDate = promptTargetDate();
if (targetDate) {
const registration = await navigator.serviceWorker.ready;
registration.showNotification('Reminder', {
tag: 'reminder',
body: "It's time to finish your greeting card!",
showTrigger: new TimestampTrigger(targetDate),
});
}
지금까지 보여드린 다른 모든 것과 마찬가지로 이는 점진적인 개선이므로 코드는 조건부로만 로드됩니다.
if ('Notification' in window && 'showTrigger' in Notification.prototype) {
import('./notification_triggers.mjs');
}
Fugu Greetings에서 리마인더 체크박스를 선택하면 카드 작성을 완료하라는 리마인더를 언제 받으려는지 묻는 메시지가 표시됩니다.
![사용자가 언제 카드 작성 완료 알림을 받으려는지를 묻는 메시지가 표시된 Fugu Greetings 앱](https://web.developers.google.cn/static/articles/progressively-enhance-your-pwa/image/fugu-greetings-app-a-pro-5fd5c6e04511a.png?authuser=6&hl=ko)
Fugu Greetings에서 예약된 알림이 트리거되면 다른 알림과 마찬가지로 표시되지만 앞서 말씀드린 대로 네트워크 연결이 필요하지 않았습니다.
![Fugu Greetings에서 트리거된 알림이 표시된 macOS 알림 센터](https://web.developers.google.cn/static/articles/progressively-enhance-your-pwa/image/macos-notification-center-87f383f0a103b.png?authuser=6&hl=ko)
Wake Lock API
Wake Lock API도 포함하고 싶습니다. 영감이 떠오를 때까지 화면을 충분히 쳐다보는 것만으로도 충분할 때가 있습니다. 이 경우 최악의 상황은 화면이 꺼지는 것입니다. Wake Lock API를 사용하면 이를 방지할 수 있습니다.
첫 번째 단계는 navigator.wakelock.request method()
로 wake lock을 가져오는 것입니다.
'screen'
문자열을 전달하여 화면 깨우기 잠금을 가져옵니다.
그런 다음 wake lock이 해제될 때 알림을 받기 위한 이벤트 리스너를 추가합니다.
예를 들어 탭 표시 상태가 변경되면 이러한 현상이 발생할 수 있습니다.
이 경우 탭이 다시 표시되면 wake lock을 다시 가져올 수 있습니다.
let wakeLock = null;
const requestWakeLock = async () => {
wakeLock = await navigator.wakeLock.request('screen');
wakeLock.addEventListener('release', () => {
console.log('Wake Lock was released');
});
console.log('Wake Lock is active');
};
const handleVisibilityChange = () => {
if (wakeLock !== null && document.visibilityState === 'visible') {
requestWakeLock();
}
};
document.addEventListener('visibilitychange', handleVisibilityChange);
document.addEventListener('fullscreenchange', handleVisibilityChange);
예. 점진적 개선이므로 브라우저에서 API를 지원할 때만 로드하면 됩니다.
if ('wakeLock' in navigator && 'request' in navigator.wakeLock) {
import('./wake_lock.mjs');
}
Fugu Greetings에는 불면증 체크박스가 있습니다. 이 체크박스를 선택하면 화면이 꺼지지 않습니다.
![불면증 체크박스가 선택된 경우 화면이 꺼지지 않습니다.](https://web.developers.google.cn/static/articles/progressively-enhance-your-pwa/image/the-insomnia-checkbox-c-fc49b7954974a.png?authuser=6&hl=ko)
Idle Detection API
화면을 몇 시간 동안 쳐다봐도 아무 소용이 없고, 인사말 카드로 무엇을 해야 할지 전혀 생각이 나지 않는 경우가 있습니다. Idle Detection API를 사용하면 앱에서 사용자 유휴 시간을 감지할 수 있습니다. 사용자가 너무 오랫동안 유휴 상태이면 앱이 초기 상태로 재설정되고 캔버스가 지워집니다. 유휴 감지의 많은 프로덕션 사용 사례가 알림과 관련이 있으므로(예: 사용자가 현재 사용 중인 기기에만 알림을 전송) 이 API는 현재 알림 권한으로 제한되어 있습니다.
알림 권한이 부여되었는지 확인한 후 유휴 감지기를 인스턴스화합니다. 사용자 및 화면 상태를 포함하여 유휴 변경사항을 리슨하는 이벤트 리스너를 등록합니다. 사용자는 활성 상태이거나 유휴 상태일 수 있으며 화면은 잠금 해제 상태이거나 잠겨 있을 수 있습니다. 사용자가 유휴 상태이면 캔버스가 지워집니다. 유휴 감지기에 60초의 기준을 설정합니다.
const idleDetector = new IdleDetector();
idleDetector.addEventListener('change', () => {
const userState = idleDetector.userState;
const screenState = idleDetector.screenState;
console.log(`Idle change: ${userState}, ${screenState}.`);
if (userState === 'idle') {
clearCanvas();
}
});
await idleDetector.start({
threshold: 60000,
signal,
});
그리고 항상 그렇듯이 이 코드는 브라우저에서 지원하는 경우에만 로드됩니다.
if ('IdleDetector' in window) {
import('./idle_detection.mjs');
}
Fugu Greetings 앱에서 일회성 체크박스가 선택되어 있고 사용자가 너무 오랫동안 유휴 상태이면 캔버스가 지워집니다.
![사용자가 너무 오랫동안 활동하지 않은 후 캔버스가 지워진 Fugu Greetings 앱](https://web.developers.google.cn/static/articles/progressively-enhance-your-pwa/image/fugu-greetings-app-a-cle-559af8955ecad.png?authuser=6&hl=ko)
마무리
와우, 굉장한 레이스였어! 하나의 샘플 앱에 이렇게 많은 API가 있습니다. 그리고 브라우저에서 지원하지 않는 기능에 대한 다운로드 비용은 사용자에게 청구하지 않습니다. 점진적 개선을 사용하면 관련 코드만 로드됩니다. 또한 HTTP/2를 사용하면 요청이 저렴하므로 이 패턴은 많은 애플리케이션에 적합하지만 매우 큰 앱의 경우 번들러를 고려하는 것이 좋습니다.
![현재 브라우저에서 지원하는 코드가 포함된 파일의 요청만 보여주는 Chrome DevTools Network 패널](https://web.developers.google.cn/static/articles/progressively-enhance-your-pwa/image/chrome-devtools-network-p-c51d72a3bad2.png?authuser=6&hl=ko)
일부 플랫폼에서는 일부 기능을 지원하지 않으므로 앱이 브라우저마다 약간 다르게 보일 수 있지만 핵심 기능은 항상 존재하며 특정 브라우저의 기능에 따라 점진적으로 개선됩니다. 앱이 설치된 앱으로 실행 중인지 또는 브라우저 탭에서 실행 중인지에 따라 동일한 브라우저에서도 이러한 기능이 달라질 수 있습니다.
![Android Chrome에서 실행 중인 Fugu Greetings. 사용 가능한 여러 기능을 보여줍니다.](https://web.developers.google.cn/static/articles/progressively-enhance-your-pwa/image/fugu-greetings-running-a-058338175547c.png?authuser=6&hl=ko)
![데스크톱 Safari에서 실행되는 Fugu Greetings. 사용 가능한 기능이 더 적게 표시됩니다.](https://web.developers.google.cn/static/articles/progressively-enhance-your-pwa/image/fugu-greetings-running-d-446d17fc11442.png?authuser=6&hl=ko)
![데스크톱 Chrome에서 실행 중인 Fugu Greetings. 다양한 기능이 표시되어 있습니다.](https://web.developers.google.cn/static/articles/progressively-enhance-your-pwa/image/fugu-greetings-running-d-9006f53b391af.png?authuser=6&hl=ko)
Fugu Greetings 앱에 관심이 있다면 앱을 찾아 GitHub에서 포크하세요.
![GitHub의 Fugu Greetings 저장소](https://web.developers.google.cn/static/articles/progressively-enhance-your-pwa/image/fugu-greetings-repo-gith-f95acb5949892.png?authuser=6&hl=ko)
Chromium팀은 고급 Fugu API를 개선하기 위해 노력하고 있습니다. 앱 개발 시 프로그레시브 개선을 적용하면 모든 사용자가 안정적인 기본 환경을 누릴 수 있지만 더 많은 웹 플랫폼 API를 지원하는 브라우저를 사용하는 사용자는 훨씬 더 나은 환경을 누릴 수 있습니다. 앱에서 프로그레시브 개선을 어떻게 활용할지 기대됩니다.
감사의 말씀
Fugu Greetings에 기여해 주신 크리스티안 리벨님과 헤만트 HM님께 감사드립니다.
이 도움말은 조 미들리와 케이스 바스케스가 검토했습니다.
Jake Archibald님이 서비스 워커 컨텍스트에서 동적 import()
의 상황을 파악하는 데 도움을 주었습니다.