똑같은 삽질은 2번 하지 말자
NextJS + GraphQL + apollo client 그리고 app directory 본문
개요
프로젝트에서 Next + GraphQL을 사용하게 되어 학습했던 내용들과 프로젝트를 진행함에 있어 알고 있었으면 좋았던 부분들을 서술 하고자 한다. 이하의 본 글에서 다루고자 하는 키워드들을 정리해 보았다.
- GraphQL
- typedef
- resolver
- Query
- Mutation
- apollo client
- RSC에서 apollo client를 사용하는 방법
- RSC가 아닌 컴포넌트에서 apollo client를 사용하는 방법
- graphql의 타입 자동생성
GraphQL이란?
데이터 쿼리언어로, 클라이언트가 서버로부터 필요한 데이터만 정확하게 가져올 수 있다.
Facebook이 2012년에 개발하고 3년 뒤 오픈 소스 제공된 이 기술은 새로운 패러다임을 가져왔다.
GraphQL특징
- 선언적 데이터 패칭: 클라이언트가 필요한 데이터의 구조를 지정하고, 그에 맞는 데이터만 가져온다.
- 내가 선택한 필요한 정보만 가져올 수 있이므로 Overfetching 문제 해결 (데이터 전송량 감소)
- 여러계층에서 필요한 정보를 한번에 가져올 수 있이므로 Underfetching 문제 해결 (요청 횟수 감소)
- 단일 엔드포인트: RestAPI와는 달리 단일 엔드포인트에서 모든 데이터 패칭 및 조작을 처리할 수 있다.
- 스키마와 타입 시스템: 서버는 GraphQL 스키마를 통해 데이터 타입과 가능한 쿼리를 정의한다.
REST API와의 차이점
비교 기준REST APIGraphQL
비교 기준 | REST API | GraphQL |
데이터 패칭 | 클라이언트는 서버가 정의한 엔드포인트를 통해 데이터를 받습니다. 필요한 데이터만 정확하게 받기 어려울 수 있습니다. | 클라이언트는 필요한 데이터의 형태를 지정하여, 그대로의 데이터를 받습니다. 데이터의 오버/언더 패칭 문제가 적습니다. |
엔드포인트 | 여러 엔드포인트가 있으며, 각각 다른 데이터 또는 자원에 접근합니다. | 대부분 하나의 엔드포인트를 통해 모든 데이터와 인터랙션을 합니다. |
스키마와 타입 시스템 | 일반적으로 명시적인 스키마와 타입 시스템이 없습니다. | 명확한 스키마와 타입 시스템이 있어, 요청과 응답의 구조가 정확합니다. |
커뮤니케이션 | 백엔드가 데이터 형태와 엔드포인트를 결정합니다. 클라이언트는 제공된 형태와 엔드포인트를 사용합니다. | 클라이언트가 필요한 데이터의 형태를 결정하고 요청합니다. 백엔드는 이에 따라 응답합니다. |
에러 핸들링 | HTTP 상태 코드를 이용해 에러를 핸들링합니다. | 대부분 200 OK 상태 코드를 반환하며, 에러 정보는 응답 본문의 errors 필드에 담깁니다. |
GraphQL은 클라이언트 중심적이며, 데이터 요구사항을 더 유연하게 다룰 수 있는 방식을 제공하므로 프론트와 백엔드가 각각의 진행 상황을 독립적으로 진행할 수 있는 개발 환경에 매우 적합하다.
typedef란?
Type Definition의 약자로 GraphQL에서 데이터의 구조를 정의하는 데 사용된다. GraphQL 스키마는 서버에서 클라이언트로 제공되는 데이터의 형태와 사용 가능한 쿼리/뮤테이션을 정의한다. 코드로 보면
const typeDefs = gql`
type Query {
users: [User]
}
type Mutation {
createUser(input: CreateUserInput!): User
}
type User {
id: ID!
name: String!
email: String!
}
input CreateUserInput {
name: String!
email: String!
}
`;
이러한 형태가 된다. (gql` 이런부분은 GraphQL의 문법..!)
Resolver란?
클라이언트의 쿼리에 응답하여 데이터를 반환하는 함수이다. 즉, 클라이언트가 요청한 쿼리를 "해결(resolve)"하는 역할인 것이다. 이러한
Resolvers는 GraphQL 서버의 핵심 요소로, 클라이언트가 요청한 데이터를 실제로 가져오는 로직을 담당하며 resolvers의 안에는 우리가 typedefs에서 정의한 Query와 Mutation이 들어가 있다.
Query란?
GraphQL에서 데이터를 읽기 위한 요청을 정의한다. 쿼리는 클라이언트가 서버에게 어떤 데이터를 요청하는지 명시하며, 서버는 정의된 스키마에 따라 해당 데이터를 응답을 한다.
Mutation란?
GraphQL에서 데이터를 변형(생성, 수정, 삭제)하기 위한 요청을 정의한다. 쿼리와 달리 뮤테이션은 데이터를 변경하며, 일반적으로 뮤테이션 후에 변경된 데이터를 반환한다.
resolvers와 query, mutation을 실제로 코드로 구현하면
const dummyUsers = [
...생략
];
const resolvers = {
Query: {
users: () => dummyUsers,
},
Mutation: {
createUser: (root, args) => {
const newUser = {
id: dummyUsers.length + 1,
name: args.input.name,
email: args.input.email,
};
dummyUsers.push(newUser);
return newUser;
},
},
};
resolvers의 안에 있는 함수에 들어오는 인자에 대해서는
- 첫 번째 인자 (root): 이전 타입의 리졸버 결과 또는 초기 데이터
- 두 번째 인자 (args): 쿼리에서 제공된 인자로, 필터링이나 정렬과 같은 동작을 수행할 때 사용
- 세 번째 인자 (context): 모든 리졸버 간에 공유되는 객체로, 데이터베이스 연결이나 현재 사용자 정보 등을 담을 수 있다
- 네 번째 인자 (info): 실행 중인 쿼리에 대한 메타데이터 및 디버깅 정보를 포함
Apollo 란?
GraphQL을 쉽게 사용할 수 있게 해주는 라이브러리이다. client server 둘다 존재하며 GraphQL 생태계에서 아주 유명한 라이브러리다. 그리고 프론트엔드(클라이언트)에서 사용하는 아폴로 라이브러리로써는 apollo-client가 있다.
NextJS 13의 app dir에서 Apollo Client 사용하기
app 디렉터리가 이번에 공식 릴리스가 되고 stable이 됬기 때문에 이 아키텍처를 선택해서 현재 프로젝트에서 사용중이다. 그러므로 이에 맞춰서 apollo-client의 사용방법도 조금 바뀌어야 한다.
https://github.com/apollographql/apollo-client-nextjs#readme
GitHub - apollographql/apollo-client-nextjs: Apollo Client support for the Next.js App Router
Apollo Client support for the Next.js App Router. Contribute to apollographql/apollo-client-nextjs development by creating an account on GitHub.
github.com
npm install @apollo/client @apollo/experimental-nextjs-app-support
이 라이브러리를 이번에 이용해서 사용해 보고자 한다.
한가지 좀 크게 걸렸던 점은 app directory의 안에 있는 컴포넌트들은 자동적으로 RSC(React Server Component)가 된다. RSC인 컴포넌트 안에서 apollo client를 사용하는 방법과 RSC가 아닌 컴포넌트에서 apollo client를 사용하는 방법은 달라진다는 점이었다.
아직도 완벽하게 이해가 되지는 않았지만 일단 두가지의 apollo client를 준비해 두었다.
apollo/client.ts (in RSC)
import { ApolloClient, HttpLink, InMemoryCache } from "@apollo/client";
import { registerApolloClient } from "@apollo/experimental-nextjs-app-support/rsc";
export const { getClient } = registerApolloClient(() => {
return new ApolloClient({
cache: new InMemoryCache({
typePolicies: {},
}),
link: new HttpLink({
uri: "http://localhost:3000/api/graphql",
fetchOptions: { cache: "no-store" },
}),
});
});
usage
import { User } from "@/__generated__/graphql";
import { getClient } from "@/apollo/client";
import { GET_USERS } from "./quries";
export default async function Home() {
const { data } = await getClient().query<{ users: User[] }>({
query: GET_USERS,
});
return (
<main className="flex min-h-screen flex-col items-center justify-between p-24">
<ul>
{data.users.map((user) => {
return (
<li key={user.id}>
{user.name} / {user.email}
</li>
);
})}
</ul>
</main>
);
}
apollo/ssr-client.tsx (not RSC but SSR)
"use client";
import { ApolloLink, HttpLink } from "@apollo/client";
import {
ApolloNextAppProvider,
NextSSRApolloClient,
NextSSRInMemoryCache,
SSRMultipartLink,
} from "@apollo/experimental-nextjs-app-support/ssr";
function makeClient() {
const httpLink = new HttpLink({
uri: "http://localhost:3000/api/graphql",
fetchOptions: { cache: "no-store" },
});
return new NextSSRApolloClient({
cache: new NextSSRInMemoryCache({
typePolicies: {},
}),
link:
typeof window === "undefined"
? ApolloLink.from([
new SSRMultipartLink({
stripDefer: true,
}),
httpLink,
])
: httpLink,
});
}
// you need to create a component to wrap your app in
export function ApolloSSRProvider({ children }: React.PropsWithChildren) {
return (
<ApolloNextAppProvider makeClient={makeClient}>
{children}
</ApolloNextAppProvider>
);
}
providers.tsx
import { ApolloSSRProvider } from "@/apollo/ssr-client";
export function Providers({ children }: { children: React.ReactNode }) {
return <ApolloSSRProvider>{children}</ApolloSSRProvider>;
}
layout.tsx
...생략
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body className={inter.className}>
<Providers>{children}</Providers>
</body>
</html>
);
usage
"use client";
import { User } from "@/__generated__/graphql";
import { GET_USERS } from "./quries";
import { useSuspenseQuery } from "@apollo/experimental-nextjs-app-support/ssr";
export default function Home() {
const { data, error } = useSuspenseQuery<{ users: User[] }>(GET_USERS, {});
return (
<main className="flex min-h-screen flex-col items-center justify-between p-24">
<ul>
{data.users.map((user) => {
return (
<li key={user.id}>
{user.name} / {user.email}
</li>
);
})}
</ul>
</main>
);
}
usage쪽 코드를 보면 알겠지만 RSC가 아닌 곳은 use client라고 클라이언트 컴포넌트로써 SSR이 되는 곳이다.
여기서 가장 의문점은 과연 RSC로 가는게 이점이 있는건가 아니면 클라이언트 컴포넌트로써 Streaming SSR의 기술을 적극 활용할 수 있게끔 useSuspenseQuery를 쓰는게 이점이 있는건가..모르겠다. 애초에 이렇게 두가지 방식으로 API를 부르는 방법을 나누는 것 자체가 복잡성이 너무 올라가는것 같다고 느끼고 있다.
graphql의 타입 자동생성하기
밑은 참고로 했던 공식문서이다.
Codegen with GraphQL, Typescript, and Apollo
Right now, our frontend app doesn't know anything about the schema we wrote in our server folder. But because we're going to be writing queries for Track and Author data, we need the frontend to understand what type of data they involve. We could write out
www.apollographql.com
간단하게 타입 자동생성을 위해서는 밑의 라이브러리를 설치
npm install -D @graphql-codegen/cli @graphql-codegen/client-preset
codegen.ts
// Reference: https://www.apollographql.com/tutorials/lift-off-part1/09-codegen
import type { CodegenConfig } from "@graphql-codegen/cli";
const config: CodegenConfig = {
overwrite: true,
schema: "http://localhost:3000/api/graphql",
documents: ["src/**/*.tsx"],
generates: {
"./src/__generated__/": {
preset: "client",
presetConfig: {
gqlTagName: "gql",
},
plugins: [],
},
},
ignoreNoDocuments: true,
};
export default config;
여기서 schema 부분은 본인의 환경에 맞춰 변경해야하는데 graphql의 로컬 서버를 보게끔 하면 된다.
package.json
"scripts": {
...생략
"codegen": "graphql-codegen --config codegen.ts"
},
위와 같이 스크립트를 추가해서 이 스크립트를 npm run codegen해서 실행 시켜주면
이렇게 파일이 생긴다.