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
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
환경 변수도 사용할 예정이기에 dotenv 패키지도 설치
Terminal
pnpm add -S dotenv
.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
env 항목이 Type Error 에 잡히지 않도록 next-constants.d.ts 파일을 생성하고 default type으로 변수 선언
next-constants.d.ts
declare namespace NodeJS {
export interface ProcessEnv {
NEXT_PUBLIC_GRAPHQL_URL: string;
}
}
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,
},
},
});
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;
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
}
}
}
}
`;
-
getStaticProps를 이용해Server에서 미리 캐시된dehydratedState를 내려준다.
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;
Result - React Query + GraphQL
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
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 함수를 exportexposeQueryKey:react quey의query key도 exportfetcher: 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
pakage.json 에 graphql-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
}
}
}
}
여기까지 진행했다면 모든 준비를 완료한 상태
현재 파일 구조는 다음과 같다.
structure
.
├── 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 들이 자동으로 생성되어 있는 것을 확인할 수 있다. (파일 내용은 생략)
Terminal
pnpm generate:gql
✔ Parse Configuration
✔ Generate outputs
자동생성된 Query Method와 Type을 통해 보다 쉽게 album의 데이터들을 호출해보자
pages폴더안에 new.tsx 파일을 다음과 같이 작성
legacy.tsx와 정확히 동일한 기능을 하는 페이지이다.
Note
useQuery관련 코드가 자동 생성된useAlbumQuery한줄로 대체
const { data } = useAlbumQuery(gqlClient, { id: '3' });
-
prefetchQuery에서key,fetcher코드를 직접 작성할 필요없이useAlbumQuery.getKey(),useAlbumQuery.fetcher()로 대체
await queryClient.prefetchQuery(
useAlbumQuery.getKey({ id: '3' }),
useAlbumQuery.fetcher(gqlClient, { id: '3' })
);
-
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;
Result
👉 CodeSandBox Sample Link
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/


Top comments (0)