서론
어느덧 인턴 한 달 이 지나고 너무 많은 배움을 통해 기록하고자 글을 쓰게 되었습니다.
인턴에 입사하기 전에는 혼자서 배포를 해보기도 하고, 여러가지 서비스를 만들 때 쓰던 기술 스택과 비슷해서 큰 어려움이 없을 것 이라고 생각했지만..! 그건 역시 오산 이었습니다. ㅎㅎ
기존에 REST API 로 Swagger 를 통해 문서화를 해서 /api/(각 엔드포인트 별)로 API를 관리하고 호출하는 형식으로 작성하고 React Query 의 캐시를 이용해 UX를 개선하는 용으로 사용했습니다.
이때 쓰던 방식도 동작은 하지만 Next의 hydration이나 staleTime으로 인한 서버 부하의 고려를 안 한 단순 프로젝트였었습니다.
물론 공부했던 내용을 실무에 적용할 수 있어서 뿌듯한 것도 많았지만, 역시 100% 같은 환경 이 아니라면, 오류가 나고 완벽하게 학습한 그대로 적용할 수 없어 새로 학습했던 일도 많았습니다..!!
그래서 오늘은 Next.js 15 로 마이그레이션하며 새로 학습했던 일을 기록하고자 합니다!
GraphQL을 쓰면 React Hook이 자동 생성된다?
REST API에서 React Query를 사용 시
1const useGetUser = (userId: string) => {2 return useQuery({3 queryKey: ['user', userId],4 queryFn: async () => {5 const response = await fetch(`/api/users/${userId}`);6 return response.json(); // 보일러플레이트 존재7 }8 });9};10
11// 타입도 직접 정의12interface User {13 id: string;14 name: string;15 email: string;16}만약 이게 자동으로 생성 된다면? 안 쓸 이유가 없지 않을까요? 라는 생각이 들었습니다. (물론 장단점은 있습니다. 주의!)
Codegen을 이용해보자
코드 제너레이션(Code Generation) 또는 코드 젠(Code gen). 말 그대로 코드를 자동으로 생성 하는 행위입니다. 그리고 코드를 자동으로 생성해 주는 도구를 Code Generator 라고 합니다.
어떤 응답 예측 가능한 파일(.gql, swagger 등)을 읽고 자동으로 Type 이나 Hook 등을 생성해주죠.
GraphQL은 왜 Codegen이 쉬울까?
1# schema.graphql - 타입이 명확하게 정의됨2
3type User {4 id: ID!5 name: String!6 email: String!7 posts: [Post!]!8}9
10type Query {11 user(id: ID!): User12 users: [User!]!13}위와 같이 GraphQL 에서 타입을 정의하면 Query를 요청할 때 id와 name만 필요한 경우는 해당 타입으로 요청을 보내면 그에 해당하는 응답을 받을 수 있습니다.
그렇게 우리가 필요한 Query 나 Mutation 을 .gql에 정의하고 그걸 Codegen이 읽어서 자동으로 Hook을 만들거나 Type을 만들거나 필요한 코드를 자동으로 생성해줍니다.
REST API는 왜 어려울까?
1# REST API 엔드포인트들2
3GET /api/users/:id4GET /api/users/:id/posts 5POST /api/users6PUT /api/users/:idREST API의 경우는 API 엔드포인트만으로는 응답 값이 정확하게 뭔지 알 수 없습니다.
GET /api/users/:id의 경우 id, name, email 등 여러 가지를 한번에 받을 수도 있고 name만 응답을 받을 수도 있고 잘 모르는데요. 그래서 Swagger/OpenAPI 를 통해서 명세서(문서화)를 만들어야 합니다.
하지만, 이런 명세서는 강제성이 없고, 서버에서 자동으로 만들어주는 것이 아니라 수동으로 관리해야 하기 때문에 신뢰할 수 없습니다. 물론 불가능하진 않습니다. 타입 또한 불일치도 많이 일어나겠죠 (ex. null or undefined)
그렇기 때문에 Codegen은 GraphQL과의 호환이 좋다 고 많이 알려져 있습니다.
다만, 정답은 없습니다. Codegen이 더 이상 업데이트가 안 되면, Codegen을 통해 만들어진 코드들은 의존성이 높아 결국 마이그레이션을 해야겠죠? 그러면 수동으로 Hook을 생성하거나 Type을 정의하는 것이 나을 수도 있습니다.
GraphQL도 REST API도 결국 장단점이 존재 하니 본인 프로젝트에 맞는 기술을 쓰면 좋을 것 같습니다!
Codegen 세팅
API의 타입이나 일부 변경 시 프론트 팀원에게 공지 후 프론트는 문서화된 글을 통해서 타입을 수정하고 프론트 쪽에서 QA까지 다시 진행 하게 되면 유지보수 비용 측면에서 여러 가지 코드를 수정해야 하니 아쉬울 수 있습니다.
의존성이 높아지긴 하지만, 풀스택 개발 을 하고 있다면 생산성 면 에서 괜찮은 선택이라고 생각합니다.
1. codegen.yml 설정
1# codegen.yml2overwrite: true3schema: "http://localhost:4000/graphql" # 백엔드 GraphQL 엔드포인트4documents: "src/**/*.gql" # .gql 파일 위치5generates:6 src/types/graphql.ts:7 plugins:8 - typescript9 - typescript-operations10 - typescript-react-query # graphql-client 시 document로 바꿀 수 있음11 config:12 # React Query Hook 자동 생성!13 fetcher: 14 func: ./fetcher#fetcher 15 exposeQueryKeys: true16 exposeFetcher: true17 addInfiniteQuery: true핵심 설정 설명:
schema: 백엔드 GraphQL 스키마 주소documents: 프론트에서 작성한.gql파일들typescript-react-query: 이게 Hook을 자동생성해주는 플러그인exposeQueryKeys:queryKey도 자동 생성addInfiniteQuery:useInfiniteQuery도 자동 생성
2. Fetcher 함수 작성
1// src/lib/fetcher.ts - fetcher 함수를 통해 보일러플레이트를 줄일 수 있음2export const fetcher = async <TData, TVariables>(3 query: string,4 variables?: TVariables5): Promise<TData> => {6 const response = await fetch('http://localhost:4000/graphql', {7 method: 'POST',8 headers: {9 'Content-Type': 'application/json',10 },11 body: JSON.stringify({12 query,13 variables,14 }),15 });16
17 const json = await response.json();18
19 if (json.errors) {20 throw new Error(json.errors[0].message);21 }22
23 return json.data;24};3. .gql 파일 작성
1# src/queries/getUser.gql - 경로는 각자 알아서2query GetUser($id: ID!) {3 user(id: $id) {4 id5 name6 email7 posts {8 id9 title10 content11 }12 }13}1# src/mutations/createPost.gql2mutation CreatePost($input: CreatePostInput!) {3 createPost(input: $input) {4 id5 title6 content7 createdAt8 }9}4. Codegen 실행
위와 같이 .gql과 기본 설정을 끝냈다면
1npm run codegen자동 생성된 코드 확인!
1// src/types/graphql.ts (자동 생성됨!)2
3// 1. 타입 자동 생성4export type User = {5 __typename?: 'User';6 id: Scalars['ID'];7 name: Scalars['String'];8 email: Scalars['String'];9 posts: Array<Post>;10};11
12export type GetUserQueryVariables = Exact<{13 id: Scalars['ID'];14}>;15
16export type GetUserQuery = {17 __typename?: 'Query';18 user?: {19 __typename?: 'User';20 id: string;21 name: string;22 email: string;23 posts: Array<{24 __typename?: 'Post';25 id: string;26 title: string;27 content: string;28 }>;29 } | null;30};31
32// 2. React Query Hook 자동 생성!33export const useGetUserQuery = <34 TData = GetUserQuery,35 TError = unknown36>(37 variables: GetUserQueryVariables,38 options?: UseQueryOptions<GetUserQuery, TError, TData>39) => {40 return useQuery<GetUserQuery, TError, TData>(41 ['GetUser', variables],42 fetcher<GetUserQuery, GetUserQueryVariables>(GetUserDocument, variables),43 options44 );45};46
47// 3. Suspense용 Hook도 자동 생성!48export const useGetUserSuspenseQuery = <49 TData = GetUserQuery,50 TError = unknown51>(52 variables: GetUserQueryVariables,53 options?: UseSuspenseQueryOptions<GetUserQuery, TError, TData>54) => {55 return useSuspenseQuery<GetUserQuery, TError, TData>(56 ['GetUser', variables],57 fetcher<GetUserQuery, GetUserQueryVariables>(GetUserDocument, variables),58 options59 );60};61
62// 4. Mutation Hook도 자동 생성!63export const useCreatePostMutation = <64 TError = unknown,65 TContext = unknown66>(67 options?: UseMutationOptions<CreatePostMutation, TError, CreatePostMutationVariables, TContext>68) => {69 return useMutation<CreatePostMutation, TError, CreatePostMutationVariables, TContext>(70 ['CreatePost'],71 (variables?: CreatePostMutationVariables) =>72 fetcher<CreatePostMutation, CreatePostMutationVariables>(CreatePostDocument, variables)(),73 options74 );75};76
77// 5. QueryKey도 자동 생성!78export const GetUserQueryKey = (variables: GetUserQueryVariables) => 79 ['GetUser', variables];위와 같이 Type, Mutation, Query 가 자동으로 생성 됩니다!
실제 사용 예시
1// 자동 생성된 Hook 바로 사용2import { useGetUserSuspenseQuery } from '@/types/graphql';3
4function UserProfile({ userId }: { userId: string }) {5 // 타입 자동 추론! 6 const { data } = useGetUserSuspenseQuery({ id: userId });7 8 // data.user.name <- 자동완성 완벽!9 return (10 <div>11 <h1>{data.user?.name}</h1>12 <p>{data.user?.email}</p>13 {data.user?.posts.map(post => (14 <div key={post.id}>{post.title}</div>15 ))}16 </div>17 );18}따로 /hooks 폴더를 만들지 않아도 컴포넌트 안에서 바로 사용 할 수 있습니다!
참고: 위 Codegen 설정은
codegen,react-query,graphql버전에 따라 상이하니 본인에 맞는 설정으로 하시길 바랍니다.(ESLint 설정과 충돌도 있으니 공식문서 참고 바람 👉 Codegen Doc.)
다음 편 예고
이렇게 자동 생성된 useSuspenseQuery를 Next.js 15 App Router 에서 어떻게 활용할까요?
Part 2 에서 다룰 내용:
- Streaming SSR로 초기 로딩 개선
- Hydration 최적화 전략
- Prefetch로 UX 끌어올리기
- 실제 성능 측정 결과
궁금하시다면 다음 편도 꼭 읽어주세요! 👋