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

by:

Softwares

Abstract

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/


Getting Started

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

Terminal

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

Exit fullscreen mode


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

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


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

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

Note

  • SSR이기에 SEOUX를 최적화 하기위해 Hydration State를 설정
  • Hydration 개념은 하기 링크 참조

  • pageProps.dehydratedStateServerside 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


GraphQlZeroalbum Resolver Query를 react query로 요청

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

Note

  • type 이나 schemaGraphQlZero의 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

preview

Image description


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

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

  • schema 에 대응하는 Type을 직접 작성
  • schema 변경이 있다면 Type 역시 Docs 를 확인한 후 직접 변경 필요
  • 요청할때마다 반복적인 코드 반복 작성

이런 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

  • schemagraphql 폴더 안에 [filename].graphql로 관리하기로 한다. (ts나 다른 확장자도 가능)
  • documents에는 gql schema 파일 형식과 위치를 설정해준다.
documents: 'graphql/**/!(*.generated).graphql,ts'
Enter fullscreen mode

Exit fullscreen mode

  • schemaGQL URL 위치. 여기서는 환경 변수로 관리 하기때문에 다음과 같이 작성
schema: $NEXT_PUBLIC_GRAPHQL_URL
Enter fullscreen mode

Exit fullscreen mode

  • 중요 옵션들은 다음과 같다.
    나머지 내용들은 하기 링크에서 확인:
    https://www.graphql-code-generator.com/plugins/typescript/typescript-react-query

    • exposeFetcher: GetStaticProps, GetServerSideProps 에 prefetch로 사용할 query fetcher 함수를 export
    • exposeQueryKey: react queyquery 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.jsongraphql-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 MethodType을 통해 보다 쉽게 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

preview

Image description


➕ Stackblitz Sample


Conclusion

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

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

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

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

Leave a Reply

Your email address will not be published.