Booking.com 모바일 사이트에서 iPhone X notch 대응 방법.(번역 예정)

애플이 iPhone X 라는 최신 플래그십 휴대폰 출시를 발표하자 미래의 모바일 기기에 새로운 기준을 마련하겠다고 약속했다. 우리는 그 디자인에 친숙 해 져야 했습니다.

“화면이 더이상 사각형이 아니네…”

큰 엣지 디스플레이 외에도, iPhone 의 가장 주목할 만한 추가 장치는 공식적으로 센서 하우징 으로 알려져 있는 ‘notch’ 이고, 그것은 완전히 새로운 도전적인 디자인 이었다: 스크린은 더이상 사각형일 필요는 없습니다. 이것은 네이티브 앱에 훨씬 더 큰 도전을 제시하지만, 웹에 대해서도 고려해야 할 몇가지 사항이 있습니다. 혐오 스럽든 영감을 주든, 센서 하우징은 그대로 유지될 것입니다. 따라서 지금이야말로 고객에게 기대할 만한 훌륭한 경험을 제공할 수 있는 절호의 기회입니다.

내가 받았던 첫인상은 “할일 겁나 많겠다”—마치 내가 CSS 미디어 쿼리를 처음 배웠을 때 처럼—하지만 몇가지 새로운 CSS 를 배우면(라고 쓰고 copy/paste 로 읽습니다.) 간단 합니다. 사실 너무 간단히 가르쳐 드릴 수 있습니다.

최신 Xcode 가 없다면 다운받아 시뮬레이터를 실행해 주세요.


문제점

iPhone X 를 가로방향(landscape) 로 회전 시키면, 왼쪽과 오른쪽의 공백 영역이 동일 하지 않습니다.

When an iPhone X is rotated into landscape orientation, the areas available for content on the left and the right of the device are unequal, which means that depending on the absolute orientation of the device and the position of the sensor housing, the horizontal ‘safe areas’ for your site’s content are either 15px or 45px wide. Our data suggests that 5–10% of iPhone X users are using landscape orientation. Whether that’s due to the larger screen size or just to see what happens with the sensor housing is up for debate, but I’m glad that they’ll see a well-adapted site when they do.

iOS’s solution to this is to ‘pillarbox’ your pages to make sure all of the content remains visible. It’s a neat enough solution for mass support, but luckily Apple has provided some additional features to help make websites take full advantage of the edge-to-edge display.

Booking.com’s homepage as it would appear on the iPhone X by default.

The 2 elements

There are 2 basic elements to accommodating iPhone X’s sensor housing:

  • A new viewport meta content property
  • Four new values for the padding CSS property.

Viewport meta content

To start, find your viewport meta tag in your site’s <head>, and add the following property to the end of the content attribute: viewport-fit=cover.

 

<meta name="viewport" content="width=device-width,minimum-scale=1.0,initial-scale=1.0,maximum-scale=1.0,user-scalable=no,viewport-fit=cover">
Booking.com’s viewport meta tag, with viewport-fit=cover added.

This will tell Safari to ditch the pillarbox and allow your site’s content to run to the edges of the display. It’s at this point it becomes apparent why Safari squashes the content by default; part of the page is now hidden beneath the sensor housing notch. We need to add some smart padding in CSS to make sure that our content stays visible.

Booking.com with viewport-fit=cover enabled.

The CSS

We want to apply padding to the elements which are obscured by the sensor housing. Kindly enough, Apple has provided a CSS function and some pre-defined CSS variables to take care of the heavy lifting:env() and safe-area-inset-*.

Since env() is only available for devices running iOS 11.2+, you’ll also need to include constant() for fallback support for now. It seems that iOS 11.2 will continue to support constant(), so go with that if you can only implement one, but I recommend you include both.

I get that it sounds a bit complicated, functions and variables in CSS? But the implementation is the same as any other property-value pair in CSS:

 

.container {
  /* Fallback */
  padding:0 10px;
  
  /* iOS 11 */
  padding-left: constant(safe-area-inset-left);
  padding-right: constant(safe-area-inset-right);
  
  /* iOS 11.2+ */
  padding-left: env(safe-area-inset-left);
  padding-right: env(safe-area-inset-right);
}
The implementation of the env() function and the environment variables used for Booking.com.

You will probably need to experiment to find the best place to apply these new styles, as it will vary a lot depending on your design. In our case, we want the background colours to fill the screen while keeping the content constrained within the safe areas, so we applied the padding to the inner elements of our containers.

Tip: use Safari’s Developer Tools on your Mac to inspect the elements in iOS Simulator’s Safari (or that of any connected iOS device).

 

.container {
  /* env() for iOS 11.2+, otherwise constant() */
  padding-top: env(safe-area-inset-top);
  padding-right: env(safe-area-inset-right);
  padding-bottom: env(safe-area-inset-bottom);
  padding-left: env(safe-area-inset-left);
}
All of the 4 new environment variables.
Booking.com after the safe area padding has been implemented.

The result

After carefully placing your paddings in the proper positions, you will have transformed the experience for your users—from ‘squashed to fit’ to ‘made to measure’! Doesn’t it look beautiful?

If you‘d like to know more, head over to Designing Websites for iPhone X on the webkit.org blog, where they also go into detail around the complications introduced when combining min() and max() with the env() function for more complex layouts.

Progressive Web Apps

우리가 웹이라고 부르는 환경은 아주 다양한 환경을 가지고 있다. 내가 이 글을 쓰는 시점에는 15인치 맥북 프로로 작성하고 있지만 누군가는 이 글을 아이폰 X로 볼 수도 있고, 갤럭시 S9으로 보고 있을 수도 있다. 혹은 아이패드 프로로 이 글을 보고 있을 수도 있겠다.

우리가 흔히 Desktop으로 부르는 환경에서 Mobile 환경으로 시대가 흘러감에 따라서 웹에서도 Mobile 환경에 적응하기 위한 다양한 노력을 하였다. 누군가는 Desktop 환경과 동일한 환경에서 CSS를 이용해 레이아웃을 변경하는 작업만 하기도 하였지만, 누군가는 Mobile 환경에 맞추어서 리소스를 최적화하고 상대적으로 부족한 컴퓨팅 환경에서 어떻게 좋은 사용자 경험을 제공할 수 있을 지에 대해서 고민하였다.

하지만 Mobile 환경이건 Desktop 환경이건 웹의 여러가지 한계점으로 인해 Application보다 좋은 사용성을 제공하기에는 일부 어려운 점이 있었다. 웹에서 콘텐츠에 접근하려면 반드시 ONLINE 상태여야했고, 리소스를 접속할 때마다 요청하기 때문에 네트워크 사용량이 많아야 했던 점들이 있다.

PWA (Progressive Web Apps)는 웹이 가지는 이러한 한계점들을 극복하고 새로운 사용자 경험을 제공하기 위한 개발 방법론의 집합이다. 따라서 그 구성이 해가 지남에 따라서 일부 달라지기도 하고, 새로이 추가되는 기능이 있기도 하며, 브라우저에서 지원하는 형태에 따라서 구현체가 달라지기도 한다.

이 글에서는 간단한 주소록 앱을 만들어 나가면서 PWA의 특성들과 그 활용법에 대해서 다루어보려고 한다.

주소록 예제는 https://rawgit.com/techhtml/pwa-contact/master/index.html 에서 확인할 수 있다.

목차

  1. FIRE 원칙
  2. Service Worker
  3. Web App manifest

Current Status of PWA

  1. iOS에서는 11.3 버전부터 Service Worker를 지원한다.
  2. Chrome은 45버전부터 지원했다.
  3. MS Edge에서도 현재는 Service Worker를 지원한다.

PWA에 있는 모든 Feature를 모든 브라우저가 대응하는 것은 아니다. iOS가 11.3 버전에서 Service Worker를 지원하고 있고, MS Edge도 지원하고 있기 때문에 현재 최신 브라우저는 모두 PWA의 핵심 Feature를 지원한다고 이야기할 수 있다.

PWA에서 Progressive는 점진적으로 개선한다는 의미로 모든 Feature를 지원하는 기기에서는 완벽한 환경을 제공할 수 있지만, 일부 Feature를 지원하지 않는 기기에서는 내가 원한 100%의 효과를 내지는 못할 수도 있다.

FIRE 원칙

  • Fast : 빠르다고 느낄 수 있게 해야한다.
  • Integrated : 유저의 기기에서 자연스럽게 느낄 수 있어야한다.
  • Reliable : 접속 환경이 안좋은 환경이더라도, 앱이 언제나 동작하여 유저의 신뢰를 깨트리지 말아야한다.
  • Engaging : 사용자 경험이 처음부터 미래의 핵심 여정 (Critical journey)까지 쭉 매력적이어야 한다.

네 단어의 앞단어를 딴 FIRE 원칙은 PWA를 관통하는 핵심 개념이라고 이야기할 수 있다. 네트워크 환경이 좋지 않거나, 디바이스의 성능이 떨어지는 경우에도 앱이 언제나 동작할 수 있도록 하여 좋은 환경을 제공하는 것이 목표라고 할 수 있다.

예를 들어 빠르다고 느끼게 하려면, Loading 중인 상태에서 Loading Progress를 보여주는 것보다 콘텐츠의 Placeholder를 보여주는 것이 더 효과적이다. 그리고 한번 로딩된 콘텐츠를 캐싱해두면 추후에 같은 웹 어플리케이션에 방문하였을 때 동일한 리소스를 다시 다운로드 받지 않아도 된다.

한국은 다른 나라에 비하면 네트워크 속도가 빠르다는 인식이 있지만, LTE 요금제를 다 사용하여 한시적 3G 요금제를 사용하거나 와이파이를 사용하는 경우에 네트워크 속도가 비약적으로 느려지는 경우가 있다. 특히 우리 집 네트워크 느리다. (슬프다) 그런 경우에도 웹을 잘 사용할 수 있도록 하는 것이 PWA에서 중요한 원칙이라고 이야기할 수 있겠다.

Service Worker

서비스 워커 (Service Worker)는 브라우저가 백그라운드에서 실행하는 스크립트로, 웹 페이지와는 별개로 동작하기 때문에 웹 페이지 또는 유저 인터렉션이 필요하지 않는 기능을 사용할 수 있다. 서비스 워커를 이용하면 푸시 알림 (Push Notification, Android Chrome 한정) 이나 백그라운드 동기화 (Background Sync, Android Chrome 한정) 가 가능하며, 다른 무엇보다 중요한 것은 서비스 워커를 이용하면 오프라인 환경을 통제할 수 있다는 점이다.

서비스 워커를 사용할 때 몇가지 유의사항이 있다.

  1. 반드시 HTTPS여야한다.
  2. 서비스 워커는 DOM에 직접 접근할 수 없다. 즉 서비스 워커 자체를 이용해서 DOM을 제어하는 건 불가하다.
  3. 서비스 워커는 페이지의 네트워크 요청 처리 방법을 제어할 수 있다.
  4. 서비스 워커는 사용되지 않을 때는 종료되고, 다음에 필요할 때 다시 시작된다. 서비스 워커가 종료 상태에서 재시작할 때 다시 사용해야하는 정보가 있는 경우 서비스 워커가 IndexedDB 에 대한 접근 권한을 가진다.
  5. 서비스 워커는 Promise를 주로 사용한다.

다만 개발자가 로컬에서 HTTPS 환경을 완전히 구축하기 어려우니, 로컬에서 작업하는 경우에는 HTTP에서도 테스트를 할 수 있다.

서비스 워커 라이프사이클

서비스 워커의 라이프사이클은 웹 페이지와 완전히 다르다.

  1. 서비스 워커를 등록한다.
  2. 서비스 워커를 설치한다.
    1. 설치 중 에러가 나면 활성화되지 않는다.
  3. 정상적으로 설치되면 활성화된다.
  4. 유휴 상태 (idle state)를 유지한다.
    1. 페이지에서 네트워크 요청이나 메시지가 생성될 때 Fetch나 Message 이벤트를 처리한다.
    2. 종료된다. (종료되더라도 제어권한을 가지면 다시 유휴 상태로 이동한다)

서비스 워커는 먼저 페이지에서 등록하고, 서비스 워커를 등록하면 브라우저가 백그라운드에서 서비스 워커를 설치한다. 서비스 워커는 설치 단계에서 Static Resource를 캐시하고, 모든 파일이 성공적으로 캐시되면 서비스 워커가 설치된다. 만약 파일 다운로드 및 캐시에 실패하면 서비스 워커가 설치되지 않는다.

설치가 완료되면 활성화 단계에 접어든다. 활성화 단계 후에는 서비스 워커 범위 안의 모든 페이지를 제어하지만 서비스 워커를 처음으로 등록한 페이지는 다시 로드해야 제어할 수 있다. 서비스 워커에 제어 권한이 부여된 경우 서비스 워커는 메모리를 절약하기 위해 종료되거나 페이지에서 네트워크 요청이나 메시지가 생성될 때 fetch 및 Message 이벤트를 처리한다.

서비스 워커 등록하기

서비스 워커를 등록하는 건 서비스 워커 스크립트 파일이 어디있는 지 브라우저에 알려주는 행위다.

index.js

// 서비스워커를 지원하는 브라우저인 경우
if ('serviceWorker' in navigator) {
  // 윈도우가 로딩되었을 때
  window.addEventListener('load', function() {
    // 서비스워커 (sw.js)를 등록한다.
    navigator.serviceWorker.register('/sw.js').then(function(registration) {
      // 등록이 성공했다면 (즉 설치가 완료되었다면)
      console.log("등록이 완료되었습니다");
    }).catch(function(err) {
      // 등록이 실패했다면
      console.log("등록이 실패했습니다");
    })
  })
}

서비스 워커를 등록하여 페이지가 아닌 브라우저 백그라운드에서 해당 도메인의 제어권을 가질 수 있다.

서비스 워커 설치하기

이번에는 설치 (install) 이벤트를 처리하는 서비스 워커를 살펴보자.

self.addEventListener('install', function(event) {
  // 설치를 여기서 진행한다
})

서비스 워커의 활용 범위에 따라서 내부가 상이하지만, 제일 먼저 하는 일은 리소스를 캐시하는 것이다.


// CACHE 네임스페이스
const CACHE_NAME = "contact-app";

// CACHE할 파일목록

const cache_urls = [
  '/',
  '/index.html',
  '/styles/contact.css',
  '/scripts/contact.js'
]

self.addEventListener('install', function(event) {
  // 설치가 시작되면 동작한다.
  event.waitUntil(
    caches.open(CACHE_NAME)
      .then(function(cache) {
         console.log('캐시가 열렸습니다');
         return cache.addAll(cache_urls);
      })
  );
})

이렇게 하면 서비스 워커에 리소스를 캐시해둘 수 있다. 여러가지 장점이 있지만 가장 큰 장점은 유저가 다음에 다시 서비스에 방문하였을 때 리소스를 다시 네트워크에서 로딩할 필요가 없다는 것이다.

오프라인 환경 제어하기

서비스 워커를 이용하는 또 다른 장점은 오프라인 환경을 제어할 수 있다는 것이다. 정확히는 브라우저에 설치된 서비스 워커가 해당 도메인 스코프 내에서 네트워크 요청이나 메시지를 제어할 수 있기 때문에, 서비스에서 요청하는 네트워크 요청이 이미 캐시된 것이라면 캐시된 리소스에서 불러오게 함으로서 오프라인 환경에서도 브라우저 캐시를 이용해 마치 네트워크에 연결된 것처럼 동작할 수 있게 되는 것이다.

코드는 아주 단순하다.

self.addEventListener('fetch', function(event) {
  // 'fetch' 이벤트가 발생하였을 때
  event.respondWith(
    // event.request와 일치하는 것을 찾는다
    caches.match(event.request)
      .then(function(response) {
        // 만약 일치하는 게 있다면 반환한다.
        if (response) {
          return response;
        }
        // 일치하는 게 없다면 이벤트의 요청을 발송한다. (즉 페이지 로딩)
        return fetch(event.request);
      }
    )
  );
});

지금은 서비스워커에서 index만 캐시해두었지만 유저의 Critical Journey에 속하는 리소스들을 미리 캐시해둔다면 유저가 오프라인에서건 온라인에서던 비슷한 사용성을 얻을 수 있다.

Web App 설정하기

웹과 앱의 차이는 무엇일까? 여러가지가 있겠지만 몇가지 큰 차이점을 살펴보자

  1. 웹은 브라우저를 통해 접근하고, 앱은 앱 아이콘을 통해 접근한다.
  2. 웹은 브라우저 Frame (주소창 등)이 노출되고, 앱은 어플리케이션 UI가 노출된다.

하지만 우리가 지금 만들고 있는건 웹 어플리케이션이니 어플리케이션에서 느꼈던 사용성을 웹에서 느낄 수 있게 만들어야 한다. 웹 앱을 설정하는 방법이 Android Chrome과 iOS가 다르지만 개발자는 둘 다 대응해야한다. 따라서 이 글에서는 iOS와 Android Chrome 대응법을 둘 다 작성해둔다.

App Icon 추가하기

App Icon은 add to homescreen 을 실행하였을 때 노출되는 아이콘으로 웹 어플리케이션에서 중요한 위치를 가지고 있다.

Android Chrome & MS Edge

Android에서는 Web App Manifest라고 부르는 JSON 규격을 이용해서 웹 앱을 정의한다. 현재 표준화를 진행하고 있으며 현재는 Android Chrome 및 Chromium을 포크한 구현체, Edge에서 대응하고 있다.

표준 스펙 : Web App Manifest

우선 가장 간단한 Web App Manifest 파일을 하나 만들어보자.

manifest.json

{
  "name": "주소록",
  "description": "조은의 번호만 있는 주소록"
}

표준 스펙에서는 webmanifest 확장자 사용을 권고하고 있으나 일반적으로 브라우저는 json 같은 확장자를 지원한다. 따라서 이 예제에서 확장자는 .json 을 사용하고자 한다.

Manifest 작성 후에는 link 요소를 이용해 페이지와 연결하면 된다.

index.html

<link rel="manifest" href="/manifest.json" />

다시 본론으로 넘어가 아이콘을 추가해보자. 안드로이드의 product 아이콘 사이즈는 48dp인데 dp는 밀도에 의존하지 않는 픽셀 (density independent pixel)이기 때문에 실제 기기의 해상도에 따라서 아이콘 크기가 달라진다.

ldpi (0.75x)	@ 48.00dp	= 36.00px
mdpi (1x)        @ 48.00dp    	= 48.00px
hdpi (1.5x)	@ 48.00dp	= 72.00px
xhdpi (2x)	@ 48.00dp	= 96.00px
xxhdpi (3x)	@ 48.00dp	= 144.00px
xxxhdpi (4x)	@ 48.00dp	= 192.00px

ldpi나 hdpi는 대응하지 않는다고 가정한다면 네종류의 아이콘을 만들어야한다.

  1. 48 x 48
  2. 96 x 96
  3. 144 x 144
  4. 192 x 192

아이콘만 있다면 실제 대응은 간단하다. 아까 작성한 manifest 파일에 아이콘을 추가하자.

manifest.json

{
  "name": "주소록",
  "description": "조은의 번호만 있는 주소록",
  "icons": [{
    "src": "icon48.png",
    "sizes": "48x48",
    "type": "image/png"
  }, {
    "src": "icon96.png",
    "sizes": "96x96",
    "type": "image/png"
  }, {
    "src": "icon144.png",
    "sizes": "144x144",
    "type": "image/png"
  }, {
    "src": "icon192.png",
    "sizes": "192x192",
    "type": "image/png"
  }]
}

이렇게하면 App icon을 사이트에 추가할 수 있다.

iOS

iOS에서는 자체 meta 규격을 이용해서 App icon을 추가할 수 있도록 하고있다.

index.html

<link rel="apple-touch-icon" sizes="180x180" href="touch-icon-iphone-retina.png">

iOS에서 권장하는 App icon 사이즈는 다음과 같다.

App Icon – Icons and Images – iOS – Human Interface Guidelines – Apple Developer

  1. 180 x 180 (60 x 60 @3x , iPhone)
  2. 120 x 120 (60 x 60 @2x, iPhone)
  3. 167 x 167 (83.5 x 83.5 @2x, iPad Pro)
  4. 152 x 152 (76 x 76 @2x, iPad & iPad mini)
  5. 1024 x 1024 (1024 x 1024 @1x, App Store)

언젠가 지원할거라는 생각은 들지만 앱 스토어에 PWA 등록은 현재 불가능하기 때문에 iPad 까지만 대응하도록 하자.

index.html

<link rel="apple-touch-icon" sizes="180x180" href="touch-icon-iphone-retina.png">
<link rel="apple-touch-icon" sizes="120x120" href="touch-icon-iphone.png">
<link rel="apple-touch-icon" sizes="167x167" href="touch-icon-ipad-pro.png">
<link rel="apple-touch-icon" sizes="152x152" href="touch-icon-ipad.png">

이렇게 하면 iOS에서 앱 아이콘을 추가할 수 있다.

Frame 제거하기

이번에는 주소창 등 브라우저에서 기본으로 제공하는 Frame을 제거해보자.

Android Chrome & MS Edge

마찬가지로 manifest 파일에서 설정하는데, display 값을 변경해서 Frame을 제거할 수 있다.

{
  "display": "standalone"
}

지원하는 속성값은 4개다.

  1. fullscreen : 디스플레이 공간에서 사용 가능한 모든 공간을 사용하며, 유저 에이전트 chrome 도 표시되지 않는다.
  2. standalone : 일반 어플리케이션처럼 보인다. 여기서 유저 에이전트는 navigation을 위한 UI 요소를 제외하지만, status bar 같은 UI 요소는 포함한다.
  3. minimul-ui : standalone 모드와 비슷하지만 navigation을 위한 UI 요소를 포함한다. 구성 요소는 브라우저에 따라 다르다.
  4. browser : 그냥 브라우저다. (Default)

iOS

iOS에서는 meta 를 이용해 Frame을 제어한다. Android에서는 다양하게 제어 가능했지만, iOS는 standalone 혹은 browser 로만 대응 가능하다.

index.html

<meta name="mobile-web-app-capable" content="yes">

마무리

이번에는 Static resource를 서비스워커에 등록하고 Web App Manifest를 등록하는 아주 간단한 과정만 진행해보았다. 이어지는 다음 글에서는 App Shell을 이용해 동적 콘텐츠와 App Shell을 분리하여 유저가 다시 다운로드 받을 리소스를 줄이고, 핵심 데이터들은 Local Storage에 저장하여 오프라인 환경에서도 불러올 수 있도록 해보자.

 

이 글의 원문은 아래에서 가져왔습니다.

http://techhtml.github.io/blog/2018/pwa/