Google 사진 수상작 갤러리

일마리 헤이키넨

Google 사진 수상작 웹사이트

Google은 최근 Google 포토그래피 상품 사이트에 갤러리 섹션을 개설했습니다. 갤러리에는 Google+에서 가져온 사진의 무한 스크롤 목록이 표시됩니다. 갤러리의 사진 목록을 관리하는 데 사용하는 AppEngine 앱에서 사진 목록을 가져옵니다. 또한 갤러리 앱을 Google 코드에 오픈소스 프로젝트로 출시했습니다.

갤러리 페이지

갤러리의 백엔드는 AppEngine 앱으로, Google+ API를 사용하여 Google 사진 수상작 해시태그 중 하나 (예: #megpp 및 #travelgpp)가 포함된 게시물을 검색합니다. 그런 다음 앱은 이러한 게시물을 검토되지 않은 사진 목록에 추가합니다. YouTube 콘텐츠 팀은 일주일에 한 번 검토되지 않은 사진 목록을 검토하고 콘텐츠 가이드라인을 위반하는 사진을 신고합니다. 검토 버튼을 누르면 신고되지 않은 사진이 갤러리 페이지에 표시되는 사진 목록에 추가됩니다.

검토 백엔드

갤러리 프런트엔드는 Google 클로저 라이브러리를 사용하여 빌드됩니다. 갤러리 위젯 자체는 클로저 구성요소입니다. 소스 파일의 상단에서 이 파일이 photographyPrize.Gallery라는 구성요소를 제공하고 앱에서 사용하는 클로저 라이브러리의 부분이 필요하다고 클로저에 알립니다.

goog.provide('photographyPrize.Gallery');

goog.require('goog.debug.Logger');
goog.require('goog.dom');
goog.require('goog.dom.classes');
goog.require('goog.events');
goog.require('goog.net.Jsonp');
goog.require('goog.style');

갤러리 페이지에는 JSONP를 사용하여 AppEngine 앱에서 사진 목록을 가져오는 자바스크립트가 있습니다. JSONP는 jsonpcallback("responseValue")와 같은 스크립트를 삽입하는 간단한 교차 출처 자바스크립트 해킹입니다. JSONP를 처리하기 위해 클로저 라이브러리에서 goog.net.Jsonp 구성요소를 사용합니다.

갤러리 스크립트는 사진 목록을 살펴보고 갤러리 페이지에 사진을 표시하기 위한 HTML 요소를 생성합니다. 무한 스크롤은 창 스크롤 이벤트에 연결하고 창 스크롤이 페이지 하단에 가까워질 때 새 사진 배치를 로드하는 방식으로 작동합니다. 새 사진 목록 세그먼트를 로드한 후 갤러리 스크립트는 사진 요소를 만들고 이를 갤러리 요소에 추가하여 표시합니다.

이미지 목록 표시

이미지 목록 표시 방법은 매우 기본적인 것입니다. 이미지 목록을 탐색하고 HTML 요소와 +1 버튼을 생성합니다. 다음 단계는 생성된 목록 세그먼트를 갤러리의 기본 갤러리 요소에 추가하는 것입니다. 아래 코드에서 일부 클로저 컴파일러 규칙을 확인할 수 있습니다. JSDoc 주석의 유형 정의와 @private 가시성을 확인하세요. 비공개 메서드의 경우 이름 뒤에 밑줄 (_)이 표시됩니다.

/**
 * Displays images in imageList by putting them inside the section element.
 * Edits image urls to scale them down to imageSize x imageSize bounding
 * box.
 *
 * @param {Array.<Object>} imageList List of image objects to show. Retrieved
 *                                   by loadImages.
 * @return {Element} The generated image list container element.
 * @private
 */
photographyPrize.Gallery.prototype.displayImages_ = function(imageList) {
  
  // find the images and albums from the image list
  for (var j = 0; j < imageList.length; j++) {
    // change image urls to scale them to photographyPrize.Gallery.MAX_IMAGE_SIZE
  }

  // Go through the image list and create a gallery photo element for each image.
  // This uses the Closure library DOM helper, goog.dom.createDom:
  // element = goog.dom.createDom(tagName, className, var_childNodes);

  var category = goog.dom.createDom('div', 'category');
  for (var k = 0; k < items.length; k++) {
    var plusone = goog.dom.createDom('g:plusone');
    plusone.setAttribute('href', photoPageUrl);
    plusone.setAttribute('size', 'standard');
    plusone.setAttribute('annotation', 'none');

    var photo = goog.dom.createDom('div', {className: 'gallery-photo'}, ...)
    photo.appendChild(plusone);

    category.appendChild(photo);
  }
  this.galleryElement_.appendChild(category);
  return category;
};

스크롤 이벤트 처리

방문자가 페이지를 하단으로 스크롤하여 새 이미지를 로드해야 하는 시점을 확인하기 위해 갤러리는 창 객체의 스크롤 이벤트에 연결합니다. 브라우저 구현의 차이점을 확인하기 위해 클로저 라이브러리의 몇 가지 편리한 유틸리티 함수를 사용하고 있습니다. goog.dom.getDocumentScroll()는 현재 문서 스크롤 위치가 있는 {x, y} 객체를 반환하고 goog.dom.getViewportSize()는 창 크기를 반환하고 HTML 문서의 높이인 goog.dom.getDocumentHeight()를 반환합니다.

/**
 * Handle window scroll events by loading new images when the scroll reaches
 * the last screenful of the page.
 *
 * @param {goog.events.BrowserEvent} ev The scroll event.
 * @private
 */
photographyPrize.Gallery.prototype.handleScroll_ = function(ev) {
  var scrollY = goog.dom.getDocumentScroll().y;
  var height = goog.dom.getViewportSize().height;
  var documentHeight = goog.dom.getDocumentHeight();
  if (scrollY + height >= documentHeight - height / 2) {
    this.tryLoadingNextImages_();
  }
};

/**
 * Try loading the next batch of images objects from the server.
 * Only fires if we have already loaded the previous batch.
 *
 * @private
 */
photographyPrize.Gallery.prototype.tryLoadingNextImages_ = function() {
  // ...
};

이미지 로드

서버에서 이미지를 로드하려면 goog.net.Jsonp 구성요소를 사용합니다. 쿼리하려면 goog.Uri가 필요합니다. 쿼리를 만들고 나면 쿼리 매개변수 객체 및 성공 콜백 함수를 사용하여 쿼리를 Jsonp 제공자에게 전송할 수 있습니다.

/**
 * Loads image list from the App Engine page and sets the callback function
 * for the image list load completion.
 *
 * @param {string} tag Fetch images tagged with this.
 * @param {number} limit How many images to fetch.
 * @param {number} offset Offset for the image list.
 * @param {function(Array.<Object>=)} callback Function to call
 *        with the loaded image list.
 * @private
 */
photographyPrize.Gallery.prototype.loadImages_ = function(tag, limit, offset, callback) {
  var jsonp = new goog.net.Jsonp(
      new goog.Uri(photographyPrize.Gallery.IMAGE_LIST_URL));
  jsonp.send({'tag': tag, 'limit': limit, 'offset': offset}, callback);
};

위에서 언급했듯이 갤러리 스크립트는 코드를 컴파일하고 축소하기 위해 클로저 컴파일러를 사용합니다. 클로저 컴파일러는 올바른 입력을 시행하는 데 유용하며 (속성 유형을 설정하려면 주석에 @type foo JSDoc 표기법을 사용), 메서드에 대한 주석이 없는 경우에도 알려줍니다.

단위 테스트

또한 갤러리 스크립트용 단위 테스트가 필요했으므로 클로저 라이브러리에 단위 테스트 프레임워크가 내장되어 있으면 편리합니다. jsUnit 규칙을 따르므로 쉽게 시작할 수 있습니다.

단위 테스트를 작성하는 데 도움이 되도록 자바스크립트 파일을 파싱하고 갤러리 구성요소의 각 메서드 및 속성에 대해 실패한 단위 테스트를 생성하는 작은 Ruby 스크립트를 작성했습니다. 다음과 같은 스크립트가 주어진 경우:

Foo = function() {}
Foo.prototype.bar = function() {}
Foo.prototype.baz = "hello";

테스트 생성기는 각 속성에 관한 빈 테스트를 생성합니다.

function testFoo() {
  fail();
  Foo();
}

function testFooPrototypeBar = function() {
  fail();
  instanceFoo.bar();
}

function testFooPrototypeBaz = function() {
  fail();
  instanceFoo.baz;
}

이렇게 자동 생성된 테스트를 통해 코드 테스트를 쉽게 작성할 수 있었고 모든 메서드와 속성이 기본적으로 다루어졌습니다. 테스트에 실패하면 심리적인 영향이 생기기 때문에 테스트를 하나씩 진행하고 적절한 테스트를 작성해야 했습니다. 코드 적용 범위 측정기와 함께 사용하면 테스트와 적용 범위를 모두 녹색으로 만드는 재미있는 게임입니다.

요약

Gallery+는 #해시태그와 일치하는 Google+ 사진의 검토된 목록을 표시하는 오픈소스 프로젝트입니다. Go 및 클로저 라이브러리를 사용하여 빌드되었습니다. 백엔드는 App Engine에서 실행됩니다. Gallery+는 Google 사진 수상작 웹사이트에서 제출 갤러리를 표시하는 데 사용됩니다. 이 도움말에서는 프런트엔드 스크립트의 유용한 부분을 살펴봤습니다. App Engine 개발자 관계팀의 동료인 Johan Euphrosine이 백엔드 앱에 대해 이야기하는 두 번째 글을 쓰고 있습니다. 백엔드는 Google의 새로운 서버 측 언어인 Go로 작성되었습니다. 따라서 Go 코드의 프로덕션 예시를 보려면 계속 지켜봐 주세요.

참조