Google 포토 웹 UI 구축 (Building the Google Photos Web UI)

이 글은 Antin HarasymivBuilding the Google Photos Web UI글을 번역했습니다. 저의 모든 글은 허가를 받고 진행 하였습니다

덮개 아래를 살짝 보세요.

몇 년 전 저는 2015년에 처음 출시된 Google Photos팀에서 엔지니어가 될 수 있는 특권을 누렸습니다. 디자이너, 제품 관리자, 연구원, 수많은 엔지니어(Android, iOS, 웹 및 서버 전체) 등 많은 사람들이 이 제품에 기여하여 주요 역할의 일부만 언급하였습니다. 제 책임은 웹 UI였고, 구체적으로는 사진 그리드를 담당(photo grid)했습니다.
우리는 야심찬 무언가를 시도하고 동시에 full-width (justified) 레이아웃을 지원하고, 각 사진의 가로 세로 비율(aspect–ratio)을 보존하고, 스크러빙(scrubbable)이 가능하고, 수십만 장의 사진을 처리하고, 60fps로 스크롤하고, 즉시 로드하기를 원했습니다.
그 당시에는 다른 사진 갤러리가 이 모든 것을 지원하지 않았고, 제가 알기로는 지금도 그렇지 않습니다. 현재 다른 많은 갤러리가 이러한 기능 중 일부를 지원하지만 대개 각 사진을 사각형으로 잘라 배치 작업을 수행합니다.
다음은 이러한 과제를 해결한 방법에 대한 기술 문서입니다. Google 포토의 웹 버전이 어떻게 작동하는지 덮개 아래를 살짝 살펴봅니다.

왜 이렇게 힘들었을까요?

두 가지 가장 큰 당면 과제는 모두 규모가 커집니다.
첫 번째 문제점은 사진 모음이 큰 사용자(일부 사용자에게는 25만 장 이상의 사진이 업로드됨)의 경우 메타데이터가 너무 많다는 것입니다. 최소 정보(사진 url, 폭, 높이 및 타임스탬프)를 전송하는 것은 전체 수집을 위한 수 메가바이트의 데이터이므로 거의 즉각적인 로딩이라는 목표에 정면으로 반하는 결과가 될 수 있습니다.
두 번째 문제점은 사진 자체입니다. 최신 HDPI 화면에서는 작은 사진 축소판(thumbnail)도 종종 50KB 이상입니다. 1,000개의 미리 보기는 50MB일 수 있으며 다운로드해야 할 용량이 많을 뿐만 아니라 웹 페이지에 즉시 모두 저장하려고 하면 브라우저 속도를 늦출 수 있습니다. 이전 Google+ 사진은 1000–2000 사진을 스크롤한 후 느려지고 Chrome은 결국 10000을 로드한 후 탭에 충돌합니다.
토론을 여러 부분으로 나눕니다(대담한 제목을 검색하면 바로 갈 수 있습니다).
  1. 스크럽이 가능한 사진(Scrubbable Photos) - 사진 라이브러리의 모든 부분으로 빠르게 이동할 수 있습니다.
  2. 길이를 맞춘 레이아웃(Justified Layout): 브라우저의 폭을 채우고 각 사진의 가로 세로 비율(aspect-ratio)을 보존합니다(no square crops).
  3. 60fps 스크롤 - 수천 장의 사진을 볼 때에도 페이지 응답성을 유지합니다.
  4. 즉각적인 느낌—무엇이든 로드될 때까지 기다리는 시간을 최소화합니다.

1. 스크럽이 가능한(Scrubbable) 사진

대규모 컬렉션을 처리하는 데는 몇 가지 방법이 있습니다. 가장 오래된 것은 아마도 페이지 매김(pagination) 일 것입니다. 여기서 고정 된 수의 결과를 표시하고 "다음"을 클릭하면 후속 일괄 처리, 광고 구역을 볼 수 있습니다. 보다 대중적인 현대적인 접근 방법은 무한 스크롤(infinite scrolling)입니다. 다음 번 배치를 자동으로 가져 와서 페이지에 반복적으로 삽입하여 끝까지 더 가까이 이동할 때 고정 된 수의 결과 만 로드하므로 이름이 지정됩니다. 잘 된 경우 사용자는 중지 할 필요없이 계속 스크롤링을 할 수 있습니다 (겉으로는 무한히 보입니다).
페이지 매김(pagination)과 무한 스크롤(infinite scrolling)은 모두 가장 오래된 콘텐츠를 모두 로드해야 한다는 점에서 유사한 결점을 공유하므로 수년 전의 사진을 찾는 것이 번거로울 수 있습니다.
일반 문서의 경우 스크롤바는 사용자가 원하는 방식으로 기능을 수행하며 더 좋은 결과를 얻기 위해 전체 섹션을 빠르게 건너 뛸 수 있으므로 끝까지 곧바로 또는 다른 지점으로 바로 이동할 수 있습니다. 페이지 매김(pagination)을 사용하면 스크롤바가 페이지 하단 (라이브러리는 아님)에 도달하게되며 무한대로 스크롤하는 페이지의 경우 스크롤 막대가 항상 변경됩니다. 끝까지 드래그하면 페이지가 길어질수록 약간 뒤로 스쳐지나갑니다 .
스크러빙 가능한 사진 그리드는 스크롤 막대가 올바르게 작동하는 세 번째 옵션을 제공합니다.
사진 섹션으로 이동할 수 있도록 페이지에 미리 공간을 할당하는 스크롤 막대가 대표적입니다. 모든 사진에 대한 정보를 가지고 있다면 비교적 쉽겠지만, 전송이 너무 많은 만큼 다른 방식으로 해야 합니다.
이곳은 다른 많은 스크러블 갤러리가 지름길로 가는 곳입니다. 모든 사진을 똑같은 사각형 모양으로 자릅니다. 이렇게 하면 전체 페이지 레이아웃을 계산하는 데 필요한 총 사진 수만 알면 됩니다. 주어진 정사각형 크기에 대해 뷰포트의 폭을 세 배로 잡고 열과 행의 수를 계산하는 데 사용할 수 있습니다.
코드의 세 줄에서만 사이징이 수행된 다음 사진을 렌더링하고 배치하는 것은 겨우 12개만 더하면 됩니다.
메타 데이터의 초기 크기를 줄이기 위해 우리가 생각한 접근 방식은 사용자의 사진 컬렉션을 개별 섹션으로 세분화하고 초기로드시 섹션과 개수를 전송하는 것이 었습니다. 예를 들어, 사진을 단면으로 나누는 간단한 방법은 서버로 계산하거나 (또는 미리 계산할 수 있습니다.) 수십억 개의 사진으로도 여전히 사소한 양의 데이터입니다. 매우 기본적인 데이터 표현은 다음과 같습니다.

극단적 인 경우 특정 월에 많은 사진을 찍는 사용자 (예 : 전문 사진 작가)에게 문제가 발생할 수 있습니다 - 섹션의 목표는 모든 버켓(bucket) 관리 가능한 양의 메타 데이터로 줄이는 것이지만 사용자가 많으면 한 달이 여전히 남아있을 수 있습니다 수천 장의 사진 (따라서 메가 바이트의 데이터)을 포함합니다. 고맙게도 똑똑한 인프라 팀은 스스로를 능가하고 모든 사용자를 위해 사용자 정의 섹션을 만드는 모든 종류의 사항 (예 : 위치 정보, 근접한 타임 스탬프 등)을 고려한 정교한 솔루션을 구축했습니다.
presentation
사진 그리드는 섹션, 세그먼트 및 타일로 나뉩니다
이 정보를 사용하여 클라이언트는 각 섹션에 필요한 공간을 추정하고 placeholder를 DOM에 넣은 다음 사용자가 빠르게 스크롤하면 서버에서 해당 사진 메타 데이터를 검색하고 전체 레이아웃을 계산하고 페이지를 업데이트 할 수 있습니다.
클라이언트에서 섹션에 대한 메타 데이터가 있으면 실제로 한 단계 더 나아가 각 섹션의 사진을 개별 날짜로 분할합니다. 우리는 미래의 큰 특징을 여전히 만들 수있는 동적 세분화 (예 : 위치, 사람, 날짜 등)에 대해 논의했습니다.
섹션 크기를 예측하는 것은 매우 쉽다는 것이 밝혀 졌기 때문에 섹션의 사진 수를 취해 정상적인 종횡비(aspect ratio)로 가장 잘 추측 할 수 있습니다.

어떻게 이것이 정확할 수 있을지 스스로에게 묻고있는 중입니까? 진실은 그렇지 않다는 것입니다.
운이 좋았던 점은 처음에는 문제의 이 부분을 간과했기 때문에 (레이아웃 섹션에서 설명 하겠지만) 견적을 필요로하지 않는다는 것이 밝혀졌습니다. (많은 수의 사진의 경우 종종 잘못되었습니다. 수만 픽셀 씩). 스크롤바가 정확성을 유지할 수 있도록 추정치가 모호하다는 점이 유일한 중요한 점입니다.
presentation
간단한 마술은 마침내 섹션을 로드 할 때 예상 높이와 실제 높이의 차이를 계산한다는 것입니다. 차이가있을 경우 그 부분만큼 모든 섹션을 이동하십시오.
스크롤 지점 위에있는 섹션을 로드하는 경우 스크롤 위치를 업데이트해야 할 수도 있습니다. 그러나 모든 것은 하나의 애니메이션 프레임 내에서 초 단위로 수행 될 수 있으므로 사용자에게는 인식할만한 차이가 없습니다.

2. 길이를 맞춘 레이아웃 Justified Layout

내가 알고있는 모든 길이를 맞춘 이미지 레이아웃은 독창적 인, 상대적으로 단순한 접근법을 사용합니다. 다양한 행 높이의 격자를 갖는 것은 괜찮습니다. 단일 행의 모든 ​​사진은 동일한 높이로 조정되며 모든 행은 동일한 너비이지만 두 행의 높이가 다를 수 있으며 일반적으로 그 차이는별로 눈에 띄지 않습니다.
균일 한 높이를 포기함으로써 일정한 간격으로 고정 너비 격자를 얻는 동시에 모든 사진의 종횡비를 유지할 수 있습니다. 이를 달성하기위한 알고리즘은 어렵지 않습니다. 사진을 그 높이로 확장하고 자동 계산에 너비를 추가하고 너비가 확대 된 뷰포트를 초과 할 때마다 최대 행 높이를 선택한 다음 하나의 사진을 선택합니다 너비에 맞을 때까지 줄의 각 사진을 내립니다 (높이는 짧아집니다).
예를 들어 14 장의 사진 배치 :
presentation
꽤 순진한 해결책이지만 잘 작동합니다. Google+는 그것을 사용했으며 Google Search는 그 형식을 사용하고 Flickr는 2016년에 이를 구현 한 소스를 친절하게 공개했습니다 (사진이 약간 더 스마트 해지고 한 장의 사진으로 축소하거나 추가로 축소하는 것이 더 나은지 확인). 코드는 다음과 같이 간단 할 수 있습니다.
그러나 처음에는 추정치가 최종 레이아웃과 깔끔하게 일치하는 것에 대해 (불필요하게) 우려했기 때문에 더 정교한 솔루션을 찾고 프로세스에서 우위를 점했습니다.
제 이론은 일단 우리가 레이아웃을 추정했다면 우리는 사진을 그 영역에 맞출 수 있어야한다는 것입니다. 이것은 본질적으로 줄 바꿈(line-wrapping) 문제입니다. 여러 가지면에서 텍스트 레이아웃 (단락의 길이를 맞추기 위해 텍스트의 단어 줄 바꿈)과 유사합니다. Knuth & Plass 줄 바꿈(line-breaking) 알고리즘은 사진 레이아웃에 맞게 조정할 수 있다고 잘 기록 된 동적 프로그래밍 방식입니다.
한 번에 한 라인 씩 결정하는 대신 전체 섹션을 전체적으로 배치하여 각 라인이 연속적인 것들의 영향을 받을 수 있도록합니다.
이것은 상자(Boxes), 접착제(glue) 및 벌칙(penalties)를 조합하여 수행합니다. 상자는 배치 할 수있는 분할 할 수없는 블록 (일반적으로 단어이지만 때로는 문자)이고, glue는 늘어나거나 축소 될 수있는 블록 (일반적으로 줄의 공백)이며, 특정 작업을 방해하기 위해 penalties를 적용 할 수 있습니다 (하이픈 넣기 또는 줄 바꿈-line breaks).
다음 다이어그램에서는 Boxes 사이의 glue가 선을 통해 크기가 다른 것을 볼 수 있습니다.
presentation
Text layout — boxes and glue
사진 레이아웃에는 몇 가지 차이점이 있지만 실제로는 더 간단합니다. 텍스트의 경우 사람들이 수용 할 수있는 변형이 훨씬 많습니다. 단어 사이의 간격을 변경하거나 단어의 문자 사이의 간격을 변경할 수도 있으며 중간 단어를 하이픈 넣을 수도 있습니다. 사진의 경우 사진 사이의 여백이 다를 때 사람들이 혼란스럽고 사진 하이픈은 의미가 없습니다.
텍스트 알고리즘이 작동하는 방법에 대한 몇 가지 훌륭한 글이 있지만 여기에 사진을 적용하는 방법이 있습니다.
사진이 상자(Boxes)가되어 접착제(glue)의 개념을 완전히 취소 할 수 있었으며 벌칙(penalties)도 단순화 될 수있었습니다. 이제 내가 말하지만, 아마 우리가 상자(Boxes)를 떨어 뜨렸다고 말하면서 사진이 접착제(glue) (즉, 우리 레이아웃의 유연한 부분은 공백이 아닌 사진입니다)라고 말하는 것이 더 낫습니다.
사진 사이의 간격을 변경하는 대신 다른 길이를 맞춘 레이아웃의 접근 방식을 따르고 행 높이를 조정합니다. 대부분의 경우 줄을 여러 줄로 감쌀 것이고, 줄 바꿈을하면 줄이 커지고 줄을 채울 때 줄 바꿈을하면 나중에 줄이 짧아집니다 (줄이기 위해 축소됩니다). 행의 모든 ​​가능한 순열을 고려하여 원하는 영역에 가장 적합한 행을 찾을 수 있습니다.
즉, 이상적인 행 높이(height), 최대 축소(shrink) 요인 (행을 이상적인 위치에서부터 얼마나 아래로 확장 할 수 있는지) 및 최대 늘이기(stretch) 요소 (확장 할 수있는 정도)의 3 가지 주요 고려 사항이있었습니다.
알고리즘은 한 번에 하나씩 모든 사진을 확인하여 허용되는 행 나누기를 찾습니다. 즉 너비에 맞게 크기가 조정 된 경우 높이가 허용 범위 (maxShrink ≤ height ≤ maxStretch)에 속하는 사진 그룹입니다. 허용되는 휴식 시간을 찾을 때마다 가능성 목록에 추가되며 모든 사진과 가능한 모든 행 집합을 고려할 때까지 허용되는 휴식 시간을 찾습니다
예를 들어 14장의 사진을 찍을 때 허용되는 행은 3장 또는 4장 사진 이후 였을 수 있습니다. 3장에서 브레이크(wrap)를 하면 6장이나 7장에 허용되는 브레이크(wrap)가 주어집니다. 그러나 4번에서 브레이크(wrap)를하면 7번에서 브레이크(wrap)를 할 수 있습니다. 또는 8. 그리드에 대해 완전히 다른 여러 개의 유효한 레이아웃을 나타냅니다.
presentation
Possible photos to wrap (break) on
마지막 부분은 각 행에 대한 나쁜(badness) 점수를 계산하는 것입니다. 즉, 비 이상적입니다. 목표 높이(height)인 행(row)은 나쁜(badness) 점수가 0이고, 행이 축소되거나 늘어나면 나쁜 점수가 높아집니다. 각 행의 최종 비용 (한 순간에 의미가 있음)은 벌점(demerits)을 사용하여 계산됩니다. 벌점은 종종 나쁜(badness) 점의 정육면체 또는 정사각형에 몇 가지 벌칙(penalties)을 더한 것입니다 (예 : 줄 바꿈). 가장 나쁜(badness) 방법과 벌점(demerits)을 계산하는 방법에 대한 많은 글이 있습니다. 우리는 각 행의 비율을 최대 스트레치(stretch) / 수축(shrink) 크기에 비례하여 사용합니다 (힘은 이상적인 곳으로부터 멀리 떨어져있는 행을 처벌합니다).
알고리즘을 실행하면 노드의 그래프(graph of nodes)로 끝납니다. 각 노드는 중단 될 수있는 사진을 나타내고 각 가장자리는 행을 나타냅니다 (지정된 노드에 대해 여러 개의 가장자리가있을 수 있습니다. 여러 개의 중단 점이있을 수 있음을 나타냅니다. 사진부터 시작). 각 에지에 대해 비용 (벌점(demerit) 값)을 지정할 수 있습니다.
예를 들어 14개의 사진, 대상 행 높이(180px) 및 특정 뷰포트(1120px)에 대해 19개의 가능한 행 정렬(edges)을 발견하여 12개의 고유한 그리드 순열(또는 그래프를 통한 경로)을 유도했습니다. 아래에 표시된 것은 고유한 각 행과 연결할 수 있는 행입니다. 푸른 길(blue route)은 가장 나쁜 것입니다. 선을 따라 가면 각 조합이 모든 사진을 포함하는 전체 그리드를 구성한다는 것을 알 수 있습니다. 두 행은 같지 않으며 두 개의 그리드는 동일하지 않습니다.
presentation
14개의 사진에 대한 고유 한 행 및 그리드 조합
최적의 사진 그리드(즉, 가장 낮은 결합된 불량 행 세트를 가진 것)를 찾는 것은 그래프를 통한 최단 경로 계산만큼 간단합니다.
운좋게도 우리가 생산하는 그래프는 DAG (Directed Acyclic Graph)로 알려져 있습니다. DAG는 루프(loops)가없고 한 방향으로 만 이동할 수있는 노드입니다 (예 : 노드 / 사진을 반복 할 수 없음). 즉, 최단 경로 계산은 선형(linear) 시간으로 수행 될 수 있습니다 (컴퓨터가 신속하게 말함). 더 나은 점은 실제로 그래프를 생성하는 동안 최단 경로를 계산할 수 있다는 것입니다.
경로의 길이를 계산하려면 각 행에 할당 된 비용을 간단하게 합산하고 노드에 연결하는 새 모서리를 찾을 때마다 해당 노드에 대한 시작 경로가 더 짧아 지는지 확인하십시오.
다음은 14장의 사진을 통해 컴퓨터가 "보는 것(sees)"을 보여줍니다. 상단의 줄은 현재보고있는 사진 (행의 시작 및 끝 사진)을 보여 주며, 아래 그래프는 중단 점이 무엇인지 보여줍니다 발견 된, 그리고 어떤 에지가 연결되는지, 모든 포인트에서 각 노드에 대한 현재 최단 경로가 분홍색으로 강조 표시됩니다. 이것은 실제로 위에 표시된 그림 그래프의 또 다른 표현입니다. 상자 사이의 각 모서리는 고유 한 행 중 하나에 해당합니다.
첫 번째 사진에서부터 114의 비용으로 index 2에서 허용되는 중단 점을 찾습니다. 그러면 9483의 훨씬 높은 비용으로 index 3에서 허용 가능한 다른 중단 점을 찾습니다. 이제 두 개의 새 index (2 3) 그들이 깰 수있는 곳. 2에서 5와 6을 찾고,이 시점에서 6에 대한 최단 경로는 2를 통해 되돌아갑니다 (114 + 1442 = 1556). 사진 3이 6의 경로를 찾으면 비용을 다시 확인하지만 처음에는 3에 도달하는 데 너무 비 쌌기 때문에 총 비용 (9483 + 1007 = 10490)은 6이 해당 충성도를 2로 유지한다는 것을 의미합니다. 애니메이션에서 11에 대한 첫 번째 경로는 비 이상적이었고 노드 8이 고려 될 때 전환된다는 것을 알 수 있습니다.
presentation
14장의 사진을 위한 최적의 행 조합 찾기
마지막 사진 (index 13)에 도달 할 때까지 사진 세트 전체에서 이 작업을 계속합니다. 그 시점에서 가장 짧은 경로 (그리고 최상의 레이아웃)는 길을 따라 표시 한 가장 짧은 경로를 따라 (그리고 애니메이션에서 파란색으로) 찾아 낼 수 있습니다.
다음은 소박한 알고리즘이 생성 한 알고리즘 (왼쪽)과 라인 랩(line-wrap) 알고리즘이 달성 한 알고리즘 (오른쪽)을 비교 한 것입니다. 두 경우 모두 180px의 타겟 높이가 주어졌습니다. 흥미로운 두 가지 사항을 볼 수 있습니다. 하나는 소박한 레이아웃이 항상 뒤 따르는 것이고 다른 하나는 라인 랩(line-wrap)도 마찬가지로 행복하다는 것입니다. 그러나 라인 랩(line-wrap) 알고리즘은 목표 높이에 훨씬 더 가까운 그리드를 생성했습니다.
presentation
타겟 높이가 180px인 경우 레이아웃 접근 방식 비교
우리는 테스트에서 라인 랩(line-wrap) -FlexLayout이라는 알고리즘이 객관적으로나 주관적으로 더 바람직한 그리드를 생성한다는 것을 발견했습니다. 그것은 더 균일 한 높이의 격자 (행 사이의 작은 편차)와 요청 된 목표에 훨씬 가까운 평균 행 높이를 일관되게 생성했습니다. 파노라마 및 기타 엣지 케이스의 경우 일반적으로 소박한 알고리즘을 사용하는 것이 좋습니다. 소박한 방법으로는 파노라마 (초광각-ultra-wide) 사진이 첫 번째 행에 추가되므로 매우 작은 크기로 조정됩니다 그 행에 이미 여러 사진이있을 수 있기 때문에 FlexLayout을 사용하면 모든 가능한 행을 고려할 수 있고 파노라마를 지나치게 축소하는 것은 나쁜(badness) 값을 가지게되어 파노라마가 단독으로 배치되거나 다른 소수의 배치로 배치되는 그리드를 선택하도록 유도합니다 .
즉, 한 행을 훨씬 나 빠지거나 (극도로 짧거나 매우 높지 않음) 방지하기 위해 조금 더 나쁜 행 (목표 높이보다 약간 더 많은 픽셀)이 있을 수 있습니다. 그것은 놀라움을 최소화합니다.
가능한 레이아웃의 수에 영향을 미치는 많은 요소가 있습니다. 더 많은 사진이 가장 큰 요인 중 하나이지만, 뷰포트 너비가 구속 할 수 있으며 수축(shrink-ability) / 스트레치 능력(stretch-ability)에 대한 실제 매개 변수도 큰 효과가 있습니다.
presentation
뷰포트 크기에 따른 25개의 사진을 위한 독특한 레이아웃
좁은, 중간, 넓은 뷰포트에 걸쳐 25장의 사진에 대한 그래프를 보면 이런 느낌을 얻을 수 있다. 좁은 창에는 단지 몇 개의 중단점(breakpoints)이 있었지만 우리는 많은 행이 필요했고, 중간 창에는 더 많은 중단점(breakpoints)이 있었고, 심지어 더 많은 중단점(breakpoints)이 있는 동안 우리는 더 많은 행이 필요하지 않았기 때문에 실제로 전체 배열은 더 적었다.
사진 수에 따라 고유한 레이아웃의 총 수가 기하급수적으로 늘어납니다. 중간 너비 뷰포트의 경우 레이아웃을 통해 사진 집합의 실제 고유 경로를 계산하고 다음을 얻었습니다.
1000장의 사진에는 컴퓨터가 측정할 수 없을 정도로 많으므로 실제로 고유 경로의 정확한 수를 셀 수 없었습니다(이 경우 알고리즘이 합리적인 시간에 자신을 확인할 수 없더라도 거의 즉각적으로 최적의 경로를 찾았다는 것을 알 수 있다는 것은 놀라운 일입니다).
행당 허용되는 평균 중단점(breakpoints) 수를 취하여 가능한 행 수의 힘으로 높임으로써 고유한 배치 순환을 추정할 수 있습니다. 대부분의 뷰포트는 행당 2~3개의 중단점(breakpoints)을 지원하며, 대부분의 행은 약 5개 이상의 사진을 사용하여 2.5^(count/5)로 배치 횟수를 볼 수 있습니다.
끝에는 0이 79개인 1000장의 사진이 있습니다. 1260장의 사진에는 레이아웃이 구골(googol)로 되어 있습니다.
소박한 접근 방식은 단일 레이아웃을 고려하고 매번 선택할 수 있지만, 라인 래핑 알고리즘은 수백만 개, 수십억 개, 수조 개, 그리고 더 많은 고유한 레이아웃을 고려하고 최고의 레이아웃을 선택합니다.
혹시 궁금하실까 봐, 역시 굉장히 빠른 속도입니다. 100장의 사진을 배치하는 데 약 2,000분의 1초(2ms)가 소요됩니다. 1000장의 사진은 10ms, 10000장의 사진은 50ms, 100만장의 사진은 1.5초밖에 걸리지 않습니다(테스트 완료). 그에 비해 순진한 알고리즘은 동일한 숫자에 대해 약 2ms, 3ms, 30ms 및 400ms가 소요됩니다. - 더 빠르지만 의미 있는 정도는 아닙니다.
따라서 원래 목적은 사용 가능한 레이아웃의 수를 사용하여 사용 가능한 공간을 가장 잘 맞는 레이아웃(예측과 일치하는 레이아웃)을 선택하는 것이었지만, 추정된 크기와 실제 크기 사이의 간격을 원활하게 조정할 수 있다는 것을 발견했기 때문에 항상 최상의 그리드를 사용자에게 제공할 수 있습니다.
이 레이아웃은 팀이 Android와 iOS로 포팅한 이후 매우 잘 작동하며, 세 가지 구현이 동기화됩니다.
마지막 배치 방법은 각 섹션에 대해 알고리즘을 두 번 실행하는 것입니다. 처음 실행하면 세그먼트 내의 모든 사진을 배치하고, 두 번째 실행하면 섹션 내의 모든 세그먼트가 배치됩니다. 이 문제의 주된 이유는 행을 채우지 않는 매우 짧은 세그먼트가 있는 경우가 있는데, 레이아웃 알고리즘에서는 해당 세그먼트를 병합하는 옵션을 제안합니다. 사진과 마찬가지로 가장 이상적인 그룹을 선택하기 위해 가능한 모든 그룹화를 살펴봅니다.
presentation
병합된 세그먼트입니다.

3. 60fps 스크롤링

스크러빙이 가능한 사진과 이상적인 레이아웃은 브라우저에서 처리할 수 없다면 그다지 중요하지 않을 것입니다. 하지만 그 자체로는 불가능합니다. 다행스럽게도 우리가 도울 수 있습니다.
웹 사이트가 (초기 로드 시간이 아닌) 느릴 수 있는 가장 큰 방법 중 하나는 웹 사이트가 사용자 상호 작용(특히 스크롤링)에 얼마나 원활하게 대응하느냐 하는 것입니다. 브라우저는 초당 60회(60fps)마다 화면의 내용을 다시 그리려고 시도하고 성공하면 화면 모양이 매우 매끄러워집니다.
60fps를 유지하려면 각 업데이트를 16ms(1/60)로 렌더링해야 하며, 브라우저에서는 이벤트를 삭제하고 스타일 정보를 구문 분석하며 레이아웃을 계산하고 모든 요소를 픽셀로 변환한 다음 마지막으로 화면에 그려야 합니다. 앱이 자체 작업을 수행하려면 약 10ms가 남습니다.
이 10초 이내에 애플리케이션은 브라우저가 불필요한 작업을 수행하지 않도록 주의해야 합니다.

일정 크기의 DOM을 유지관리합니다.

페이지 성능의 가장 나쁜 점 중 하나는 너무 많은 요소를 가지고 있다는 것입니다. 문제는 두 가지입니다. 브라우저에 더 많은 메모리를 소비합니다(예: 50KB 썸네일 1,000장의 사진은 50메가바이트, 크롬을 충돌시키는 데 사용된 10,000장의 사진은 0.5기가바이트임). 또한 브라우저가 배치하는 동안 스타일과 위치를 계산하는 데 필요한 개별 조각이 더 많습니다.
presentation
불필요한 요소를 모두 제거합니다.
대부분의 사용자는 라이브러리에 수천 장의 사진을 저장하지만 화면은 대개 수십 장에 불과합니다.
따라서 모든 사진을 페이지에 배치하고 보관하는 대신 사용자가 스크롤할 때마다 어떤 사진이 표시되어야 하는지 계산하여 문서에 있는지 확인합니다.
문서에는 있었지만 더 이상 보이지 않는 사진의 경우 다시 꺼내겠습니다.
페이지를 스크롤하는 동안 수만 장의 사진을 스크롤하더라도 50장 이상의 사진은 존재하지 않을 수 있습니다. 이렇게 하면 페이지가 항상 빠르게 유지되고 탭이 손상되지 않습니다.
또한 사진을 세그먼트와 섹션으로 그룹화하므로 개별 사진 대신 전체 그룹을 분리할 수 있습니다.

변경을 최소화합니다.

Google 개발자 사이트에는 성능을 렌더링하는 방법과 Google Chrome에 내장된 강력한 분석 도구를 사용하는 방법에 대한 몇 가지 훌륭한 글들이 있습니다. 사진에 적용되는 몇 가지 측면에 대해 설명하겠지만, 다른 기록들은 읽을 만한 가치가 충분히 있습니다. 가장 먼저 이해해야 할 것은 페이지 렌더링 수명 주기입니다.
presentation
(크롬) 픽셀 파이프라인입니다.
페이지가 변경될 때마다(일반적으로 JavaScript에 의해 트리거되지만 때로는 CSS 스타일 또는 애니메이션) 브라우저에서 영향을 받는 요소에 적용되는 스타일을 확인하고 레이아웃(크기와 위치)을 재계산한 다음 모든 요소(텍스트, 이미지 등)를 픽셀로 변환)합니다. 효율을 위해 브라우저는 일반적으로 페이지를 여러 섹션으로 분할하여 레이어를 호출하고 별도로 페인트를 칠합니다. 따라서 이러한 레이어를 합성-compositing(배열-arranging)하는 마지막 단계가 수행됩니다.
이 문제에 대해 생각할 필요가 없는 대부분의 경우 브라우저가 매우 영리하지만, 브라우저에서 계속 페이지를 변경하는 경우(예: 사진을 계속 추가하거나 제거하는 등) 이를 효율적으로 수행할 필요가 있습니다.
presentation
Sections, segments, and tiles are positioning absolutely
업데이트를 최소화하는 방법 중 하나는 부모에 대한 모든 것을 배치하는 것입니다. 단면은 그리드를 기준으로 절대적(absolutely)으로 배치되고, 단면은 해당 단면을 기준으로 배치되며, 타일(사진)은 절대적으로 세그먼트를 기준으로 배치됩니다.
이것이 의미하는 것은 우리가 필요할 움직인 부분 때문에 예상과 실제 레이아웃 높이 다른, 대신 필요할 수백(또는 수천개)의 변화에 모든 사진. 우리는 오직 다음의 최고 위치를 업데이트할 필요가 있어요. 이 구조는 그리드의 각 부분을 불필요한 업데이트로부터 분리하는 데 도움이 됩니다.
최신 CSS는 브라우저에 알릴 수 있는 방법까지 제공합니다. 키 워드(the containkeyword)를 사용하면 DOM과 독립적으로 요소를 고려할 수 있는 정도를 지정할 수 있습니다. 섹션과 세그먼트에 주석을 달았습니다.
예를 들어 스크롤 이벤트가 단일 프레임 내에서 여러 번 실행될 수 있고 크기 조정에도 동일한 방식으로 실행될 수 있습니다. 어쨌든 두 번째 변경한 경우 브라우저에서 첫 번째 이벤트에 대한 스타일과 레이아웃을 강제로 재계산할 필요가 없습니다.
다행히도 그것을 피할 수 있는 편리한 방법이 있습니다. window.requestAnimationFrame(callback)을 사용하여 브라우저에 다음 repaint 전에 특정 기능을 실행하도록 요청할 수 있습니다. 스크롤 및 크기 조정 핸들러에서는 이를 사용하여 즉시 업데이트하는 대신 단일 콜백을 예약합니다. 크기를 조정하기 위해 한 단계 더 나아가 사용자가 최종 창 크기를 결정할 때까지 업데이트 시간을 0.5초간 지연합니다.
두 번째 일반적인 함정은 배치 스레싱(layout thrashing)이라고 알려져 있습니다. 브라우저가 레이아웃을 계산하면 레이아웃을 캐시하므로 모든 요소의 너비, 높이 또는 위치를 매우 빠르게 요청할 수 있습니다. 그러나 레이아웃에 영향을 미칠 수 있는 속성(예: 너비, 높이, 상단, 왼쪽)을 변경하면 캐시가 즉시 무효화되며, 이러한 속성 중 하나를 다시 읽으려고 하면 브라우저가 레이아웃을 강제로 재계산합니다(아마도 동일한 프레임에서 여러 번).
이 경우 많은 요소(예: 수백 장의 사진)에 대한 업데이트와 함께 루프에서 레이아웃 속성 중 하나를 읽은 다음 각 루프를 변경하면(예: 사진 또는 섹션을 올바른 위치로 이동) 루프의 모든 단계에 대해 새로운 레이아웃 계산이 트리거됩니다.
이를 방지하는 간단한 방법은 먼저 필요한 값을 모두 읽은 다음 모든 값(즉, 배치 및 쓰기에서 읽기를 분리)을 작성하는 것입니다. 우리의 경우 값을 읽는 것을 피하고 대신 모든 사진이 들어가야 하는 크기와 위치를 추적하고 모든 사진을 배치합니다. 스크롤 또는 크기 조정 시 추적한 위치를 기준으로 모든 계산을 다시 실행(re-run)할 수 있으며, 스레싱되지 않을 것임을 알고 안전하게 업데이트할 수 있습니다. 다음은 일반적인 스크롤 프레임의 모습입니다(모든 것을 한 번만 호출됨).
presentation
일반 스크롤 업데이트를 위한 이벤트 순서를 Rendering 및 Painting합니다.

long running code를 피합니다.

웹 작업자와 Fetch API와 같은 일부 기본 비동기 처리기를 제외하고 탭의 모든 것은 기본적으로 동일한 스레드(렌더링 및 JavaScript)에서 실행됩니다. 즉, 개발자가 실행하는 모든 코드는 페이지가 완료될 때까지(예: long-running 스크롤 이벤트 핸들러) 다시 그리기(redrawing)를 방지합니다.
우리의 그리드가 하는 두 가지 가장 많은 시간이 소요되는 일은 배치와 요소 생성입니다. 두 가지 모두 필수 작업으로 제한하려고 합니다.
예를 들어 레이아웃 알고리즘은 1,000장의 사진에는 10ms, 10,000의 경우 50ms의 시간이 소요됩니다. 이 경우 전체 프레임 허용 용량이 모두 소모될 수 있습니다. 그러나 그리드를 섹션과 세그먼트로 세분화하면 보통 언제든지 몇 백 장의 사진만 배치하면 됩니다(2~3ms 소요).
가장 "비용적인(costly)" 레이아웃 이벤트는 브라우저 크기 조정이어야 합니다. 모든 섹션의 크기를 다시 계산해야 하기 때문입니다. 대신 로드된 섹션의 경우에도 단순 추정 계산으로 돌아가서 현재 표시된 섹션에 대해서만 전체 FlexLayout을 수행합니다. 그런 다음 다른 섹션으로 스크롤할 때까지 전체 레이아웃 계산을 연기할 수 있습니다.
요소 생성도 마찬가지입니다. 사진 타일은 필요하기 직전에만 작성합니다.

결과

모든 고된 작업의 최종 결과는 60fps의 대부분을 유지할 수 있는 그리드이며, 때로는 일부 프레임을 떨어뜨려도 마찬가지입니다.
일반적으로 이러한 프레임은 주요 레이아웃 이벤트가 발생할 때(예: 새 섹션 삽입) 또는 브라우저가 매우 오래된 요소에 대해 garbage-collection을 수행할 때 발생합니다.
presentation
Framerate during scroll

4. 순간적인 느낌

저는 대부분의 프런트엔드 엔지니어들이 손재주가 좋은 많은 UI에서 역할을 한다는 것에 동의할 것이라고 생각합니다. 어떤 트릭을 사용할지, 어떻게 거울을 각도로 맞출지를 선택하는 것이 요령입니다.
제가 가장 좋아하는 예는 유튜브에 있는 동료가 저와 공유했던 비밀입니다. 처음 탐색 진행 표시줄을 구현했을 때(페이지 변경 시 맨 위에 나타나는 빨간색 막대) 실제로 진행 상황을 측정할 방법이 없었기 때문에 대부분의 페이지가 걸리는 속도로 재생한 다음, 페이지가 로드될 때까지 끝을 향해 "일치"합니다. 현재 버전이 여전히 가장하고 있는지, 실제로 작동하는지 전혀 알 수 없지만 중요한 것은 그것이 중요하지 않다는 것입니다.
presentation
YouTube progress bar
정확할 필요는 없었습니다. 중요한 것은 페이지 응답에 도움이 되었다는 것입니다.
이 섹션에서는 Google 포토가 실제 사진보다 조금 더 빠르게 나타나도록 만드는 몇 가지 요령(대부분 이미지 로드 시간을 위장하는 방법)에 대해 설명합니다.
첫 번째, 아마도 가장 효과적인 것은 여러분이 곧 보게 될 것으로 생각되는 컨텐츠를 사전에 로드하는 것입니다.
presentation
표시된 타일을 로드한 후 미리 보기에서 스크롤할 때까지 미리 보기를 로드하도록 한 페이지를 앞에 두려고 합니다.
그러나 특히 HDPI 화면(더 큰 미리 보기를 로드해야 하는 경우)의 경우 빠르게 스크롤할 경우 네트워크 연결이 이러한 모든 요청을 제 시간에 충족하지 못할 수 있습니다.
우리는 미래에 최대 4개 또는 5개의 전체 스크린에 극소량의 placeholders를 적재하고, 일단 뷰포트에 가까워지면 이들을 교체함으로써 이 문제를 해결합니다.
즉, 모든 사진을 실제로 보는 데 적합한 속도로 비교적 느리게 스크롤하는 경우 로딩이 표시되지 않아야 하며, 빠르게(사진 검색 중임을 암시하는 속도로) 빠르게 스크러빙하는 경우 검색을 안내할 수 있도록 시각적 컨텍스트를 충분히 제공할 수 있습니다.
이것은 불필요한 작업을 지나치게 많이 하는 것과 더 나은 경험을 제공하는 것 사이의 복잡한 절충입니다.
우리는 몇 가지 요인을 고려합니다. 첫 번째는 스크롤 방향을 관찰하고 사용자가 향하는 방향으로만 컨텐츠를 사전 로드하는 것입니다. 또한 스크롤 속도를 측정하고 스크러빙할 것으로 판단되는 즉시 전체 리소스 축소 이미지 로딩을 건너뛰고, 훨씬 높은 임계값에서 콘텐츠를 통과하면 저수준 사전 로드를 비활성화합니다.
각 케이스(일반 섬네일 및 low-res)에서 영상을 스케일링합니다. 이제 현대 화면에는 고해상도 이미지가 있으므로 이미지가 바삭해 보이도록 하는 일반적인 방법은 채우려는 공간의 두 배 크기인 이미지를 로드한 다음 축소하는 것입니다(따라서 실제 픽셀이 차지하는 공간보다 더 많음). low-res placeholders의 경우 매우 작은 이미지와 낮은 압축 품질(예: 25%)을 요청하고 크기를 조정합니다.
여기 sleepy leopard의 예가 있습니다. 왼쪽 이미지는 타일이 완전히 로드될 때 그리드에서 사용됩니다(반 크기로 축소됨). 오른쪽 이미지는 빠르게 스크롤할 때만 볼 수 있는 low-res placeholder입니다(크기가 증가함).
presentation
Normal and low-res placeholder tiles
또한 바이트 크기를 관찰합니다. HDPI 썸네일은 71.2KB(gzipped)였고, 낮은 자리 표시자는 889B(gzipped)에 불과했습니다. — thumbnail이 80배 더 컸습니다! 다시 말해, 그리드에 있는 단일 타일은 4페이지 이상의 low-res placeholders와 동일합니다.
네트워크 트래픽이 매우 적게 증가하면 사용자에게 훨씬 더 나은 환경을 제공할 수 있으며, 항상 꽉 찼다고 느끼고 항상 시각적 컨텍스트를 제공할 수 있습니다.
저층 타일과의 마지막 접촉은 브라우저에 그것들을 렌더링해 달라고 요청한 방법입니다. 기본적으로 이미지를 스케일업할 때 브라우저가 이미지를 약간 부드럽게 만들지만(아래 중앙 이미지) 이 방법은 그다지 좋아 보이지 않습니다. 흐릿한 필터(가장 오른쪽 이미지)를 적용하면 보다 고의적으로 보일 수 있지만, 단점은 필터가 계산적으로 비싸다는 것입니다. 수백 개의 요소에 적용하면 렌더링 및 스크롤 성능에 부정적인 영향을 미칩니다. 그래서 우리는 다른 방향으로 이동하여 브라우저에 픽셀링된(가장 왼쪽 이미지) 이미지를 그대로 두도록 요청하여 로우레즈 룩으로 기울였습니다. — 솔직히 말해서, 이것이 오늘날에도 제품에 들어 있는지 잘 모르겠습니다. 몇 가지 리팩터링이 있었습니다.
presentation
Low-res thumbnail rendering options
뷰포트에 있는 후 교체할 때(빠른 스크롤을 하는 동안 제외) 사용자가 low-res 이미지를 볼 수 없기를 희망하지만, 이전에 빠른 애니메이션을 사용하여 로딩된 것처럼 보이도록 했습니다(점멸하는 대신). 두 사진을 오버레이하고 불투명도를 애니메이션화하면 쉽게 얻을 수 있습니다(완전 투명에서 완전 불투명). — 이 크로스 페이드 기술은 웹에서 매우 보편화되었습니다. 예를 들어 이 중간 포스트의 모든 이미지가 그랬을 수 있습니다. 그 이후로 저온/고온 교차 페이드가 꺼졌다고 생각하지만, 빈(회색) 타일에서 이미지로 여전히 발생합니다.
이미지가 로드되는 것처럼 보이게 합니다. 우리는 이것을 빠르게 (100ms) 해냈는데, 이것은 관대함을 느끼지 않고, 가장자리를 뗄 수 있는 충분한 시간입니다. 좀 더 관찰할 수 있도록 아래 애니메이션의 속도를 줄였습니다.
presentation
Loading transition at slower speed
썸네일 사진에서 전체 화면 보기로 전환할 때 이 기술을 두 번째로 사용합니다. 사용자가 타일을 클릭하면 즉시 전체 이미지 로딩이 시작되고 그 동안 축소 이미지 크기를 조정하고 미리 보기를 애니메이션하면 전체 이미지가 로드되면 오버레이되고 그 사이에 불투명도 애니메이션을 수행합니다. 유일한 차이점은 이번에는 단일 요소에만 적용하기 때문에 더 비싼 블라인드 필터(화소 효과가 큰 영상에서 덜 매력적이기 때문에 유용)를 사용할 수 있다는 점입니다.
presentation
Transition from photo grid to full-screen
항상 사진을 스크롤하거나 전체 화면 보기로 전환할 때 콘텐츠가 준비되지 않은 경우에도 항상 사용자의 입력에 응답하고 있다고 생각하는 사용자에게 원활한 환경을 제공하기 위해 노력하고 있습니다. 타일을 클릭했을 때 빈 화면이 표시되거나 전체 사진이 로드될 때까지 아무것도 수행하지 않은 경우 이 느낌을 비교합니다.
빈 부분에도 이 개념을 적용합니다. 상기한 경우 스크러빙 가능한 그리드는 필요할 때만 섹션을 로드합니다(타일처럼 근처의 섹션을 사전 로드하려고 시도함). 즉, 특히 스크롤 막대를 잡고 앞으로 경주를 하는 경우 아직 로드되지 않은 섹션으로 이동할 수 있습니다. 그리드에 미리 할당된 공간이 있지만 어떤 사진이 있는지, 어떤 레이아웃인지 알 수 없습니다.
스크롤을 좀 더 자연스럽게 하기 위해 대상 행 크기와 같은 높이에 빈 타일처럼 보이도록 색칠된 적재되지 않은 부분에 텍스처를 넣었습니다. 처음 시작할 때는 행(가장 왼쪽 사진)처럼 보였지만, 팀은 최근 사진의 질감을 행과 기둥(가장 오른쪽 사진)으로 변경했습니다. 가운데 그림은 단면이 로드되었지만 타일이 로드되지 않은 모습입니다.
presentation
Grid patterns during loading states
사진 로드 상태의 animal tracks과 비슷합니다. 다음 번에 Google 포토에서 이 차이를 확인할 수 있는지 확인하십시오.
텍스쳐에 이미지를 사용하는 대신 실제로 CSS를 사용하여 생성되었습니다. 따라서 너비와 높이를 그리드에 사용되는 목표 행 높이에 맞게 동적으로 생성할 수 있다는 추가 보너스가 제공됩니다.
몇 가지 다른 요령이 있지만 네트워크 요청의 우선순위를 정하는 것이 대부분입니다. 예를 들어 네트워크에 100개의 사진 미리 보기 요청이 쇄도하는 대신 한 번에 10개씩 배치하므로 사용자가 갑자기 다시 스크롤을 시작해도 90장의 사진을 로드했지만 사용하지 않았습니다. 마찬가지로, 우리는 항상 화면 밖의 미리 보기보다 눈에 보이는 미리 보기를 로드하는 우선 순위를 정합니다.
또한 유사한 크기의 섬네일을 이미 로드했으며 대신 사용할 수 있는지 여부도 확인합니다. 이 마지막 사용 사례는 주로 브라우저 크기 조정 후이며, 대개 거의 동일하지만 몇 픽셀만 다른 그리드 레이아웃으로 끝나는 경우가 많습니다. 모든 사진을 다시 다운로드하는 대신 이미 있는 이미지의 크기를 약간 조정합니다(차이가 너무 큰 경우에만 새 이미지를 선택).

결론

구글 포토 체험의 모든 세부사항에 엄청난 양의 관심과 관심이 쏟아집니다. 그리고 사진 그리드는 훨씬 더 큰 제품의 한 부분일 뿐입니다.
처음에는 간단하고 심지어 정지된 것처럼 보일 수도 있지만, 그리드는 거의 항상 컨텐츠를 로드, 프리페치, 애니메이션, 생성, 제거 및 프레젠테이션하는 것을 생각하고 있습니다.
그리드의 성능을 지속적으로 향상시키는 것이 팀의 지속적인 우선 과제입니다. 또한 스크롤 프레임률, 섹션 및 이미지 로드 시간 및 기타 여러 메트릭을 측정하기 위한 포괄적인 모니터링 기능을 갖추고 있으며 매년 성능 및 경험을 지속적으로 개선합니다.
다음은 갤러리를 스크롤하는 모양을 캡처한 짧은 화면입니다. 느린 속도에서는 전체 이미지만 볼 수 있습니다. 속도를 높이면 픽셀링된 placeholders를 볼 수 있습니다. 다시 속도를 늦추면 바로 해결됩니다. 앞으로 경주할 때 빈 회색 타일은 그리드가 따라잡을 때까지 잠깐 볼 수 있습니다.
Scrolling and scrubbing the photo grid
Photos Vincent Mo의 전 매니저에게 감사 드립니다. 그는 그의 지원 외에도 이 게시물 전체에 사용된 모든 멋진 사진들을 촬영했습니다(개발 중 테스트 세트로 사용됨). 또한 Photos Web Lead인 Jeremy Selier와 그의 팀에게도 Photos Web UI를 지속적으로 유지하고 개선해 줍니다.
Thanks to Laura Granka, Teon Harasymiv, Jeremy Selier, and Barbara Eldredge.

이 글은 번역 글입니다. 원본 링크입니다.


안녕하세요! Early adopter입니다.
페이스북 [DTF] 디자인 번역 공장 - Design Translation Factory 그룹도 많이 가입해주시길 바랍니다.
"보버"에서 "디자인 번역 공장" 연재를 저와 함께 해주실 분을 찾습니다. 하단 "리뷰" 또는 "페이스북"으로 편하게 메시지 주세요!
PS. 제가 사용하는 블로그 "보버"에 "함께 쓰는 블로그"라는 기능이 요번에 추가됐네요 ㅋ (미디엄에 퍼블리케이션 같은 기능..)
하단 링크 글을 보시면 "디자인 번역 공장"에 어떻게 함께 연재할 수 있는지 자세히 설명되어있습니다. 또는 쉽게 FB 메시지 주세요!
https://bit.ly/2LxR0Bz