웹 앱에 푸시 알림 추가

1. 개요

푸시 메시지는 사용자의 재참여를 유도하는 간단하고 효과적인 방법을 제공합니다. 이 Codelab에서는 웹 앱에 푸시 알림을 추가하는 방법을 알아봅니다.

학습할 내용

  • 푸시 메시지를 위해 사용자 수신 및 수신 거부 방법
  • 수신된 푸시 메시지 처리 방법
  • 알림을 표시하는 방법
  • 알림 클릭에 응답하는 방법

필요한 항목

  • Chrome 52 이상
  • Chrome용 웹 서버 또는 자체 웹 서버
  • 텍스트 편집기
  • HTML, CSS, JavaScript, Chrome DevTools에 관한 기본 지식
  • 샘플 코드 (설정하기 참조)

2. 설정

샘플 코드 다운로드

이 Codelab의 샘플 코드를 가져오는 방법은 두 가지가 있습니다.

  • Git 저장소를 복제합니다.
git clone https://github.com/GoogleChrome/push-notifications.git
  • ZIP 파일을 다운로드합니다.

소스를 ZIP 파일로 다운로드한 경우 압축을 풀면 루트 폴더 push-notifications-master가 생성됩니다.

웹 서버 설치 및 확인

자체 웹 서버를 사용해도 되지만 이 Codelab은 Chrome용 웹 서버 앱에서도 잘 작동하도록 설계되었습니다. 아직 해당 앱을 설치하지 않은 경우 Chrome 웹 스토어에서 다운로드할 수 있습니다.

Web Server for Chrome 앱을 설치한 후 북마크바에서 바로가기를 클릭합니다.

946bcaaad66e5c8e.png

Apps 창에서 Web Server 아이콘을 클릭합니다.

9f3c21b2cf6cbfb5.png

그러면 로컬 웹 서버를 구성할 수 있는 대화상자가 표시됩니다.

73543edeb27c3d6f.png

Choose folder(폴더 선택) 버튼을 클릭하고 다운로드한 push-notifications 폴더에서 app 폴더를 선택합니다. 이렇게 하면 대화상자의 웹 서버 URL 섹션에 표시되는 URL을 통해 진행 중인 작업을 제공할 수 있습니다.

아래와 같이 옵션에서 index.html 자동 표시 옆에 있는 체크박스를 선택합니다.

5ac11bca86ce7369.png

그런 다음 웹 서버: 시작됨 전환 버튼을 왼쪽으로 슬라이드하여 서버를 중지했다가 다시 시작합니다.

d42f87972f9fec24.png

웹 서버 URL을 클릭하여 웹브라우저에서 사이트를 방문합니다. 그러면 다음과 같은 페이지가 표시됩니다. 다만 실제 버전에서는 주소로 127.0.0.1:8887이 표시될 수 있습니다.

00-push-codelab.png

서비스 워커 항상 업데이트

개발 중에는 서비스 워커를 항상 최신 상태로 유지하고 최신 변경 사항을 적용하는 것이 좋습니다.

Chrome에서 이를 설정하려면 다음 단계를 따르세요.

  1. Push Codelab 탭으로 이동합니다.
  2. DevTools를 엽니다. Windows와 Linux에서는 Ctrl-Shift-I, macOS에서는 Cmd-Option-I를 누릅니다.
  3. Application 패널을 선택하고 Service Workers 탭을 클릭한 다음 Update on Reload 체크박스를 선택합니다. 이 확인란을 선택하면 페이지를 새로 고칠 때마다 서비스 워커가 강제로 업데이트됩니다.

e7d384fb77885b99.png

3. 서비스 워커 등록

완료된 코드

app 디렉터리에 sw.js라는 빈 파일이 있습니다. 이 파일이 서비스 워커가 됩니다. 지금은 비어 있을 수 있습니다. 나중에 코드를 추가합니다.

먼저 이 파일을 서비스 워커로 등록해야 합니다.

app/index.html 페이지가 scripts/main.js을(를) 로드합니다. 이 JavaScript 파일에서 서비스 워커를 등록합니다.

다음 코드를 scripts/main.js에 추가합니다.

if ('serviceWorker' in navigator && 'PushManager' in window) {
  console.log('Service Worker and Push are supported');

  navigator.serviceWorker.register('sw.js')
  .then(function(swReg) {
    console.log('Service Worker is registered', swReg);

    swRegistration = swReg;
  })
  .catch(function(error) {
    console.error('Service Worker Error', error);
  });
} else {
  console.warn('Push messaging is not supported');
  pushButton.textContent = 'Push Not Supported';
}

이 코드는 브라우저에서 서비스 워커와 푸시 메시지를 지원하는지 확인합니다. 지원되는 경우 코드는 sw.js 파일을 등록합니다.

사용해 보기

브라우저에서 Push Codelab 탭을 새로고침하여 변경사항을 확인합니다.

다음과 같이 Chrome DevTools의 콘솔에서 Service Worker is registered message를 확인합니다.

5d7ad383d6f235d5.png

애플리케이션 서버 키 가져오기

이 Codelab을 작업하려면 애플리케이션 서버 키를 생성해야 합니다. 컴패니언 사이트(web-push-codelab.glitch.me)에서 이 작업을 수행할 수 있습니다.

여기에서 공개 키와 비공개 키 쌍을 생성할 수 있습니다.

push-codelab-04-companion.png

공개 키를 scripts/main.js에 복사하여 <Your Public Key> 값을 바꿉니다.

const applicationServerPublicKey = '<Your Public Key>';

중요: 비공개 키를 웹 앱에 넣어서는 안 됩니다.

4. 상태 초기화

완료된 코드

현재 웹 앱의 사용 설정 버튼이 사용 중지되어 있어 클릭할 수 없습니다. 이는 푸시 버튼을 기본적으로 비활성화하고 브라우저에서 푸시 메시지를 지원한다는 것을 알고 사용자가 현재 메시지를 구독하고 있는지 확인한 후에 활성화하는 것이 좋은 방법이기 때문입니다.

scripts/main.js에 함수 두 개를 만들어야 합니다.

  • initializeUI: 사용자가 현재 구독 중인지 확인
  • updateBtn: 사용자의 구독 여부에 따라 버튼을 사용 설정하고 텍스트를 변경합니다.

다음과 같이 initializeUI 함수를 main.js에 추가합니다.

function initializeUI() {
  // Set the initial subscription value
  swRegistration.pushManager.getSubscription()
  .then(function(subscription) {
    isSubscribed = !(subscription === null);

    if (isSubscribed) {
      console.log('User IS subscribed.');
    } else {
      console.log('User is NOT subscribed.');
    }

    updateBtn();
  });
}

새 메서드는 이전 단계의 swRegistration를 사용하고, 거기에서 pushManager 속성을 가져오고, 거기에서 getSubscription()를 호출합니다.

pushManager getSubscription()는 현재 정기 결제가 있는 경우 현재 정기 결제로 확인되는 프로미스를 반환합니다. 그렇지 않으면 null이 반환됩니다. 이를 통해 사용자가 이미 정기 결제했는지 확인하고 isSubscribed 값을 설정한 다음 updateBtn()를 호출하여 버튼을 업데이트할 수 있습니다.

updateBtn() 함수를 main.js에 추가합니다.

function updateBtn() {
  if (isSubscribed) {
    pushButton.textContent = 'Disable Push Messaging';
  } else {
    pushButton.textContent = 'Enable Push Messaging';
  }

  pushButton.disabled = false;
}

이 함수는 버튼을 사용 설정하고 사용자의 구독 여부에 따라 버튼 텍스트를 변경합니다.

마지막으로 할 일은 서비스 워커가 main.js에 등록될 때 initializeUI()를 호출하는 것입니다.

navigator.serviceWorker.register('sw.js')
.then(function(swReg) {
  console.log('Service Worker is registered', swReg);

  swRegistration = swReg;
  initializeUI();
})

사용해 보기

Push Codelab 탭을 새로고침합니다. 이제 Enable Push Messaging 버튼이 사용 설정되고 (클릭할 수 있음) 콘솔에 User is NOT subscribed가 표시되어야 합니다.

a1553f4a0483d227.png

이 Codelab의 나머지 부분을 진행하면서 구독 또는 구독 취소할 때마다 버튼 텍스트가 변경되는 것을 확인할 수 있습니다.

5. 사용자 구독

완료된 코드

현재 푸시 메시지 사용 설정 버튼으로는 별다른 작업을 수행할 수 없습니다. 이 문제를 해결해보겠습니다.

initializeUI() 함수에서 버튼에 클릭 리스너를 추가합니다.

function initializeUI() {
  pushButton.addEventListener('click', function() {
    pushButton.disabled = true;
    if (isSubscribed) {
      // TODO: Unsubscribe user
    } else {
      subscribeUser();
    }
  });

  // Set the initial subscription value
  swRegistration.pushManager.getSubscription()
  .then(function(subscription) {
    isSubscribed = !(subscription === null);

    updateSubscriptionOnServer(subscription);

    if (isSubscribed) {
      console.log('User IS subscribed.');
    } else {
      console.log('User is NOT subscribed.');
    }

    updateBtn();
  });
}

사용자가 버튼을 클릭할 때, 사용자가 버튼을 다시 클릭할 수 없도록 버튼을 비활성화합니다. 푸시 메시지를 구독하는 데 다소 시간이 걸릴 수 있기 때문입니다.

그런 다음 사용자가 현재 정기 결제를 신청하지 않은 경우 subscribeUser()를 호출합니다. 이를 위해 다음 코드를 scripts/main.js에 붙여넣어야 합니다.

function subscribeUser() {
  const applicationServerKey = urlB64ToUint8Array(applicationServerPublicKey);
  swRegistration.pushManager.subscribe({
    userVisibleOnly: true,
    applicationServerKey: applicationServerKey
  })
  .then(function(subscription) {
    console.log('User is subscribed.');

    updateSubscriptionOnServer(subscription);

    isSubscribed = true;

    updateBtn();
  })
  .catch(function(error) {
    console.error('Failed to subscribe the user: ', error);
    updateBtn();
  });
}

이 코드가 수행하는 작업과 사용자의 푸시 메시지를 구독하는 방법을 단계별로 살펴보겠습니다.

먼저 Base64 URL 보안으로 인코딩된 애플리케이션 서버의 공개 키를 가져와 UInt8Array로 변환합니다. subscribe() 호출의 예상 입력이기 때문입니다. urlB64ToUint8Array() 함수는 scripts/main.js의 상단에 있습니다.

값을 변환한 후 서비스 워커의 pushManager에서 subscribe() 메서드를 호출하여 애플리케이션 서버의 공개 키와 userVisibleOnly: true 값을 전달합니다.

const applicationServerKey = urlB64ToUint8Array(applicationServerPublicKey);
swRegistration.pushManager.subscribe({
  userVisibleOnly: true,
  applicationServerKey: applicationServerKey
})

userVisibleOnly 매개변수는 푸시 메시지가 전송될 때마다 알림을 표시하도록 보장합니다. 현재 이 값은 필수이며 true여야 합니다.

subscribe()를 호출하면 다음 단계 후 확인되는 프로미스가 반환됩니다.

  1. 사용자가 알림을 표시할 권한을 부여했습니다.
  2. 브라우저가 PushSubscription를 생성하는 데 필요한 데이터를 가져오기 위해 푸시 서비스에 네트워크 요청을 보냈습니다.

이러한 단계가 성공하면 subscribe() 프로미스가 PushSubscription로 확인됩니다. 사용자가 권한을 부여하지 않거나 사용자 구독에 문제가 있는 경우 프로미스는 오류와 함께 거부됩니다. 이렇게 하면 Codelab에 다음과 같은 프라미스 체인이 제공됩니다.

swRegistration.pushManager.subscribe({
  userVisibleOnly: true,
  applicationServerKey: applicationServerKey
})
.then(function(subscription) {
  console.log('User is subscribed.');

  updateSubscriptionOnServer(subscription);

  isSubscribed = true;

  updateBtn();

})
.catch(function(err) {
  console.log('Failed to subscribe the user: ', err);
  updateBtn();
});

이를 통해 구독을 가져오고 사용자를 구독한 것으로 처리하거나 오류를 포착하여 콘솔에 로깅합니다. 두 시나리오에서 모두 updateBtn()를 호출하여 버튼이 다시 사용 설정되고 적절한 텍스트가 있는지 확인합니다.

실제 애플리케이션에서 updateSubscriptionOnServer() 함수는 정기 결제 데이터를 백엔드로 보내는 위치이지만 Codelab에서는 UI에 정기 결제를 표시하기만 하면 됩니다. scripts/main.js에 다음 함수를 추가합니다.

function updateSubscriptionOnServer(subscription) {
  // TODO: Send subscription to application server

  const subscriptionJson = document.querySelector('.js-subscription-json');
  const subscriptionDetails =
    document.querySelector('.js-subscription-details');

  if (subscription) {
    subscriptionJson.textContent = JSON.stringify(subscription);
    subscriptionDetails.classList.remove('is-invisible');
  } else {
    subscriptionDetails.classList.add('is-invisible');
  }
}

사용해 보기

Push Codelab 탭으로 이동하여 페이지를 새로고침한 후 버튼을 클릭합니다. 그러면 다음과 같은 권한 메시지가 표시됩니다.

fcc61267a0194e81.png

권한을 부여하면 콘솔에 기록된 User is subscribed가 표시됩니다. 버튼의 텍스트가 Disable Push Messaging으로 바뀌고 페이지 하단에서 구독을 JSON 데이터로 확인할 수 있습니다.

5c5505f2ead037c.png

6. 권한 거부됨 처리

완료된 코드

한 가지 아직 처리하지 않은 것은 사용자가 권한 요청을 차단하면 어떻게 되는지입니다. 이 경우 사용자가 권한을 차단하면 웹 앱이 권한 메시지를 다시 표시할 수 없고 사용자를 구독할 수 없게 되므로 독특한 고려사항이 필요합니다. 최소한 푸시 버튼을 비활성화하여 사용자가 버튼을 사용할 수 없음을 알 수 있도록 해야 합니다.

이 시나리오를 처리하기 위한 명확한 위치는 updateBtn() 함수 내에 있습니다. 다음과 같이 Notification.permission 값을 확인하기만 하면 됩니다.

function updateBtn() {
  if (Notification.permission === 'denied') {
    pushButton.textContent = 'Push Messaging Blocked';
    pushButton.disabled = true;
    updateSubscriptionOnServer(null);
    return;
  }

  if (isSubscribed) {
    pushButton.textContent = 'Disable Push Messaging';
  } else {
    pushButton.textContent = 'Enable Push Messaging';
  }

  pushButton.disabled = false;
}

권한이 denied인 경우 사용자를 구독할 수 없으며 더 이상 할 수 있는 작업이 없으므로 버튼을 영구적으로 사용 중지하는 것이 가장 좋은 방법입니다.

사용해 보기

이전 단계에서 이미 웹 앱에 권한을 부여했으므로 URL 표시줄의 원 안에 있는 i를 클릭하고 알림 권한을 전체 기본값 사용 (요청)으로 변경해야 합니다.

54495592074f10ae.png

이 설정을 변경한 후 페이지를 새로고침하고 Enable Push Messaging 버튼을 클릭한 다음 권한 대화상자에서 Block을 선택하세요. 버튼이 사용 중지되고 Push Messaging Blocked라는 텍스트가 표시됩니다.

d4cf22468f6defda.png

이 변경사항으로 이제 사용자를 구독하여 가능한 권한 시나리오를 처리할 수 있습니다.

7. 푸시 이벤트 처리

완료된 코드

백엔드에서 푸시 메시지를 보내는 방법을 배우기 전에, 구독한 사용자가 푸시 메시지를 수신할 때 실제로 어떤 일이 일어날지 생각해봐야 합니다.

푸시 메시지를 트리거하면 브라우저는 푸시 메시지를 수신하고, 푸시의 대상이 되는 서비스 워커를 파악하고, 해당 서비스 워커를 활성화하고, 푸시 이벤트를 발송합니다. 이 이벤트를 수신하고 그 결과로 알림을 표시해야 합니다.

다음 코드를 sw.js 파일에 추가하세요.

self.addEventListener('push', function(event) {
  console.log('[Service Worker] Push Received.');
  console.log(`[Service Worker] Push had this data: "${event.data.text()}"`);

  const title = 'Push Codelab';
  const options = {
    body: 'Yay it works.',
    icon: 'images/icon.png',
    badge: 'images/badge.png'
  };

  event.waitUntil(self.registration.showNotification(title, options));
});

이 코드를 단계별로 진행해 보겠습니다. 이벤트 리스너를 추가하여 서비스 워커에서 push 이벤트를 수신 대기합니다.

self.addEventListener('push', ... );

이전에 Web Workers를 사용해 본 적이 없다면 self은(는) 새로운 앱일 가능성이 높습니다. 서비스 워커 파일에서 self는 서비스 워커 자체를 참조합니다.)

푸시 메시지가 수신되면 이벤트 리스너가 호출되며, 서비스 워커의 registration 속성에서 showNotification()를 호출하여 알림을 만듭니다. showNotification()에는 title가 필요합니다. options 객체를 부여하여 본문 메시지, 아이콘, 배지를 설정할 수도 있습니다. (배지는 작성 시점에 Android에서만 사용됩니다.)

const title = 'Push Codelab';
const options = {
  body: 'Yay it works.',
  icon: 'images/icon.png',
  badge: 'images/badge.png'
};
self.registration.showNotification(title, options);

push 이벤트 처리에서 마지막으로 다룰 내용은 event.waitUntil()입니다. 이 메서드는 프라미스를 사용하여 전달된 프라미스가 해결될 때까지 브라우저가 서비스 워커의 활성 및 실행을 유지하도록 합니다.

위의 코드를 더 쉽게 이해할 수 있도록 다음과 같이 다시 작성할 수 있습니다.

const notificationPromise = self.registration.showNotification(title, options);
event.waitUntil(notificationPromise);

푸시 이벤트를 단계별로 진행했으므로 이제 푸시 이벤트를 테스트해 보겠습니다.

사용해 보기

서비스 워커에서 푸시 이벤트 처리를 사용하면 가짜 푸시 이벤트를 트리거하여 메시지가 수신될 때 어떤 일이 일어나는지 테스트할 수 있습니다.

웹 앱에서 푸시 메시지를 구독하고 콘솔에 User IS subscribed가 표시되는지 확인합니다. DevTools의 Application 패널에 있는 Service Workers 탭에서 Push 버튼을 클릭합니다.

1ee499267eeccd1c.png

푸시를 클릭하면 다음과 같은 알림이 표시됩니다.

379105dfb0ea56d8.png

참고: 이 단계를 진행해도 문제가 해결되지 않으면 DevTools Application 패널에서 Unregister 링크를 사용하여 서비스 워커의 등록을 취소하고 서비스 워커가 중지될 때까지 기다린 다음 페이지를 새로고침하세요.

8. 알림 클릭

완료된 코드

이러한 알림 중 하나를 클릭해도 아무 일도 일어나지 않습니다. 서비스 워커에서 notificationclick 이벤트를 수신 대기하여 알림 클릭을 처리할 수 있습니다.

먼저 sw.jsnotificationclick 리스너를 추가합니다.

self.addEventListener('notificationclick', function(event) {
  console.log('[Service Worker] Notification click received.');

  event.notification.close();

  event.waitUntil(
    clients.openWindow('https://developers.google.com/web')
  );
});

사용자가 알림을 클릭하면 notificationclick 이벤트 리스너가 호출됩니다.

이 코드는 클릭된 알림을 먼저 닫습니다.

event.notification.close();

그런 다음 새 창이나 탭이 열리고 URL https://developers.google.com/web이 로드됩니다. 자유롭게 변경하세요.

event.waitUntil(
    clients.openWindow('https://developers.google.com/web/')
  );

event.waitUntil()는 새 창이나 탭이 표시되기 전에 브라우저에서 서비스 워커를 종료하지 않도록 합니다.

사용해 보기

DevTools에서 푸시 메시지를 다시 트리거하고 알림을 클릭해 보세요. 이제 알림이 닫히고 새 탭이 열립니다.

9. 푸시 메시지 보내기

웹 앱이 DevTools를 사용하여 알림을 표시할 수 있음을 확인하고 클릭하여 알림을 닫는 방법을 살펴보았습니다. 다음 단계는 실제 푸시 메시지를 보내는 것입니다.

일반적으로 이를 위해서는 웹페이지에서 백엔드로 구독을 전송해야 합니다. 그러면 백엔드가 구독의 엔드포인트에 API 호출을 실행하여 푸시 메시지를 트리거합니다.

이는 이 Codelab에서 다루지 않지만 호환 사이트 ( web-push-codelab.glitch.me)를 사용하여 실제 푸시 메시지를 트리거할 수 있습니다. 페이지 하단에 구독정보를 붙여넣습니다.

bb202867962a0249.png

그런 다음 아래와 같이 호환 사이트의 Subscription to Send To 텍스트 영역에도 붙여넣으세요.

a0dd93bc33a9e8cf.png

보낼 텍스트에서 푸시 메시지와 함께 전송할 문자열을 추가합니다.

푸시 메시지 보내기 버튼을 클릭합니다.

a5e8e89411ec034.png

그러면 푸시 메시지가 수신됩니다. 사용한 텍스트가 콘솔에 기록됩니다.

f6815a356d4f9aaa.png

그러면 데이터 보내기 및 수신을 테스트하고 결과적으로 알림을 조작할 수 있습니다.

호환 앱은 웹 푸시 라이브러리를 사용하여 메시지를 전송하는 노드 서버일 뿐입니다. GitHub의 web-push-libs 조직을 검토하여 어떤 라이브러리를 사용하여 푸시 메시지를 보낼 수 있는지 확인해 보는 것이 좋습니다. 그러면 푸시 메시지를 트리거하기 위해 많은 세부정보가 처리됩니다.

여기에서 컴패니언 사이트의 모든 코드를 확인할 수 있습니다.

10. 사용자 수신 거부

완료된 코드

한 가지 누락된 것은 사용자가 푸시를 구독 취소할 수 있는 기능입니다. 이렇게 하려면 PushSubscription에서 unsubscribe()를 호출해야 합니다.

scripts/main.js 파일로 돌아가서 initializeUI()pushButton 클릭 리스너를 다음과 같이 변경합니다.

pushButton.addEventListener('click', function() {
  pushButton.disabled = true;
  if (isSubscribed) {
    unsubscribeUser();
  } else {
    subscribeUser();
  }
});

이제 새 함수 unsubscribeUser()를 호출합니다. 이 함수에서는 현재 구독을 가져와서 unsubscribe()를 호출합니다. scripts/main.js에 다음 코드를 추가합니다.

function unsubscribeUser() {
  swRegistration.pushManager.getSubscription()
  .then(function(subscription) {
    if (subscription) {
      return subscription.unsubscribe();
    }
  })
  .catch(function(error) {
    console.log('Error unsubscribing', error);
  })
  .then(function() {
    updateSubscriptionOnServer(null);

    console.log('User is unsubscribed.');
    isSubscribed = false;

    updateBtn();
  });
}

이 함수를 단계별로 진행해 보겠습니다.

먼저 getSubscription()를 호출하여 현재 정기 결제를 가져옵니다.

swRegistration.pushManager.getSubscription()

그러면 PushSubscription로 확인되는 프로미스가 있는 경우 이를 반환합니다. 그렇지 않으면 null을 반환합니다. 구독이 있는 경우 unsubscribe()를 호출하여 PushSubscription를 무효화합니다.

swRegistration.pushManager.getSubscription()
.then(function(subscription) {
  if (subscription) {
    // TODO: Tell application server to delete subscription
    return subscription.unsubscribe();
  }
})
.catch(function(error) {
  console.log('Error unsubscribing', error);
})

unsubscribe()를 호출하면 완료하는 데 다소 시간이 걸릴 수 있으므로 프로미스가 반환됩니다. 체인의 다음 then()unsubscribe()가 완료될 때까지 기다리도록 이 프로미스를 반환합니다. unsubscribe() 호출 시 오류가 발생하는 경우 catch 핸들러를 추가합니다. 그런 다음 UI를 업데이트할 수 있습니다.

.then(function() {
  updateSubscriptionOnServer(null);

  console.log('User is unsubscribed.');
  isSubscribed = false;

  updateBtn();
})

사용해 보기

웹 앱에서 Enable Push Messaging 또는 Disable Push Messaging을 누를 수 있어야 하며, 로그에는 사용자의 구독 여부가 표시됩니다.

81a07119235b53da.png

11. 완료됨

축하합니다. 이 Codelab을 완료했습니다.

이 Codelab에서는 웹 앱에 푸시 알림을 추가하여 설정하고 실행하는 방법을 알아봤습니다. 웹 알림으로 할 수 있는 작업에 대해 자세히 알아보려면 이 문서를 확인하세요.

사이트에 푸시 알림을 배포하고 싶다면 구형 브라우저 또는 GCM을 사용하는 표준 비호환 브라우저에 대한 지원을 추가하는 것이 좋습니다. 자세히 알아보기

추가 자료

관련 블로그 게시물