SharePoint Online에 대한 탐색 옵션

 

마지막으로 수정된 항목: 2015-03-09

요약: 구조적 탐색 및 검색 기반 탐색을 사용하여 SharePoint Online의 페이지 로드 시간을 개선하는 방법을 설명합니다.

전역 탐색 및 구조적 탐색을 구축하는 데 필요한 쿼리는 SharePoint Online에서 페이지를 더 느리게 로드할 수 있습니다. 이러한 각 쿼리가 SQL 서버에 다른 요청을 보내기 때문입니다. 보유하고 있는 각 사이트 및 하위 사이트에 대해 SQL 서버로 더 많은 요청이 수행됩니다. 이 문제는 마스터 페이지에도 영향을 줍니다. 즉, 전역 탐색도 영향을 받게 됩니다.

일부 SharePoint 사이트에는 크고 복잡한 구조가 필요합니다. 쿼리된 콘텐츠를 사용하는 기본 구조적 탐색을 활용하면 여러 사이트 계층으로 인해 페이지를 로드하는 시간이 느려질 수 있습니다. 또한 하위 사이트의 각 계층이 다른 쿼리를 만듭니다.

SharePoint에는 두 가지 기본 탐색 옵션 외에도 사용자 지정 검색 기반 접근 방법이 있습니다. 각 옵션에는 다음의 표에 요약된 것과 같은 장단점이 있습니다.

 

구조적 탐색 관리 탐색 검색 기반 탐색

장점:

  • 쉬운 구성

  • 보안 조정

  • 사이트를 추가할 때 자동으로 업데이트

장점:

  • 쉬운 유지 관리

  • 복잡한 탐색도 제대로 수행

장점:

  • 보안 조정

  • 사이트를 추가할 때 자동으로 업데이트

  • 빠른 로드 시간 및 로컬로 캐시된 탐색 구조

단점:

  • 복잡한 사이트 구조로 성능 저하

단점:

  • 보안이 조정되지 않음

  • 사이트 구조를 반영하도록 자동으로 업데이트되지 않음

단점:

  • 사이트를 쉽게 정렬하는 기능이 없음

  • 마스터 페이지의 사용자 지정 필요(기술 필요)

사이트에 하위 사이트가 많고 구조적 탐색을 사용하는 경우 페이지 로드 속도가 크게 느려질 수 있습니다. 사이트에 가장 적합한 옵션은 사이트 요구 사항과 사용자 기술에 따라 다릅니다. 사용자 지정 마스터 페이지 사용에 익숙하고 조직이 SharePoint Online의 기본 마스터 페이지에서 발생할 수 있는 변경 내용을 유지 관리할 수 있는 능력을 보유한 경우 검색 기반 옵션을 사용하면 최적의 사용자 환경을 생성할 수 있습니다. 기본 구조적 탐색과 검색 사이에서 간단한 절충안을 찾고 싶은 경우 관리 탐색을 사용하는 것이 좋습니다. 관리 탐색 옵션은 구성을 통해 유지 관리할 수 있고, 코드 사용자 지정 파일이 필요하지 않으며, 기본 구조적 탐색보다 훨씬 더 빠릅니다.

또 다른 방법은 기존 사이트를 재구성하고 필요한 탐색 항목 및 하위 사이트의 수를 줄이는 것입니다. 구조적 탐색은 사이트 구조 및 탐색이 그리 복잡하지 않을 때만 제대로 수행되기 때문입니다.

이 문서에서는 예제 사이트 모음에서 다양한 접근 방법을 비교합니다. 이 예제 사이트 모음에서 하위 사이트는 11개가 있고 각 하위 사이트에는 4개 이상의 추가 하위 사이트가 있습니다.

사이트 및 하위 사이트를 보여 주는 스크린샷

이 방법은 기본적으로 사용되는 탐색이며, 대부분의 상황에서 가장 간단하고 적합한 솔루션입니다. 여러 하위 사이트 또는 여러 수준의 하위 사이트로 이루어진 복잡한 구조가 아니라면 구조적 탐색은 제대로 수행됩니다. 이 방법의 주요 이점은 보안이 조정되고, 새 사이트가 추가될 때 자동으로 업데이트되며, 마스터 페이지의 사용자 지정이 필요하지 않다는 것입니다. 관련 기술 지식이 없는 사용자도 설정 페이지에서 항목을 쉽게 추가하거나 숨기고, 탐색을 관리할 수 있습니다.

구조적 탐색 및 하위 사이트 표시 옵션을 설정한 상태로 표준 SharePoint Online 솔루션의 성능을 확인할 수 있습니다. 다음은 사이트 설정 > 탐색 페이지에 나오는 스크린샷 설정입니다.

하위 사이트를 보여 주는 스크린샷

SharePoint 페이지의 성능을 분석하려면 Internet Explorer에서 F12 개발자 도구의 네트워크 탭을 사용합니다.

F12 개발자 도구 네트워크 탭을 보여 주는 스크린샷

네트워크 탭에서 로드할 .aspx 페이지와 세부 정보 탭을 차례로 클릭합니다.

세부 정보 탭을 보여 주는 스크린샷

응답 헤더를 클릭합니다.

세부 정보 탭의 스크린샷

SharePoint는 유용한 일부 진단 정보를 응답 헤더에 반환합니다. 매우 유용한 정보 중 하나는 서버에서 요청을 처리하는 데 걸린 시간(밀리초) 값을 나타내는 SPRequestDuration입니다.

다음 스크린샷에서는 구조적 탐색에 대한 하위 사이트 표시가 선택되어 있지 않습니다. 즉, 전역 탐색에 사이트 모음 링크만 나와 있습니다.

부하 시간을 요청 기간으로 나타내는 스크린샷

SPRequestDuration 키의 값은 245밀리초입니다. 이 값은 요청을 반환하는 데 걸린 시간을 나타냅니다. 사이트에 탐색 항목이 하나만 있으므로 이 방식은 SharePoint Online이 과도한 탐색 없이 작동하는 방식에 대한 적합한 기준이 됩니다. 아래의 스크린샷에서는 하위 사이트를 추가할 때 이 키에 어떤 영향을 주는지를 보여 줍니다.

2502ms의 요청 기간을 나타내는 스크린샷

하위 사이트를 추가하면 페이지 요청을 반환하는 데 걸리는 시간이 크게 증가했습니다.

일반적인 구조적 탐색을 사용할 경우 순서를 쉽게 구성하고, 사이트를 숨기고, 페이지를 추가하고, 결과의 보안이 조정되며, SharePoint Online에서 사용하는 지원되는 마스터 페이지를 벗어나지 않을 수 있다는 이점이 있습니다. 사이트를 신중하게 구성하고 사이트 모음의 하위 사이트 양을 최소화하면 구조적 탐색의 성능이 향상됩니다.

관리 탐색은 구조적 탐색과 동일한 정렬 기능을 다시 만드는 데 사용할 수 있는 기본 옵션 중 하나입니다.

관리되는 메타데이터를 사용하면 쿼리된 콘텐츠를 사용하여 사이트 탐색을 작성하는 것보다 데이터를 훨씬 더 빠르게 검색할 수 있다는 이점이 있습니다. 속도는 훨씬 빠르지만 결과의 보안을 조정할 방법이 없으므로 사용자가 지정된 사이트에 액세스할 수 없는 경우, 링크는 계속 표시되지만 오류 메시지가 나타나게 됩니다.

관리 탐색 및 결과의 구현 방법

TechNet의 여러 문서에 관리 탐색에 대한 자세한 내용이 나와 있습니다. 예를 들면 다음과 같습니다.

SharePoint Server 2013의 관리 탐색 개요

관리 탐색을 구현하려면 용어 저장소 관리자 권한이 필요합니다. 사이트 모음의 구조와 일치하는 URL이 포함된 용어를 설정하여 구조적 탐색 대신 관리 탐색을 사용할 수 있습니다. 예를 들면 다음과 같습니다.

Subsite1 예의 스크린샷

아래 예에서는 관리 탐색을 사용한 경우의 복잡한 탐색의 성능을 보여 줍니다.

SPRequestDuration 예의 스크린샷

관리 탐색을 일관되게 사용하면 쿼리된 콘텐츠 구조적 탐색 접근 방식보다 성능이 향상됩니다.

검색을 사용하면 연속 크롤링을 통해 백그라운드에서 작성된 인덱스를 사용할 수 있습니다. 즉, 과도한 콘텐츠 쿼리가 진행되지 않습니다. 검색 결과를 검색 인덱스에서 가져오고 결과의 보안이 조정됩니다. 이 방식은 일반적인 콘텐츠 쿼리를 사용하는 경우보다 더 빠릅니다. 구조적 탐색에 검색을 사용하면(특히 사이트 구조가 복잡한 경우) 페이지 로드 시간이 매우 빨라집니다. 이 방식이 관리 탐색보다 나은 점은 보안 조정의 이점을 얻을 수 있다는 것입니다.

이 과정에서 사용자 지정 마스터 페이지가 생성되고 기본 탐색 코드가 사용자 지정 HTML로 바뀝니다. 파일 seattle.html의 탐색 코드를 바꾸려면 아래의 절차를 따릅니다.

이 예에서 seattle.html 파일을 열고 전체 요소 id = "DeltaTopNavigation"을 사용자 지정 HTML 코드로 바꿉니다.

예: 마스터 페이지에서 기본 탐색 코드를 바꾸려면
  1. 사이트 설정 페이지로 이동합니다.

  2. 마스터 페이지를 클릭하여 마스터 페이지 갤러리를 엽니다.

  3. 여기에서 라이브러리를 탐색하고 seattle.master 파일을 다운로드할 수 있습니다.

  4. 텍스트 편집기를 사용하여 코드를 편집하고 아래의 스크린샷과 같이 코드 블록을 삭제합니다.

    삭제할 DeltaTopNavigation 코드의 스크린샷
  5. <SharePoint:AjaxDelta id = "DeltaTopNavigation">과 <\SharePoint:AjaxDelta> 태그 사이의 코드를 제거하고 다음과 같은 조각으로 바꿉니다.

    <div id="loading">
      <!--Replace with path to loading image.-->
      <div style="background-image: url(''); height: 22px; width: 22px; ">
      </div>
    </div>
    <!-- Main Content-->
    <div id="navContainer" style="display:none">
      <div data-bind="foreach: hierarchy" class="noindex ms-core-listMenu-horizontalBox">
        <ul id="menu" data-bind="foreach: $data.children" style="padding-left:20px">
          <li class="static dynamic-children">
            <a class="static dynamic-children menu-item ms-core-listMenu-item ms-displayInline ms-navedit-linkNode" data-bind="attr: { href: item.Url, title: item.Title }">
              <span aria-haspopup="true" class="additional-background ms-navedit-flyoutArrow dynamic-children">
                <span class="menu-item-text" data-bind="text: item.Title">
                </span>
              </span>
            </a>
            <ul id="menu" data-bind="foreach: children" class="dynamic">
              <li class="dynamic">
                <a class="dynamic menu-item ms-core-listMenu-item ms-displayInline ms-navedit-linkNode" data-bind="attr: { href: item.Url, title: item.Title }">
                  <span class="menu-item-text" data-bind="text: item.Title">
                  </span>
                </a>
              </li>
            </ul>
          </li>
        </ul>
      </div>
    </div>
    
  6. 맨 앞의 로딩 이미지 앵커 태그의 URL을 사이트 모음의 로딩 이미지에 대한 링크로 바꿉니다. 변경한 후에 파일 이름을 바꾸고, 마스터 페이지 갤러리에 해당 파일을 업로드합니다. 이렇게 하면 새 .master 파일이 생성됩니다.

  7. 이 HTML은 JavaScript 코드에서 반환되는 검색 결과로 채워지는 기본 태그입니다. 다음과 같은 코드를 편집하여 아래의 조각과 같이 var root = "site collection URL"의 값을 변경해야 합니다.

    var root = "https://spperformance.sharepoint.com/sites/NavigationBySearch";
    

    전체 JavaScript 파일은 다음과 같습니다.

    //Models and Namespaces
    var SPO = SPO || {};
    SPO.Models = SPO.Models || {}
    SPO.Models.NavigationNode = function () {
    
        this.Url = ko.observable("");
        this.Title = ko.observable("");
        this.Parent = ko.observable("");
    
    };
    
    var root = "https://spperformance.sharepoint.com/sites/NavigationBySearch";
    var baseUrl = root + "/_api/search/query?querytext=";
    var query = baseUrl + "'contentClass=\"STS_Web\"+path:" + root + "'&trimduplicates=false&rowlimit=300";
    
    var baseRequest = {
        url: "",
        type: ""
    };
    
    //Parses a local object from JSON search result.
    function getNavigationFromDto(dto) {
        var item = new SPO.Models.NavigationNode();
        if (dto != undefined) {
    
            item.Title(dto.Cells.results[3].Value);
            item.Url(dto.Cells.results[6].Value);
            item.Parent(dto.Cells.results[19].Value);
        }
    
        return item;
    }
    //Parse a local object from the serialized cache.
    function getNavigationFromCache(dto) {
        var item = new SPO.Models.NavigationNode();
    
        if (dto != undefined) {
    
            item.Title(dto.Title);
            item.Url(dto.Url);
            item.Parent(dto.Parent);
        }
    
        return item;
    }
    
    /* create a new OData request for JSON response */
    function getRequest(endpoint) {
        var request = baseRequest;
        request.type = "GET";
        request.url = endpoint;
        request.headers = { ACCEPT: "application/json;odata=verbose" };
        return request;
    };
    /* Navigation Module*/
    function NavigationViewModel() {
        "use strict";
        var self = this;
        self.nodes = ko.observableArray([]);
        self.hierarchy = ko.observableArray([]);;
        self.loadNavigatioNodes = function () {
            //Check local storage for cached navigation datasource.
            var fromStorage = localStorage["nodesCache"];
            if (false) {
                var cachedNodes = JSON.parse(localStorage["nodesCache"]);
                var timeStamp = localStorage["nodesCachedAt"];
                if (cachedNodes && timeStamp) {
                    //Check for cache expiration. Currently set to 3 hrs.
                    var now = new Date();
                    var diff = now.getTime() - timeStamp;
                    if (Math.round(diff / (1000 * 60 * 60)) < 3) {
    
                        //return from cache.
                        var cacheResults = [];
                        $.each(cachedNodes, function (i, item) {
                            var nodeitem = getNavigationFromCache(item, true);
                            cacheResults.push(nodeitem);
                        });
    
                        self.buildHierarchy(cacheResults);
                        self.toggleView();
    addEventsToElements();
                        return;
                    }
                }
            }
            //No cache hit, REST call required.
            self.queryRemoteInterface();
        };
    //Executes a REST call and builds the navigation hierarchy.
        self.queryRemoteInterface = function () {
            var oDataRequest = getRequest(query);
            $.ajax(oDataRequest).done(function (data) {
                var results = [];
                $.each(data.d.query.PrimaryQueryResult.RelevantResults.Table.Rows.results, function (i, item) {
    
                    if (i == 0) {
                        //Add root element.
                        var rootItem = new SPO.Models.NavigationNode();
                        rootItem.Title("Root");
                        rootItem.Url(root);
                        rootItem.Parent(null);
                        results.push(rootItem);
                    }
                    var navItem = getNavigationFromDto(item);
                    results.push(navItem);
                });
                //Add to local cache
                localStorage["nodesCache"] = ko.toJSON(results);
                localStorage["nodesCachedAt"] = new Date().getTime();
                self.nodes(results);
                if (self.nodes().length > 0) {
    var unsortedArray = self.nodes();
    var sortedArray = unsortedArray.sort(self.sortObjectsInArray);
                    self.buildHierarchy(sortedArray);
                    self.toggleView();
    addEventsToElements();
                }
            }).fail(function () {
                //Handle error here!!
                $("#loading").hide();
                $("#error").show();
            });
        };
    self.toggleView = function () {
            var navContainer = document.getElementById("navContainer");
            ko.applyBindings(self, navContainer);
            $("#loading").hide();
            $("#navContainer").show();
    
        };
        //Uses linq.js to build the navigation tree.
        self.buildHierarchy = function (enumerable) {
    
            self.hierarchy(Enumerable.From(enumerable).ByHierarchy(function (d) {
                return d.Parent() == null;
            }, function (parent, child) {
                if (parent.Url() == null || child.Parent() == null)
                    return false;
                return parent.Url().toUpperCase() == child.Parent().toUpperCase();
            }).ToArray());
        };
    self.sortObjectsInArray = function (a,b){
    if (a.Title() > b.Title())
    return -1;
        if (a.Title() < b.Title())
    return 1;
        return 0;
    }
    }
    //Loads the navigation on load and binds the event handlers for mouse interaction.
    $(document).ready(function () {
        "use strict";
        var viewModel = new NavigationViewModel();
        viewModel.loadNavigatioNodes();
    });
    function addEventsToElements(){
        //events.
        $("li.dynamic-children").mouseover(function () {
            var position = $(this).position();
            $(this).find("ul").css({ width: 125, left: position.left + 10, top: 50 });
    
        })
    .mouseout(function () {
      $(this).find("ul").css({ width: 0, left: -99999, top: 0 });
     });
    }
    
    
    

    jQuery $(document).ready 함수에서 위에 표시된 코드를 요약하기 위해 viewModel 개체가 만들어진 후 이 개체에 대해 loadNavigationNodes() 함수가 호출됩니다. 이 함수는 클라이언트 브라우저의 HTML5 로컬 저장소에 저장된 이전에 작성한 탐색 계층 구조를 로드하거나 queryRemoteInterface() 함수를 호출합니다.

    QueryRemoteInterface()는 앞에서 스크립트에 정의된 쿼리 매개 변수를 getRequest() 함수와 함께 사용하여 요청을 빌드한 후 서버에서 데이터를 반환합니다. 이 데이터는 기본적으로 다양한 속성을 가진 데이터 전송 개체로 표현되는 사이트 모음의 모든 사이트 배열입니다. 이 데이터는 앞서 정의한 SPO.Models.NavigationNode 개체로 구문 분석됩니다. 이 개체는 Knockout.js를 통해 정의한 HTML로 값을 바인딩하여 데이터에 사용될 식별 가능한 속성을 만듭니다. 그러면 개체가 결과 배열에 배치됩니다. 이 배열은 Knockout을 사용하여 JSON으로 구문 분석되고 향후 페이지 로드 시 속도 향상을 위해 로컬 브라우저 저장소에 저장됩니다.

  8. 그러면 결과가 self.nodes 배열에 할당되고, 출력을 self.heirarchy 배열에 할당하는 linq.js를 사용하여 계층 구조가 기본 개체에서 구축됩니다. 이 배열은 HTML에 바인딩되는 개체입니다. 이 작업은 자체 개체를 ko.applyBinding() 함수에 전달하여 toggleView() 함수에서 수행됩니다. 이에 따라 계층 구조 배열이 아래의 HTML에 바인딩됩니다.

    <div data-bind="foreach: hierarchy" class="noindex ms-core-listMenu-horizontalBox">
    

    마지막으로 mouseenter 및 mouseexit에 대한 이벤트 처리기가 최상위 탐색에 추가되어 addEventsToElements() 함수에서 수행되는 하위 사이트 드롭다운 메뉴를 처리합니다.

    탐색 결과는 아래 스크린샷에서 확인할 수 있습니다.

    탐색 결과의 스크린샷

    복잡한 탐색의 로컬 캐싱이 없는 새로운 페이지 로드의 예는 서버에서 소요된 시간이 벤치마크 구조적 탐색보다 감소하여 관리 탐색 방법과 비슷한 결과를 보였습니다.

    SPRequestDuration 301의 스크린샷

    이 방법의 주요 이점 중 하나는 사용자가 다음에 페이지를 로드할 때를 대비해 HTML5 로컬 저장소를 사용하여 탐색이 로컬로 저장된다는 것입니다.

구조적 탐색에 검색 API를 사용하여 주요 성능을 향상시킬 수 있지만 이 기능을 실행하고 사용자 지정하기 위한 기술이 어느 정도 필요합니다. 이 예제 구현에서는 사이트가 기본 구조적 탐색과 동일하게 사전 순서에 따라 정렬됩니다. 이 순서로 수행하지 않으려는 경우 개발과 유지 관리가 더 복잡해집니다. 또한 이 방법을 사용하려면 지원되는 마스터 페이지를 수정해야 합니다. 사용자 지정 마스터 페이지를 유지 관리하지 않으면 Microsoft가 마스터 페이지에 제공하는 업데이트 및 향상된 기능을 사이트에서 사용할 수 없습니다.

 
표시: