중간계의 프런트엔드

멀티 디바이스 개발 둘러보기

Daniel Isaksson
Daniel Isaksson
Einar Öberg
Einar Öberg

Chrome 실험실 기능인 중간계를 가로지르는 여정의 개발에 관한 첫 번째 기사에서는 휴대기기용 WebGL 개발에 초점을 맞췄습니다. 이 도움말에서는 나머지 HTML5 프런트엔드를 만들 때 직면한 문제, 문제 및 해결 방법을 설명합니다.

동일한 사이트의 세 가지 버전

먼저 이 실험을 화면 크기 및 기기 기능 관점에서 데스크톱 컴퓨터와 휴대기기 모두에서 작동하도록 조정해 보겠습니다.

전체 프로젝트는 매우 '시네마틱(영화)' 스타일을 기반으로 하며, 우리는 디자인 측면에서는 영화의 마법과 같은 느낌을 유지하기 위해 가로 모드의 고정 프레임 내에서 경험을 유지하고자 했습니다. 프로젝트의 상당 부분이 대화형 미니 '게임'으로 구성되어 있기 때문에 이러한 미니 게임이 프레임을 넘치게 하는 것도 적절치 않습니다.

방문 페이지를 다양한 크기에 맞게 디자인을 조정하는 방법을 예로 들어보겠습니다.

독수리가 우리를 방문 페이지에 떨어뜨렸어요.
독수리가 방금 방문 페이지에 떨어뜨렸어요.

사이트에는 데스크톱, 태블릿, 모바일의 3가지 모드가 있습니다. 레이아웃을 처리할 뿐만 아니라 런타임에 로드되는 애셋을 처리하고 다양한 성능 최적화를 추가해야 하기 때문입니다. 데스크톱 컴퓨터 및 노트북보다 해상도는 높지만 휴대전화보다는 성능이 낮은 기기의 경우 궁극적인 규칙을 정의하기는 쉽지 않습니다.

Google에서는 사용자 에이전트 데이터를 사용하여 휴대기기를 감지하고 이 중에서 태블릿 (645px 이상)을 타겟팅하는 표시 영역 크기 테스트를 사용하고 있습니다. 레이아웃이 미디어 쿼리 또는 자바스크립트를 사용한 상대/백분율 위치를 기반으로 하기 때문에 서로 다른 각 모드에서 실제로 모든 해상도를 렌더링할 수 있습니다.

이 경우 디자인은 그리드나 규칙을 기반으로 하지 않고 여러 섹션 간에 매우 독특하기 때문에 어떤 중단점이나 스타일을 사용할지와 관련된 특정 요소와 시나리오에 따라 달라집니다. 좋은 믹스인과 미디어 쿼리로 완벽한 레이아웃을 설정한 다음에는 마우스 위치 또는 동적 객체에 기반한 효과를 추가해야 했고, 결국 모든 것을 JavaScript로 재작성하게 되었습니다.

또한, SCSS의 다음 예와 같이 스타일에서 이 정보를 사용할 수 있도록 head 태그에 현재 모드가 있는 클래스도 추가합니다.

.loc-hobbit-logo {

  // Default values here.

  .desktop & {
     // Applies only in desktop mode.
  }

 .tablet &, .mobile & {
   
   // Different asset for mobile and tablets perhaps.

   @media screen and (max-height: 760px), (max-width: 760px) {
     // Breakpoint-specific styles.
   }

   @media screen and (max-height: 570px), (max-width: 400px) {
     // Breakpoint-specific styles.
   }
 }
}

YouTube는 약 360x320의 모든 크기를 지원하며 이는 몰입도 높은 웹 환경을 구축하기에는 꽤 어려운 일입니다. 데스크톱에서는 가능한 한 더 큰 표시 영역에서 사이트를 경험할 수 있도록 스크롤바를 표시하기 전에 최소 크기를 정합니다. 휴대기기에서는 대화형 환경까지 가로 모드와 세로 모드를 모두 허용하기로 결정했으며, 그에 따라 사용자는 기기를 가로 모드로 전환해야 합니다. 이에 반대하는 주장은 세로 모드만큼 몰입도가 높지 않지만 사이트 확장성이 상당히 좋아서 이를 유지했습니다.

중요한 점은 레이아웃과 입력 유형, 기기 방향, 센서 등의 기능 감지와 섞여서는 안 됩니다. 이러한 기능은 모든 모드에 존재할 수 있으며 모든 모드에 걸쳐 있어야 합니다. 한 가지 예로 마우스와 터치를 동시에 지원하는 것을 들 수 있습니다. 망막의 품질에 대한 보상이 있지만, 무엇보다도 성능이 다르고 품질이 낮은 것이 더 나은 경우도 있습니다. 예를 들어 캔버스는 레티나 디스플레이의 WebGL 환경에서 해상도의 절반입니다. 그렇지 않으면 픽셀 수의 4배를 렌더링해야 합니다.

우리는 개발 중에 DevTools에서 에뮬레이터 도구를 자주 사용했으며, 특히 새롭게 개선된 기능과 많은 사전 설정이 적용된 Chrome Canary에서 많이 사용했습니다. 디자인 검증을 빠르게 할 수 있는 좋은 방법입니다. 실제 기기에서 정기적으로 테스트해야 했습니다. 한 가지 이유는 사이트가 전체 화면에 맞게 조정되고 있기 때문입니다. 세로 스크롤이 있는 페이지에서는 대부분의 경우 스크롤할 때 브라우저 UI가 숨겨지지만 (현재 iOS7의 Safari에서는 이 문제에 문제가 있음) 그와는 별도로 모든 항목을 맞춰야 했습니다. 또한 에뮬레이터에서 프리셋을 사용하고 화면 크기 설정을 변경하여 사용 가능한 공간 손실을 시뮬레이션했습니다. 실제 기기에서 테스트하는 것도 메모리 소비 및 성능 모니터링에 중요합니다.

상태 처리

방문 페이지 다음에는 중간계 지도에 갑니다. URL이 변경되었나요? 이 사이트는 History API를 사용하여 라우팅을 처리하는 단일 페이지 애플리케이션입니다.

사이트의 각 섹션은 DOM 요소, 전환, 애셋 로드, 삭제 등과 같은 기능의 상용구를 상속하는 자체 객체입니다. 사이트의 여러 부분을 탐색할 때 섹션이 시작되고 DOM에 요소가 추가 및 제거되며 현재 섹션의 애셋이 로드됩니다.

사용자는 언제든지 브라우저의 뒤로 버튼을 누르거나 메뉴를 통해 탐색할 수 있으므로 생성된 모든 것을 어느 시점에 폐기해야 합니다. 시간 제한 및 애니메이션은 중지 및 삭제해야 하며, 그러지 않으면 원치 않는 동작, 오류, 메모리 누수가 발생할 수 있습니다. 마감일이 다가오고 있으며 최대한 빨리 모든 것을 처리해야 할 때 이는 쉬운 일이 아닙니다.

위치 표시

중간계의 아름다운 설정과 캐릭터를 선보이기 위해 우리는 드래그하거나 가로로 스와이프할 수 있는 이미지 및 텍스트 구성요소로 이루어진 모듈식 시스템을 구축했습니다. 여기서는 스크롤바를 활성화하지 않았습니다. 클립이 재생될 때까지 동작을 옆으로 멈추는 이미지 시퀀스와 같이 다양한 범위에서 다른 속도를 원하기 때문입니다.

스란두일의 집
Thranduil's Hall 타임라인

타임라인

개발이 시작되었을 때는 각 위치의 모듈 내용을 몰랐습니다. 우리는 모든 것을 6번이나 다시 지을 필요 없이 6개의 서로 다른 위치 프레젠테이션을 진행할 수 있는 자유로운 방식으로, 다양한 유형의 미디어와 정보를 가로 타임라인에 표시할 수 있는 템플릿 방식을 원한다는 것을 알고 있었습니다. 이를 관리하기 위해 설정 및 모듈의 동작에 따라 모듈의 패닝을 처리하는 타임라인 컨트롤러를 만들었습니다.

모듈 및 동작 구성요소

우리가 추가한 다양한 모듈은 이미지 시퀀스, 정지 이미지, 시차 장면, 포커스 시프트 장면 및 텍스트입니다.

시차 장면 모듈에는 정확한 위치를 위해 표시 영역 진행률을 수신 대기하는 맞춤 수의 레이어가 있는 불투명한 배경이 있습니다.

포커스 시프트 장면은 시차 버킷의 변형으로, 포커스 변경을 시뮬레이션하기 위해 페이드 인 및 아웃하는 각 레이어에 두 개의 이미지를 사용합니다. 블러 필터를 사용하려고 했지만 여전히 비용이 많이 들기 때문에 CSS 셰이더가 나올 때까지 기다리겠습니다.

텍스트 모듈의 콘텐츠는 TweenMax 플러그인 Draggable로 드래그가 사용 설정됩니다. 스크롤휠이나 두 손가락으로 스와이프하여 수직으로 스크롤할 수도 있습니다. 스와이프했다가 놓을 때 플링 스타일의 물리학을 추가하는 throw-props-plugin에 유의하세요.

또한 모듈에는 구성요소 집합으로 추가되는 다양한 동작이 있을 수 있습니다. 모두 고유한 타겟 선택기와 설정이 있습니다. 변환하여 요소 이동, 확대/축소 배율, 정보 오버레이의 핫스팟, 시각적 테스트를 위한 디버그 측정항목, 시작 제목 오버레이, 플레어 레이어 등을 사용할 수 있습니다. 이러한 매개변수는 DOM에 추가되거나 모듈 내에서 타겟 요소를 제어합니다.

이를 사용하면 로드할 애셋을 정의하고 다양한 종류의 모듈과 구성요소를 설정하는 구성 파일로 다양한 위치를 만들 수 있습니다.

이미지 시퀀스

성능 및 다운로드 크기 측면에서 모듈 중 가장 어려운 점은 이미지 시퀀스입니다. 이 주제에 대해 읽어볼 만한 자료가 많이 있습니다. 휴대기기 및 태블릿에서는 정지 이미지로 대체됩니다. 모바일에서 적절한 품질을 원한다면 디코딩하고 메모리에 저장하기에는 데이터가 너무 많습니다. 우리는 배경 이미지와 스프라이트 시트를 먼저 사용하는 등 여러 가지 대안을 시도했지만, 메모리 문제가 발생하고 GPU가 스프라이트 시트 간에 전환해야 할 때 지연이 발생했습니다. 그런 다음 img 요소를 바꿔보았지만 속도가 너무 느렸습니다. 스프라이트 시트에서 캔버스로 프레임을 그리는 것이 가장 성능이 뛰어나므로 이를 최적화하기 시작했습니다. 각 프레임의 계산 시간을 절약하기 위해 캔버스에 쓸 이미지 데이터는 임시 캔버스를 통해 사전 처리되고 putImageData()로 배열에 저장되고, 디코딩되어 사용 가능합니다. 그러면 원래 스프라이트 시트가 가비지로 수집될 수 있고 메모리에 필요한 최소한의 데이터만 저장할 수 있습니다. 디코딩되지 않은 이미지를 저장하는 것이 실제로는 더 적을 수 있지만 이러한 방식으로 시퀀스를 스크러빙하면 성능이 향상됩니다. 프레임은 640x400으로 상당히 작지만 스크러빙 중에만 보입니다. 중지하면 고해상도 이미지가 로드되고 빠르게 페이드 인됩니다.

var canvas = document.createElement('canvas');
canvas.width = imageWidth;
canvas.height = imageHeight;

var ctx = canvas.getContext('2d');
ctx.drawImage(sheet, 0, 0);

var tilesX = imageWidth / tileWidth;
var tilesY = imageHeight / tileHeight;

var canvasPaste = canvas.cloneNode(false);
canvasPaste.width = tileWidth;
canvasPaste.height = tileHeight;

var i, j, canvasPasteTemp, imgData, 
var currentIndex = 0;
var startIndex = index * 16;
for (i = 0; i < tilesY; i++) {
  for (j = 0; j < tilesX; j++) {
    // Store the image data of each tile in the array.
    canvasPasteTemp = canvasPaste.cloneNode(false);
    imgData = ctx.getImageData(j * tileWidth, i * tileHeight, tileWidth, tileHeight);
    canvasPasteTemp.getContext('2d').putImageData(imgData, 0, 0);

    list[ startIndex + currentIndex ] = imgData;

    currentIndex++;
  }
}

스프라이트 시트는 Imagemagick으로 생성됩니다. 폴더 안에 있는 모든 이미지의 스프라이트 시트를 만드는 방법을 보여주는 간단한 GitHub 예시는 다음과 같습니다.

모듈 애니메이션

타임라인에 모듈을 배치하기 위해, 화면 밖에 표시되는 타임라인의 숨겨진 표현이 '플레이헤드'와 타임라인의 너비를 추적합니다. 이 작업은 코드만으로 수행할 수 있지만, 개발 및 디버깅 시 시각적 표현을 제공하는 데 적합했습니다. 실제로 실행할 때는 크기 조정을 위해 크기를 설정하기만 하면 됩니다. 일부 모듈은 표시 영역을 채우고 일부는 자체 비율을 가지고 있으므로 모든 것이 표시되고 너무 많이 잘리지 않도록 모든 해상도에서 모든 항목을 조정하고 배치하기가 어려웠습니다. 각 모듈에는 진행률 표시기가 두 개 있습니다. 하나는 화면에 표시되는 위치를 표시하고 다른 하나는 모듈 자체의 지속 시간을 나타냅니다. 시차를 움직일 때 객체가 시야에 있을 때 예상 위치와 동기화하기 위해 객체의 시작 및 종료 위치를 계산하기 어려울 때가 많습니다. 모듈이 뷰에 들어가거나, 내부 타임라인을 재생하는 시점, 뷰에서 벗어나 애니메이션으로 다시 전환되는 시점을 정확히 아는 것이 좋습니다.

각 모듈의 상단에는 불투명도를 조정하는 은은한 검은색 레이어가 있어 중앙 위치에 있을 때 완전히 투명하게 보입니다. 이렇게 하면 한 번에 하나의 모듈에 집중할 수 있으므로 환경이 개선됩니다.

페이지 실적

제대로 작동하는 프로토타입에서 버벅거림이 없는 출시 버전으로 전환한다는 것은 브라우저에서 어떤 일이 일어나는지를 짐작하는 것에서 벗어날 수 있다는 것을 의미합니다. 이럴 때 Chrome DevTools를 사용하면 좋습니다.

우리는 사이트를 최적화하는 데 많은 시간을 투자했습니다. 하드웨어 가속을 강제 적용하는 것은 매끄러운 애니메이션을 얻는 데 있어 가장 중요한 도구 중 하나입니다. 하지만 Chrome DevTools에서 색상이 풍부한 열과 빨간색 직사각형을 헌팅할 수도 있습니다. 주제에 관한 유용한 도움말이 많이 있으므로 모두 읽어보는 것이 좋습니다. 프레임 건너뛰기를 제거하면 그에 따른 보상은 즉시 받을 수 있지만, 프레임 건너뛰기가 다시 돌아왔을 때 느끼는 좌절감도 마찬가지입니다. 그리고 그렇게 할 것입니다. 반복이 필요한 지속적인 프로세스입니다.

저는 Greensock의 TweenMax를 속성, 변환 및 CSS 트위닝에 사용하고 싶습니다. 컨테이너를 생각하고, 새 레이어를 추가할 때 구조를 시각화하세요. 새 변환이 기존 변환을 덮어쓸 수 있다는 점에 유의하세요. 2D 값만 트윈하는 경우 CSS 클래스에서 하드웨어 가속을 강제 적용한 TranslateZ(0)가 2D 행렬로 대체됩니다. 이 경우 레이어를 가속 모드로 유지하려면 트윈에서 'force3D:true' 속성을 사용하여 2D 행렬 대신 3D 행렬을 만드세요. CSS와 JavaScript 트윈을 결합하여 스타일을 설정하는 것은 잊어버리기 쉽습니다.

필요하지 않은 곳에서 하드웨어 가속을 강제 적용하지 마세요. GPU 메모리는 특히 메모리에 제약이 많은 iOS에서 많은 컨테이너를 하드웨어 가속화해야 할 때 빠르게 채워지고 원치 않는 결과가 발생할 수 있습니다. 더 작은 애셋을 로드하고 css로 확장하고 모바일 모드에서 일부 효과를 비활성화하기 위해 큰 개선이 이루어졌습니다.

메모리 누수 역시 기술을 개선하기 위해 필요한 분야였습니다. 여러 WebGL 환경 간에 탐색할 때 많은 객체, 머티리얼, 텍스처, 도형이 생성됩니다. 다른 곳으로 이동하여 섹션을 삭제할 때 가비지 컬렉션을 사용할 준비가 되지 않은 경우 메모리가 부족할 때 잠시 후 기기가 비정상 종료될 수 있습니다.

실패한 삭제 함수가 있는 섹션을 종료합니다.
실패한 삭제 함수가 있는 섹션 종료
훨씬 낫습니다!
훨씬 좋습니다.

누수를 찾아내기 위해 DevTools에서 타임라인을 기록하고 힙 스냅샷을 캡처하는 매우 간단한 워크플로였습니다. 3D 도형이나 특정 라이브러리와 같은 특정 객체가 있으면 필터링할 수 있습니다. 위의 예에서는 3D 장면이 여전히 주변에 있고 도형을 저장한 배열도 지워지지 않았습니다. 객체가 있는 위치를 찾기 어려운 경우 이를 확인할 수 있는 보존 경로라는 유용한 기능이 있습니다. 힙 스냅샷에서 검사하려는 객체를 클릭하면 아래 패널에 정보가 표시됩니다. 작은 객체가 포함된 적절한 구조를 사용하면 참조를 찾을 때 도움이 됩니다.

이 장면은 EffectComposer에서 참조되었습니다.
이 장면은 EffectComposer에서 참조되었습니다.

일반적으로 DOM을 조작하기 전에 한 번 더 생각하는 것이 좋습니다. 이때 효율성에 대해 생각해 보세요. 가능하면 게임 루프 내에서 DOM을 조작하지 마세요. 재사용할 수 있도록 변수에 참조를 저장합니다. 요소를 검색해야 하는 경우 전략적 컨테이너에 대한 참조를 저장하고 가장 가까운 상위 요소 내부를 검색하여 최단 경로를 사용합니다.

새로 추가된 요소의 크기 읽기를 지연시키거나 레이아웃 버그가 발생하는 경우 클래스를 삭제/추가할 때 지연합니다. 또는 레이아웃이 트리거됨을 확인하세요. 브라우저가 스타일을 일괄 변경하고 다음 레이아웃이 트리거된 후 업데이트되지 않는 경우가 있습니다. 이것은 정말로 큰 문제가 될 수도 있지만 여기에는 이유가 있을 수 있으므로, 이면에서 어떻게 작동하는지 알아두면 많은 것을 얻을 수 있습니다.

전체 화면

가능한 경우 Fullscreen API를 통해 메뉴에서 사이트를 전체 화면 모드로 설정할 수 있습니다. 하지만 기기에서는 브라우저에서 전체화면으로 전환하기도 합니다. 이전에 iOS용 Safari에는 이 기능을 제어할 수 있는 해킹이 있었지만 더 이상 사용할 수 없으므로 스크롤되지 않는 페이지를 만들 때 이 디자인이 없어도 작동하도록 디자인해야 합니다. 많은 웹 앱이 중단되었기 때문에 향후 업데이트에서 이에 대한 업데이트가 이루어질 것으로 예상됩니다.

애셋

실험에 대한 애니메이션 안내
실험용 애니메이션 안내

사이트 전반에서 다양한 유형의 자산을 보유하고 있으며 이미지 (PNG 및 JPEG), SVG (인라인 및 배경), 스프라이트 시트 (PNG), 맞춤 아이콘 글꼴, Adobe Edge 애니메이션을 사용합니다. 벡터 기반이 될 수 없는 애셋 및 애니메이션 (스프라이트 시트)에는 PNG를 사용합니다. 그렇지 않으면 SVG를 최대한 사용하려고 합니다.

벡터 형식을 사용하면 크기를 조정하더라도 품질이 저하되지 않습니다. 모든 기기용 파일 1개

  • 파일 크기가 작습니다.
  • 각 부분에 개별적으로 애니메이션을 적용할 수 있습니다 (고급 애니메이션에 적합). 예를 들어 축소하면 호빗 로고의 '부제목'(스마우그의 폐허)을 숨깁니다.
  • SVG HTML 태그로 삽입하거나 추가 로드 없이 배경 이미지로 사용할 수 있습니다 (html 페이지와 동시에 로드됨).

아이콘 글꼴은 확장성 면에서 SVG와 동일한 장점이 있으며, 색상만 변경하면 되는 아이콘(예: 마우스 오버, 활성 상태)에 SVG 대신 작은 요소(예: 아이콘)에 사용됩니다. 또한 아이콘은 매우 쉽게 재사용할 수 있으며 요소의 CSS 'content' 속성만 설정하기만 하면 됩니다.

애니메이션

경우에 따라 코드를 사용해 SVG 요소에 애니메이션을 적용하는 데 시간이 매우 오래 걸릴 수 있습니다. 특히 디자인 과정에서 애니메이션을 많이 변경해야 하는 경우에는 더욱 그렇습니다. 디자이너와 개발자 간의 워크플로를 개선하기 위해 일부 애니메이션 (게임 전 안내)에 Adobe Edge를 사용합니다. 애니메이션 워크플로는 Flash와 매우 가까우며 팀에 도움이 되었지만 특히 애셋 로드 프로세스에 Edge 애니메이션을 통합하는 경우 자체 로더와 구현 로직이 수반되기 때문에 몇 가지 단점이 있습니다.

웹에서 자산과 핸드메이드 애니메이션을 처리하기 위한 완벽한 워크플로를 갖추기까지 갈 길이 멀다고 생각합니다. Edge와 같은 도구가 앞으로 어떻게 발전할지 기대됩니다. 다른 애니메이션 도구 및 워크플로에 관한 제안은 댓글에 자유롭게 추가해 주세요.

결론

이제 프로젝트의 모든 부분을 공개하고 최종 결과를 살펴보면 최신 모바일 브라우저의 상태에 꽤 감명받았다고 말할 수 있을 것입니다. 이 프로젝트를 시작했을 때는 얼마나 원활하고 통합되고 우수한지에 대한 기대가 훨씬 낮았습니다. 훌륭한 학습 경험이었습니다. 반복하고 테스트하는 데 많은 시간을 투자한 덕분에 최신 브라우저의 작동 방식을 더 잘 이해할 수 있었습니다. 이것이 바로 이러한 유형의 프로젝트에서 추측에서 인지에 이르기까지 프로덕션 시간을 단축하려는 경우에 필요한 과정입니다.