SlideShare a Scribd company logo
eBrain Lab
정 병 태
Service Worker 를 이용한 

Offline Web Application 구현
Speaker
정 병 태 eBrain Lab
- Web Developer

- OKKY.KR Contributor

- https://github.com/1angerhans
- http://okky.kr/user/info/26138
- btjung@ebrain.kr
내용
• Service Worker
• Service Worker 란?
• Service Worker 로 할 수 있는(있을) 것들
• 지원 현황
• 기본 사용 방법
• Offline Web Application 구현하기
• Cache 를 이용한 서비스 구현
• ‘Offline 우선’ 서비스 구현
Service Worker
Service Worker 란?
• Offline 상태에서, 웹 어플리케이션의 준비 단계를 가로채는 방법
을 제공하고, 지속적인 background 처리를 지원.
• 문서 및 관련 자원에 대한 설치, 버전, 업그레이드를 관리 할 수
있는 event-driven Web Worker.
• 웹 플랫폼을 확장 하는 다른 사양들의 background 처리에 대한
시작점 역할.
출처 : https://slightlyoff.github.io/ServiceWorker/spec/service_worker/
Service Worker 란?
• Network Proxy : 응답을 가로채고, 조작이 가능
• Dom Thread 와는 별개의 Thread 로 동작하므로,

Dom 및 Global Scope (window) 접근은 불가능함.
• postMessage 를 이용한 양방향 데이터 교환 가능.
• Page 단위로 동작되는 것이 아니기 때문에, register 이후에는
Background 에서 동작 함. (새 탭을 열어도 동일한 Worker 이용)
• Service Worker Thread 는 사용되지 않을 때는 종료되고, 필요할
때 재시작 되기 때문에 전역상태를 유지할 수 없음.
Service Worker 란?
Server
HTTP/Cache
Page
Server
HTTP/Cache
Page
ServiceWorker Cache API
•Offline Cache
•Task Scheduler
•Background Sync
•Push notification
•Geofencing
할 수 있는(있을) 것들
Q: 그건 그렇고, IE 에서 돼?
A: ……
Chrome (40+)
Opera (28+)
Firefox (Nightly Build)
Internet Explorer (지원 고려중)
Sapari (지원 안함)
지원 현황
출처 : https://jakearchibald.github.io/isserviceworkerready/
Service Worker
기본 사용 방법
Requirements
•HTTPS

: 중간자공격(Man-in-the-middle Attack) 방지

* localhost 에서는 http로 가능
•Cache API Polyfill

: https://github.com/coonsta/cache-polyfill
•Promise

: 비동기 작업에 대해 순차적인 프로그래밍을 할 수 있도록 도와주는 패턴.
if('serviceWorker' in navigator) {

navigator.serviceWorker.register(‘/service-worker.js’,{
scope: ‘/'
}).then(function() {

console.log('Service worker resisted');

}).catch(function(error) {

console.log('Service worker resistration faild');

});

}
register
• Service Worker 를 최초로 등록
• Origin relativity
scope: ‘/‘
service-worker.js
self.addEventListener('install', function(event) {

console.log('Service worker installed');

});
self.addEventListener('active', function(event) {

console.log(‘Service worker activated’);

});



self.addEventListener('fetch', function(event) {

console.log(‘Fetching…’);

});
• Service Worker 의 동작을 intall / active / fetch Event 등 를 통
해 정의
Offline Web Application
잠깐
Q: 지금 우리 AppCache 무시하셈?
A: 명세를 다 꽤차고 계시다면,
계속 AppCache를 쓰셔도 좋습니다.
아니면	 읽고	 또	 읽으세요.

피눈물	 날	 때까지.
출처 : https://www.youtube.com/watch?t=119&v=4uQMl7mFB6g
•Manifest - 동작을 완벽히 이해하기 위해서는 복잡한 명세를 숙달
•온라인 상태에서도 항상 캐시에서 호출
• 캐시 되지 않은 리소스는 캐시된 페이지에서 로드 되지 않음

: http://appcache-demo.s3-website-us-east-1.amazonaws.com/without-network/
•캐시는 Manifest 를 변경할 때만 업데이트 됨
•Manifest 에 대한 캐시 문제
AppCache 의 한계
참고 : Application Cache is a Douchebag - by Jake Archibald
Cache를 이용한 서비스 구현
index.html
<!DOCTYPE html>

<html>

<head lang="en">

<meta charset="UTF-8">

<title></title>

<link rel="stylesheet" href=“/assets/app.css">

<script src="/assets/jquery-1.11.2.min.js"></script>

<script src="/assets/app.js"></script>

</head>

<body>

<div id="feeds"></div>

</body>

</html>
register
if('serviceWorker' in navigator) {

navigator.serviceWorker.register('/sw.js', 

{scope: '/'})

.then(function() {

console.log('Service worker resisted');

})

.catch(function(error) {

console.log('Service worker faild');

});

}
app.js



var getFeeds = function() {

return $.get('data/feeds.json');

};



var setPage = function(feeds) {

return $.Deferred(function(deferred) {



$('#feeds').html('');



$(feeds).each(function(i, feed) {

// View 처리

});



return deferred.resolve();

});

};



getFeeds()

.done(setPage);
sw.js - install
importScripts(‘/assets/serviceworker-cache-polyfill.js');



var CACHE_NAME = 'cache-v1';



self.version = 1;



self.addEventListener('install', function(event) {

event.waitUntil(

caches.open(CACHE_NAME)

.then(function(cache) {

return cache.addAll([

'/',

'/index.html',

'/assets/jquery-1.11.2.min.js',

'/assets/app.js',

'/assets/app.css',

‘/data/feeds.json’

]);

})

);

});
sw.js - fetch
self.addEventListener('fetch', function(event) {

event.respondWith(

caches.match(event.request).then(function(response) {

return response;

})

);

});
Cache 동기화 / Offline 우선
서비스 구현
app.js



var getFeeds = function() {

return $.get('data/feeds.json');

};



var setPage = function(feeds) {

return $.Deferred(function(deferred) {



$('#feeds').html('');



$(feeds).each(function(i, feed) {

// View 처리

});



return deferred.resolve();

});

};



var showingLiveFeeds = false;


getFeeds()

.done(function() {

showingLiveFeeds = true;

})

.done(setPage);
app.js
var getCachedFeeds = function() {

return $.Deferred(function (deferred) {

if('serviceWorker' in navigator) {

$.ajax('data/feeds.json',{

headers: {'x-use-cache': 'true'}

}).then(function (data) {

deferred.resolve(data);

});

} else {

deferred.reject();

}

});

};



getCachedFeeds()

.done(function(feeds) {

if(!showingLiveFeeds) {

return setPage(feeds)

}

});
sw.js - install
importScripts(‘/assets/serviceworker-cache-polyfill.js');



var CACHE_NAME = 'cache-v1';



self.version = 1;



self.addEventListener('install', function(event) {

event.waitUntil(

caches.open(CACHE_NAME)

.then(function(cache) {

return cache.addAll([

'/',

'/index.html',

'/assets/jquery-1.11.2.min.js',

'/assets/app.js',

'/assets/app.css',

‘/data/feeds.json’

]);

})

);

});
sw.js - fetch
var feedsResponse = function(request) {

if(request.headers.has('x-use-cache')) {

return caches.match(request).then(function(response) {

console.log(response);

return response;

});

} else {

return fetch(request.clone()).then(function(response) {

console.log(response);

if(response.status == 200) {

return caches.delete(request).then(function () {

return caches.open(CACHE_NAME)

}).then(function (feedsCache) {

feedsCache.add("/data/feeds.json");

return response;

});

} else {

return Promise.reject(Error('400'));

}

});

}

};
sw.js - fetch
self.addEventListener('fetch', function(event) {

var requestURL = new URL(event.request.url);

if(requestURL.pathname.indexOf("/data/feeds.json") > -1) {

event.respondWith(feedsResponse(event.request));

} else {

event.respondWith(

caches.match(event.request).then(function(response) {

return response;

})

);

}

});
참고 자료
• Service Worker Specification

https://slightlyoff.github.io/ServiceWorker/spec/service_worker/
• ServiceWorker.org

http://www.serviceworker.org
• Introduction to Service Worker (HTML5ROCKS)

http://www.html5rocks.com/ko/tutorials/service-worker/introduction/
• Application Cache is a Douchebag

http://alistapart.com/article/application-cache-is-a-douchebag

감사합니다.

More Related Content

PDF
Front-end Development with Ruby on Rails
PDF
Service workers 기초 및 활용 (Korean)
PDF
현실적 PWA
PDF
웹 Front-End 실무 이야기
PDF
AngularJS In Production
PDF
iOS9 소개
PDF
모바일 무한 스크롤 개발
PPTX
Vue.js와 Firebase를 활용한 웹 서비스 개발
Front-end Development with Ruby on Rails
Service workers 기초 및 활용 (Korean)
현실적 PWA
웹 Front-End 실무 이야기
AngularJS In Production
iOS9 소개
모바일 무한 스크롤 개발
Vue.js와 Firebase를 활용한 웹 서비스 개발

What's hot (20)

PPTX
[JCO 컨퍼런스] 웹사이트 Front-End 성능 최적화
PDF
도구를 활용한 더 나은 웹 개발: Yeoman
PDF
프로그레시브 웹앱(Pwa)
PDF
응답하라 반응형웹 - 4. angular
PPTX
[125]react로개발자2명이플랫폼4개를서비스하는이야기 심상민
PDF
Hybrid App Platform - HyWAI 3.5
PDF
실시간으로 안드로이드 프론트엔드 작업하기
PDF
Vue SSR vs Prerender
PDF
[D2 오픈세미나]3.web view hybridapp
PPT
자동화된 인프라구축 - 2009년 자료
PPTX
Single-page Application
PDF
FCGI, C++로 Restful 서버 개발
PDF
Python server-101
PPSX
Gulp 입문
PPTX
[123] electron 김성훈
PDF
HTML5로 만드는 데스크탑 어플리케이션 (Node-Webkit)
PDF
캠프앱 개발 사례를 통해 본 하이브리드앱 어디까지 | Devon 2012
PDF
오늘 당장 시작하는 HTML5
PDF
Electron mainprocess
PPTX
Deview2013 track1 session7
[JCO 컨퍼런스] 웹사이트 Front-End 성능 최적화
도구를 활용한 더 나은 웹 개발: Yeoman
프로그레시브 웹앱(Pwa)
응답하라 반응형웹 - 4. angular
[125]react로개발자2명이플랫폼4개를서비스하는이야기 심상민
Hybrid App Platform - HyWAI 3.5
실시간으로 안드로이드 프론트엔드 작업하기
Vue SSR vs Prerender
[D2 오픈세미나]3.web view hybridapp
자동화된 인프라구축 - 2009년 자료
Single-page Application
FCGI, C++로 Restful 서버 개발
Python server-101
Gulp 입문
[123] electron 김성훈
HTML5로 만드는 데스크탑 어플리케이션 (Node-Webkit)
캠프앱 개발 사례를 통해 본 하이브리드앱 어디까지 | Devon 2012
오늘 당장 시작하는 HTML5
Electron mainprocess
Deview2013 track1 session7
Ad

Viewers also liked (20)

PDF
Service Worker 101 (한국어)
KEY
Classroom 2.0: The Best Places to Learn Online
PPT
1. introduction (2)
PDF
20120111 두번째단추 마스터클래스_소셜네트워크게임_나영환_sng_contents at life
PPTX
자기소개용
PPT
Web Design Trends & Best Practices (by Blueliner Marketing CEO, Arman Rousta)
PPT
Trends in advertising and Public Relation
PDF
[2014널리세미나] 장애 환경의 온라인 체험 및 활용 방법
PDF
"Service Worker: Let Your Web App Feel Like a Native "
PDF
Apple Spectation 한방에 끝내기
PPTX
Apple iTV - Korean
PDF
Self Introduction
PPTX
svp_self_intro_jihyeonbyeon
PPTX
Restful web service
PPTX
[2016널리세미나] 남의 문제 나의 문제 우리의 문제
PDF
Google’s PRPL Web development pattern
PDF
[2014널리세미나] 접근성 빅(?) 데이터, 새로운 법칙의 발견!
PPTX
SharePoint User Experience Best Practices
PDF
Storyboarding : Workshop for TMC|X
PDF
[122] line on apple watch
Service Worker 101 (한국어)
Classroom 2.0: The Best Places to Learn Online
1. introduction (2)
20120111 두번째단추 마스터클래스_소셜네트워크게임_나영환_sng_contents at life
자기소개용
Web Design Trends & Best Practices (by Blueliner Marketing CEO, Arman Rousta)
Trends in advertising and Public Relation
[2014널리세미나] 장애 환경의 온라인 체험 및 활용 방법
"Service Worker: Let Your Web App Feel Like a Native "
Apple Spectation 한방에 끝내기
Apple iTV - Korean
Self Introduction
svp_self_intro_jihyeonbyeon
Restful web service
[2016널리세미나] 남의 문제 나의 문제 우리의 문제
Google’s PRPL Web development pattern
[2014널리세미나] 접근성 빅(?) 데이터, 새로운 법칙의 발견!
SharePoint User Experience Best Practices
Storyboarding : Workshop for TMC|X
[122] line on apple watch
Ad

Similar to Service Worker 를 이용한 
Offline Web Application 구현 (20)

PDF
Progressive Web Apps
PDF
Progressive Web App(PWA) 테코톡 발표자료 - 마르코(장원석)
PDF
PWA로 출퇴근 기록 시스템 개발하기 (2019 HTML5 Conf Seoul)
PDF
PWA 파헤치기
PPTX
PWA - overview [written in KOREAN]
PDF
[제14회 JCO 컨퍼런스] 개발자를 위한 서버이중화 by JAVACAFE
PDF
Service Worker 201 (한국어)
PDF
Accelerate spring boot application with apache ignite
PDF
PWA (Progressive Web Apps)
PPTX
Web workers
PPTX
7. html5 api
PDF
PWA를 활용한 Daily Check In - 주니어 프론트 개발자가 혼자 웹앱 만든 썰
PPTX
HTML5의 web worker
PDF
[WhaTap DevOps Day] 세션 6 : 와탭랩스 DevOps 이야기
PPTX
[Azure bootcamp2017] Azure App Service로 서비스 탄탄하게 관리하기
PDF
T11-2 장기효_Progressive Web Apps - 미래가 아닌 현재
PDF
조은 - AMP PWA 101 [WSConf.Seoul.2017. Vol.2]
PPTX
파워서포트 가이드
PDF
서버학개론(백엔드 서버 개발자를 위한)
PDF
안정적인 서비스 운영 2013.08
Progressive Web Apps
Progressive Web App(PWA) 테코톡 발표자료 - 마르코(장원석)
PWA로 출퇴근 기록 시스템 개발하기 (2019 HTML5 Conf Seoul)
PWA 파헤치기
PWA - overview [written in KOREAN]
[제14회 JCO 컨퍼런스] 개발자를 위한 서버이중화 by JAVACAFE
Service Worker 201 (한국어)
Accelerate spring boot application with apache ignite
PWA (Progressive Web Apps)
Web workers
7. html5 api
PWA를 활용한 Daily Check In - 주니어 프론트 개발자가 혼자 웹앱 만든 썰
HTML5의 web worker
[WhaTap DevOps Day] 세션 6 : 와탭랩스 DevOps 이야기
[Azure bootcamp2017] Azure App Service로 서비스 탄탄하게 관리하기
T11-2 장기효_Progressive Web Apps - 미래가 아닌 현재
조은 - AMP PWA 101 [WSConf.Seoul.2017. Vol.2]
파워서포트 가이드
서버학개론(백엔드 서버 개발자를 위한)
안정적인 서비스 운영 2013.08

Service Worker 를 이용한 
Offline Web Application 구현

  • 1. eBrain Lab 정 병 태 Service Worker 를 이용한 
 Offline Web Application 구현
  • 2. Speaker 정 병 태 eBrain Lab - Web Developer
 - OKKY.KR Contributor
 - https://github.com/1angerhans - http://okky.kr/user/info/26138 - btjung@ebrain.kr
  • 3. 내용 • Service Worker • Service Worker 란? • Service Worker 로 할 수 있는(있을) 것들 • 지원 현황 • 기본 사용 방법 • Offline Web Application 구현하기 • Cache 를 이용한 서비스 구현 • ‘Offline 우선’ 서비스 구현
  • 5. Service Worker 란? • Offline 상태에서, 웹 어플리케이션의 준비 단계를 가로채는 방법 을 제공하고, 지속적인 background 처리를 지원. • 문서 및 관련 자원에 대한 설치, 버전, 업그레이드를 관리 할 수 있는 event-driven Web Worker. • 웹 플랫폼을 확장 하는 다른 사양들의 background 처리에 대한 시작점 역할. 출처 : https://slightlyoff.github.io/ServiceWorker/spec/service_worker/
  • 6. Service Worker 란? • Network Proxy : 응답을 가로채고, 조작이 가능 • Dom Thread 와는 별개의 Thread 로 동작하므로,
 Dom 및 Global Scope (window) 접근은 불가능함. • postMessage 를 이용한 양방향 데이터 교환 가능. • Page 단위로 동작되는 것이 아니기 때문에, register 이후에는 Background 에서 동작 함. (새 탭을 열어도 동일한 Worker 이용) • Service Worker Thread 는 사용되지 않을 때는 종료되고, 필요할 때 재시작 되기 때문에 전역상태를 유지할 수 없음.
  • 8. •Offline Cache •Task Scheduler •Background Sync •Push notification •Geofencing 할 수 있는(있을) 것들
  • 9. Q: 그건 그렇고, IE 에서 돼?
  • 11. Chrome (40+) Opera (28+) Firefox (Nightly Build) Internet Explorer (지원 고려중) Sapari (지원 안함) 지원 현황 출처 : https://jakearchibald.github.io/isserviceworkerready/
  • 13. Requirements •HTTPS
 : 중간자공격(Man-in-the-middle Attack) 방지
 * localhost 에서는 http로 가능 •Cache API Polyfill
 : https://github.com/coonsta/cache-polyfill •Promise
 : 비동기 작업에 대해 순차적인 프로그래밍을 할 수 있도록 도와주는 패턴.
  • 14. if('serviceWorker' in navigator) {
 navigator.serviceWorker.register(‘/service-worker.js’,{ scope: ‘/' }).then(function() {
 console.log('Service worker resisted');
 }).catch(function(error) {
 console.log('Service worker resistration faild');
 });
 } register • Service Worker 를 최초로 등록 • Origin relativity scope: ‘/‘
  • 15. service-worker.js self.addEventListener('install', function(event) {
 console.log('Service worker installed');
 }); self.addEventListener('active', function(event) {
 console.log(‘Service worker activated’);
 });
 
 self.addEventListener('fetch', function(event) {
 console.log(‘Fetching…’);
 }); • Service Worker 의 동작을 intall / active / fetch Event 등 를 통 해 정의
  • 18. Q: 지금 우리 AppCache 무시하셈?
  • 19. A: 명세를 다 꽤차고 계시다면, 계속 AppCache를 쓰셔도 좋습니다. 아니면 읽고 또 읽으세요.
 피눈물 날 때까지. 출처 : https://www.youtube.com/watch?t=119&v=4uQMl7mFB6g
  • 20. •Manifest - 동작을 완벽히 이해하기 위해서는 복잡한 명세를 숙달 •온라인 상태에서도 항상 캐시에서 호출 • 캐시 되지 않은 리소스는 캐시된 페이지에서 로드 되지 않음
 : http://appcache-demo.s3-website-us-east-1.amazonaws.com/without-network/ •캐시는 Manifest 를 변경할 때만 업데이트 됨 •Manifest 에 대한 캐시 문제 AppCache 의 한계 참고 : Application Cache is a Douchebag - by Jake Archibald
  • 22. index.html <!DOCTYPE html>
 <html>
 <head lang="en">
 <meta charset="UTF-8">
 <title></title>
 <link rel="stylesheet" href=“/assets/app.css">
 <script src="/assets/jquery-1.11.2.min.js"></script>
 <script src="/assets/app.js"></script>
 </head>
 <body>
 <div id="feeds"></div>
 </body>
 </html>
  • 23. register if('serviceWorker' in navigator) {
 navigator.serviceWorker.register('/sw.js', 
 {scope: '/'})
 .then(function() {
 console.log('Service worker resisted');
 })
 .catch(function(error) {
 console.log('Service worker faild');
 });
 }
  • 24. app.js
 
 var getFeeds = function() {
 return $.get('data/feeds.json');
 };
 
 var setPage = function(feeds) {
 return $.Deferred(function(deferred) {
 
 $('#feeds').html('');
 
 $(feeds).each(function(i, feed) {
 // View 처리
 });
 
 return deferred.resolve();
 });
 };
 
 getFeeds()
 .done(setPage);
  • 25. sw.js - install importScripts(‘/assets/serviceworker-cache-polyfill.js');
 
 var CACHE_NAME = 'cache-v1';
 
 self.version = 1;
 
 self.addEventListener('install', function(event) {
 event.waitUntil(
 caches.open(CACHE_NAME)
 .then(function(cache) {
 return cache.addAll([
 '/',
 '/index.html',
 '/assets/jquery-1.11.2.min.js',
 '/assets/app.js',
 '/assets/app.css',
 ‘/data/feeds.json’
 ]);
 })
 );
 });
  • 26. sw.js - fetch self.addEventListener('fetch', function(event) {
 event.respondWith(
 caches.match(event.request).then(function(response) {
 return response;
 })
 );
 });
  • 27. Cache 동기화 / Offline 우선 서비스 구현
  • 28. app.js
 
 var getFeeds = function() {
 return $.get('data/feeds.json');
 };
 
 var setPage = function(feeds) {
 return $.Deferred(function(deferred) {
 
 $('#feeds').html('');
 
 $(feeds).each(function(i, feed) {
 // View 처리
 });
 
 return deferred.resolve();
 });
 };
 
 var showingLiveFeeds = false; 
 getFeeds()
 .done(function() {
 showingLiveFeeds = true;
 })
 .done(setPage);
  • 29. app.js var getCachedFeeds = function() {
 return $.Deferred(function (deferred) {
 if('serviceWorker' in navigator) {
 $.ajax('data/feeds.json',{
 headers: {'x-use-cache': 'true'}
 }).then(function (data) {
 deferred.resolve(data);
 });
 } else {
 deferred.reject();
 }
 });
 };
 
 getCachedFeeds()
 .done(function(feeds) {
 if(!showingLiveFeeds) {
 return setPage(feeds)
 }
 });
  • 30. sw.js - install importScripts(‘/assets/serviceworker-cache-polyfill.js');
 
 var CACHE_NAME = 'cache-v1';
 
 self.version = 1;
 
 self.addEventListener('install', function(event) {
 event.waitUntil(
 caches.open(CACHE_NAME)
 .then(function(cache) {
 return cache.addAll([
 '/',
 '/index.html',
 '/assets/jquery-1.11.2.min.js',
 '/assets/app.js',
 '/assets/app.css',
 ‘/data/feeds.json’
 ]);
 })
 );
 });
  • 31. sw.js - fetch var feedsResponse = function(request) {
 if(request.headers.has('x-use-cache')) {
 return caches.match(request).then(function(response) {
 console.log(response);
 return response;
 });
 } else {
 return fetch(request.clone()).then(function(response) {
 console.log(response);
 if(response.status == 200) {
 return caches.delete(request).then(function () {
 return caches.open(CACHE_NAME)
 }).then(function (feedsCache) {
 feedsCache.add("/data/feeds.json");
 return response;
 });
 } else {
 return Promise.reject(Error('400'));
 }
 });
 }
 };
  • 32. sw.js - fetch self.addEventListener('fetch', function(event) {
 var requestURL = new URL(event.request.url);
 if(requestURL.pathname.indexOf("/data/feeds.json") > -1) {
 event.respondWith(feedsResponse(event.request));
 } else {
 event.respondWith(
 caches.match(event.request).then(function(response) {
 return response;
 })
 );
 }
 });
  • 33. 참고 자료 • Service Worker Specification
 https://slightlyoff.github.io/ServiceWorker/spec/service_worker/ • ServiceWorker.org
 http://www.serviceworker.org • Introduction to Service Worker (HTML5ROCKS)
 http://www.html5rocks.com/ko/tutorials/service-worker/introduction/ • Application Cache is a Douchebag
 http://alistapart.com/article/application-cache-is-a-douchebag