AppCache(Application Cache)를 적용할 때, 가끔씩 석연치 않은 동작이 발견될 때가 있다는 제보가 들어왔습니다.

하여, AppCache를 사용할 때의 유의점 또는 자체적인 문제가 있는 지를 알아보겠습니다.



AppCache?

AppCache는 웹어플리케이션을 오프라인에서 동작할 수 있도록 지원하기 위해서 개발된 메카니즘입니다.

이 기술을 적용함으로써 다음과 같은 이점을 가질 수 있습니다.


오프라인 브라우징: 사용자가 오프라인인 상태에서도 페이지를 전환할 수 있다.

속도: 캐시된 리소스는 로컬에 위치하며, 더욱 빠르게 로드된다.

서버 부하 감소: 서버에서 변경된 리소스만을 다운로드 한다.


자세한 내용은 아래의 링크를 참조하세요.

https://developer.mozilla.org/ko/docs/Web/HTML/Using_the_application_cache

https://developer.mozilla.org/en-US/docs/Web/HTML/Using_the_application_cache


여기에 중요한 메시지가 있는데, 영문 페이지에서만 보여집니다. -_-;;

그 내용은 아래와 같습니다.

Deprecated

This feature has been removed from the Web standards. Though some browsers may still support it, it is in the process of being dropped. Avoid using it and update existing code if possible; see the compatibility table at the bottom of this page to guide your decision. Be aware that this feature may cease to work at any time.

AppCache는 웹표준에서 삭제되었으며, 아직까지는 많은 browser가 이를 지원하지만 점차 퇴출될 것이다.


영문 위키피디아에서 AppCache를 찾아보니, 대신 Service worker를 사용하라고 권고하고 있네요.

https://en.wikipedia.org/wiki/Cache_manifest_in_HTML5


AppCache는 어떤 문제가 있을까요?


재미있게도 Service worker guide 문서에서 잘 정리된 내용을 찾을 수 있었습니다.

AppCache의 디자인이 단일페이지에서만 특히 잘 동작하고 복수의 페이지를 가진 환경에서 훌륭하게 동작하지 않으며, 실제로 많은 문제를 가지고 있다고 합니다. 문제에 대한 내용은 link를 참조하세요. (#)



Service worker?

오프라인을 통제할 수 있는 권한을 개발자에게 부여하여 오프라인 환경을 지원할 수 있도록 해주는 API 입니다.

chrome browser를 통해 google, youtube, facebook이 notification을 띄우는 녀석의 정체가 드러났습니다. :-)


Service worker는 DOM에 직접 접근할 수 없습니다.

Service worker를 사용하기 위해서는 Promise에 대한 이해가 필수입니다.

또한 개발 중에 localhost를 통해서 사용할 수는 있으나, 이를 사용하기 위해서는 서버에 https 설정이 필요합니다.


Service worker 등록

if ('serviceWorker' in navigator) {
window.addEventListener('load', function() {
navigator.serviceWorker.register('/sw.js').then(function(registration) {
// Registration was successful
console.log('ServiceWorker registration successful with scope: ', registration.scope);
}).catch(function(err) {
// registration failed :(
console.log('ServiceWorker registration failed: ', err);
});
});
}


serviceWorker를 모든 browser가 지원하는 것이 아니기 때문에, 우선 지원여부를 판단해야 합니다.

mobile 쪽 browser별 지원 상황을 보면, 현재는 chrome, fireFox만 지원하고 있습니다.

https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API


지원하는 browser라면 별도의 파일(sw.js)에 선언한 내용을 service worker로 등록합니다.

그 뒤 등록 결과를 log로 출력합니다.


설치 (Offline cache 예제)

var CACHE_NAME = 'my-site-cache-v1';
var urlsToCache = [
'/static_pages/greeting.html',
'/assets/test.jpg'
];

self.addEventListener('install', function (event) {
// Perform install steps
event.waitUntil(
caches.open(CACHE_NAME)
.then(function (cache) {
console.log('설치완료');
return cache.addAll(urlsToCache);
}).catch(function () {
console.log('설치실패')
})
);
});


urlsToCache에 선언한 resource를 cache에 등록합니다.

특정 이름의 cache에 url array를 등록하는 절차를 거칩니다.

이름이 필요한 이유는 향후 업데이트 등에서 활용하기 위함입니다.

이를테면, 더 이상 cache할 필요가 없는 url에 대해서 삭제를 하는 등의 활용이 있겠죠.



Service worker의 문제

개발되고 있는 최신 기술이기 때문에 아직 문제가 있다고 합니다.

지원하지 않는 browser가 있다는 것이 가장 큰 문제일 것이며, 설치 실패 시 처리가 매끄럽지 않은 부분이 있을 수 있습니다.

또한 https 환경이 아닌 경우 적용할 수 없다는 점도 AppCache의 대용으로 사용하기에는 조금 문제가 있는 부분입니다.


처참한 mobile browser 지원 현황



Service worker 관리

위의 예제를 테스트하면 service worker가 browser에 등록됩니다.

등록된 service workers는 chrome의 개발자 도구에서 관리할 수 있습니다.



Application tab의 Service workers sub tab을 click하면 목록이 나타나며,

등록한 항목을 찾아 Unregister 버튼을 클릭하는 것으로 삭제할 수 있습니다.



더 자세한 내용은 공식 guide를 참조하시길 바랍니다.

https://developers.google.com/web/fundamentals/getting-started/primers/service-workers?hl=ko



References

AppCache

https://developer.mozilla.org/ko/docs/Web/HTML/Using_the_application_cache

https://developer.mozilla.org/en-US/docs/Web/HTML/Using_the_application_cache

https://msdn.microsoft.com/ko-kr/library/hh673545(v=vs.85).aspx

http://b.mytears.org/2010/09/2272

https://mytory.net/archives/12616

http://blog.jamesdbloom.com/ProblemsWithApplicationCache.html

https://en.wikipedia.org/wiki/Cache_manifest_in_HTML5


ServiceWorker

https://developers.google.com/web/fundamentals/getting-started/primers/service-workers?hl=ko

https://vnthf.github.io/blog/serviceworker/



'Javascript' 카테고리의 다른 글

Ionic 2 Unit test  (0) 2017.07.31
Implement base controller  (0) 2017.07.26
npm lite-server  (0) 2017.07.06
Calculate distance between two geolocations.  (1) 2017.06.28

Client의 View(html, css) 및 Controller(javascript)를 browser를 간단하게 browser를 통해서 확인하려면 어떻게 할까요?

WAS를 설치하고 파일을 복사하고 web browser를 실행시켜서 url로 접속??

이건 정석적이지만, 절차가 너무 번거롭습니다.


혹은 file:// protocol을 이용하는 것도 하나의 방법입니다.

다만, 이 경우에는 localhost의 특수성을 이용할 수 없습니다.

이를테면 registration of worker service, navigator.geolocation 등은 https protocol인 경우 또는 localhost인 경우에만 정상적으로 동작합니다.



이를 어떻게 손쉽게 테스트할 수 있을까 고민하게 됩니다.

그리고 npm에서 그 해결책을 발견했습니다.


npm은 정말 보물창고이네요. :)


이러한 목적에 부합하는 lite-server라는 module이 있습니다.


lite-server? (#)

공식적인 소개 문구는 다음과 같습니다.

"Lightweight development only node server that serves a web app, opens it in the browser, refreshes when html or javascript change, injects CSS changes using sockets, and has a fallback page when a route is not found."

웹앱을 브라우저에서 열거나 html 또는 javascript의 변경을 감지하고, socket을 이용하여 css변경을 주입하는 등의 기능을 제공하는 가벼운 노드 서버입니다. 그리고, route를 찾을 수 없는 경우에는 fallback페이지를 출력합니다.


local NPM install

설치: npm install lite-server --save-dev


global installation

설치: npm install -g lite-server

실행: lite-server


command를 실행하는 folder의 ./index.html 파일을 launch합니다.


References

https://www.npmjs.com/package/lite-server

'Javascript' 카테고리의 다른 글

Ionic 2 Unit test  (0) 2017.07.31
Implement base controller  (0) 2017.07.26
Service workers.  (0) 2017.07.06
Calculate distance between two geolocations.  (1) 2017.06.28

Code coverage 측면에서 가능한 모든 코드가 test 되어야겠지만, 사실 여러 가지 요인으로 이 수준을 달성하기가 쉽지 않습니다.

결국은 코드의 중요도를 매기고 그에 따라 선별적으로 TDD 적용을 시작하는 것이 합리적이라고 생각합니다.


그에 따라 AngularJs의 framework에서는 contoller보다는 service 부분이 더욱 TDD가 필요한 부분이라는 생각이 듭니다.

이 글에서는 AngularJs의 service를 테스트하는 방법을 알아 보겠습니다.


우선은 테스트할 대상이 되는 service가 있어야 겠지요.

현재 위치를 확인하는 용도로 gpsService를 만들어 보겠습니다.

getCurrentPosition function은 단말의 GPS 센서를 통해 위치를 가져오거나 navigator.geolocation을 통해서 위치를 가져오도록 구현할 예정입니다.

(참고로 후자의 경우는 protocol이 https가 아니라면 그 요청이 fail처리됩니다.)



gpsService


var app = angular.module('app', []);
app.service('gpsService', ['$q', 'nativeService', function ($q, nativeService) {
this.getCurrentPosition = function () {
var deferred = $q.defer();

// native call.
if (nativeService.isNative()) {
// check
nativeService.getCurrentPosition()
.then(function (position) {
deferred.resolve(new Position(position.lat, position.long);
}, function (fail) {
deferred.reject(fail); // turned off.
});
} else {
var geo = navigator.geolocation;
if (geo) {
try {
geo.getCurrentPosition(function (position) {
var coords = position.coords;
var position = new Position(coords.latitude, coords.longitude);
deferred.resolve(position);
}, function (fail) {
deferred.reject(fail.message);
});
} catch (err) {
deferred.reject(err);
}
} else {
deferred.reject('can\'t detect location via web.');
};
};
return deferred.promise;
};
}]);


promise를 활용하기 위해서 $q를, KEMP native를 연동하기 위하여 nativeService를 참조했습니다.

promise는 async call을 효과적으로 지원하기 위한 spec으로써, 자세한 내용은 link를 참고하시길 바랍니다. (#)

요점만 정리하면 async call에 대해서 성공한 요청은 resolve, 실패한 요청은 reject으로 그리고 기타 알림은 notify로 응답을 전달합니다.

(promise, deferred에 대해서 정말 잘 설명이 되어 있습니다.)


이 서비스는 nativeService를 이용할 수 있는 경우 (즉, service app이 device에서 수행되는 경우)에는 GPS 센서를 통해서 위치를 취득합니다.

그렇지 않은 경우에는 navigator.geolocation을 통해서 위치를 취득합니다.

위치 취득에 성공하는 경우 resolve를 반환하며 반대로 실패하는 경우에는 reject를 반환할 것입니다.

resolve로 전달하는 인자는 Position이라는 별도의 class로 포장하여 일원화하였습니다.



test code


describe('app.gpsService', function () {
// const
var timeout = 800;

// variable
var $rootScope;
var $q;
var gpsService;
var createService;


beforeEach(module('app'));
beforeEach(inject(function (_$rootScope_, _$q_) {
$rootScope = _$rootScope_;
$q = _$q_;
}));
beforeEach(inject(function ($injector) {
createService = function () {
return $injector.get('gpsService');
};
createMockService = function () {
nativeService = $injector.get('nativeService');
nativeService.isNative = function () { return true; };
nativeService.getCurrentPosition = function () {
var deferred = $q.defer();
deferred.resolve(new Position(37.4137701, 127.1284930));
return deferred.promise;
};
return $injector.get('gpsService');
};
}));
it('GPS서비스의 getCurrentPosition이 응답하는지 확인(device가 아닌 경우)', function (done) {
gpsService = createService();
var position, failreason;
var promise = gpsService.getCurrentPosition();
promise.then(function (_position) {
position = _position;
}, function (err) {
failreason = err;
});

setTimeout(function () {
$rootScope.$apply();
if (position) {
expect(position).toBeTruthy();
} else {
expect(failreason).toBeTruthy();
};
console.log('position', position);
console.log('failreaonse', failreason);
done();
}, timeout);
});
it('GPS서비스의 getCurrentPosition이 응답하는지 확인(device인 경우)', function (done) {
gpsService = createMockService();
var promise = gpsService.getCurrentPosition();
var position, failreason;
promise.then(function (_position) {
position = _position;
}, function (err) {
failreason = err;
});
setTimeout(function () {
$rootScope.$apply();
if (position) {
expect(position).toBeTruthy();
} else {
expect(failreason).toBeTruthy();
};
done();
}, timeout);
});
});
});


코드가 좀 기네요...

서비스의 코드량을 가뿐히 능가하고 있습니다. -_-;;


자아, 숨을 가다듬고 천천히 살펴봅시다.


describe는 진입점이라고 생각하시면 됩니다. 두 번째 인자인 function이 실제 역할을 수행하는 부분입니다.


beforeEach

beforEach는 각각의 test를 수행하기 전에 공통적으로 수행할 코드가 담기는 영역입니다.

테스트에 필요한 'app'을 로드하고 $rootScope, $q를 가져오는 기능을 하고 있습니다.


마지막으로 gpsService의 instance가 필요합니다.

이는 세 번째 beforeEach function을 참고하시길 바랍니다.

$injector를 인자로 넘김으로써 AngularJs의 $injector를 참조하고, 그 뒤에 $injector의 get function을 통해서 gpsService를 반환 받았습니다.


it

it의 첫 번째 인자는 test에 대한 설명 구문이며 두 번째 인자가 실제 test코드가 있는 function입니다.

이 function에 선택적으로 전달되는 done 인자는 비동기 function을 테스트하기 위해서 받는 인자이며, 비동기 function의 동작이 마무리 된 이후에 done()을 호출함으로써 테스트를 완료할 수 있습니다.


두 번째 test를 보면 createService() 가 아닌 createMockService()를 통해서 service의 instance를 가져옵니다.

MockService는 테스트를 위해 설정한 natvieService를 반환함으로써 실제 device인 척 가장하고 테스트를 할 수 있게 해 줍니다.

이는 mock을 이용한 테스트 기법입니다.


또한, jasmine을 통해 async function을 테스트하기 위해서는 $rootScope.$apply()을 호출할 필요가 있습니다.

그렇지 않은 경우에는 undefined가 반환됩니다.

왜 $rootScope.$apply()를 호출해야 하는 가에 대해서는 link를 참고하시길 바랍니다. (#)



Conclusion

gpsService의 최초 구조는 위와 같지 않았습니다. 좀 더 많은 function이 있었습니다.

그러나 TDD를 통해서 gpsService를 테스트하면서 gpsService를 참조하는 코드를 작성하면서 복잡함과 불편함을 느끼게 되었고,

이를 통해서 gpsService자체를 refactoring하게 되었습니다.

이 과정에서 nativeService를 분리하였고, @Justin이 그 구현을 도와줌으로써 일에 대한 효율적인 분배도 이뤄낼 수 있었습니다.


잦은 refactoring이 TDD의 핵심입니다.

단순 code coverage만이 목적이라면 개발하기 전에 test를 하라는 개발방법론은 의미가 없습니다.


잦은 refactoring의 원동력은 해당 module을 사용하는 입장이 되어 코드를 작성한다는 점입니다.

Test code는 또한 실제 module을 이용하는 개발자가 참고할 수 있는 훌륭한 sample code가 됩니다.


이렇게 해서 개발한 gpsService는 nativeService와 훌륭하게 연계하여, quickOn POC에 단번에 녹아들어 훌륭하게 동작하였습니다.

이것은 하루를 기분 좋게 만드는 정말로 좋은 경험이었습니다. :)



References

Jasmine expect's matchers => https://jasmine.github.io/api/2.6/matchers.html

inject custom service => https://nathanleclaire.com/blog/2014/04/12/unit-testing-services-in-angularjs-for-fun-and-for-profit/

=> https://coderwall.com/p/l4fvmq/injecting-custom-services-in-an-angularjs-unit-test

get currentPosition => https://developer.mozilla.org/ko/docs/Web/API/Geolocation/getCurrentPosition

setTimeout() method => https://www.w3schools.com/jsref/met_win_settimeout.asp

unit test promise $q => http://www.bradoncode.com/blog/2015/07/13/unit-test-promises-angualrjs-q/

AngularJs $q => https://docs.angularjs.org/api/ng/service/$q

promise controller example code => http://jsfiddle.net/jsengel/8fzmqy4y/

promise? deferred? => http://webframeworks.kr/tutorials/angularjs/angularjs_promise_deferred/

'Dev pattern' 카테고리의 다른 글

[펌] 자바스크립트 코딩 컨벤션 가이드  (0) 2017.10.16
WPF Developement  (0) 2017.07.21
Jasmine getting started to test javascript (Angular)  (5) 2017.05.26

+ Recent posts