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 등 많은 기업에서 사용합니다.