검색 위젯으로 검색 인터페이스 만들기

검색 위젯은 웹 애플리케이션에 맞춤설정 가능한 검색 인터페이스를 제공합니다. 소량의 HTML 및 자바스크립트만 구현하면 속성 및 페이지로 나누기와 같은 일반 검색 기능을 사용할 수 있습니다. CSS와 자바스크립트를 사용하여 인터페이스 일부을 맞춤설정할 수도 있습니다.

위젯이 제공하는 것보다 더 많은 유연성이 필요한 경우 Query API를 사용하는 것이 좋습니다. Query API를 사용하여 검색 인터페이스를 만드는 방법에 대한 자세한 내용은 Query API로 검색 인터페이스 만들기를 참조하세요.

검색 인터페이스 빌드

검색 인터페이스를 빌드하려면 다음과 같은 여러 단계를 거쳐야 합니다.

  1. 검색 애플리케이션 구성
  2. 애플리케이션용 클라이언트 ID 생성
  3. 검색창 및 결과용 HTML 마크업 추가
  4. 페이지에 위젯 로드
  5. 위젯 초기화

검색 애플리케이션 구성

각 검색 인터페이스에는 관리 콘솔에서 정의된 검색 애플리케이션이 있어야 합니다. 검색 애플리케이션은 데이터 소스, 패싯, 검색 품질 설정과 같은 쿼리의 추가 정보를 제공합니다.

검색 애플리케이션을 만들려면 커스텀 검색 환경 만들기를 참조하세요.

애플리케이션용 클라이언트 ID 생성

Google Cloud Search API에 대한 액세스 구성 단계 외에도 웹 애플리케이션용 클라이언트 ID를 생성해야 합니다.

프로젝트 구성

프로젝트를 구성할 때 다음을 수행합니다.

  • 웹브라우저 클라이언트 유형을 선택합니다.
  • 앱의 원본 URI을 제공합니다.
  • 생성된 클라이언트 ID를 기록합니다. 다음 단계를 완료하려면 이 클라이언트 ID가 필요합니다. 위젯에서는 클라이언트 보안 비밀번호가 필요하지 않습니다.

자세한 내용은 클라이언트 측 웹 애플리케이션용 OAuth 2.0을 참조하세요.

HTML 마크업 추가

위젯이 작동하려면 약간의 HTML 작업이 필요하며 다음을 제공해야 합니다.

  • 검색창의 input 요소
  • 제안 팝업을 고정하는 요소
  • 검색결과를 포함하는 요소
  • (선택사항) 속성 제어를 포함하는 요소 제공

다음 HTML 스니펫은 결합될 요소가 id 속성으로 식별되는 검색 위젯의 HTML을 보여줍니다.

serving/widget/public/with_css/index.html
<div id="search_bar">
  <div id="suggestions_anchor">
    <input type="text" id="search_input" placeholder="Search for...">
  </div>
</div>
<div id="facet_results"></div>
<div id="search_results"></div>

위젯 로드

위젯은 로더 스크립트를 통해 동적으로 로드됩니다. 로더를 포함하려면 다음과 같이 <script> 태그를 사용합니다.

serving/widget/public/with_css/index.html
<!-- Google API loader -->
<script src="https://apis.google.com/js/api.js?mods=enable_cloud_search_widget&onload=onLoad" async defer></script>

스크립트 태그에 onload 콜백을 제공해야 합니다. 이 함수는 로더가 준비되면 호출됩니다. 로더가 준비되면 gapi.load()를 호출하여 위젯을 계속 로드하여 API 클라이언트, Google 로그인, Cloud Search 모듈을 로드합니다.

serving/widget/public/with_css/app.js
/**
* Load the cloud search widget & auth libraries. Runs after
* the initial gapi bootstrap library is ready.
*/
function onLoad() {
  gapi.load('client:auth2:cloudsearch-widget', initializeApp)
}

initializeApp() 함수는 모든 모듈이 로드된 후에 호출됩니다.

위젯 초기화

먼저 생성된 클라이언트 ID와 https://www.googleapis.com/auth/cloud_search.query 범위로 gapi.client.init() 또는 gapi.auth2.init()를 호출하여 클라이언트 라이브러리를 초기화합니다. 그런 다음 gapi.cloudsearch.widget.resultscontainer.Buildergapi.cloudsearch.widget.searchbox.Builder 클래스를 사용하여 위젯을 구성하고 HTML 요소에 결합합니다.

다음 예시에서는 위젯을 초기화하는 방법을 보여줍니다.

serving/widget/public/with_css/app.js
/**
 * Initialize the app after loading the Google API client &
 * Cloud Search widget.
 */
function initializeApp() {
  // Load client ID & search app.
  loadConfiguration().then(function() {
    // Set API version to v1.
    gapi.config.update('cloudsearch.config/apiVersion', 'v1');

    // Build the result container and bind to DOM elements.
    var resultsContainer = new gapi.cloudsearch.widget.resultscontainer.Builder()
      .setSearchApplicationId(searchApplicationName)
      .setSearchResultsContainerElement(document.getElementById('search_results'))
      .setFacetResultsContainerElement(document.getElementById('facet_results'))
      .build();

    // Build the search box and bind to DOM elements.
    var searchBox = new gapi.cloudsearch.widget.searchbox.Builder()
      .setSearchApplicationId(searchApplicationName)
      .setInput(document.getElementById('search_input'))
      .setAnchor(document.getElementById('suggestions_anchor'))
      .setResultsContainer(resultsContainer)
      .build();
  }).then(function() {
    // Init API/oauth client w/client ID.
    return gapi.auth2.init({
        'clientId': clientId,
        'scope': 'https://www.googleapis.com/auth/cloud_search.query'
    });
  });
}

위 예에서는 다음과 같이 정의된 구성에 사용되는 변수 두 개를 참조합니다.

serving/widget/public/with_css/app.js
/**
* Client ID from OAuth credentials.
*/
var clientId = "...apps.googleusercontent.com";

/**
* Full resource name of the search application, such as
* "searchapplications/<your-id>".
*/
var searchApplicationName = "searchapplications/...";

로그인 환경 맞춤설정

기본적으로 위젯은 사용자가 쿼리를 입력하기 시작할 때 로그인하고 앱을 승인하라는 메시지를 표시합니다. 웹사이트용 Google 로그인을 사용하여 사용자에게 보다 맞춤화된 로그인 환경을 제공할 수 있습니다.

사용자 직접 승인

Google 계정으로 로그인을 사용하여 사용자의 로그인 상태를 모니터링하고 필요에 따라 사용자를 로그인 또는 로그아웃시킬 수 있습니다. 예를 들어 다음 예에서는 isSignedIn 상태를 관찰하여 로그인 변경사항을 모니터링하고 GoogleAuth.signIn() 메서드를 사용하여 버튼 클릭에서 로그인을 시작합니다.

serving/widget/public/with_signin/app.js
// Handle sign-in/sign-out.
let auth = gapi.auth2.getAuthInstance();

// Watch for sign in status changes to update the UI appropriately.
let onSignInChanged = (isSignedIn) => {
  // Update UI to switch between signed in/out states
  // ...
}
auth.isSignedIn.listen(onSignInChanged);
onSignInChanged(auth.isSignedIn.get()); // Trigger with current status.

// Connect sign-in/sign-out buttons.
document.getElementById("sign-in").onclick = function(e) {
  auth.signIn();
};
document.getElementById("sign-out").onclick = function(e) {
  auth.signOut();
};

자세한 내용은 Google 계정으로 로그인을 참고하세요.

사용자 자동 로그인

로그인 환경을 더욱 간소화하려면 조직 내 사용자를 대신하여 애플리케이션을 사전 승인하면 됩니다. 이 방법은 Cloud Identity Aware Proxy를 사용하여 애플리케이션을 보호하는 경우에도 유용합니다.

자세한 내용은 IT 앱으로 Google 로그인 사용을 참조하세요.

인터페이스 맞춤설정

여러 방법을 함께 활용하여 검색 인터페이스의 모양을 변경할 수도 있습니다.

  • CSS를 사용하여 스타일 재정의
  • 어댑터를 사용하여 요소 데코레이션
  • 어댑터를 사용하여 커스텀 요소 만들기

CSS를 사용하여 스타일 재정의

검색 위젯에는 자체 CSS가 포함되어 있어 제안 및 결과 요소뿐만 아니라 페이지로 나누기 제어 스타일을 정의할 수 있습니다. 필요에 따라 이러한 요소의 스타일을 재정의할 수 있습니다.

로드 중에 검색 위젯은 기본 스타일시트를 동적으로 로드합니다. 이는 애플리케이션 스타일시트가 로드된 후에 수행되어 규칙의 우선순위를 높입니다. 자체 스타일이 기본 스타일보다 우선 시 되도록 상위 선택기를 사용하여 기본 규칙의 특정성을 높입니다.

예를 들어 문서의 정적 link 또는 style 태그에 로드되면 다음 규칙은 아무런 영향을 미치지 않습니다.

.cloudsearch_suggestion_container {
  font-size: 14px;
}

대신 페이지에서 선언된 상위 컨테이너의 ID 또는 클래스로 규칙을 검증합니다.

#suggestions_anchor .cloudsearch_suggestion_container {
  font-size: 14px;
}

지원 클래스 목록과 위젯에서 생성된 HTML 예는 지원되는 CSS 클래스 참조를 참조하세요.

어댑터를 사용하여 요소 데코레이션

렌더링하기 전에 요소를 장식하려면 decorateSuggestionElement 또는 decorateSearchResultElement.와 같은 장식 메서드 중 하나를 구현하는 어댑터를 만들고 재구성합니다.

예를 들어 다음 어댑터는 제안 및 결과 요소에 커스텀 클래스를 추가합니다.

serving/widget/public/with_decorated_element/app.js
/**
 * Search box adapter that decorates suggestion elements by
 * adding a custom CSS class.
 */
function SearchBoxAdapter() {}
SearchBoxAdapter.prototype.decorateSuggestionElement = function(element) {
  element.classList.add('my-suggestion');
}

/**
 * Results container adapter that decorates suggestion elements by
 * adding a custom CSS class.
 */
function ResultsContainerAdapter() {}
ResultsContainerAdapter.prototype.decorateSearchResultElement = function(element) {
  element.classList.add('my-result');
}

위젯을 초기화할 때 어댑터를 등록하려면 각 Builder 클래스의 setAdapter() 메서드를 사용합니다.

serving/widget/public/with_decorated_element/app.js
// Build the result container and bind to DOM elements.
var resultsContainer = new gapi.cloudsearch.widget.resultscontainer.Builder()
  .setAdapter(new ResultsContainerAdapter())
  // ...
  .build();

// Build the search box and bind to DOM elements.
var searchBox = new gapi.cloudsearch.widget.searchbox.Builder()
  .setAdapter(new SearchBoxAdapter())
  // ...
  .build();

데코레이터는 컨테이너 요소뿐만 아니라 하위 요소의 속성을 모두 수정할 수 있습니다. 또한 데코레이션 중에 하위 요소를 추가 또는 삭제할 수 있습니다. 그러나 요소의 구조를 변경할 경우에는 요소를 데코레이션하는 대신 직접 만드는 것이 좋습니다.

어댑터를 사용하여 커스텀 요소 만들기

추천, 속성 컨테이너 또는 검색결과의 맞춤 요소를 만들려면 createSuggestionElement, createFacetResultElement 또는 createSearchResultElement를 각각 구현하는 어댑터를 만들고 등록합니다.

다음 어댑터는 HTML <template> 태그를 사용하여 맞춤 추천 및 검색 결과 요소를 만드는 방법을 보여줍니다.

serving/widget/public/with_custom_element/app.js
/**
 * Search box adapter that overrides creation of suggestion elements.
 */
function SearchBoxAdapter() {}
SearchBoxAdapter.prototype.createSuggestionElement = function(suggestion) {
  let template = document.querySelector('#suggestion_template');
  let fragment = document.importNode(template.content, true);
  fragment.querySelector('.suggested_query').textContent = suggestion.suggestedQuery;
  return fragment.firstElementChild;
}

/**
 * Results container adapter that overrides creation of result elements.
 */
function ResultsContainerAdapter() {}
ResultsContainerAdapter.prototype.createSearchResultElement = function(result) {
  let template = document.querySelector('#result_template');
  let fragment = document.importNode(template.content, true);
  fragment.querySelector('.title').textContent = result.title;
  fragment.querySelector('.title').href = result.url;
  let snippetText = result.snippet != null ?
    result.snippet.snippet : '';
  fragment.querySelector('.query_snippet').innerHTML = snippetText;
  return fragment.firstElementChild;
}

위젯을 초기화할 때 어댑터를 등록하려면 각 Builder 클래스의 setAdapter() 메서드를 사용합니다.

serving/widget/public/with_custom_element/app.js
// Build the result container and bind to DOM elements.
var resultsContainer = new gapi.cloudsearch.widget.resultscontainer.Builder()
  .setAdapter(new ResultsContainerAdapter())
  // ...
  .build();

// Build the search box and bind to DOM elements.
var searchBox = new gapi.cloudsearch.widget.searchbox.Builder()
  .setAdapter(new SearchBoxAdapter())
  // ...
  .build();

createFacetResultElement로 커스텀 패싯 요소를 만들 때 다음과 같은 몇 가지 제한사항이 적용됩니다.

  • 사용자가 버킷을 전환하기 위해 클릭하는 요소에 CSS 클래스 cloudsearch_facet_bucket_clickable를 연결해야 합니다.
  • 포함하는 요소의 각 버킷을 CSS 클래스 cloudsearch_facet_bucket_container로 래핑해야 합니다.
  • 응답에 나타난 것과 다른 순서로 버킷을 렌더링할 수 없습니다.

예를 들어 다음 스니펫은 체크박스 대신 링크를 사용하여 속성을 렌더링합니다.

serving/widget/public/with_custom_facet/app.js
/**
 * Results container adapter that intercepts requests to dynamically
 * change which sources are enabled based on user selection.
 */
function ResultsContainerAdapter() {
  this.selectedSource = null;
}

ResultsContainerAdapter.prototype.createFacetResultElement = function(result) {
  // container for the facet
  var container = document.createElement('div');

  // Add a label describing the facet (operator/property)
  var label = document.createElement('div')
  label.classList.add('facet_label');
  label.textContent = result.operatorName;
  container.appendChild(label);

  // Add each bucket
  for(var i in result.buckets) {
    var bucket = document.createElement('div');
    bucket.classList.add('cloudsearch_facet_bucket_container');

    // Extract & render value from structured value
    // Note: implementation of renderValue() not shown
    var bucketValue = this.renderValue(result.buckets[i].value)
    var link = document.createElement('a');
    link.classList.add('cloudsearch_facet_bucket_clickable');
    link.textContent = bucketValue;
    bucket.appendChild(link);
    container.appendChild(bucket);
  }
  return container;
}

// Renders a value for user display
ResultsContainerAdapter.prototype.renderValue = function(value) {
  // ...
}

검색 동작 맞춤설정

검색 애플리케이션 설정은 검색 인터페이스의 기본 구성을 나타내며 정적입니다. 사용자가 데이터 소스를 전환할 수 있도록 해주는 동적 필터나 속성을 구현하려면 어댑터로 검색 요청을 가로채서 검색 애플리케이션 설정을 재정의하면 됩니다.

실행 전에 Search API에 대한 요청을 수정하려면 interceptSearchRequest 메서드로 어댑터를 구현합니다.

예를 들어 다음 어댑터는 요청을 가로채서 사용자가 선택한 소스에 대한 쿼리를 제한합니다.

serving/widget/public/with_request_interceptor/app.js
/**
 * Results container adapter that intercepts requests to dynamically
 * change which sources are enabled based on user selection.
 */
function ResultsContainerAdapter() {
  this.selectedSource = null;
}
ResultsContainerAdapter.prototype.interceptSearchRequest = function(request) {
  if (!this.selectedSource || this.selectedSource == 'ALL') {
    // Everything selected, fall back to sources defined in the search
    // application.
    request.dataSourceRestrictions = null;
  } else {
    // Restrict to a single selected source.
    request.dataSourceRestrictions = [
      {
        source: {
          predefinedSource: this.selectedSource
        }
      }
    ];
  }
  return request;
}

위젯을 초기화할 때 어댑터를 등록하려면 ResultsContainer를 빌드할 때 setAdapter() 메서드를 사용합니다.

serving/widget/public/with_request_interceptor/app.js
var resultsContainerAdapter = new ResultsContainerAdapter();
// Build the result container and bind to DOM elements.
var resultsContainer = new gapi.cloudsearch.widget.resultscontainer.Builder()
  .setAdapter(resultsContainerAdapter)
  // ...
  .build();

다음 HTML은 소스별로 필터링할 선택 상자를 표시합니다.

serving/widget/public/with_request_interceptor/index.html
<div>
  <span>Source</span>
  <select id="sources">
    <option value="ALL">All</option>
    <option value="GOOGLE_GMAIL">Gmail</option>
    <option value="GOOGLE_DRIVE">Drive</option>
    <option value="GOOGLE_SITES">Sites</option>
    <option value="GOOGLE_GROUPS">Groups</option>
    <option value="GOOGLE_CALENDAR">Calendar</option>
    <option value="GOOGLE_KEEP">Keep</option>
  </select>
</div>

다음 코드는 변경사항을 리슨하고 선택사항을 설정한 후 필요한 경우 쿼리를 다시 실행합니다.

serving/widget/public/with_request_interceptor/app.js
// Handle source selection
document.getElementById('sources').onchange = (e) => {
  resultsContainerAdapter.selectedSource = e.target.value;
  let request = resultsContainer.getCurrentRequest();
  if (request.query) {
    // Re-execute if there's a valid query. The source selection
    // will be applied in the interceptor.
    resultsContainer.resetState();
    resultsContainer.executeRequest(request);
  }
}

어댑터에 interceptSearchResponse를 구현하여 검색 응답을 가로챌 수도 있습니다.

API 버전 고정

기본적으로 위젯은 안정적인 최신 버전의 API를 사용합니다. 특정 버전을 잠그려면 위젯을 초기화하기 전에 cloudsearch.config/apiVersion 구성 매개변수를 원하는 버전으로 설정합니다.

serving/widget/public/basic/app.js
gapi.config.update('cloudsearch.config/apiVersion', 'v1');

API 버전을 설정하지 않거나 유효하지 않은 값으로 설정하면 기본값인 1.0이 설정됩니다.

위젯 버전 고정

검색 인터페이스가 예기치 않게 변경되지 않도록 cloudsearch.config/clientVersion 구성 매개변수를 다음과 같이 설정합니다.

gapi.config.update('cloudsearch.config/clientVersion', 1.1);

위젯 버전을 설정하지 않거나 유효하지 않은 값으로 설정하면 기본값인 1.0이 설정됩니다.

검색 인터페이스 보호

검색 결과에는 매우 민감한 정보가 포함되어 있습니다. 특히, 클릭재킹(clickjacking) 공격으로부터 웹 애플리케이션을 보호하는 권장사항을 따릅니다.

자세한 내용은 OWASP 가이드 프로젝트를 참조하세요.

디버깅 사용 설정하기

interceptSearchRequest를 사용하여 검색 위젯의 디버깅을 사용 설정합니다. 예를 들면 다음과 같습니다.

  if (!request.requestOptions) {
  // Make sure requestOptions is populated
  request.requestOptions = {};
  }
  // Enable debugging
  request.requestOptions.debugOptions = {enableDebugging: true}

  return request;