Logo

dev-resources.site

for different kinds of informations.

How to use GraphQL and React Query with GraphQL Code Generator (based on Next.Js)

Published at
8/10/2022
Categories
gql
reactquery
gqlcodegen
nextjs
Author
soom
Categories
4 categories in total
gql
open
reactquery
open
gqlcodegen
open
nextjs
open
Author
4 person written this
soom
open
How to use GraphQL and React Query with GraphQL Code Generator (based on Next.Js)

Abstract

Introducing how to apply GraphQL with GraphQL Code Generator & React Query on Next.js framework base.

GraphQL. 기쑴의 REST API 호좜 방식을 λ„˜μ–΄ schemaλ₯Ό μ΄μš©ν•΄ 마치 DBλ₯Ό λ‹€λ£¨λŠ” sqlκ³Ό 같이 데이터 ν˜ΈμΆœμ„ λ‹€λ£°μˆ˜ μžˆλŠ” μƒˆλ‘œμš΄ κ°œλ….

GQL은 이미 개발자라면 μ΅μˆ™ν•œ μš©μ–΄κ°€ λ˜λ²„λ Έμ§€λ§Œ 아직도 ν˜„μž¬ μ§„ν–‰ν˜•μ΄λ©° 이번 ν¬μŠ€νŒ…μ—λŠ” Query μ†”λ£¨μ…˜μΈ React Query와 기쑴의 GQL의 pain point 쀑 ν•˜λ‚˜μΈ Typeκ³Ό Schema 관리, λΆˆν•„μš”ν•œ 반볡적인 μ½”λ“œ μž‘μ„±μ„ μžλ™μœΌλ‘œ ν•΄κ²°ν•΄μ£ΌλŠ” GraphQL Code Generatorλ₯Ό μ΄μš©ν•΄ GQL을 μ§κ΄€μ μœΌλ‘œ κ΄€λ¦¬ν•˜λŠ” 방법을 μ†Œκ°œν•˜κ³ μžν•œλ‹€.

GQL에 λŒ€ν•œ λ‚΄μš©μ€ ν•˜κΈ° μ°Έμ‘°
GQL μ΄λž€?: https://tech.kakao.com/2019/08/01/graphql-basic/


Getting Started

μ›ν•˜λŠ” ν”„λ‘œμ νŠΈ 폴더에 Next.Js TypeScript ν”„λ‘œμ νŠΈλ₯Ό 생성

Terminal
pnpm create next-app . --typescript
Enter fullscreen mode Exit fullscreen mode

React Query둜 GQLλ₯Ό μ‚¬μš©ν•˜κ³ μž ν•„μš”ν•œ νŒ¨ν‚€μ§€λ₯Ό μ„€μΉ˜

Note

졜근 React QueryλŠ” νŒ¨ν‚€μ§€ λͺ…이 TanStack Query 큰 μΉ΄ν…Œκ³ λ¦¬λ‘œ λ¬Άμ˜€λŠ”λ° μΆ”ν›„ Sevelte Query λ“± λ‹€μ–‘ν•œ ν”Œλž«νΌμ„ 지원할 >μ˜ˆμ •μ΄λΌ ν•œλ‹€.

Terminal
pnpm add -S @tanstack/react-query graphql graphql-request

pnpm add -D @tanstack/react-query-devtools
Enter fullscreen mode Exit fullscreen mode

ν™˜κ²½ λ³€μˆ˜λ„ μ‚¬μš©ν•  μ˜ˆμ •μ΄κΈ°μ— dotenv νŒ¨ν‚€μ§€λ„ μ„€μΉ˜

Terminal
pnpm add -S dotenv
Enter fullscreen mode Exit fullscreen mode

.env.local 을 μƒμ„±ν•œλ’€ API URL 을 등둝

GraphQL API μ£Όμ†ŒλŠ” Fake GraphQL을 μ œκ³΅ν•˜λŠ” GraphQLZeroλ₯Ό μ΄μš©ν•˜μ˜€λ‹€.

GraphQLZero Link: https://graphqlzero.almansi.me/

.env.local
NEXT_PUBLIC_GRAPHQL_URL=https://graphqlzero.almansi.me/api
Enter fullscreen mode Exit fullscreen mode

env ν•­λͺ©μ΄ Type Error 에 μž‘νžˆμ§€ μ•Šλ„λ‘ next-constants.d.ts νŒŒμΌμ„ μƒμ„±ν•˜κ³  default type으둜 λ³€μˆ˜ μ„ μ–Έ

next-constants.d.ts
declare namespace NodeJS {
    export interface ProcessEnv {
        NEXT_PUBLIC_GRAPHQL_URL: string;
    }
}
Enter fullscreen mode Exit fullscreen mode

Common Approach by React Query + GraphQL

react query 섀정을 μœ„ν•΄ _app.tsx 을 λ‹€μŒκ³Ό 같이 μˆ˜μ •

Note

  • SSR이기에 SEO와 UXλ₯Ό μ΅œμ ν™” ν•˜κΈ°μœ„ν•΄ Hydration Stateλ₯Ό μ„€μ •
  • Hydration κ°œλ…μ€ ν•˜κΈ° 링크 μ°Έμ‘°
  • pageProps.dehydratedState λŠ” Serverside Props μ—μ„œ μ΄μš©ν•  μ˜ˆμ •
  • refetchλ₯Ό μ΅œμ†Œν™”ν•΄μ„œ UX μ΅œμ ν™”
const queryClient = new QueryClient({
    defaultOptions: {
        queries: {
            refetchOnMount: false,
            refetchOnWindowFocus: false,
            refetchOnReconnect: false,
        },
    },
});
Enter fullscreen mode Exit fullscreen mode
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;
Enter fullscreen mode Exit fullscreen mode

GraphQlZero의 album Resolver Queryλ₯Ό react query둜 μš”μ²­

legacy.tsx νŒŒμΌμ„ μƒμ„±ν•˜κ³  μ‹€μ œλ‘œ 데이터가 λ“€μ–΄μ˜€λŠ” 것을 ν™•μΈν•œλ‹€.

Note

  • type μ΄λ‚˜ schemaλŠ” GraphQlZero의 Docsλ₯Ό μ°Έκ³ ν•΄μ„œ μ›ν•˜λŠ” λ°μ΄ν„°λ§Œ μž„μ˜λ‘œ μž‘μ„±
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
                }
            }
        }
    }
`;
Enter fullscreen mode Exit fullscreen mode
  • getStaticPropsλ₯Ό μ΄μš©ν•΄ Serverμ—μ„œ 미리 μΊμ‹œλœ dehydratedStateλ₯Ό λ‚΄λ €μ€€λ‹€.
export const getStaticProps = async () => {
    await queryClient.prefetchQuery(['album'], useAlbumFetcher);

    return {
        props: {
            dehydratedState: dehydrate(queryClient),
        },
    };
};
Enter fullscreen mode Exit fullscreen mode
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;
Enter fullscreen mode Exit fullscreen mode

Result - React Query + GraphQL

gql_gen_1

Note - Pain Point

μ—¬κΈ°κΉŒμ§€κ°€ 기쑴의 gql + react query 방식을 톡해 데이터λ₯Ό λ°›μ•„μ˜€λŠ” 과정이닀.
ν•˜μ§€λ§Œ 이런 λ°©μ‹μ—λŠ” Pain Pointκ°€ μ‘΄μž¬ν•œλ‹€.

  • schema 에 λŒ€μ‘ν•˜λŠ” Type을 직접 μž‘μ„±
  • schema 변경이 μžˆλ‹€λ©΄ Type μ—­μ‹œ Docs λ₯Ό ν™•μΈν•œ ν›„ 직접 λ³€κ²½ ν•„μš”
  • μš”μ²­ν• λ•Œλ§ˆλ‹€ 반볡적인 μ½”λ“œ 반볡 μž‘μ„±

New Approach by React Query + GraphQL + GraphQL Code Generator

이런 Pain Pointλ₯Ό μžλ™μœΌλ‘œ ν•΄κ²°ν•΄μ£ΌλŠ” 것이 GraphQL Code Generator λ‹€.

GQL Code Generator κ΄€λ ¨ νŒ¨ν‚€μ§€λ₯Ό μ„€μΉ˜

Terminal
# 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
Enter fullscreen mode Exit fullscreen mode

codegen.yml νŒŒμΌμ„ μƒμ„±ν•œ λ’€ μ„€μ •κ°’ μž…λ ₯

Note

  • schema λŠ” graphql 폴더 μ•ˆμ— [filename].graphql둜 κ΄€λ¦¬ν•˜κΈ°λ‘œ ν•œλ‹€. (tsλ‚˜ λ‹€λ₯Έ ν™•μž₯μžλ„ κ°€λŠ₯)
  • documentsμ—λŠ” gql schema 파일 ν˜•μ‹κ³Ό μœ„μΉ˜λ₯Ό μ„€μ •ν•΄μ€€λ‹€.
documents: 'graphql/**/!(*.generated).{graphql,ts}'
  • schema λŠ” GQL URL μœ„μΉ˜. μ—¬κΈ°μ„œλŠ” ν™˜κ²½ λ³€μˆ˜λ‘œ 관리 ν•˜κΈ°λ•Œλ¬Έμ— λ‹€μŒκ³Ό 같이 μž‘μ„±
schema: ${NEXT_PUBLIC_GRAPHQL_URL}
  • μ€‘μš” μ˜΅μ…˜λ“€μ€ λ‹€μŒκ³Ό κ°™λ‹€. λ‚˜λ¨Έμ§€ λ‚΄μš©λ“€μ€ ν•˜κΈ° λ§ν¬μ—μ„œ 확인: https://www.graphql-code-generator.com/plugins/typescript/typescript-react-query
    • exposeFetcher: GetStaticProps, GetServerSideProps 에 prefetch둜 μ‚¬μš©ν•  query fetcher ν•¨μˆ˜λ₯Ό export
    • exposeQueryKey: react quey의 query key 도 export
    • fetcher : fetcher둜 μ‚¬μš©ν•  λͺ¨λ“ˆ. μ—¬κΈ°μ„œλŠ” 기쑴에 μ‚¬μš©ν–ˆλ˜ graphql-request μ‚¬μš©ν•œλ‹€.
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
Enter fullscreen mode Exit fullscreen mode

pakage.json 에 graphql-codegen scriptμΆ”κ°€

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

Enter fullscreen mode Exit fullscreen mode

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
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

μ—¬κΈ°κΉŒμ§€ μ§„ν–‰ν–ˆλ‹€λ©΄ λͺ¨λ“  μ€€λΉ„λ₯Ό μ™„λ£Œν•œ μƒνƒœ
ν˜„μž¬ 파일 κ΅¬μ‘°λŠ” λ‹€μŒκ³Ό κ°™λ‹€.

structure
.
β”œβ”€β”€ graphql/
β”‚   └── album.graphql
β”œβ”€β”€ pages/
β”‚   β”œβ”€β”€ _app.tsx
β”‚   β”œβ”€β”€ index.tsx
β”‚   └── legacy.tsx
β”œβ”€β”€ ...
β”œβ”€β”€ codegen.yml
β”œβ”€β”€ next-constants.d.ts
β”œβ”€β”€ package.json
└── ...
Enter fullscreen mode Exit fullscreen mode

이제 GraphQL Generatorλ₯Ό μ‚¬μš©ν•΄ Type, Method λ₯Ό μžλ™ 생성이 κ°€λŠ₯

scriptλ₯Ό μ‹€ν–‰ν•˜λ©΄ graphql 폴더 μ•ˆμ— generated.tsκ°€ 생성

이 νŒŒμΌμ•ˆμ—λŠ” schema에 λŒ€μ‘ν•˜λŠ” Query Function, Type 듀이 μžλ™μœΌλ‘œ μƒμ„±λ˜μ–΄ μžˆλŠ” 것을 확인할 수 μžˆλ‹€. (파일 λ‚΄μš©μ€ μƒλž΅)

Terminal
pnpm generate:gql

βœ” Parse Configuration
βœ” Generate outputs
Enter fullscreen mode Exit fullscreen mode

μžλ™μƒμ„±λœ Query Method와 Type을 톡해 보닀 μ‰½κ²Œ album의 데이터듀을 ν˜ΈμΆœν•΄λ³΄μž

pagesν΄λ”μ•ˆμ— new.tsx νŒŒμΌμ„ λ‹€μŒκ³Ό 같이 μž‘μ„±

legacy.tsx와 μ •ν™•νžˆ λ™μΌν•œ κΈ°λŠ₯을 ν•˜λŠ” νŽ˜μ΄μ§€μ΄λ‹€.

Note

  • useQuery κ΄€λ ¨ μ½”λ“œκ°€ μžλ™ μƒμ„±λœ useAlbumQuery ν•œμ€„λ‘œ λŒ€μ²΄
const { data } = useAlbumQuery(gqlClient, { id: '3' });
Enter fullscreen mode Exit fullscreen mode
  • prefetchQuery μ—μ„œ key, fetcher μ½”λ“œλ₯Ό 직접 μž‘μ„±ν•  ν•„μš”μ—†μ΄ useAlbumQuery.getKey(), useAlbumQuery.fetcher() 둜 λŒ€μ²΄
await queryClient.prefetchQuery(
    useAlbumQuery.getKey({ id: '3' }),
    useAlbumQuery.fetcher(gqlClient, { id: '3' })
);
Enter fullscreen mode Exit fullscreen mode
  • Type은 μžλ™ μ„ μ–Έλ˜μ–΄ μ—°κ²°λ˜μ–΄μžˆκΈ° λ•Œλ¬Έμ— λ”°λ‘œ μž‘μ„± ν•„μš” λΆˆκ°€
  • 이후 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;
Enter fullscreen mode Exit fullscreen mode

Result

πŸ‘‰ CodeSandBox Sample Link

gql_gen_2


Conclusion

λ³Έ ν¬μŠ€νŒ…μ—μ„œλŠ” 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 에 λŒ€ν•΄ μžμ„Έν•œ λ‚΄μš© ν•˜κΈ° λ§ν¬μ—μ„œ 확인할 수 μžˆλ‹€.
Link: https://www.graphql-code-generator.com/

Featured ones: