유럽 배낭여행에서 느낀 Offline Web App 의 무한한 성장가능성

10월 한 달간 유럽 배낭여행을 하면서 느낀 Offline Service 에 대해서 생각을 정리해보았습니다.

  • 초당 10MB 가 넘는 데이터를 무리 없이 제공하는 한국에 있다 보니 자연스럽게 인터넷이 느린 사용자의 입장에서 생각을 하지 않고, 한국의 빠른 인터넷 속도 기준으로 어플리케이션을 제작해왔다.
  • Software 분야에서는 한국이 결코 후진국이라는 말이 있고, 나 또한 그렇게 믿고 있었지만, 내가 경험하고 본 유럽의 현황은 사실 우리나라 보다 더 뒤처져 있는 것 같았다.
  • 느린 통신사 data 망과 길거리에서 찾아보기 힘든 Public Wifi 는 현지인들이 지하철에서 핸드폰보다는 책이나 신문을 더 보게 하고, 관광지에 놀러 온 여행객들은 Google Trips 처럼 offline 에서도 작동하는 앱을 사용하기보다, 언제나 그래왔듯이 꾸준히 지도를 펼쳐가며 방향을 잡고 주민들에게 물어보며 장소를 찾아가는데 한 몫을 한다.
  • 물론, Google Trips 앱이 나온 지가 얼마 되지 않았고, 사람들에게는 Google Map 보다 관광 지도로 보는 것이 더 편하고 익숙하겠지만. 대다수를 위한 서비스를 하기 위해서는, 그 대다수가 선호하는 방식을 알고 있어야 하고, 그 패턴을 분석해서 보다 더 나은 서비스를 제공할 수 있는 통찰력이 필요하다는 생각이 들었다. 그리고 이러한 안목으로 서비스를 개발해 나가는 것이 세상을 더 편리하게 하는 Software Engineer 가 함양해야 할 자세가 아닐까?
  • 어플리케이션 성능을 최대한 최적화 해서 빠른 사용자 경험을 제공한다고 하더라도, 인터넷이 느린 국가에서는 사실 그렇게 의미가 많지 않은 작업이라고 생각이 든다. 그렇기 때문에 구글에서도 Progressive Web App 이라는 새로운 Web Application 개념으로, 보다 더 많은 사용자들이 Offline Service 로 혜택을 볼 수 있게끔, Web 기술을 발전시켜 나가고 있는 게 아닐까 라는 생각이 들었다.
  • Offline Web Service Era 가 도래할만한 충분한 배경과 현실이 있는 세상이다. Offline Service 부분에 좀 더 집중해서 나처럼 배낭 여행하는 여행객들과 현지인들이 편하게 여행하고, 지하철에서도 충분히 자기가 원하는 서비스들을 offline 으로 제공받는데 도움이 되었으면 좋겠다.

To be continued after this backpacking trip..

Advertisements

TLS / SSL 소개 및 Tomcat 에 HTTPS 적용하는 방법

TLS / SSL 소개

  • TLS : Transport Layer Security
  • SSL : Secure Sockets Layer
  • 웹 브라우저와 웹 서버 간의 통신을 안전하게 보장하는 기술이다.
  • 데이터를 보낼 때 데이터를 암호화하고, 데이터를 받을 때 암호화된 데이터를 복호화 하는 식의 기본 구조를 갖는다.
  • 이 통신 구조는 양방향 이기 떄문에, 웹 브라우저와 웹 서버 모두 데이터를 내보낼 때 암호화를 진행한다.

TLS / SSL 인증

  • 웹 브라우저에서 암호화된 연결로 웹 서버와 통신하려고 하면, 웹 서버에서 브라우저에게 몇가지 증명서(Certificate)를 요구한다.
  • 이 증명서는 클라이언트 인증 (Client Authentication) 이라고 하며, 개인 사용자 간 보다는 B2B 형태에서 더 많이 사용된다.
  • 대부분의 SSL-enabled 웹 서버 는 클라이언트 인증을 요구하지 않는다.

Certicates 인증서

  • SSL 를 구현하려면 웹 서버에 Certificate이 있어야 하는데, 이 Certificate 은 암호화된 연결을 수용하는 외부 인터페이스를 보장해준다.
  • 이러한 SSL 설계 방식은 “서버에서 어떤 형태로든 내가 생각했던 상대방이 맞다” 라는 것을 서버에서 증명하는 것이다

따라서, Certificate 을 해당 인터넷 주소에 대한 Digital Passport 로 볼 수 있다.

  • Certificate 은 암호화되어 발급되는데, 최초 발급자 말고는 사실 발급하기가 매우 힘들다.
  • 방문자 브라우저에서 해당 사이트를 접근하면 보통 경고가 뜨는데, 이때 Certificate Authorities 라는 신뢰되는 3rd 파티에 서명하면 경고가 뜨지 않는다.
  • Java 에서는 상대적으로 단순한 keytool 커맨드라인 툴을 제공하고, 명령어로 self-signed Certificate 을 생성할 수 있다.
  • Self-signed Certificates 는 사용자가 간단하게 생성한 인증서이기 때문에 테스트 시나리오에 적합하고, 실제 상용제품에는 적합하지 않다.

SSL 구동시 팁

  • SSL 로 웹 사이트를 구성할 때는 모든 사이트 자원들울 SSL 로 제공해야 한다. 이렇게 하면 해커들이 자바스크립트 파일 등에 악성 컨텐츠를 넣을 수가 없어 보안을 우회할 수가 없다.
  • 웹 사이트의 보안을 강화하고 싶으면 HSTS header 를 사용하는 것도 고려해봐야 한다.
    • HSTS header : 브라우저에서 항상 HTTPS 통신으로 사이트를 접근하게 한다.

Tomcat 에서 인증서 설정하기

Certificate Keystore 준비하기

  • Tomcat 에서 수용하는 keystore 타입은 JKS, PKCS11 또는 PKCS12.
    • JKS : Java 표준 키스토어 형식. JDK 에 포함된 keytool 커맨드 라인툴로 생성 가능
    • PKCS12 : 인터넷 표준 형식. Open SSL 이나 Microsoft Key-Manager 로 조작 가능
  • keystore 의 목록들은 alias 의 문자열 값으로 구분할 수 있다. 대부분은 대소문자 구분을 하지 않지만, PKCS11 형식은 대소문자 구분을 한다.

  • OpenSSL 을 이용하여 당신의 CA로 서명된 Certificate 을 PKCS12 keystore 에 임포트하려면 아래 명령어를 사용한다.
    openssl pkcs12 -export -in mycert.crt -inkey mykey.key
    -out mycert.p12 -name tomcat -CAfile myCA.crt
    -caname root -chain
    
  • JKS keystore 를 새로 생성하려면 아래 명령어를 커맨드창에서 실행한다.
    Windows:
    "%JAVA_HOME%\bin\keytool" -genkey -alias tomcat -keyalg RSA
    
    Unix:
    $JAVA_HOME/bin/keytool -genkey -alias tomcat -keyalg RSA
    
  1. 위 명령어의 결과는 실행한 위치의 홈 디렉토리 아래에 .keystore 라는 이름의 새로운 파일을 생성한다.
  2. 만약 다른 위치나 파일이름을 지정하고 싶으면, -keystore 변수를 사용한다. (변경된 위치 또한 server.xml 에 반영해줘야 함)
    Windows:
    "%JAVA_HOME%\bin\keytool" -genkey -alias tomcat -keyalg RSA
    -keystore \path\to\my\keystore
    
    Unix:
    $JAVA_HOME/bin/keytool -genkey -alias tomcat -keyalg RSA
    -keystore /path/to/my/keystore
    
  3. 위 명령어를 실행하면 비밀번호를 입력하라는 창이 나오는 데 default 값은 changeit 으로 되어 있다. 원하는 비밀번호로 변경이 가능하고, 이 또한 server.xml 에 반영해줘야 한다.

  4. 비밀번호를 입력하고 나면, 회사 / 이름 등에 대한 일반적인 정보를 입력하라고 한다. 이 정보는 이후 사용자가 암호화된 페이지 접근시에 표시된다.
  5. 마지막으로, 인증서(Certificate) 에 대한 key password 를 입력한다. 별도로 비밀번호를 입력하지 않고 엔터를 치면 위 1번에서 사용한 keystore 의 암호와 동일하게 입력된다.

Tomcat Configuration 파일 변경하기

  • 톰캣은 SSL 통신 구현방법에는 JSSE, APR 2가지가 있다.
    • JSSE : Java 런타임에서 제공.
    • APR : OpenSSL 엔진을 기본값으로 사용.
  • 여기서 JSSE connector 를 정의하는 방법은

1

2

  • APR 을 사용하려면

3

  • Configuration 을 위한 최종 단계는 $CATALINA_BASE/conf/server.xmlConnector 를 설정하는 것이다.

4

  • 위 절차를 모두 마치면 브라우저에서 https://localhost:8443/ 를 입력했을 때 Tomcat 기본페이지를 볼 수 있다.

Legacy 웹 사이트에 Progressive Web App 적용하기 (Part I)

Why did I start to work on this project?

  • 배경 : 팀내에서 고객과의 질의응답 채널로 포탈 사이트를 운영하고 있다.
  • 현상 : 포탈 사이트가 개발된지 약 2년이 된 것 같은데 (소스를 보니 2014년에 만들어진 것으로 추정), 시대를 역행하는 Non Responsive Design 과 고객 Q&A에 Notification 기능이 없어 수시로 사이트 접속하여 게시글과 댓글을 모니터링 해야하는 불편함이 있었다.
  • 계획 : Mobile 에서도 간단한 게시글 확인 및 알람 기능을 추가하여 사용성을 늘리기로 결정했고, Progressive Web 의 몇가지 기능들을 넣어보기로 결정했다.

What is Progressive Web App?

  • 구글에서 최근에 밀어붙이는 웹앱으로 Responsive Design, App Like, Re-engagable, Installable, Safe, … 등의 특징과 함께 빠른 로딩과 높은 사용자 경험을 제공하는 것이 특징이다.
  • Mobile 에서만 지원되었던 Push 알림, Mobile Icon, 앱 설치 배너 등을 웹 어플리케이션에서 지원하기 때문에, 일반 웹 개발로 Mobile Application 의 Appy 한 느낌을 낼 수 있다.
  • 자세한 내용은 Google Web Fundamental 참고

Non Responsive to Responsive Design

  • 기존에 적용된 UI Framework 은 반응형이 지원되지 않는 Bootstrap 이다. 무슨 이유에서인지 모르겠으나 반응형 지원을 하지 않고, 철저히 PC 기준으로만 사이트를 제작하였다. 아마도, 모바일에서의 화면 Layout 까지 고려하기에는 시간이 빠듯했거나, 요구사항 자체가 Mobile 에서도 볼 수 있게 해달라는 요청이 없었기 때문인 것 같다.
  • Boostrap 외에도 jstree, jquery-ui, jquery-remodal, jquery-multifile, NHN Smart Editor 등의 외부 라이브러리들을 가져다가 화면을 구성해놓았다.

레거시 사이트 UI

1365 * 600 해상도 PC 화면

pc_login
PC 로그인 페이지
  • 그냥 로그인 기능만 되면 문제 없어보이는 UI…

 

pc_main
PC 게시판 목록
  • 전체적으로 균형은 잘 잡혀 있어 보인다.

 

pc_list
PC 트리 선택 후 게시판 이동
  • 왼쪽 트리를 펼쳐 게시판 목록을 펼친다.

 

pc_viewpost
PC 게시글 조회
  • 게시판 질의 응답 기능에만……. 충실한 화면 Layout 과 UI

iPhone 6 브라우저 화면

1
Mobile 로그인 페이지
  • 로그인 페이지는 그럭저럭 봐줄만 하다.

 

2
Mobile 게시판 목록
  • 모바일에서 보는 게시판 페이지는 거의 재앙에 가깝다.

 

3
페이지 확대해서 트리 선택
  • 페이지를 이동하려면 왼쪽 트리를 클릭해야 하는데, 페이지 확대를 해서 저 작은 리스트를 클릭해야 페이지 이동이 된다… 참 난감하다.

 

4
Mobile 에서 게시글 조회
  • 게시글 조회 시 게시글과 답변 모두 확대 하지 않고는 보기가 거의 불가능하다.

 

Re-designing UI & UX with Critical User Journeys

  • 지난 6월 Google Campus 에서 진행된 UI & UX 워크샵에서 배운 “Critical User Journeys” 기법을 이번 사이트에 적용 해보았다.
  • Critical User Journeys : 사용자의 입장에서 해당 서비스를 사용할 때 모든 동작에 대해 UI 와 Layout 을 고려하고 이를 기반으로 UI Design 을 가다듬는 것.
  • UI 개선을 위해 가장 첫 번째로 해야할 질문들 (The very first thing to do is ask these questions)
  1. Who is using your website? 누가 사이트를 사용하는가?
  2. When are they using their website? 사용자들은 사이트를 언제 사용하는가?
  3. Why are they using the website? 사용자들은 왜 사이트를 사용하는가?
  4. Where are they using the website? 사용자들이 사이트를 어디에서 사용하는가?
  • 사이트를 사용하는 유저 입장에서, 로그인 절차부터 게시판 및 기타 기능들을 사용할 때 까지 위의 질문들을 염두에 두고 다음과 같이 개편하였다.
  1. Mobile First Design
  2. 버튼에 아이콘을 추가하여 직관화
  3. 글쓰기 / 글읽기 / 답글달기 할 때 보여지는 기능들의 우선순위를 정하여 UI 컴포넌트 재배치
  4. 게시글 정보를 중요도 순으로 다시 배치하고 간결하게 표현
  5. 댓글에 자신의 사진을 표시하여 책임감 및 흥미 부여 (화장실에 담당자 사진 걸어놓는 것과 동일한 목적)
  6. 전체적인 Tone & Manner 를 맞추기 위해 기존 jsTree 라이브러리 제거 후 Collapsible 과 Collection 으로 트리 구현

개선된 사이트 UI

15인치 노트북 PC에 최적화된 화면

new_pc_login
PC 로그인 페이지
  • MaterializeCSS 의 기본 Login Template 을 적용했다.

 

new_pc_list
PC 게시글 목록
  • UI 의 전체적인 느낌을 통일하기 위해 왼쪽 jstree 라이브러리를 걷어내고, collipsible & collection 조합으로 왼쪽에 트리를 구현하였다.

 

new_pc_viewpost
PC 게시글 조회
  • 글 수정 / 삭제 / 목록 등의 버튼에 아이콘을 이용하여 직관적인 표시를 하였고, 답글과 댓글 영역을 명확히 분리하였다. 댓글 리스트는 우선순위 기준으로 정보를 재배치 및 계정에 프로필 사진을 추가하여, 댓글 구분이 쉽도록 하였다.

 

new_pc_delete
게시물 삭제시 권한 확인 팝
  • 기존 화면의 불필요한 영역을 차지하는 ID & PW 를, 게시글을 삭제할 때 확인하도록 변경했다.

 

iPhone 6 브라우저 화면

new_mobile_login
Mobile 로그인 페이지
  • PC Login 화면에서 크기만 작아진다.

 

new_mobile_main
Mobile 메인 페이지
  • 로그인 후 메인화면, 모바일에서는 폴더 트리를 Global Navigation 으로 뺐다.

 

new_mobile_list
Mobile 게시글 목록 페이지
  • 게시글 목록은 다음과 같이 표시된다. 테이블 cell 의 더 세부적인 css 스타일링으로 가독성을 높이는 작업이 더 필요한 것 같다.

 

new_mobile_tree
Mobile 네비게이션 바
  • Global Navigation Bar 를 이용하여 페이지 간 이동을 한다.

 

new_mobile_viewpost
Mobile 게시글 조회
  • Mobile 에서 유용하게 사용할만한 보기 & 댓글 기능에 주안점을 두고, 가독성을 높이는데 주력했다.

 

Back-End Service Analysis

기술 및 기법

  • 기존의 시스템은 Spring FW 3.4 로 구성이 되어 있었고, 다음과 같은 기술 및 기법 들을 쓰고 있었다.
  • Spring Security : 권한 관리를 위한 스프링 기술스택
  • Controller Annotation in bean.xml : base.package 에 지정된 패키지 안에 해당되는 모든 @Controller 에 대해 처리해준다.
  • ControllerAdvice Annotation in bean.xml : 위와 동일한 성격으로, 여기서는 Controller 에서 발생하는 에러 케이스 들에 대한 Exception 처리를 지정해주었다.
  • Multipart Resolver in beans.xml : 파일 또는 이미지를 Client 에서 Stream 방식으로 서버로 보낼 때, Stream 형식으로 받아 처리해줄 수 있는 Resolver
  • JDBC Template in applicationContext.xml : 스프링에서 DB 연결시 사용하는 전형적인 스프링 JDBC 연결방법
  • DataSource Transcation Manager in applicationContext.xml : 스프링에서 제공하는 다양한 Transaction Manager중의 하나
  • DBCP DataSource in applicationContext.xml : DB와 어플리케이션을 효율적으로 연결하는 커넥션 풀을 제공. 일정 커넥션을 유지하다가 필요하면 사용하고 반납하여 재사용하는 형태

문제점

  • 기존 레거시 시스템의 가장 큰 문제점은 특정 주기 간격으로, 서비스가 올라가 있는 Tomcat 을 주당 2회 정도 계속 재시작을 해줘야 했다.
  • 그 이유를 진단하기 위해 아래 쿼리를 넣었고
show status like `%connect%`;
  • 진단 결과는 다음과 같았다.

“페이지 마다 수행되는 쿼리에 대해서 커넥션이 닫히지 않고, 계속 커넥션 수가 누적된다.”

Back-End Service Refactoring

안티패턴 1

  • 위와 같은 문제점을 해결하기 위해 구현된 Spring 로직을 분석한 결과, 다음과 같은 안티패턴을 발견했다.
@Controller
public class BoardController extends ExceptionControllerAdvice {

private BoardService getBoardService() {
ApplicationContext context = new GenericXmlApplicationContext("applicationContext.xml");
return context.getBean("boardService", BoardService.class);
}

// ...
}
  • 위의 안티패턴은 해당 컨트롤러가 수행이 될 때 마다, ApplicationConext 를 매번 생성하여 메모리 낭비 및 DB 커넥션 수를 불필요하게 늘리게 된다.
  • 해결책 : ApplicationContext는 WAS (Web Application Server) 에 Spring Container가 올라가는 최초에만 생성되고, 이후에는 생성된 Context 를 공유하는 형식으로 바꿨다.
@Controller
public class BoardController extends ExceptionControllerAdvice {

@Autowired
private BoardService boardService; // Application Context 에서 생성된 BoardService 를 참조한다.

// ...
}

안티패턴 2

  • Controller.java 에서 해당 URL에 관한 로직을 처리 후 URL redirect 을 다음과 같이 한 안티패턴이 발견되었다.
@RequestMapping(value = "replyBoard", method = RequestMapping.POST)
public String replyBoard(
HttpSession session,
@RequestParam("folder") String folder,
@RequestParam("subCategory") String subCategory, ... ) throws UserException {
ModelAndView mv = new ModelAndView;
mv.addObject("folder",folder);
mv.addObject("subCategory",subCategory);
// ...

return "redirect:/viewPost?folder="+folder+"&subCategory="+subCategory;
}
  • ModelAndView 를 사용하여 redirect 를 할 경우에는 ModelAndView 에 추가된 Object 변수들이 redirect 시에 자동으로 붙지 않는다. 따라서, 이를 아래와 같이 Model model 을 이용하면,
@RequestMapping(value = "replyBoard", method = RequestMapping.POST)
public String replyBoard(
HttpSession session,
@RequestParam("folder") String folder,
@RequestParam("subCategory") String subCategory, Model model ) throws UserException {
model.addObject("folder",folder);
model.addObject("subCategory",subCategory);
// ...

return "redirect:/viewPost";
}

// URL 결과 : `viewPost?folder="folder"&subCategory="subCategory"`
  • 위처럼 Model 을 이용하여 View에 데이터를 넘겨주면, redirect 시에 자동으로 변수가 붙게되는 이점이 있다.

Progressive Web App Native Feature 적용은 Part 2 에서 이어집니다…

Web App Manifest 소개 및 사용법

왜 주목 받는가?

  • App Store 가 생긴 이후로 웹 개발자들은 어떻게 하면 Web Application 을 Mobile Application 의 느낌으로 보이게 할지 연구해왔다.
  • Apple, Chrome, IE (Microsoft) 등이 Web App Manifest 파일을 차례로 도입하면서, Mobile Icon과 Launch Screen (Splash Screen) 등을 Mobile 에 등록할 수 있게 하면서 Web Application 이 점점 Mobile Application 의 형태를 띄게 된다.

구현? 사용법?

  • Web App 에 Native 느낌을 더하기 위해 Apple 이 추가한 몇 가지 태그는 아래와 같다.

11

  • 그리고 이후에 Google 이 다음과 같은 태그를 추가한다.

22

33

Building Manifest File

  • 메니페스트 파일은 JSON 파일과 비슷한 형태를 갖고 있다.
{
"name": "Super Racer 2000",
"short_name": "Racer2K",
"icons": [{
"src": "icon/lowres.png",
"sizes": "64x64",
"type": "image/webp"
}, {
"src": "icon/hd_small.png",
"sizes": "64x64"
}, {
"src": "icon/hd_hi.png",
"sizes": "128x128",
"density": 2
}],
"scope": "/racer/",
"start_url": "/racer/start.html",
"display": "fullscreen",
"orientation": "landscape"
}
  • 위의 속성들을 설명하자면,
  • name : icon 에 표시되는 이름
  • short_name : Web Application 이름의 짧은 버전. 공간이 충분하지 않아 full name 이 나올 수 없을 때 사용된다.
  • start_url : 실행시에 시작되는 URL 주소
  • display : 앱이 어떤식으로 실행될지 정하는 속성 (옵션 : fullscreen, minimul-ui, standalone, browser)
  • orientation : 웹 어플리케이션의 화면 방향을 정의 (옵션 : any, landscape, portrait, …)

Icon

  • 아이콘은 앱을 표시하기 위한 이미지
  • 아이콘에는 앱 표시에 사용되는 여러가지의 이미지들의 특성이 포함되어 있다.
  • src : 이미지 위치를 가리킨다
  • type : 아이콘 파일 유형을 정한다
  • sizes : 이미지 크기를 정한다
  • density : 기기의 pixel density 에 맞춰 어떤 아이콘이 사용될지 정한다. (지정하지 않을 경우 default 값은 1.0)

Scope & Navigation Scope

  • Navigate outside the app : 앱 유효범위의 밖으로 이동하려고 하면 (a 태그 클릭시) 새로운 브라우저를 실행한다.
  • Navigate into the app : deep linking 이라고 불린다. 매니페스트 파일 내의 유효범위에 있는 URL로 이동하면, 앱 밖으로 벗어나지 않는다. 웹 페이지 뿐만 아니라 네이티브 앱에서도 웹 앱을 여는 것이 가능하다.

Lesson & Learned

  • iOS 에서 즐겨찾기로 추가한 아이콘으로 웹 앱을 실행하면, 웹 내에서 href 태그 접근 시 새로운 브라우저를 띄우면서 scope 이 바뀐다.
  • 해결책 : <a href="#"> 없앤다.

Reference

Understanding the Manifest for Web App

Critical Rendering Path

What is it?

  • The sequence of steps the browser goes through to render the page, to convert the HTML, CSS and Javascript into actual pixels on the screen.
  • Always measure first! and Optimize it!

Converting HTML To The DOM
htmlToDOM

  • Make a request to Server with HTML -> The HTML will be converted to DOM (The browser will be processing the HTML and building the DOM)
  • The specific rules
  1. Every time the browser meets a tag bracket, it automatically emits a token.
  2. e.g)
    makingAtoken
  3. This entire process is done by the tokenizer.
  4. While it’s being processed, another process is happening which is consuming the token to create nodes.

nodesRelationship

  • DOM : a tree structure that captures the content and properties of the HTML and all the relationships between the nodes.
  • How Google does optimize it? :
  • [Incremental HTML Delivery] Think about the Google Search Engine. The header renders first and the rest of the HTML based on your search query will be shown to the user. (Returning the partial HTML could be a really nice performance optimization)
  • StackTrace in Timeline :
    timeline
  • (1) Send Request –> (2) Receive Response -> (3) Receive Data -> (4) Finish Loading -> (5) Parse HTML (Request CSS, Javascript and Images)

The CSSOM

Render Tree

  • Combine above those two DOM tree and CSSOM into the render tree.
  • Render tree only captures visible contents
  • How does it work?

Layout

  • To know where and how all the elements are positioned on the page
  • “ setting the width as your device width. e.g) device width=320px is width=320px

CRP workflow

  1. Request HTML Resources
  2. Receive Data
  3. Parse HTML (Converting the received Bytes to DOM tree)
  4. Request CSS / Images / Javascript
  5. Contruct CSS Object Model
  6. Recalculate Style (Layout : Build Render Tree (Computing all the styles for the visible contents)
  7. Layout (Compute the location and the size of the render tree elements)
  8. Paint (Render the page on screen)

Service Worker Introduction II

Service Worker 실행하기 위한 환경

  • 지원되는 브라우저 : Chrome 46 ↑, Firefox, Opera, Safari (지원예정)
  • 브라우저 지원에 대한 상세한 내용은 여기 참고
  • HTTPS 통신이 가능한 서버에서만 동작한다 (테스트를 위한 localhost 제외)

Service Worker 등록하기

  • 아래 코드처럼 javascript로 서비스워커를 등록한다.
if ('serviceWorker' in navigator) {
  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);
  });
}
  • 위 코드를 살펴보면, 먼저 서비스워커 API 존재 유무를 파악한다.
  • 존재할 시에 /sw.js 파일을 등록한다.
  • 한가지 알아둘 것은, 페이지가 로딩될 때 마다 register() API를 호출해도 된다. 브라우저가 서비스워커 실행 유무를 판단하여 알아서 처리하기 떄문이다.
  • 서비스워커 파일은 도메인 루트에 위치한다. 예를 들어, 도메인이 /example/sw.js 인 경우, /example/로 시작하는 모든 도메인에 대하여 서비스워커가 실행된다.
  • 크롬브라우저 주소창에 chrome://inspect /#service-workers 입력하면 서비스워커 콘솔을 사용할 수 있다.
  • 등록된 서비스워커는 chrome://serviceworker-internals 을 통해 확인 및 관리 할 수 있다.

Service Worker 설치하기

  • 위 등록 절차를 거쳤다면, 이젠 서비스워커에서 사용할 자원들을 설치할 차례다.
  • 아래 코드를 이용하여 어떤 파일들을 캐싱할 것인지 결정한다.
self.addEventListener('install', function(event) {
  // Perform install steps
});
  • install 콜백 함수 안에 다음 3가지 순서를 추가한다.
    1. [열기] cache 열기
    2. [캐싱] 사용할 파일들 캐싱하기
    3. [확인] 해당 파일들이 모두 캐싱 되었는지 확인
var CACHE_NAME = 'my-site-cache-v1';
var urlsToCache = [
  '/',
  '/styles/main.css',
  '/script/main.js'
];

self.addEventListener('install', function(event) {
  // Perform install steps
  event.waitUntil(
    caches.open(CACHE_NAME)
      .then(function(cache) {
        console.log('Opened cache');
        return cache.addAll(urlsToCache);
      })
  );
});
  • caches.open()에 정의한 캐쉬명 변수를 이용하여 캐쉬 사용을 시작한다.
  • 다음 cache.addAll()를 이용하여 array 안에 선언한 필요파일들을 모두 캐싱한다.
  • event.waitUntil() 메서드로 설치가 얼마나 오래 걸리던 간에, 설치 후에 해당 이벤트를 수행할 수 있도록 한다. (Javascript Promise 사용됨)
  • 결론적으로, 모든 파일들이 성공적으로 캐싱되면 서비스워커가 정상적으로 설치된다.
  • 주의할 점은, 여기서 한 개의 파일이라도 캐싱에 실패할 경우 인스톨 전체의 프로세스가 종료된다는 것이다. 따라서 신중하게 캐싱할 파일 리스트를 정한다.

Cache 와 Return 요청

  • 서비스워커 설치까지 완료했다면, 이제 캐쉬된 결과를 받아볼 차례다
  • 설치 완료후에 페이지 이동이나 갱신을 하게 되면, 서비스워커는 fetch라는 이벤트를 수행하게 된다.
self.addEventListener('fetch', function(event) {
  event.respondWith(
    caches.match(event.request)
      .then(function(response) {
        // Cache hit - return response
        if (response) {
          return response;
        }
        return fetch(event.request);
      }
    )
  );
});
  • caches.match()는 들어오는 request에 대해서 서비스워커가 생성한 캐쉬가 있는지 확인한다
  • 만약 생성된 캐쉬가 있다면, 캐쉬 값을 리턴한다. 그렇지 않은 경우에는 fetch() 를 콜한다.
  • fetch() 네트워크로부터 받을 데이터가 있다면, 네트워크 요청을 보내 해당 데이터를 받는다.
  • 네트워크 요청을 각각 캐쉬로 저장하고 싶다면, 아래와 같은 형식으로 구현하면 된다.
self.addEventListener('fetch', function(event) {
  event.respondWith(
    caches.match(event.request)
      .then(function(response) {
        // Cache hit - return response
        if (response) {
          return response;
        }

        // IMPORTANT: Clone the request. A request is a stream and
        // can only be consumed once. Since we are consuming this
        // once by cache and once by the browser for fetch, we need
        // to clone the response.
        var fetchRequest = event.request.clone();

        return fetch(fetchRequest).then(
          function(response) {
            // Check if we received a valid response
            if(!response || response.status !== 200 || response.type !== 'basic') {
              return response;
            }

            // IMPORTANT: Clone the response. A response is a stream
            // and because we want the browser to consume the response
            // as well as the cache consuming the response, we need
            // to clone it so we have two streams.
            var responseToCache = response.clone();

            caches.open(CACHE_NAME)
              .then(function(cache) {
                cache.put(event.request, responseToCache);
              });

            return response;
          }
        );
      })
    );
});

위 코드의 로직은 다음과 같다.

  1. fetch reqeust에 대해 promise (then) 콜백 호출
  2. 받은 response에 대하여 아래와 같은 절차를 수행
    • response 유효성 검사
    • status 값이 200인지 확인
    • type 값이 basic인지 확인 (our origin 인지 확인 / 3rd party 자원은 캐쉬가 되지 않는다는 의미)
  3. 위 절차들을 통과하면, response를 복제한다. 이유는 response는 Stream 방식이기 떄문에, body는 오직 한번만 실행 가능하다. 캐쉬 후에 브라우저에도 response를 던져야 하기 때문에, 복제를 해서 한개는 캐쉬를 한개는 브라우저에 각각 사용한다.

Service Worker 업데이트 하기

  • 서비스워커를 업데이트 해야하는 시점에서의 작업 절차는 다음과 같다.
    1. 서비스워커를 업데이트 하라. 사용자가 사이트를 네비게이팅 할 떄, 서비스워커 파일이 1byte라도 다를 경우 새로운 서비스워커로 간주한다.
    2. 새로운 서비스 워커가 시작되고, install 이벤트가 발생한다.
    3. 이 시점에서, 예전에 등록된 서비스워커가 현재 페이지를 제어하고 있기 때문에 새로운 서비스 워커는 waiting 상태로 진입한다.
    4. 사이트에서 열려있었던 페이지가 닫히면, 이전 서비스워커는 종료되고 새로운 서비스워커가 제어를 넘겨받는다
    5. 새로운 서비스워커로 제어가 넘어오면, activate 이벤트가 발생된다.
  • activate 콜백에서 발생하는 가장 흔한 작업은 cache management이다.
  • 이유는 바로 install 단계에서 이전 캐쉬를 다 지우게 된다면, 현재 페이지의 제어를 담당하는 old 서비스워커(현재 페이지에서 사용되고 있는 서비스워커 : 새로운 서비스워커에 비교해 old로 간주)의 경우 캐쉬에서 파일을 제공할 수가 없기 때문이다.
  • 예를 들어, my-site-cache-v1 캐쉬라는 파일이 있다고 가정하자. 그리고 이 캐쉬를 한개는 페이지에 한개는 블로그 포스트에 사용한다고 하자.
  • 이 의미는 install 단계에서 pages-cache-v1blog-posts-cache-v1 라는 두개의 캐쉬를 생성하고, 기존의 my-site-cache-v1 캐쉬는 지운다 는 것이다.
  • 아래의 코드를 확인해보면, cacheWhitelist에 존재하지 않는 캐쉬는 모두 서비스워커에서 삭제한다.
self.addEventListener('activate', function(event) {

  var cacheWhitelist = ['pages-cache-v1', 'blog-posts-cache-v1'];

  event.waitUntil(
    caches.keys().then(function(cacheNames) {
      return Promise.all(
        cacheNames.map(function(cacheName) {
          if (cacheWhitelist.indexOf(cacheName) === -1) {
            return caches.delete(cacheName);
          }
        })
      );
    })
  );
});

Rough Edges and Gotchas (현재 떠안고 있는 문제들)

서비스워커에는 현재 다음 두가지 이슈가 있다. 설치 실패 여부 확인 어려움, fetch() 디폴트 값

  1. 설치 실패 여부 확인 어려움
    • 서비스워커가 등록이 되더라도, chrome://inspect/#service-workerschrome://serviceworker-internals 로 확인하기 어렵다.
    • 따라서, chrome://serviceworker-internals 에서 Open DevTools window and pause JavaScript execution on service worker startup for debugging.를 체크하여 install event에 디버깅 문구를 넣어 확인한다.
  2. fetch() 디폴트 값
    • No Credentials by Default : fetch() 이벤트를 default로 사용시에는 쿠키 같은 credential들을 포함하지 않는다. 만약 credential을 포함하고 싶다면 아래와 같이
    fetch(url, {
    credentials: 'include'
    })
    
    • Non-CORS Fail by Default : 3rd party URL을 통한 자원 획득은 허용되지 않는다(CORS 지원하지 않는다면). 만약 CORS를 지원하려면 no-CORS 옵션을 추가한다. 하지만 이 방법은 opaque 응답을 야기하는데, 받은 응답이 성공인지 실패인지 확인할 수 가 없는 단점이 있다.
    cache.addAll(urlsToPrefetch.map(function(urlToPrefetch) {
    return new Request(urlToPrefetch, { mode: 'no-cors' });
    })).then(function() {
    console.log('All resources have been fetched and cached.');
    });
    

Service Worker Introduction I

Service Worker 란 무엇인가

  • Rich offline experiences, periodic background syncs, push notifications 등 네이티브 앱에서만 가능했던 기능들을 웹에서 사용할 수 있도록 지원하는 스크립트
  • 웹 페이지와는 별개로 브라우저의 백그라운드에서 수행되는 스크립트
  • 오늘 기준으로 push notificationsbackground sync 를 지원한다.
  • 오프라인 사용에 대한 완벽한 지원을 한다
  • Service Worker 이전에는 SPA의 AppCache 와 같은 기능들이 존재 했었지만, multiple page에 대한 지원이 되지 않았다.

Service Worker 를 통해 할 수 없는 것들은?

  • Javascript Worker 이기에 DOM 에 직접 접근이 불가. (하지만 원하면 postMessage 와의 인터페이스를 통해 접근 가능)
  • 프로그래밍 가능한 네트워크 프록시이기 떄문에, 페이지 핸들링에 관련된 네트워크 요청을 제어할 수 있다.
  • 사용하지 않을 때는 종료된다. 따라서, onfetch & onmessage 핸들러를 통한 global state에 접근이 불가능 하지만, 원한다면 IndexedDB API를 이용하여 상태를 보존할 수 있다.

Service Worker Lifecycle

  • 웹 페이지와 완전 별개의 라이프싸이클을 갖고 있다.
  • [등록] 서비스워커 사용을 위해서는 먼저 페이지의 자바스크립트를 사용하여 등록해야 한다.
  • [설치] 설치하는 과정에서 static한 자원들을 캐싱하고, 캐싱이 완료되면 서비스 워커가 설치가 된다. 한 개의 파일일라도 캐싱에 실패하면, 설치가 종료되고, 서비스워커는 다시 활성화되지 않는다.
  • [활성] 설치가 되고 나면, 활성 스텝으로 넘어오고, 이 떄 이전(오래된) 캐쉬들을 다룰 수 있는 상태가 된다.
  • [제어] 활성화 스텝 이후에는 서비스 워커가 본격적으로 모든 페이지를 제어하기 시작한다. 서비스워커에게 제어권이 돌아가면, 보통 아래 2가지 상태(Fetch, Terminated)로 나뉘게 된다.
  • [페치/메시지] 네트워크 요청을 받거나 메시지를 페이지로부터 전달받았을 때 데이터를 fetch하거나 메시지 이벤트를 처리한다
  • [종료] 메모리 효율을 위해 서비스워커를 종료한다

Service Worker Overview Imagesw-lifecycle

Service Worker Reference