dev-resources.site
for different kinds of informations.
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
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/
Featured ones: