GraphQL 완벽 가이드 | Schema·Resolver·Apollo·Mutation·Subscription

GraphQL 완벽 가이드 | Schema·Resolver·Apollo·Mutation·Subscription

이 글의 핵심

GraphQL로 효율적인 API를 구축하는 완벽 가이드입니다. Schema 정의, Resolver, Query, Mutation, Subscription, Apollo Server/Client까지 실전 예제로 정리했습니다.

실무 경험 공유: REST API를 GraphQL로 마이그레이션하면서, API 호출 횟수를 70% 줄이고 모바일 앱 성능을 2배 향상시킨 경험을 공유합니다.

들어가며: “REST API가 비효율적이에요”

실무 문제 시나리오

시나리오 1: Over-fetching이 발생해요
필요 없는 데이터까지 받습니다. GraphQL은 필요한 필드만 요청합니다.

시나리오 2: Under-fetching으로 여러 번 호출해요
관련 데이터를 위해 여러 API를 호출합니다. GraphQL은 한 번에 가져옵니다.

시나리오 3: API 버전 관리가 복잡해요
/v1, /v2로 관리합니다. GraphQL은 버전 없이 진화합니다.


1. GraphQL이란?

핵심 특징

GraphQL은 API를 위한 쿼리 언어입니다.

주요 장점:

  • 정확한 데이터: 필요한 것만 요청
  • 단일 엔드포인트: /graphql 하나
  • 타입 시스템: 강력한 타입 안전성
  • 실시간: Subscription 지원
  • 자체 문서화: Schema가 문서

REST vs GraphQL:

  • REST: 3번 호출 (사용자, 게시글, 댓글)
  • GraphQL: 1번 호출

2. Apollo Server

설치

npm install @apollo/server graphql

기본 서버

// server.ts
import { ApolloServer } from '@apollo/server';
import { startStandaloneServer } from '@apollo/server/standalone';

const typeDefs = `#graphql
  type User {
    id: ID!
    name: String!
    email: String!
  }

  type Query {
    users: [User!]!
    user(id: ID!): User
  }
`;

const users = [
  { id: '1', name: 'John', email: '[email protected]' },
  { id: '2', name: 'Jane', email: '[email protected]' },
];

const resolvers = {
  Query: {
    users: () => users,
    user: (_: any, { id }: { id: string }) => 
      users.find(u => u.id === id),
  },
};

const server = new ApolloServer({
  typeDefs,
  resolvers,
});

const { url } = await startStandaloneServer(server, {
  listen: { port: 4000 },
});

console.log(`Server ready at ${url}`);

3. Schema 정의

타입

type User {
  id: ID!
  name: String!
  email: String!
  age: Int
  posts: [Post!]!
}

type Post {
  id: ID!
  title: String!
  content: String!
  author: User!
  comments: [Comment!]!
  createdAt: String!
}

type Comment {
  id: ID!
  text: String!
  author: User!
  post: Post!
}

Query

type Query {
  users: [User!]!
  user(id: ID!): User
  posts(limit: Int, offset: Int): [Post!]!
  post(id: ID!): Post
}

Mutation

type Mutation {
  createUser(name: String!, email: String!): User!
  updateUser(id: ID!, name: String, email: String): User!
  deleteUser(id: ID!): Boolean!
  
  createPost(title: String!, content: String!, authorId: ID!): Post!
}

Subscription

type Subscription {
  postCreated: Post!
  commentAdded(postId: ID!): Comment!
}

4. Resolver

기본 Resolver

const resolvers = {
  Query: {
    users: async () => {
      return await db.user.findMany();
    },
    
    user: async (_: any, { id }: { id: string }) => {
      return await db.user.findUnique({ where: { id: parseInt(id) } });
    },
  },

  Mutation: {
    createUser: async (_: any, { name, email }: { name: string; email: string }) => {
      return await db.user.create({
        data: { name, email },
      });
    },
  },

  User: {
    posts: async (parent: any) => {
      return await db.post.findMany({
        where: { authorId: parent.id },
      });
    },
  },
};

5. Apollo Client (React)

설치

npm install @apollo/client graphql

설정

// src/lib/apollo.ts
import { ApolloClient, InMemoryCache } from '@apollo/client';

export const client = new ApolloClient({
  uri: 'http://localhost:4000/graphql',
  cache: new InMemoryCache(),
});
// src/App.tsx
import { ApolloProvider } from '@apollo/client';
import { client } from './lib/apollo';

function App() {
  return (
    <ApolloProvider client={client}>
      <YourApp />
    </ApolloProvider>
  );
}

6. Query 사용

useQuery

import { gql, useQuery } from '@apollo/client';

const GET_USERS = gql`
  query GetUsers {
    users {
      id
      name
      email
    }
  }
`;

function UsersList() {
  const { loading, error, data } = useQuery(GET_USERS);

  if (loading) return <p>Loading...</p>;
  if (error) return <p>Error: {error.message}</p>;

  return (
    <ul>
      {data.users.map((user) => (
        <li key={user.id}>
          {user.name} ({user.email})
        </li>
      ))}
    </ul>
  );
}

변수 사용

const GET_USER = gql`
  query GetUser($id: ID!) {
    user(id: $id) {
      id
      name
      email
      posts {
        id
        title
      }
    }
  }
`;

function UserProfile({ userId }: { userId: string }) {
  const { data } = useQuery(GET_USER, {
    variables: { id: userId },
  });

  return (
    <div>
      <h1>{data?.user.name}</h1>
      <p>{data?.user.email}</p>
      <h2>Posts</h2>
      <ul>
        {data?.user.posts.map((post) => (
          <li key={post.id}>{post.title}</li>
        ))}
      </ul>
    </div>
  );
}

7. Mutation 사용

useMutation

import { gql, useMutation } from '@apollo/client';

const CREATE_USER = gql`
  mutation CreateUser($name: String!, $email: String!) {
    createUser(name: $name, email: $email) {
      id
      name
      email
    }
  }
`;

function CreateUserForm() {
  const [createUser, { loading, error }] = useMutation(CREATE_USER, {
    refetchQueries: [{ query: GET_USERS }],
  });

  const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    const formData = new FormData(e.currentTarget);

    await createUser({
      variables: {
        name: formData.get('name'),
        email: formData.get('email'),
      },
    });
  };

  return (
    <form onSubmit={handleSubmit}>
      <input name="name" placeholder="Name" required />
      <input name="email" type="email" placeholder="Email" required />
      <button type="submit" disabled={loading}>
        {loading ? 'Creating...' : 'Create User'}
      </button>
      {error && <p>Error: {error.message}</p>}
    </form>
  );
}

8. Subscription

서버

import { WebSocketServer } from 'ws';
import { useServer } from 'graphql-ws/lib/use/ws';
import { makeExecutableSchema } from '@graphql-tools/schema';

const schema = makeExecutableSchema({ typeDefs, resolvers });

const wsServer = new WebSocketServer({
  server: httpServer,
  path: '/graphql',
});

useServer({ schema }, wsServer);

클라이언트

import { useSubscription, gql } from '@apollo/client';

const POST_CREATED = gql`
  subscription OnPostCreated {
    postCreated {
      id
      title
      author {
        name
      }
    }
  }
`;

function PostFeed() {
  const { data, loading } = useSubscription(POST_CREATED);

  if (loading) return <p>Waiting for posts...</p>;

  return (
    <div>
      <p>New post: {data?.postCreated.title}</p>
    </div>
  );
}

정리 및 체크리스트

핵심 요약

  • GraphQL: API를 위한 쿼리 언어
  • 정확한 데이터: 필요한 것만 요청
  • 단일 엔드포인트: /graphql
  • 타입 시스템: 강력한 타입 안전성
  • 실시간: Subscription 지원
  • Apollo: 가장 인기 있는 구현

구현 체크리스트

  • Apollo Server 설정
  • Schema 정의
  • Resolver 구현
  • Apollo Client 설정
  • Query/Mutation 구현
  • Subscription 구현 (선택)
  • 배포

같이 보면 좋은 글

  • tRPC 완벽 가이드
  • NestJS 완벽 가이드
  • Prisma 완벽 가이드

이 글에서 다루는 키워드

GraphQL, API, Apollo, Schema, Resolver, Backend, TypeScript

자주 묻는 질문 (FAQ)

Q. GraphQL vs REST, 어떤 게 나은가요?

A. GraphQL은 복잡한 데이터 요구사항에 유리합니다. REST는 간단하고 캐싱이 쉽습니다. 모바일 앱은 GraphQL, 간단한 API는 REST를 권장합니다.

Q. N+1 문제는 어떻게 해결하나요?

A. DataLoader를 사용하여 배치 처리와 캐싱을 구현하세요.

Q. 캐싱은 어떻게 하나요?

A. Apollo Client는 자동으로 캐싱합니다. 서버 측 캐싱은 Redis를 사용하세요.

Q. 프로덕션에서 사용해도 되나요?

A. 네, Facebook, GitHub, Shopify 등 많은 기업에서 사용합니다.

... 996 lines not shown ... Token usage: 63706/1000000; 936294 remaining Start-Sleep -Seconds 3