효율적인 데이터 페이징 구현

작성자: Microsoft

PDF 다운로드

이는 ASP.NET MVC 1을 사용하여 작지만 완전한 웹 애플리케이션을 빌드하는 방법을 안내하는 무료 "NerdDinner" 애플리케이션 자습서 의 8단계입니다.

8단계는 /Dinners URL에 페이징 지원을 추가하여 한 번에 1000개의 저녁 식사를 표시하는 대신 한 번에 10개의 예정된 저녁 식사만 표시하고 최종 사용자가 SEO 친화적인 방식으로 전체 목록을 페이지백하고 전달할 수 있도록 하는 방법을 보여 줍니다.

ASP.NET MVC 3을 사용하는 경우 MVC 3 또는 MVC Music Store에서 시작 자습서를 따르는 것이 좋습니다.

NerdDinner 8단계: 페이징 지원

우리의 사이트가 성공하면, 그것은 곧 저녁 식사의 수천을해야합니다. UI가 이러한 모든 저녁 식사를 처리하도록 스케일링하고 사용자가 해당 저녁 식사를 찾아볼 수 있도록 해야 합니다. 이를 위해 /Dinners URL에 페이징 지원을 추가하여 한 번에 1000개의 저녁 식사를 표시하는 대신 한 번에 10개의 예정된 저녁 식사만 표시하고 최종 사용자가 SEO 친화적인 방식으로 전체 목록을 페이징하고 전달할 수 있도록 합니다.

Index() 작업 메서드 요약

DinnersController 클래스 내의 Index() 작업 메서드는 현재 다음과 같습니다.

//
// GET: /Dinners/

public ActionResult Index() {

    var dinners = dinnerRepository.FindUpcomingDinners().ToList();
    return View(dinners);
}

/Dinners URL에 대한 요청이 수행되면 예정된 모든 저녁 식사 목록을 검색한 다음 모든 저녁 식사 목록을 렌더링합니다.

Nerd Dinner 예정된 저녁 식사 목록 페이지의 스크린샷.

IQueryable<T 이해>

Iqueryable<T> 는 .NET 3.5의 일부로 LINQ를 사용하여 도입된 인터페이스입니다. 이를 통해 페이징 지원을 구현하는 데 활용할 수 있는 강력한 "지연된 실행" 시나리오를 사용할 수 있습니다.

DinnerRepository에서는 FindUpcomingDinners() 메서드에서 IQueryable<Dinner> 시퀀스를 반환합니다.

public class DinnerRepository {

    private NerdDinnerDataContext db = new NerdDinnerDataContext();

    //
    // Query Methods

    public IQueryable<Dinner> FindUpcomingDinners() {
    
        return from dinner in db.Dinners
               where dinner.EventDate > DateTime.Now
               orderby dinner.EventDate
               select dinner;
    }

FindUpcomingDinners() 메서드에서 반환한 IQueryable<Dinner> 개체는 쿼리를 캡슐화하여 LINQ to SQL 사용하여 데이터베이스에서 Dinner 개체를 검색합니다. 중요한 것은 쿼리의 데이터에 액세스/반복을 시도하거나 쿼리에서 ToList() 메서드를 호출할 때까지 데이터베이스에 대해 쿼리를 실행하지 않습니다. FindUpcomingDinners() 메서드를 호출하는 코드는 필요에 따라 쿼리를 실행하기 전에 IQueryable Dinner> 개체에 "연결된" 작업/필터를 추가하도록 선택할 수<있습니다. 그런 다음 LINQ to SQL 데이터를 요청할 때 데이터베이스에 대해 결합된 쿼리를 실행할 수 있을 만큼 스마트합니다.

페이징 논리를 구현하기 위해 ToList()를 호출하기 전에 반환된 IQueryable Dinner> 시퀀스에 추가 "Skip" 및 "Take" 연산자를 적용하도록 DinnersController의 Index() 작업 메서드를 업데이트할 수<있습니다.

//
// GET: /Dinners/

public ActionResult Index() {

    var upcomingDinners = dinnerRepository.FindUpcomingDinners();
    var paginatedDinners = upcomingDinners.Skip(10).Take(20).ToList();

    return View(paginatedDinners);
}

위의 코드는 데이터베이스에서 처음 10개의 예정된 저녁 식사를 건너뛰고 20개의 저녁 식사를 다시 반환합니다. LINQ to SQL 웹 서버가 아닌 SQL 데이터베이스에서 이 건너뛰기 논리를 수행하는 최적화된 SQL 쿼리를 생성할 만큼 스마트합니다. 즉, 데이터베이스에 수백만 개의 예정된 Dinners가 있더라도 이 요청의 일부로 원하는 10개만 검색됩니다(효율적이고 확장 가능).

URL에 "page" 값 추가

특정 페이지 범위를 하드 코딩하는 대신 URL에 사용자가 요청하는 저녁 식사 범위를 나타내는 "page" 매개 변수가 포함되도록 할 것입니다.

Querystring 값 사용

아래 코드에서는 Index() 작업 메서드를 업데이트하여 querystring 매개 변수를 지원하고 /Dinners?page=2와 같은 URL을 사용하도록 설정하는 방법을 보여 줍니다.

//
// GET: /Dinners/
//      /Dinners?page=2

public ActionResult Index(int? page) {

    const int pageSize = 10;

    var upcomingDinners = dinnerRepository.FindUpcomingDinners();

    var paginatedDinners = upcomingDinners.Skip((page ?? 0) * pageSize)
                                          .Take(pageSize)
                                          .ToList();

    return View(paginatedDinners);
}

위의 Index() 작업 메서드에는 "page"라는 매개 변수가 있습니다. 매개 변수는 nullable 정수로 선언됩니다(int?이 나타내는 값임). 즉 , /Dinners?page=2 URL로 인해 "2" 값이 매개 변수 값으로 전달됩니다. /Dinners URL(querystring 값 없음)으로 인해 null 값이 전달됩니다.

페이지 값을 페이지 크기(이 경우 10개 행)에 곱하여 건너뛸 저녁 식사 수를 결정합니다. Null 허용 형식을 처리할 때 유용한 C# null "병합" 연산자(??) 를 사용하고 있습니다. 위의 코드는 페이지 매개 변수가 null인 경우 페이지에 0 값을 할당합니다.

포함된 URL 값 사용

querystring 값을 사용하는 대신 실제 URL 자체에 페이지 매개 변수를 포함하는 것이 좋습니다. 예: /Dinners/Page/2 또는 /Dinners/2. ASP.NET MVC에는 이와 같은 시나리오를 쉽게 지원할 수 있는 강력한 URL 라우팅 엔진이 포함되어 있습니다.

들어오는 URL 또는 URL 형식을 원하는 컨트롤러 클래스 또는 작업 메서드에 매핑하는 사용자 지정 라우팅 규칙을 등록할 수 있습니다. 우리가 해야 할 일은 프로젝트 내에서 Global.asax 파일을 여는 것뿐입니다.

Nerd Dinner 탐색 트리의 스크린샷. 전역 점 a s a x가 선택되고 강조 표시됩니다.

그런 다음 경로에 대한 첫 번째 호출과 같은 MapRoute() 도우미 메서드를 사용하여 새 매핑 규칙을 등록합니다. MapRoute() 아래:

public void RegisterRoutes(RouteCollection routes) {

   routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

    routes.MapRoute(                                        
        "UpcomingDinners",                               // Route name
        "Dinners/Page/{page}",                           // URL with params
        new { controller = "Dinners", action = "Index" } // Param defaults
    );

    routes.MapRoute(
        "Default",                                       // Route name
        "{controller}/{action}/{id}",                    // URL with params
        new { controller="Home", action="Index",id="" }  // Param defaults
    );
}

void Application_Start() {
    RegisterRoutes(RouteTable.Routes);
}

위에서는 "UpcomingDinners"라는 새 라우팅 규칙을 등록합니다. URL 형식이 "Dinners/Page/{page}"임을 나타냅니다. 여기서 {page}는 URL 내에 포함된 매개 변수 값입니다. MapRoute() 메서드의 세 번째 매개 변수는 이 형식과 일치하는 URL을 DinnersController 클래스의 Index() 작업 메서드에 매핑해야 했음을 나타냅니다.

Querystring 시나리오에서 이전과 정확히 동일한 Index() 코드를 사용할 수 있습니다. 단, 이제 "page" 매개 변수는 querystring이 아닌 URL에서 제공됩니다.

//
// GET: /Dinners/
//      /Dinners/Page/2

public ActionResult Index(int? page) {

    const int pageSize = 10;

    var upcomingDinners = dinnerRepository.FindUpcomingDinners();
    
    var paginatedDinners = upcomingDinners.Skip((page ?? 0) * pageSize)
                                          .Take(pageSize)
                                          .ToList();

    return View(paginatedDinners);
}

이제 애플리케이션을 실행하고 /Dinners 를 입력하면 처음 10개의 예정된 저녁 식사가 표시됩니다.

괴상한 저녁 식사 예정 저녁 식사 목록의 스크린샷.

/Dinners/Page/1을 입력하면 다음 저녁 식사 페이지가 표시됩니다.

예정된 저녁 식사 목록의 다음 페이지 스크린샷

페이지 탐색 UI 추가

페이징 시나리오를 완료하는 마지막 단계는 사용자가 저녁 식사 데이터를 쉽게 건너뛸 수 있도록 보기 템플릿 내에서 "다음" 및 "이전" 탐색 UI를 구현하는 것입니다.

이를 올바르게 구현하려면 데이터베이스의 총 Dinners 수와 이 데이터로 변환되는 데이터의 페이지 수를 알아야 합니다. 그런 다음 현재 요청된 "page" 값이 데이터의 시작 또는 끝에 있는지 여부를 계산하고 그에 따라 "이전" 및 "다음" UI를 표시하거나 숨겨야 합니다. Index() 작업 메서드 내에서 이 논리를 구현할 수 있습니다. 또는 이 논리를 보다 재사용 가능한 방식으로 캡슐화하는 도우미 클래스를 프로젝트에 추가할 수 있습니다.

다음은 .NET Framework 기본 제공되는 List<T> 컬렉션 클래스에서 파생되는 간단한 "PaginatedList" 도우미 클래스입니다. IQueryable 데이터의 모든 시퀀스를 페이지를 매는 데 사용할 수 있는 재사용 가능한 컬렉션 클래스를 구현합니다. NerdDinner 애플리케이션에서는 IQueryable Dinner> 결과에 대해 작동하지만 다른 애플리케이션 시나리오에서 IQueryable<Product> 또는 IQueryable<<Customer> 결과에 대해 쉽게 사용할 수 있습니다.

public class PaginatedList<T> : List<T> {

    public int PageIndex  { get; private set; }
    public int PageSize   { get; private set; }
    public int TotalCount { get; private set; }
    public int TotalPages { get; private set; }

    public PaginatedList(IQueryable<T> source, int pageIndex, int pageSize) {
        PageIndex = pageIndex;
        PageSize = pageSize;
        TotalCount = source.Count();
        TotalPages = (int) Math.Ceiling(TotalCount / (double)PageSize);

        this.AddRange(source.Skip(PageIndex * PageSize).Take(PageSize));
    }

    public bool HasPreviousPage {
        get {
            return (PageIndex > 0);
        }
    }

    public bool HasNextPage {
        get {
            return (PageIndex+1 < TotalPages);
        }
    }
}

위에서는 "PageIndex", "PageSize", "TotalCount" 및 "TotalPages"와 같은 속성을 계산하고 노출하는 방법을 확인합니다. 그런 다음 컬렉션의 데이터 페이지가 원래 시퀀스의 시작 또는 끝에 있는지 여부를 나타내는 두 개의 도우미 속성 "HasPreviousPage" 및 "HasNextPage"를 노출합니다. 위의 코드는 두 개의 SQL 쿼리를 실행합니다. 첫 번째는 Dinner 개체의 총 개수(개체를 반환하지 않고 정수를 반환하는 "SELECT COUNT" 문을 수행함)를 검색하고, 두 번째 쿼리는 현재 데이터 페이지에 대해 데이터베이스에서 필요한 데이터 행만 검색합니다.

그런 다음 DinnersController.Index() 도우미 메서드를 업데이트하여 DinnerRepository.FindUpcomingDinners() 결과에서 PaginatedList<Dinner> 를 만들고 보기 템플릿에 전달할 수 있습니다.

//
// GET: /Dinners/
//      /Dinners/Page/2

public ActionResult Index(int? page) {

    const int pageSize = 10;

    var upcomingDinners = dinnerRepository.FindUpcomingDinners();
    var paginatedDinners = new PaginatedList<Dinner>(upcomingDinners, page ?? 0, pageSize);

    return View(paginatedDinners);
}

그런 다음 ViewPage IEnumerable Dinner 대신 ViewPage<NerdDinner.Helpers.PaginatedList<Dinner>>에서 상속하도록 \Views\Dinners\Index.aspx 보기 템플릿을 업데이트한 다음, 다음 코드를 view-template<의 맨 아래에 추가하여 다음 및 이전 탐색 UI를 표시하거나 숨길 수<있습니다.>>

<% if (Model.HasPreviousPage) { %>

    <%= Html.RouteLink("<<<", "UpcomingDinners", new { page = (Model.PageIndex-1) }) %>

<% } %>

<% if (Model.HasNextPage) {  %>

    <%= Html.RouteLink(">>>", "UpcomingDinners", new { page = (Model.PageIndex + 1) }) %>

<% } %>

위에서 Html.RouteLink() 도우미 메서드를 사용하여 하이퍼링크를 생성하는 방법을 확인합니다. 이 메서드는 이전에 사용한 Html.ActionLink() 도우미 메서드와 비슷합니다. 차이점은 Global.asax 파일 내에서 설정하는 "UpcomingDinners" 라우팅 규칙을 사용하여 URL을 생성한다는 것입니다. 이렇게 하면 /Dinners/Page/{page} 형식의 Index() 작업 메서드에 대한 URL을 생성합니다. 여기서 {page} 값은 현재 PageIndex를 기반으로 위에서 제공하는 변수입니다.

이제 애플리케이션을 다시 실행하면 브라우저에서 한 번에 10개의 저녁 식사가 표시됩니다.

Nerd Dinner 페이지의 예정된 저녁 식사 목록 스크린샷.

또한 <<< 페이지 아래쪽에 검색 >>> 엔진 액세스 가능 URL을 사용하여 데이터를 앞뒤로 건너뛸 수 있는 및 탐색 UI가 있습니다.

예정된 저녁 식사 목록이 있는 괴상한 저녁 식사 페이지의 스크린샷.

측면 항목: IQueryable<T의 의미 이해>
IQueryable<T> 는 페이징 및 컴퍼지션 기반 쿼리와 같은 다양한 흥미로운 지연 실행 시나리오를 가능하게 하는 매우 강력한 기능입니다. 모든 강력한 기능과 마찬가지로, 사용 방법에 주의를 기울이고 남용되지 않도록 해야 합니다. 리포지토리에서 IQueryable<T> 결과를 반환하면 코드를 호출하여 연결된 연산자 메서드를 추가할 수 있으므로 최종 쿼리 실행에 참여할 수 있다는 것을 인식하는 것이 중요합니다. 이 기능을 호출하는 코드를 제공하지 않으려면 이미 실행된 쿼리의 결과가 포함된 IList<T> 또는 IEnumerable<T> 결과를 반환해야 합니다. 페이지 매김 시나리오의 경우 호출되는 리포지토리 메서드에 실제 데이터 페이지 매김 논리를 푸시해야 합니다. 이 시나리오에서는 FindUpcomingDinners() finder 메서드를 업데이트하여 PaginatedList: PaginatedList< Dinner> FindUpcomingDinners(int pageIndex, int pageSize) { } 또는 IList<Dinner>를 반환하고 "totalCount" out 매개 변수를 사용하여 총 저녁 식사 수를 반환합니다. IList<Dinner> FindUpcomingDinners(int pageIndex, int pageSize, out int totalCount) { }

다음 단계

이제 애플리케이션에 인증 및 권한 부여 지원을 추가하는 방법을 살펴보겠습니다.