👉 시작하면서
웹 푸시 알림(Web Push Notification)이란, 말 그대로 브라우저 환경에서 푸시 알림을 받을 수 있는 기술을 의미한다.
푸시 알림이라고 하면 네이티브 앱의 전유물이라고 느낄 수 있지만, 웹 푸시 기술을 통해 웹에서도 푸시 알림 기능을 구현하고 사용할 수 있다.
브라우저에 구현되어있는 Push API와 자세한 스펙은 아래 문서에서 확인해볼 수 있다.
본 포스팅에서는 웹 푸시가 동작하는 메커니즘에 대해 조금 세부적으로 살펴보고, 직접 데모 코드를 살펴보며 웹 푸시에 대한 전반적인 이해를 할 수 있도록 진행할 예정이다.
분량을 조절하기 위해 모든 API 기능에 대해 설명하진 않으니 자세한 내용은 글 중간중간에 추가해둔 참조 링크(문서)를 참고하길 바란다.
본 포스팅에서 다루는 대부분의 내용은 과거에 출간했던 저서의 내용과 동일하며, PWA에 관심이 있다면 한 번 살펴보면 좋을 것 같다.
🍎 애플과 웹 푸시
안드로이드 및 Chromium 기반 브라우저 등에서는 꽤 오래전부터 웹 푸시를 지원해왔기 때문에 웹에서 푸시 알림을 제공할 수 있었으나, 애플의 Safari 브라우저는 이를 지원하지 않았다.
사실 본인은 애플이 PWA를 내다 버린 줄 알았다.
애플은 항상 폐쇄적인 모습을 보이는데, 웹에게 너무 많은 기능을 제공하여 네이티브 앱과의 경계를 허물리 없다고 생각했다.
역시나 애플은 구글과 마이크로소프트 등의 웹 생태계를 주도적으로 이끌어가는 조직과 다르게 PWA 관련 기술(푸시 알림 포함)에 대해 적극적으로 대응하지 않았고, 그 결과로 Safari 브라우저에서만 지원하지 않는 API가 여럿 생기게 되었다.
현 상황에서 macOS 그리고 iOS와 iPadOS 유저들이 사용하는 Safari 브라우저의 점유율은 결코 무시할 수 없는 수준이기에, 웹(PWA)이 힘을 얻고 영향력을 펼치려면 Safari 브라우저의 적극적인 PWA 지원이 필요한 상황이었다.
PWA에 대해 관심이 많았던 본인은, WWDC20부터 웹 푸시에 대한 내용을 기다려왔으나 항상 기대했던 것과는 다른 내용만 얻었기에 다소 아쉬움이 많았다. 그러던 중 최근 진행되었던 애플 개발자 행사인 WWDC22의 Meet Web Push for Safari 세션에서 드디어 웹 푸시에 관한 내용이 언급되었다.
Web Push is supported in Mac Safari beginning with macOS Ventura. And Web Push will be coming to iOS and iPadOS next year.
이번에 발표한 내용에 따르면 웹 푸시를 지원하는 사파리 버전은 아래와 같다.
- Safari 16 (macOS 13, Ventura 부터)
- iOS 및 iPadOS는 내년(2023년)부터 지원
다행인 점은 애플 독자적인 웹 푸시가 아니라, 다른 브라우저들에서 구현되어있는 웹 푸시 표준을 따른다고 한다. (애플의 복잡한 인증서 없이도 간단히 푸시 알림을 제공할 수 있다)
아직 MDN Web Docs의 웹 푸시 API 호환성 표가 업데이트되지 않았는데, 곧 업데이트될 것 같고 내년에는 iOS Safari 에도 체크 아이콘이 생기게 될 것으로 보인다.
조만간 위 호환성 표 속의 Safari X 아이콘은 역사 속으로 사라질 것이다.
어쨌든, 애플이 포기한 줄만 알았던 웹 푸시를 지원해준다고 하니 앞으로의 웹 기술이 어떻게 변화할지 기대가 된다.
👀 웹 푸시 동작 살펴보기
웹 푸시가 전반적으로 어떻게 동작하는지 메커니즘에 대해 살펴보도록 하자.
🚀 풀(Pull)과 푸시(Push)
먼저 풀(Pull)과 푸시(Push)의 차이를 아래 그림을 보며 살펴보자.
풀(Pull)은 대부분의 애플리케이션에서 쉽게 살펴볼 수 있는 동작 방식이다.
클라이언트 기준으로 보았을 때, 풀(Pull) 단어의 의미와 동일하게 데이터를 당겨오는 모습을 볼 수 있다.
서버에 있는 특정 데이터를 받기 위해 먼저 요청하고, 그 결과를 돌려받는 형태를 의미한다.
푸시(Push)는 풀과 동작하는 모습이 조금 다르다.
서버에서 클라이언트로 데이터를 밀어 넣는(Push) 모습을 볼 수 있으며, 클라이언트가 먼저 요청을 보내지 않더라도 서버가 데이터를 밀어 넣어주는 형태이다.
👨👩👧👦 웹 푸시의 구성 요소
웹 푸시가 동작하기 위해서는 기본적으로 필요한 구성 요소가 있다.
- 브라우저 (사용자)
- 푸시 알림을 발송할 서버
- 푸시 알림을 사용자에게 전달할 푸시 서비스
크게 보면 총 3가지로 이루어져 있다.
서버에서 어떤 사용자에게, 어떤 메시지를 보낼지에 대한 내용이 담긴 메시지 데이터를 푸시 서비스로 전달하면, 푸시 서비스에서 사용자 정보를 식별한 후 목적지로 전달한다.
웹 푸시이기 때문에 여기서 이야기하는 목적지는 브라우저가 된다.
어떤 요소로 이루어져 있고, 대략 어떤 흐름인지 알 것만 같은데 밑줄로 강조한 부분에 대해선 의문점이 생겼을 것이다.
이 부분을 알기 위해서는 웹 푸시 기능을 제공하기 위해 정해진 규약. 즉, 웹 푸시 프로토콜(Web Push Protocol)에 대해 알아야 한다.
🤙 웹 푸시 프로토콜
웹 푸시 프로토콜(Web Push Protocol)은 푸시 알림을 수신하는 브라우저와 발송하는 서버가 푸시 서비스와 상호작용하기 위해 정해놓은 규약이다.
클라이언트(브라우저) 정보를 등록하고, 서버에서 메시지를 발송하고, 최종적으로 사용자에게 도달하기까지 여러 절차를 거치게 된다.
절차에 대한 부분은 아래 그림을 통해 간략히 살펴볼 수 있다.
먼저 푸시를 수신하게 될 클라이언트(브라우저)에서는 푸시 서비스로 내가 누군지 알린다.
이 과정을 구독(Subscription)이라고 하며 조금 더 자세한 절차는 아래 그림과 같다.
푸시 서비스로 구독 요청을 보내고, 성공적으로 구독된 경우 푸시 서비스에서는 구독 정보를 브라우저에게 돌려준다.
구독 정보에는 브라우저를 식별하는 정보가 담겨있으며, 서버에서 특정 브라우저(사용자)에게 푸시 메시지를 발송할 때 이 정보가 필요하다.
구독 정보는 서버에 저장해두었다가, 푸시 알림 발송이 필요할 때 꺼내서 사용하는 형태가 된다.
여기까지의 과정이 마무리되었다면, 브라우저가 푸시 알림을 수신하기 위한 과정은 모두 끝났다.
이제 서버에서 푸시 서비스로 구독 정보와 메시지를 전달하면 해당하는 브라우저로 푸시 알림이 전달될 것이다.
🔐 안전한 메시지를 위한 VAPID
우리가 놓치지 말아야 할 부분은 서버에서 푸시 메시지를 전달할 때 그냥 전달하지 않는다는 점이다. 푸시 메시지는 민감하기 때문에 안전하게 암호화되어야 하고, 메시지가 어떤 서버에서 발송되었는지 알 수 있어야 한다.
웹 푸시에서는 어떤 서버에서 메시지를 발송했는지 식별하기 위해 VAPID(Voluntray Application Server Identification) 인증 방식을 사용한다.
VAPID는 자발적으로 서버(푸시 메시지를 발송하는)를 식별하는 인증 방식이다.
VAPID는 간단하게 보면 공개키 암호화 방식의 키 쌍으로 검증 절차를 거치는데, 먼저 공개키 암호화 방식에 대해 간략히 알아보자.
공개키 암호화는 비공개 키(Private Key)와 공개 키(Public Key) 쌍을 갖는다.
공개키 암호화의 경우 비공개 키와 공개 키의 값이 일치하지 않는 비대칭 암호화 방식이다.
암호화 과정이 있다면 복호화도 필요한 법인데, 공개 키로 암호화했다면 비공개 키로 복호화해야 하고, 비공개 키로 암호화했다면 공개 키로 복호화해야 한다.
이러한 특성으로 인해 전자 서명이나 인증이 필요한 부분에서 널리 사용되고 있는 암호화 기법이다.
다시 본 내용으로 돌아와서, VAPID도 공개키 암호화 방식의 키 쌍으로 이루어져 있다.
서버에서 푸시 서비스로 메시지를 전달할 때 VAPID 명세에 따른 정보가 담긴 JWT(JSON Web Token)을 함께 전달하게 되는데, 이때 이 토큰을 VAPID의 비공개 키로 서명(암호화)한다.
서명된 토큰은 반대로 푸시 서비스에서 공개 키로 복호화하여 유효성을 검증하게 된다.
이러한 절차를 통해 푸시 서비스에서 어떤 서버로부터 수신한 메시지인지, 유효한 메시지인지 검증할 수 있게 된다.
지금까지 살펴본 내용을 모아 보면 아래와 같은 흐름으로 나타낼 수 있다.
- (1) 브라우저에서 푸시 서비스 구독 + VAPID 공개 키 전달
- (2) 구독 정보 수신
- (3) 구독 정보 서버로 전달
- (4) 구독 정보 + 메시지 + VAPID 비공개 키로 암호화된 JWT
- (5) VAPID 공개 키로 유효성 검증 & 검증된 경우 구독 정보에 해당하는 브라우저로 푸시 메시지 발송
4, 5번 과정에서 데이터가 위조되거나, 키가 일치하지 않는 경우 푸시 메시지는 유효하지 않은 상태가 되어 브라우저까지 도달하지 못하고 푸시 서비스에서 폐기된다.
웹 푸시의 전체적인 흐름은 지금까지 살펴본 것과 같다.
복잡하게 느껴질 수도 있지만 코드를 작성하다보면 훨씬 쉽게 와닿을 것이다.
🔥 웹 푸시 구현하기
원활한 진행을 위해 웹 푸시 데모 코드를 준비하였다.
아래 깃허브 주소에서 코드를 다운로드 받고, 코드와 동작을 확인해보자.
https://github.com/leegeunhyeok/web-push
README 파일에 정리되어있는 내용대로 환경을 세팅하도록 하자.
준비가 되었다면 데모 서버를 실행시킨 후 페이지에 접속하여 다음 내용들을 하나씩 살펴보자.
웹 푸시 기능은 웹 워커 중 한 종류인 서비스 워커(Servier Worker)를 통해 구현할 수 있으며, 브라우저의 백그라운드 환경에서 처리된다.
(서비스 워커에 대한 자세한 내용은 추후 작성할 예정이다)
모던 브라우저(이젠 Safari 포함)에는 웹 푸시 알림을 제공하기 위한 API들이 구현되어있다.
푸시 서비스를 구독하고, 메시지를 수신하기 위한 Push API, 그리고 알림을 사용자에게 노출시키기 위한 Notification API가 있다.
이에 대한 부분을 간략히 살펴보고 지나가도록 하자.
🙋 Notification API
브라우저에서 시스템 알림을 띄우기 위한 기능을 제공한다. 우리는 이를 통해 여러 플랫폼(윈도우, 맥, 리눅스, 안드로이드, iOS)에 맞는 시스템 알림을 쉽게 띄울 수 있다.
Notification API를 통해 수행 가능한 대표적인 기능은 아래와 같다.
- 알림 권한 요청하기
- 알림 권한 상태
- 알림 띄우기
(색상으로 강조한 부분에 대해서만 간단히 살펴본다)
알림 권한은 아래와 같이 쉽게 파악할 수 있다.
Notification.permission; // 'default', 'denied', 'granted'
기본 상태는 default이며, 아직 브라우저에서 사용자에게 권한을 물어보지 않은 상태를 의미한다.
브라우저에서 사용자에게 권한을 물어본 후 사용자가 선택한 동작에 따라 두 가지 상태로 나눠진다.
사용자가 거부한 경우 denied, 허가한 경우 granted가 되며, 이 상태에서는 더 이상 권한 변경이 불가하다. (사용자가 직접 브라우저 설정을 통해 변경해야 한다.)
권한이 허가 상태인 경우 알림을 띄울 수 있게 된다. 알림을 띄우는 방법은 크게 두 가지가 있는데 알림 객체를 생성하는 것과 서비스 워커의 showNotification 메소드를 호출하는 것이다. 여기서는 후자 방식에 대해 알아본다.
먼저 알림을 띄우는 코드를 살펴보자.
registration.showNotification(title, {
body: 'Some message',
});
서비스 워커 등록 객체에는 알림을 띄울 수 있는 showNotification 메소드가 구현되어있다.
첫 번째 인자로는 알림의 제목, 두 번째 인자로는 알림의 본문 그리고 기타 옵션들이 전달된다.
알림 권한이 부여(granted)되어있다면, 정상적으로 시스템 알림이 표시될 것이다.
🔔 Push API
Push API는 푸시 서비스로부터 전송된 메시지를 수신할 수 있는 기능을 제공한다.
이를 통해 사용자가 푸시 알림을 수신하고, 웹 애플리케이션에 재참여할 수 있도록 기반 기능을 제공한다.
웹 애플리케이션이 푸시 메시지를 수신하기 위해서는 활성화 되어있는 서비스 워커가 필요하다.
서비스 워커는 브라우저 백그라운드 환경에서 동작하고, 푸시 메시지를 수신했을 때 이벤트 핸들러를 통해 수신한 메시지를 처리할 수 있다.
Push API의 주요 기능을 나열해보면 아래와 같다.
- 푸시 서비스 구독 및 해지
- 푸시 메시지 수신
⚙️ 서비스 워커 등록하기
푸시를 수신하기에 앞서 서비스 워커 등록이 필요하다. 서비스 워커는 다른 웹 워커와 마찬가지로 워커 스크립트를 로드하여 설치 & 활성화시킬 수 있다.
async function registerServiceWorker () {
if (!('serviceWorker' in navigator)) return;
// 이미 등록되어있는 정보 가져오기
let registration = await navigator.serviceWorker.getRegistration();
if (!registration) {
// 없으면 서비스 워커 등록
registration = await navigator.serviceWorker.register('/service-worker.js');
}
}
navigator 객체에 serviceWorker가 존재하는지 확인하여 현재 브라우저가 서비스 워커를 지원하는지 확인할 수 있다.
존재하지 않을 경우 기능 사용이 불가하니 분기 처리가 반드시 필요하다.
navigator.serviceWorker.getRegistration을 통해 서비스 워커의 등록 객체를 가져올 수 있다.
아직 서비스 워커가 등록되어있지 않다면 null을 반환하기 때문에 값의 유무를 통해 기존 등록 객체를 사용할지, 새로 서비스 워커를 등록할지 결정할 수 있다.
서비스 워커 등록은 navigator.serviceWorker.register를 통해 진행할 수 있다. 첫 번째 인자에는 서비스 워커 스크립트가 위치한 경로를 문자열로 전달하면 되고, 다운로드와 설치 과정은 브라우저에서 알아서 처리한다.
🌝 푸시 서비스 구독하기
서비스 워커가 등록되었다면 등록 객체를 통해 푸시 매니저(Push Manager)에 접근할 수 있다.
registration.pushManager;
서비스 워커 등록 객체에 pushManager가 없다면, 푸시 알림을 지원하지 않는 브라우저이니 이 또한 조건을 통해 분기 처리가 가능하다.
앞서 살펴보았던 내용 중 푸시 알림이 동작하는 흐름을 되짚어보면, 첫 번째로 클라이언트(브라우저)에서 푸시 서비스를 구독했던 것을 기억할 것이다.
푸시 서비스 구독은 푸시 매니저를 통해 처리 가능하고, 코드로 구현하자면 아래와 같다.
const subscription = await registration.pushManager.subscribe({
applicationServerKey: VAPID_PUBLIC_KEY,
userVisibleOnly: true,
});
푸시 매니저에 구현되어있는 subscribe 메소드를 통해 푸시 서비스에게 구독 요청을 보낼 수 있고, 이 과정에서 알림을 수신하기 위해 브라우저가 사용자에게 권한을 확인하는 절차를 수행하게 된다.
구독 요청을 위해 subscribe를 호출할 경우 반드시 사용자 제스처가 필요한데, 이는 개발자가 악의적으로 푸시 서비스에 구독하는 것을 방지하기 위함이다. 즉, 반드시 사용자가 어떠한 행위를 해야 구독 요청이 허용된다. (예: 버튼 클릭 등)
구독 요청을 보내기 위해서는 서버를 식별하기 위한 VAPID 공개키(applicationServerKey)와, 푸시 메시지가 사용자에게 보이는 용도로 사용된다는 것을 의미하는 플래그 값(userVisibleOnly)이 필요하다.
데모 코드 환경을 준비하면서 VAPID 키 쌍을 생성했을텐데 default.json에 정상적으로 넣어두었다면, API를 통해 공개키를 받아온 후 applicationServerKey 프로퍼티 값으로 사용될 것이다.
userVisibleOnly 값은 반드시 true 이어야 하고, 수신한 푸시 메시지는 알림을 통해 반드시 사용자에게 보여주어야 함을 의미한다.
정상적으로 구독 되었다면, 구독 정보를 받아볼 수 있다.
구독 정보는 아래와 같은 형태로 구성되어 있으며, 서버에서는 푸시 메시지를 발송할 때 해당 구독 정보를 함께 전달하게 된다.
{
"endpoint": "https://web.push.apple.com/QHnG_a...BVMI",
"keys": {
"p256dh": "BC56...kSB9-Vcq8",
"auth": "fz8...sA"
}
}
데모 코드를 살펴보면 아래와 같이 구독 정보를 서버로 전달하는 코드를 확인할 수 있다.
async function postSubscription (subscription?: PushSubscription) {
...
const response = await fetch('/subscription', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ userId, subscription }),
});
}
데모 코드에서는 전달한 구독 정보를 사용자 ID와 매핑하여 서버 측에 저장해두게 된다.
🌚 푸시 서비스 구독 취소하기
푸시 서비스 구독 취소는 더 간단하게 구현할 수 있다.
푸시 서비스 구독 후 전달받은 구독 정보에 구현되어있는 unsubscribe 메소드를 호출하기만 하면 된다.
const unsubscribed = await subscription.unsubscribe();
Promise 결과 값으로는 취소 성공 여부를 boolean 값으로 반환한다.
데모 코드에서는 구독을 취소한 경우 서버에도 이 사실을 알려 저장해두었던 사용자의 구독 정보를 함께 제거하도록 구현되어있다.
✉️ 푸시 메시지 보내고 & 받기
푸시 메시지를 보내기 위해서는, 서버에서 웹 푸시 프로토콜에 따라 푸시 서비스로 메시지를 전달해야 한다.
직접 구현한다면 상당히 복잡할 텐데, 다행히 웹 푸시 프로토콜 표준에 맞게 구현되어있는 여러 라이브러리들이 존재한다.
데모 코드에서는 web-push 라이브러리를 사용하여 구현했으니 참고 바란다.
서버 쪽에서 푸시 메시지를 어떻게 보내고 있는지 코드를 확인해보자.
import webpush from 'web-push';
webpush.setGCMAPIKey(GCM_KEY);
webpush.setVapidDetails(
SUBJECT,
VAPID_PUBLIC,
VAPID_PRIVATE
);
webpush.sendNotification(subscription, JSON.stringify({
title: 'Web Push | Getting Started',
body: message || '(Empty message)',
}));
서버 코드를 살펴보면 위와 같은 코드가 존재하는데, web-push 라이브러리를 통해 아주 간단히 발송 코드를 구현할 수 있다.
푸시 알림을 발송하기 전에 기본적인 구성(GCM 키 및 VAPID 키 등록)을 진행하고, 라이브러리 수준에 구현되어있는 sendNotification 메소드를 호출하여 푸시 메시지를 발송한다.
발송할 때, 구독 정보와 메시지 데이터를 함께 전달하는 것을 확인할 수 있다.
발송한 푸시 메시지는 푸시 서비스를 거쳐 목적지인 브라우저에 도달하게 된다.
브라우저에 푸시 메시지가 도달한 경우 서비스 워커의 push 이벤트를 통해 이를 처리할 수 있는데, 서비스 워커 코드를 잠시 살펴보도록 하자.
self.addEventListener('push', (event: PushEvent) => {
const message = event.data?.json() as PushMessage;
event.waitUntil(
self.registration.showNotification(message.title, {
body: message.body,
})
);
});
self.addEventListener('notificationclick', (event: NotificationEvent) => {
self.clients.openWindow('https://github.com/leegeunhyeok/web-push');
});
서비스 워커 코드의 일부분을 가져왔다.
여기서 확인할 부분은 push 이벤트 핸들러 부분이다. push 이벤트는 푸시 서비스로부터 메시지를 수신했을 때 트리거 되며, 브라우저가 닫혀있더라도 서비스 워커에서 이를 수신하여 처리할 수 있다.
self.registration 부분은 서비스 워커 등록 객체를 의미하며, showNotification은 앞서 살펴보았던 Notification API의 기능 중 하나이다. 즉, 해당 코드는 이벤트 객체에서 수신한 메시지 데이터를 꺼내온 후 알림을 띄우는 코드라고 볼 수 있다.
아래의 notificationclick 이벤트는 이벤트 명에서 유추할 수 있듯이 사용자가 알림을 눌렀을 때 트리거되는 이벤트이다.
사용자가 푸시 알림을 수신하고, 이를 클릭했을 때 어떠한 동작을 처리해야 할 때 해당 이벤트 핸들러에서 로직을 구현하면 된다.
(예: 푸시 알림 클릭 시 특정 웹 페이지 열어주기)
⭐️ 푸시 알림 동작 확인해보기
지금까지 핵심 기능과 코드를 살펴보았다.
푸시 서비스를 구독하고, 실제로 푸시 메시지를 받아 시스템 알림이 뜨는지 전반적으로 확인해보자.
데모 서버를 실행하고 페이지에 접속하면, 로그인 화면으로 이동할 것이다. 여기에서 사용자를 식별하기 위해 임의의 아이디를 입력하고 홈 화면으로 이동하자.
유저 ID를 safari_user로 입력한 후 이동한 홈 화면 모습이다.
각 섹션에 대해 간략히 소개하도록 하겠다.
- Status: 서비스 워커 등록 여부, 푸시 알림 지원 여부, 푸시 알림 권한 그리도 구독 정보를 확인할 수 있다.
- Subscribe: 푸시 서비스 구독 & 해지
- Send Push Notification: 특정 사용자 ID로 푸시 메시지 전송
- Logout: 로그인 화면으로 이동
먼저 푸시 서비스 구독을 위해 Subscribe 버튼을 눌러보자. 그러면 아래와 같이 알림 권한을 물어볼 것이다. 동작을 확인할 것이기 때문에 허용해주도록 하자.
(만약 거부하거나 취소한 경우 권한은 거부(denied) 상태가 되는데, 브라우저 설정으로 이동하여 권한을 재설정한 후 페이지를 새로 고침 하면 된다)
정상적으로 구독된 경우 아래와 같이 구독 정보를 Status 섹션에서 확인할 수 있다. 현재 띄워져 있는 브라우저는 푸시 알림을 수신할 수 있다.
Send Push Notification 섹션에서 Target User ID 부분에 현재 본인의 아이디를 입력한 후 Send 버튼을 눌러보자.
서버를 통해 푸시 서비스로 메시지가 전달되고, 푸시 서비스에서 본인 브라우저로 푸시 메시지를 전달하여 알림이 노출될 것이다.
데모를 통해 본인에게 푸시 메시지를 전송하거나, 다른 사용자에게 메시지를 전송할 수도 있다.
다른 사용자에게 푸시 메시지를 전송하는 과정은 아래 영상을 참고하면 좋을 것 같다.
지금까지 웹 푸시에 대해 전반적으로 살펴보았다.
네이티브에서만 구현 가능하던 푸시 알림을 웹 환경에서 구현하고, 사용자에게 기능을 제공할 수 있다는 점은 웹이라는 플랫폼에 큰 무기를 쥐어준 것이라고 생각한다.
이번에 알아본 푸시 알림 뿐만 아니라 결제 요청, 생체 인증, 파일 시스템 등 여러 가지 API 사양들이 탄생하고 구현되고 있는 가운데, 앞으로의 웹이 어떻게 변화할지 더욱더 궁금해진다.
References