Next 15 + Apollo + GraphQL API 구성

Jayden ·2025년 6월 11일

1. GraphQL

1) GraphQL이란?

  • 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 키워드를 통해서 스키마를 유연하게 구현할 수 있다는 생각이 들었다.

2) 프로젝트 설치

(1) next 프로젝트 설치

npx create-next-app@latest

Typescript --- yes
EsLint --- yes
Tailwind CSS --- no
@ alias 사용 --- yes
Turbopack --- yes
App router --- yes
src 폴더 사용 --- yes

(2) GraphQL / Apollo Server / Apollo Client / Apollo Server Integrations 설치

npm install @apollo/client-integration-nextjs @apollo/client @apollo/server graphql

(3) mongoDB 설치

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

3) 기본 파일 세팅

⭐ 서버 세팅

(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와 유사한 역할을 한다.
하지만, controllerURL/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/graphqlgraphql.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>
  );
}

⭐ 클라이언트에서 API 호출

(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에 뿌려주기
     )

4) 기타

(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
    }
  }
}
profile
프론트엔드 개발자

0개의 댓글