- GraphQL is a query language for your API, and a server-side runtime for executing queries using a type system you define for your data.
- API를 위한 쿼리 언어,
-데이터를 얻기 위한 타입 시스템을 사용하는 쿼리를 실행하는 서버 사이드 런타임
REST API는 서버에서 정의된 스키마를 그대로 활용한다. 이와 달리 GraphQL은 서버에서 스키마의 타입(구조)을 우선 정의한 후, 클라이언트에서 필요한 데이터의 스키마를 요청하여 REST API보다는 좀 더 유연하게 데이터를 받아올 수 있다.
회사에서는 백엔드에서 대부분 REST API를 사용했다. 프론트엔드 개발을 경험해보니, 생각보다 화면에 데이터를 뿌려주거나, 상태관리 등 데이터를 운용하는데 있어서 생각보다 백엔드 스키마의 영향을 많이 받는(종속)다고 생각했다.
또한 페이지마다 공통된 데이터를 포함하는 경우가 많았는데, type, fragment 키워드를 통해서 스키마를 유연하게 구현할 수 있다는 생각이 들었다.
npx create-next-app@latest
Typescript --- yes
EsLint --- yes
Tailwind CSS --- no
@ alias 사용 --- yes
Turbopack --- yes
App router --- yes
src 폴더 사용 --- yes
npm install @apollo/client-integration-nextjs @apollo/client @apollo/server graphql
npm install mongodb
(참조)
👆 GraphQL
https://graphql.org/learn/
👆 Apollo/client
https://www.apollographql.com/docs/apollo-server/getting-started
👆 Apollo/Server
https://www.apollographql.com/docs/apollo-server/getting-started
👆 Apollo-client-integrations
https://github.com/apollographql/apollo-client-integrations/tree/main/packages/nextjs
(1) src/app/route/graphql.ts : 핸들러 설정.
import { startServerAndCreateNextHandler } from '@as-integrations/next';
import { ApolloServer } from '@apollo/server';
import { resolvers } from '@/app/lib/resolvers';
import { typeDefs } from '@/app/lib/typeDefs';
const server = new ApolloServer({
resolvers,
typeDefs,
introspection : true
});
const handler = startServerAndCreateNextHandler(server);
export { handler as GET, handler as POST };
(2) type.ts : 스키마 타입 정의
apollo/server에서 스키마 타입을 정의. typescript에서의 타입 정의와 유사하지만, typescript와는 무관하며, 사용하는 타입도 다르다.(예) 정수 타입 : ts : number / apollo-server : Int) .apollo/server에서 사용하는 고유한 타입 정의 기능이다.
export const typeDefs = `#graphql
type Resort {
_id: ID
name: String
location: String
imgUrl : String
city : String
rating : String
isFullRefund : Boolean
costPrice : Int
salePrice : Int
totalPrice : Int
grade : Int
}
type Detail {
_id: ID
name: String
location: String
keyword: [String]
imgUrl : String
city : String
rating : String
isFullRefund : Boolean
costPrice : Int
salePrice : Int
totalPrice : Int
grade : Int
}
type Query {
result(keyword: String): [Resort]
detail(id : String) : Detail
}
`;
(3) resolver.ts : 리졸버 정의
REST 아키텍처에서 Controller와 유사한 역할을 한다.
하지만, controller가 URL/HTTP Method에 따라 동작하는 반면, ㄷresolver Query/Mutation 필드에 따라 동작하는 차이점이 있다.
import clientPromise from "./mongo";
import { ObjectId } from 'mongodb';
export const resolvers = {
Query: {
result : async (_: any, args: { keyword?: string }) => {
const client = await clientPromise;
const db = client.db();
const collection = db.collection('resort');
const filter = args.keyword ? { keyword : args.keyword } : {};
const results = await collection.find(filter).toArray();
return results;
},
detail : async (_: any, args: { id?: string }) => {
const client = await clientPromise;
const db = client.db();
const collection = db.collection('resort');
const result = await collection.findOne({ _id: new ObjectId(args.id) });
return result;
},
},
};
(4) mongo.ts : mongoDB 연동
import { MongoClient } from 'mongodb';
const uri = process.env.MONGODB_URI!;
const options = {};
let client: MongoClient;
let clientPromise: Promise<MongoClient>;
if (!global._mongoClientPromise) {
client = new MongoClient(uri, options);
global._mongoClientPromise = client.connect();
}
clientPromise = global._mongoClientPromise;
export default clientPromise;
(1) ApolloWrapper.tsx
http://localhost:3000/api/graphql가 graphql.ts의 핸들러로 호출
"use client";
import { HttpLink } from "@apollo/client";
import {
ApolloNextAppProvider,
ApolloClient,
InMemoryCache,
} from "@apollo/client-integration-nextjs";
function makeClient() {
const httpLink = new HttpLink({
uri: "http://localhost:3000/api/graphql",
fetchOptions: {
},
});
return new ApolloClient({
cache: new InMemoryCache(),
link: httpLink,
});
}
export function ApolloWrapper({ children }: React.PropsWithChildren) {
return (
<ApolloNextAppProvider makeClient={makeClient}>
{children}
</ApolloNextAppProvider>
);
}
(1) page.tsx
import { Suspense } from "react";
import SearchResult from "../SearchResult";
export default async function Search({ params } : { params : Promise<{ keyword : string}>}) {
const keyword = (await params).keyword;
return (
<Suspense fallback ={<p>로딩중 입니다.</p>}>
<SearchResult keyword={keyword}/>
</Suspense>
)
}
(2) Detail.tsx
query 뒤에 작성한 GetSearchResult 쿼리의 Operation name라고 한다.
기술적으로 필수는 아니나, 로깅 및 디버깅을 위해서 가급적 작성하는 것이 좋다.
Operation name은 클라이언트에서 임의로 작성하면 된다.
apollo-client-integrations 라이브러리는 react-query와 유사하게
useSuspenseQuery 기능을 지원하는데, react의 Suspense와 연동하여 사용할 수 있다. 요청 중 에러가 발생할 경우, 감지된 에러는 next에서 지원하는 error.tsx파일을 통하여 검출하고 표시할 수 있다.
export interface ISearchResult{
_id : string;
name : string;
location : string;
city : string;
imgUrl : string;
rating : string;
isFullRefund : boolean;
costPrice : number;
salePrice : number;
totalPrice : number;
grade : 1 | 2 | 3 | 4 | 5;
accomType : string;
}
export default function SearchResult({ keyword } : { keyword : string}) {
const GET_SEARCH_RESULT = gql`
query GetSearchResult($keyword: String) {
result(keyword: $keyword) {
_id
name
city
imgUrl
location
rating
isFullRefund
costPrice
salePrice
totalPrice
grade
}
}
`
const { data } = useSuspenseQuery<{ result: ISearchResult[] }>(
GET_SEARCH_RESULT,
{ variables : { keyword },
errorPolicy : 'all'}
); // 에러는 error.tsx를 통하여 확인
return(
// 조회한 데이터 View에 뿌려주기
)
(1) fragment
GraphQL의 핵심 문법 중 하나로 반복되는 필드를 재사용하기 위해 사용됨.
(사용 예시)
import { gql } from '@apollo/client';
const USER_FIELDS = gql`
fragment UserFields on User {
id
name
email
}
`;
const GET_USER = gql`
query GetUser($id: ID!) {
user(id: $id) {
...UserFields
}
}
${USER_FIELDS}
`;
(2) inline fragment
🧡 스키마를 구조가 반복되지 않을 경우
위에 (1)번 예시에서 사용된 GetUser를 다음과 같이 사용할 수 있다.
const GET_USER = gql`
query GetUser($id: ID!) {
user(id: $id) {
... on User {
id
name
email
}
}
}
`;
💛 Union 또는 Interface 타입 분기시에 사용
union SearchResult = User | Post
query Search {
search(term: "graphql") {
... on User {
id
name
}
... on Post {
id
title
}
}
}