Pagination이 대체 뭘까?

책의 page를 하나하나씩 넘기다 보면 잠이 옴...

클라이언트는 서버에서 데이터를 받을 때 양이 많을 경우, 전체를 한번에 받지 않고 마치 책의 한 페이지(page)를 넘기는 것처럼 page를 나누어서 전달받게 되는데, 이를 처리하는 작업을 페이지네이션(Pagination)이라고 한다.  Pagination은 보통 아래의 두가지 방법을 사용한다.

 

1. 오프셋 기반(Offset-based) 페이지네이션

           - 데이터베이스에서 page를 끊어 읽을 단위인 offset을 조절하여 가져옴

           - 예) SELECT title FROM 'board' ORDER BY id DESC LIMIT 10, 20

 

 

2. 커서 기반(Cursor-based) 페이지네이션

            - 클라이언트에서 읽은 마지막 row의 다음 n개의 row들을 가져옴

 

 

오프셋 기반의 페이지네이션의 경우, 구현이 매우 쉽고 가장 일반적인 방법이 되겠다. 데이터들을 일정한 갯수씩 잘라 1~10를 1page로, 11~20를 2page로 지정해 해당 page를 다 읽으면 다음 페이지를 가져오는 식이다.

 

이때, 미리 잘라둔 페이지를 가져오는 것이 아니라. 다음 페이지를 요청할 때마다 자르기 때문에 이전의 데이터가 중복될 경우가 있다. 예를 들어1page에서 10개까지 다 읽고 2page를 읽으려고 하기 전에 새로운 데이터가 1개 들어온다면 데이터의 순위가 하나씩 밀려 2page의 첫번째 row는 아까 보았던 1page의 마지막 row 데이터를 중복해서 조회하게 된다. 이것이 오프셋 기반(Offset-based) 페이지네이션의 가장 큰 단점이다.

 

또한 오프셋 기반의 페이지네이션은 page를 계산할 때마다 비효율적인 전체 Table Scan을 해야하기 때문에 offset이 작은 수라면 문제가 되지 않으나 데이터의 수가 많을 경우 효율이 많이 떨어진다. 

 

커서 기반 페이지네이션의 경우, 마지막으로 가져온 데이터를 기준으로 다음의 n개의 데이터를 가져오는데 이렇게 되면 중복되는 데이터를 읽을 일이 없다. 예를 들어 1~10의 1page에서 다음 데이터를 요청할 때, 이 다음 10개의 데이터까지 이동(skip)해서 20~11의 2page를 요청하는 것이 아니라, 마지막으로 읽었던 10번 데이터를 기준으로 10개를 요청하는 것이다. 이렇게 하면 새로운 데이터가 들어와도 중복을 피할 수 있다. (물론 새로 들어온 데이터는 refetch 하기 전까지는 확인할 수 없겠지만...)

 

 

Apollo에서 지원하는 Pagination

 

Pagination overview

A guide to using the Apollo GraphQL Client with React

www.apollographql.com

이전의 RestAPI에서는 페이지네이션 처리를 위해서는 일일이 로직을 짜는 고생을 했었지만 다행스럽게도 Apollo 라이브러리에서는 이러한 Pagination을 도와주는 기능을 하고 있다. Docs에서 읽은 사용 방법은 아래와 같다.

 

여기에서는 Cursor-based Pagination만 설명하도록 하겠다. 설명을 위해서 가장 많이 쓰이는 FlatList 컴포넌트를 이용한 무한 스크롤 방식의 pagination을 구현해 보겠다.

 

const COMMENTS_QUERY = gql`
  query Comments($cursor: String) {
    comments(first: 10, after: $cursor) {
      edges {
        node {
          author
          text
        }
      }
      pageInfo {
        endCursor
        hasNextPage
      }
    }
  }
`;

 

댓글(Comments)의 갯수를 10개씩 끊어서 가져오는 Query이다. 파라미터로 받는 first에는 가져오는 갯수, cursor에는 이전의 마지막 커서(endCursor)를 넣으면 계속해서 다음 page를 가져올 수 있다. GraphQL 서버에 요청하는 Query에서 pageInfo 라는 필드 내에서 마지막 데이터의 Cursor 주소인 endCursor과 다음 페이지의 존재 여부를 파악하는 hasNextPage 필드를 확인할 수 있다.

 

다음으로는 화면을 구현해보았다. 

 

export const CommentScreen = () => {
    const { loading, data, error, refetch, fetchMore } 
    = useQuery<{ comments: CommentList }, QueryCommentsArgs>(COMMENTS_QUERY, { variables: { first: 10 } })
    const [idleState, setIdleState] = useState<boolean>(false);
    return (
            <FlatList
            		style={{flex:1}}
                    showsVerticalScrollIndicator={false}
                    onRefresh={refetch}
                    refreshing={loading}
                    data={data?.faqs.edges}
                    renderItem={({ item, index }) => (
                        <RenderListTile item={item} />
                    )}
                    onEndReached={() => {
                        if (!idleState && !loading && data && data.comments.pageInfo.hasNextPage) {
                            setIdleState(true);
                            console.log('refetchMore...');
                            fetchMore({ variables: { after: data.comments.pageInfo.endCursor } }).then((data) => {
                                setIdleState(false);
                            }).catch(error => {
                                console.log('Fatch Error', error);
                                setIdleState(false);
                            })
                        }
                    }}
                    onEndReachedThreshold={0.5}
                    keyExtractor={(item, index) => (`${item.node.text}`)}
                />
    );
}

 

useQuery를 사용해 해당 쿼리를 서버의 요청하여 각각의 3가지 상태 (요청중: loading, 가져온 데이터: data, 오류: error)를 비구조화 할당으로 작성하였다. 이때, refetch는 새로고침이고, fetchMore은 다음의 데이터를 요청하는 것이다. flatList에서 onRefresh를 사용하여 refetch를 연결하면 터치를 하여 밑으로 내리면 Indicator가 돌아가며 새로고침하는 것을 확인할 수 있다.

 

 

 

onEndReached는 flatlist가 하단 바닥의 도달할 경우 동작을 하게 되는데, 이때 onEndReached가 하단에서 어느 정도까지 도달했을 때 동작하는 지 정하는 것이 바로 onEndReachedThreshold이다. 위에서는 0.5로 되어 있으므로 현재의 화면에서 절반정도에 도달 했을 때 실행된다.

 

 

 fetchMore({
 variables:{ after: data.comments.pageInfo.endCursor
 } })
 .then((data) => {setIdleState(false);}));

 

 

 

 

리엑트 훅(React Hooks)을 사용하여 idleState라는 boolean 타입의 변수를 생성하였다. 이 변수를 사용하여 flatList의 아래에 도착했을 때 fetchMore 함수가 두 번 이상 실행되는 것을 막기 위함이다. fetchMore을 통해 다음의 데이터를 가져오기 위해 파라미터로 after필드에 현재까지 읽은 데이터의 마지막 커서인 endCursor를 넣어준다.

 

 

 

이제 가장 중요한 부분인데 바로 Apollo Client에서 지원하는 relayStylePagination 함수를 사용하여 Cache에 있는 데이터의 커서 기반 페이지네이션을 처리하는 부분이다.

 

 

import { relayStylePagination } from "@apollo/client/utilities";

const cache = new InMemoryCache({
  typePolicies: {
    Query: {
      fields: {
        comments: relayStylePagination(),
      },
    },
  },
});

 

 

 

InMemoryCache에서 페이지네이션 처리를 하려는 데이터 필드명과 relayStylePagination() 만 작성하면 Cursor-based의 페이지네이션이 자동으로 이루어진다! 실로 간편하지 않을 수 없다. 추가적인 파라미터에 따라 pagination 처리를 각기 하려면 relayStylePagination()의 파라미터로 relayStylePagination(["filter", "sort"]) 처럼 배열을 삽입하면 원하는 pagination 처리를 할 수 있다.

 

 

 Flutter나 React-Native에서 빌드를 하다가 Execution failed for task ':app:mergeDexDebug' 에러를 종종 볼 수 있다. minSdkVersion이 20 이하로 설정되어 있으면 multidex 지원 라이브러리를 사용해야 한다.

 

보다 자세한 것은

64K가 넘는 메서드의 앱에 관해 멀티덱스 사용 설정  |  Android 개발자  |  Android Developers

 

64K가 넘는 메서드의 앱에 관해 멀티덱스 사용 설정  |  Android 개발자  |  Android Developers

앱이 여러 DEX 파일을 빌드하고 읽을 수 있도록 하는 multidex라는 앱 구성의 사용설정 방법에 관해 알아보세요.

developer.android.com

 

'프로그래밍 > Flutter' 카테고리의 다른 글

Flutter 안드로이드에서 scrollview 반짝임(glow) 제거  (0) 2020.11.04
Flutter SNS Login Package  (0) 2020.08.24
Shared Preferences  (1) 2020.04.28
Cupertino ActionSheet  (0) 2020.04.05
image shake animation  (0) 2020.04.04

진짜 짱

 

 

아래 명령어 중 하나를 사용하면 된다.

 

# /etc/init.d/nginx restart

 

# /etc/init.d/nginx reload

 

# service nginx restart

 

# service nginx reload

 

손으로 당기면 생기는 저거...

플러터로 작성한 어플리케이션을 안드로이드 디바이스에서 실행하면 위처럼 스크롤 기능이 있는 위젯에서 터치에 의해 당겨지는 느낌의 effect를 볼 수 있다.

예전부터 너무 지우고 싶었던 효과라 stackoverflow에서 찾다가 발견해서 기록해둔다.

 

glow effect를 제거하는 Behavior을 생성한다. (ScrollBehavior 상속)

api.flutter.dev/flutter/widgets/ScrollBehavior-class.html

 

ScrollBehavior class - widgets library - Dart API

Describes how Scrollable widgets should behave. Used by ScrollConfiguration to configure the Scrollable widgets in a subtree. Annotations Constructors ScrollBehavior() Creates a description of how Scrollable widgets should behave. const Properties hashCode

api.flutter.dev

class MyBehavior extends ScrollBehavior {
  @override
  Widget buildViewportChrome(
      BuildContext context, Widget child, AxisDirection axisDirection) {
    return child;
  }
}

 

앱 전체에 적용할 경우 최상단 MaterialApp에 아래와 같이 적용시킨다.

MaterialApp(
  builder: (context, child) {
    return ScrollConfiguration(
      behavior: MyBehavior(),
      child: child,
    );
  },
  home: new MyHomePage(),
);

 

특정 ListView의 적용할 경우,

ScrollConfiguration(
  behavior: MyBehavior(),
  child: ListView(
    ...
  ),
)

'프로그래밍 > Flutter' 카테고리의 다른 글

Execution failed for task ':app:mergeDexDebug'  (0) 2021.02.20
Flutter SNS Login Package  (0) 2020.08.24
Shared Preferences  (1) 2020.04.28
Cupertino ActionSheet  (0) 2020.04.05
image shake animation  (0) 2020.04.04

1. Sequelize에서 boolean형 컬럼 토글 시키기(toggle)

model.update({ name: Sequelize.literal('NOT name') }, { where: { id } });

 

 

2. Mysql group by 에러

select 문 실행시 group by 관련 에러가 발생하였다.

Expression #1 of SELECT list is not in GROUP BY clause and contains nonaggregated column 'returntr_prod.tbl_customer_pod_uploads.id' which is not functionally dependent on columns in GROUP BY clause; this is
incompatible with sql_mode=only_full_group_by

워크벤치로 db 서버에 접속하여 아래와 같이 설정해 주었더니 해결 되었다.

SET GLOBAL sql_mode=(SELECT REPLACE(@@sql_mode,'ONLY_FULL_GROUP_BY',''));

 

3. Mysql 여러개의 레코드 update, delete할 때 안전모드 해제

mysql에서 한 개 이상의 레코드를 업데이트 하려는데 아래와 같은 에러가 발생하였다.

Error Code: 1175. You are using safe update mode and you tried to update a table without a WHERE that uses a KEY column To disable safe mode, toggle the option in Preferences -> SQL Editor and reconnect.

테이블에서 key를 이용한 (where id = 23 같이) update와 delete만을 허용하도록 되어 있는데, 그 보다 큰 범위에 적용하는 sql의 경우 workbench에서 경고를 주는 것이다.

 

아래와 같은 sql로 환경변수를 변경해 준다. (safe 모드 해제)

set sql_safe_updates=0;

 

 

4. 파이어 베이스 로그인 오류 Error: Command requires authentication, please run firebase login

오류에서 알 수 있듯, 권한이 부여되지 않았기에 로그인을 해주면 된다.

콘솔에 아래 명령어를 입력해 로그인을 해준다.

firebase login
Error: Cannot run login in non-interactive mode. See login:ci to generate a token for use in non-interactive environments.

라는 에러가 뜬다면

firebase login --interactive

+ Recent posts