Как использовать GraphQL и React Query с помощью генератора кода GraphQL (на основе Next.Js)


Абстрактный

GraphQL. 기존의 REST API 호출 방식을 넘어 schema를 이용해 마치 DB를 다루는 sql과 같이 데이터 호출을 다룰수 있는 새로운 개념.

GQL은 이미 개발자라면 익숙한 용어가 되버렸지만 아직도 현재 진행형이며 이번 포스팅에는 Query 솔루션인 React Query와 기존의 GQL의 pain point 중 하나인 TypeSchema 관리, 불필요한 반복적인 코드 작성을 자동으로 해결해주는 GraphQL Code Generator를 이용해 GQL을 직관적으로 관리하는 방법을 소개하고자한다.

GQL에 대한 내용은 하기 참조
GQL 이란?: https://tech.kakao.com/2019/08/01/graphql-basic/


Начало работы

원하는 프로젝트 폴더에 Next.Js TypeScript 프로젝트를 생성

Терминал
pnpm create next-app . --typescript  
Вход в полноэкранный режим Выход из полноэкранного режима

React QueryGQL를 사용하고자 필요한 패키지를 설치

Примечание

최근 React Query는 패키지 명이 TanStack Query 큰 카테고리로 묶였는데 추후 Sevelte Query 등 다양한 플랫폼을 지원할 예정이라 한다.

Терминал
pnpm add -S @tanstack/react-query graphql graphql-request

pnpm add -D @tanstack/react-query-devtools
Вход в полноэкранный режим Выход из полноэкранного режима

환경 변수도 사용할 예정이기에 dotenv 패키지도 설치

Терминал
pnpm add -S dotenv
Вход в полноэкранный режим Выход из полноэкранного режима

.env.local 을 생성한뒤 API URL 을 등록

GraphQL API 주소는 Fake GraphQL을 제공하는 GraphQLZero를 이용하였다.

GraphQLZero Ссылка: https://graphqlzero.almansi.me/

.env.local
NEXT_PUBLIC_GRAPHQL_URL=https://graphqlzero.almansi.me/api
Вход в полноэкранный режим Выйти из полноэкранного режима

env 항목이 Type Error 에 잡히지 않도록 next-constants.d.ts 파일을 생성하고 default type으로 변수 선언

next-constants.d.ts
declare namespace NodeJS {
    export interface ProcessEnv {
        NEXT_PUBLIC_GRAPHQL_URL: string;
    }
}
Вход в полноэкранный режим Выход из полноэкранного режима

비교를 위한 기존의 react query + gql 방식을 구현

react query 설정을 위해 _app.tsx 을 다음과 같이 수정

Примечание

    • Ссылка: https://blog.saeloun.com/2021/12/16/hydration.html
const queryClient = new QueryClient({
    defaultOptions: {
        queries: {
            refetchOnMount: false,
            refetchOnWindowFocus: false,
            refetchOnReconnect: false,
        },
    },
});
Вход в полноэкранный режим Выход из полноэкранного режима
pages/__app.tsx
import '../styles/globals.css';

import type { AppProps } from 'next/app';

import { Hydrate, QueryClientProvider, QueryClient } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';

export const queryClient = new QueryClient({
    defaultOptions: {
        queries: {
            refetchOnMount: false,
            refetchOnWindowFocus: false,
            refetchOnReconnect: false,
        },
    },
});

function MyApp({ Component, pageProps }: AppProps) {
    return (
        <QueryClientProvider client={queryClient}>
            <Hydrate state={pageProps.dehydratedState}>
                <Component {...pageProps} />;
                <ReactQueryDevtools initialIsOpen />
            </Hydrate>
        </QueryClientProvider>
    );
}

export default MyApp;
Вход в полноэкранный режим Выход из полноэкранного режима

GraphQlZeroalbum Resolver Query를 react query로 요청

legacy.tsx 파일을 생성하고 실제로 데이터가 들어오는 것을 확인한다.

Примечание

interface AlbumQuery {
    album: {
        id: string;
        title: string;
        user: {
            id: string;
            name: string;
            username: string;
            email: string;
            company: {
                name: string;
                bs: string;
            };
        };
        photos: {
            data: {
                id: string;
                title: string;
                url: string;
            };
        };
    };
}

const albumQueryDocument = gql`
    query album($id: ID!) {
        album(id: $id) {
            id
            title
            user {
                id
                name
                username
                email
                company {
                    name
                    bs
                }
            }
            photos {
                data {
                    id
                    title
                    url
                }
            }
        }
    }
`;
Вход в полноэкранный режим Выход из полноэкранного режима
export const getStaticProps = async () => {
    await queryClient.prefetchQuery(['album'], useAlbumFetcher);

    return {
        props: {
            dehydratedState: dehydrate(queryClient),
        },
    };
};
Войти в полноэкранный режим Выход из полноэкранного режима
pages/legacy.tsx
import type { NextPage } from 'next';

import { useQuery, dehydrate } from '@tanstack/react-query';
import { request, gql } from 'graphql-request';
import { queryClient } from './_app';

interface AlbumQuery {
    album: {
        id: string;
        title: string;
        user: {
            id: string;
            name: string;
            username: string;
            email: string;
            company: {
                name: string;
                bs: string;
            };
        };
        photos: {
            data: {
                id: string;
                title: string;
                url: string;
            };
        };
    };
}

const albumQueryDocument = gql`
    query album($id: ID!) {
        album(id: $id) {
            id
            title
            user {
                id
                name
                username
                email
                company {
                    name
                    bs
                }
            }
            photos {
                data {
                    id
                    title
                    url
                }
            }
        }
    }
`;

const useAlbumFetcher = async () =>
    await request<AlbumQuery, { id: string }>(
        process.env.NEXT_PUBLIC_GRAPHQL_URL,
        albumQueryDocument,
        {
            id: '2',
        }
    );

export const getStaticProps = async () => {
    await queryClient.prefetchQuery(['album'], useAlbumFetcher);

    return {
        props: {
            dehydratedState: dehydrate(queryClient),
        },
    };
};

const Legacy: NextPage = () => {
    const { data } = useQuery<AlbumQuery>(['album'], useAlbumFetcher);
    const { album } = data!;

    return (
        <>
            <header style={{ textAlign: 'center' }}>
                <h1>Hello GraphQL + React Query !</h1>
            </header>
            <hr />
            <main>
                <p style={{ textAlign: 'center', color: 'grey' }}>{JSON.stringify(album)}</p>
            </main>
        </>
    );
};

export default Legacy;
Войти в полноэкранный режим Выход из полноэкранного режима
предварительный просмотр


여기까지가 기존의 gql + react query 방식을 통해 데이터를 받아오는 과정이다.

하지만 이런 방식에는 Pain Point가 존재한다.

  • 요청할때마다 반복적인 코드 반복 작성

이런 Pain Point를 자동으로 해결해주는 것이 GraphQL Code Generator 다.

GQL Code Generator 관련 패키지를 설치

Терминал
# code generator core 패키지
pnpm add -D @graphql-codegen/cli @graphql-codegen/typescript @graphql-codegen/typescript-operations

# code generator react query 관련 패키지
pnpm add -D @graphql-codegen/typescript-react-query @graphql-codegen/typescript-graphql-request

# code generator yaml loader 패키지
pnpm add -D yaml-loader
Вход в полноэкранный режим Выход из полноэкранного режима

codegen.yml 파일을 생성한 뒤 설정값 입력

Примечание

documents: 'graphql/**/!(*.generated).{graphql,ts}'
Вход в полноэкранный режим Выход из полноэкранного режима
schema: ${NEXT_PUBLIC_GRAPHQL_URL}
Войти в полноэкранный режим Выход из полноэкранного режима
  • 중요 옵션들은 다음과 같다.나머지 내용들은 하기 링크에서 확인: https://www.graphql-code-generator.com/plugins/typescript/typescript-react-query
codegen.yml
documents: 'graphql/**/!(*.generated).{graphql,ts}'
schema: ${NEXT_PUBLIC_GRAPHQL_URL}
require:
  - ts-node/register
generates:
  graphql/generated.ts:
    plugins:
      - typescript
      - typescript-operations
      - typescript-react-query
    config:
      interfacePrefix: 'I'
      typesPrefix: 'I'
      skipTypename: true
      declarationKind: 'interface'
      noNamespaces: true
      pureMagicComment: true
      exposeQueryKeys: true
      exposeFetcher: true
      withHooks: true
      fetcher: graphql-request
Вход в полноэкранный режим Выход из полноэкранного режима

pakage.jsongraphql-codegen script추가

package.json
{
    ...
    "scripts": {
        ...
        "generate:gql": "graphql-codegen --require dotenv/config --config codegen.yml dotenv_config_path=.env.local"
    },
    ...
}

Вход в полноэкранный режим Выход из полноэкранного режима

graphql 폴더를 생성한뒤 요청할 schema 파일을 작성
여기서는 album 관련 schema를 album.graphql에 작성

graphql/album.graphql
query album($id: ID!) {
    album(id: $id) {
        id
        title
        user {
            id
            name
            username
            email
            company {
                name
                bs
            }
        }
        photos {
            data {
                id
                title
                url
            }
        }
    }
}
Вход в полноэкранный режим Выйти из полноэкранного режима

여기까지 진행했다면 모든 준비를 완료한 상태
현재 파일 구조는 다음과 같다.

структура
.
├── graphql/
│   └── album.graphql
├── pages/
│   ├── _app.tsx
│   ├── index.tsx
│   └── legacy.tsx
├── ...  
├── codegen.yml
├── next-constants.d.ts
├── package.json
└── ...
Вход в полноэкранный режим Выход из полноэкранного режима

이제 GraphQL Generator를 사용해 Type, Method 를 자동 생성이 가능

script를 실행하면 graphql 폴더 안에 generated.ts가 생성

이 파일안에는 schema에 대응하는 Query Function, Type 들이 자동으로 생성되어 있는 것을 확인할 수 있다. (파일 내용은 생략)

Терминал
pnpm generate:gql

✔ Parse Configuration
✔ Generate outputs
Вход в полноэкранный режим Выход из полноэкранного режима

자동생성된 Query MethodType을 통해 보다 쉽게 album의 데이터들을 호출해보자

pages폴더안에 new.tsx 파일을 다음과 같이 작성

legacy.tsx와 정확히 동일한 기능을 하는 페이지이다.

Примечание

const { data } = useAlbumQuery(gqlClient, { id: '3' });
Вход в полноэкранный режим Выход из полноэкранного режима
await queryClient.prefetchQuery(
        useAlbumQuery.getKey({ id: '3' }),
        useAlbumQuery.fetcher(gqlClient, { id: '3' })
    );

Войти в полноэкранный режим Выход из полноэкранного режима
  • 이후 server spec 변경으로 인한 schema 변경시 code generate 명령 한줄로 반복적인 type 매칭, 재작성 과정을 생략할 수 있다.
pages/new.tsx
import type { NextPage } from 'next';

import { dehydrate } from '@tanstack/react-query';
import { GraphQLClient } from 'graphql-request';
import { useAlbumQuery } from '../graphql/generated';
import { queryClient } from './_app';

const gqlClient = new GraphQLClient(process.env.NEXT_PUBLIC_GRAPHQL_URL);

export const getStaticProps = async () => {
    await queryClient.prefetchQuery(
        useAlbumQuery.getKey({ id: '2' }),
        useAlbumQuery.fetcher(gqlClient, { id: '2' })
    );

    return {
        props: {
            dehydratedState: dehydrate(queryClient),
        },
    };
};

const New: NextPage = () => {
    const { data } = useAlbumQuery(gqlClient, { id: '2' });

    const { album } = data!;

    return (
        <>
            <header style={{ textAlign: 'center' }}>
                <h1>Hello GraphQL + React Query !</h1>
            </header>
            <hr />
            <main>
                <p style={{ textAlign: 'center', color: 'grey' }}>{JSON.stringify(album)}</p>
            </main>
        </>
    );
};

export default New;
Вход в полноэкранный режим Выход из полноэкранного режима
предварительный просмотр


➕ Образец Stackblitz


Заключение

본 포스팅에서는 GraphQL Code Generator 를 통해 Server Spec 변경할때마다 schema 변경뿐만 아니라 type 까지 재작성을 해야하는 GQL의 Pain Point 를 해결하는 방법을 소개하였다. 추가적으로 SSR을 통해 데이터 관련하여 hydration하는 technique도 같이 소개하였다.

현재까지도 주류는 REST API 이다. 하지만 큰 변화가 없는 REST API와 달리 GQL에서는 여러가지 기능이 꾸준히 소개되고 발전하고 있다. 특히 Backend 와 Frontend 사이의 Communication Gap 을 줄여주는 방향으로 GQL은 꾸준히 발전하고 있으며 이는 실무의 인적 비용과도 직접적으로 연결되는 방향이다.

이는 개발자라면 GQL에 관해 앞으로도 꾸준히 관심을 가질만한 충분한 이유가 될것이다.

GQL Code Generator 에 대해 자세한 내용 하기 링크에서 확인할 수 있다.
Ссылка: https://www.graphql-code-generator.com/

Оцените статью
devanswers.ru
Добавить комментарий