본문으로 건너뛰기

🔄 REST API vs GraphQL

📖 정의

REST API는 HTTP 메서드를 사용하여 리소스 중심으로 설계된 전통적인 API 아키텍처입니다. GraphQL은 Facebook이 개발한 쿼리 언어로, 클라이언트가 필요한 데이터를 정확하게 요청할 수 있는 유연한 API 방식입니다. REST는 여러 엔드포인트를 가지며, GraphQL은 단일 엔드포인트에서 모든 데이터를 처리합니다.

🎯 비유로 이해하기

레스토랑 vs 뷔페

REST API = 레스토랑 메뉴
├─ 정해진 메뉴만 주문 가능
├─ 각 메뉴마다 정해진 구성
├─ "피자 주세요" → 피자 전체가 옴
├─ 토핑만 빼거나 추가하기 어려움
└─ 간단하고 예측 가능

GraphQL = 뷔페
├─ 원하는 음식만 골라서 가져옴
├─ 필요한 만큼만 선택
├─ "치즈만 주세요" → 치즈만 받음
├─ 자유롭게 조합 가능
└─ 유연하지만 복잡할 수 있음

도서관 책 대출

REST API = 전통적인 대출
사서: "어떤 책이 필요하신가요?"
나: "컴퓨터 책 주세요"
사서: "이 책들 전부 드릴게요" (책 10권)
나: "저는 1장만 필요한데..." (나머지는 불필요)

GraphQL = 스마트 대출
나: "컴퓨터 책의 3장만 필요해요"
사서: "3장만 복사해드릴게요" (정확히 필요한 것만)
나: "완벽해요!"

⚙️ 작동 원리

1. 데이터 요청 방식 비교

REST API: 여러 엔드포인트
GET /users/1 → 사용자 정보
GET /users/1/posts → 사용자의 게시글
GET /posts/1/comments → 게시글의 댓글

총 3번의 요청 필요!

GraphQL: 단일 엔드포인트
POST /graphql
{
user(id: 1) {
name
posts {
title
comments {
text
}
}
}
}

1번의 요청으로 모든 데이터!

2. Over-fetching vs Under-fetching

REST API 문제

Over-fetching (불필요한 데이터 받음)
GET /users/1
{
"id": 1,
"name": "김철수",
"email": "kim@example.com",
"phone": "010-1234-5678",
"address": "서울시...",
"createdAt": "2024-01-01",
// 이름만 필요한데 모든 정보를 받음!
}

Under-fetching (부족한 데이터)
GET /users/1 → 사용자 정보
GET /users/1/posts → 추가 요청 필요
GET /users/1/friends → 또 추가 요청
// 여러 번 요청해야 함!

GraphQL 해결

정확히 필요한 것만
{
user(id: 1) {
name // 이름만 요청!
}
}
→ { "name": "김철수" }

한 번에 모든 것
{
user(id: 1) {
name
posts { title }
friends { name }
}
}
→ 모든 데이터를 1번에!

3. API 설계 철학

REST: 리소스 중심
┌─────────────────┐
│ /users │ → 사용자 목록
│ /users/1 │ → 특정 사용자
│ /posts │ → 게시글 목록
│ /posts/1 │ → 특정 게시글
└─────────────────┘
각 리소스마다 엔드포인트

GraphQL: 쿼리 중심
┌─────────────────┐
│ /graphql │ → 모든 요청
└─────────────────┘

┌────────┐
│ Query │ → 데이터 읽기
│Mutation│ → 데이터 변경
│Subscribe│ → 실시간 구독
└────────┘

💡 실제 예시

REST API 예시 (Express.js)

// Express.js REST API 구현
const express = require('express');
const app = express();

app.use(express.json());

// 데이터 (실제로는 데이터베이스 사용)
const users = [
{
id: 1,
name: '김철수',
email: 'kim@example.com',
age: 25
},
{
id: 2,
name: '이영희',
email: 'lee@example.com',
age: 30
}
];

const posts = [
{
id: 1,
userId: 1,
title: 'REST API 소개',
content: 'REST는...'
},
{
id: 2,
userId: 1,
title: 'GraphQL 소개',
content: 'GraphQL은...'
}
];

// ========== GET: 모든 사용자 조회 ==========
app.get('/api/users', (req, res) => {
res.json(users);
});

// ========== GET: 특정 사용자 조회 ==========
app.get('/api/users/:id', (req, res) => {
const user = users.find(u => u.id === parseInt(req.params.id));
if (!user) {
return res.status(404).json({ error: '사용자를 찾을 수 없습니다' });
}
res.json(user);
});

// ========== POST: 사용자 생성 ==========
app.post('/api/users', (req, res) => {
const newUser = {
id: users.length + 1,
name: req.body.name,
email: req.body.email,
age: req.body.age
};
users.push(newUser);
res.status(201).json(newUser);
});

// ========== PUT: 사용자 수정 ==========
app.put('/api/users/:id', (req, res) => {
const user = users.find(u => u.id === parseInt(req.params.id));
if (!user) {
return res.status(404).json({ error: '사용자를 찾을 수 없습니다' });
}

user.name = req.body.name || user.name;
user.email = req.body.email || user.email;
user.age = req.body.age || user.age;

res.json(user);
});

// ========== DELETE: 사용자 삭제 ==========
app.delete('/api/users/:id', (req, res) => {
const index = users.findIndex(u => u.id === parseInt(req.params.id));
if (index === -1) {
return res.status(404).json({ error: '사용자를 찾을 수 없습니다' });
}

users.splice(index, 1);
res.status(204).send();
});

// ========== GET: 사용자의 게시글 조회 ==========
app.get('/api/users/:id/posts', (req, res) => {
const userId = parseInt(req.params.id);
const userPosts = posts.filter(p => p.userId === userId);
res.json(userPosts);
});

// ========== GET: 특정 게시글 조회 ==========
app.get('/api/posts/:id', (req, res) => {
const post = posts.find(p => p.id === parseInt(req.params.id));
if (!post) {
return res.status(404).json({ error: '게시글을 찾을 수 없습니다' });
}
res.json(post);
});

app.listen(3000, () => {
console.log('REST API 서버 실행: http://localhost:3000');
});

REST API 클라이언트 사용

// ========== REST API 사용 ==========

// 1. 사용자 목록 조회
const response1 = await fetch('http://localhost:3000/api/users');
const users = await response1.json();
console.log(users);

// 2. 특정 사용자 조회
const response2 = await fetch('http://localhost:3000/api/users/1');
const user = await response2.json();
console.log(user);

// 3. 사용자 생성
const response3 = await fetch('http://localhost:3000/api/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
name: '박민수',
email: 'park@example.com',
age: 28
})
});
const newUser = await response3.json();

// 4. 사용자 수정
const response4 = await fetch('http://localhost:3000/api/users/1', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
age: 26
})
});

// 5. 사용자 삭제
await fetch('http://localhost:3000/api/users/1', {
method: 'DELETE'
});

// ========== 문제: 여러 요청 필요 ==========
// 사용자와 게시글을 함께 가져오려면?
const userResponse = await fetch('http://localhost:3000/api/users/1');
const user = await userResponse.json();

const postsResponse = await fetch('http://localhost:3000/api/users/1/posts');
const posts = await postsResponse.json();

console.log({ user, posts }); // 2번의 요청!

GraphQL 예시 (Apollo Server)

// GraphQL 서버 구현
const { ApolloServer, gql } = require('apollo-server');

// 데이터 (실제로는 데이터베이스 사용)
const users = [
{ id: 1, name: '김철수', email: 'kim@example.com', age: 25 },
{ id: 2, name: '이영희', email: 'lee@example.com', age: 30 }
];

const posts = [
{ id: 1, userId: 1, title: 'REST API 소개', content: 'REST는...' },
{ id: 2, userId: 1, title: 'GraphQL 소개', content: 'GraphQL은...' },
{ id: 3, userId: 2, title: 'Node.js 튜토리얼', content: 'Node는...' }
];

// ========== 스키마 정의 (Type Definitions) ==========
const typeDefs = gql`
# 사용자 타입
type User {
id: Int!
name: String!
email: String!
age: Int
posts: [Post!]! # 사용자의 게시글 (관계)
}

# 게시글 타입
type Post {
id: Int!
title: String!
content: String!
author: User! # 게시글 작성자 (관계)
}

# 쿼리 (데이터 읽기)
type Query {
# 모든 사용자
users: [User!]!

# 특정 사용자
user(id: Int!): User

# 모든 게시글
posts: [Post!]!

# 특정 게시글
post(id: Int!): Post
}

# 뮤테이션 (데이터 변경)
type Mutation {
# 사용자 생성
createUser(name: String!, email: String!, age: Int): User!

# 사용자 수정
updateUser(id: Int!, name: String, email: String, age: Int): User

# 사용자 삭제
deleteUser(id: Int!): Boolean!

# 게시글 생성
createPost(userId: Int!, title: String!, content: String!): Post!
}
`;

// ========== 리졸버 (데이터 가져오는 방법) ==========
const resolvers = {
Query: {
// 모든 사용자 조회
users: () => users,

// 특정 사용자 조회
user: (parent, args) => {
return users.find(u => u.id === args.id);
},

// 모든 게시글 조회
posts: () => posts,

// 특정 게시글 조회
post: (parent, args) => {
return posts.find(p => p.id === args.id);
}
},

Mutation: {
// 사용자 생성
createUser: (parent, args) => {
const newUser = {
id: users.length + 1,
name: args.name,
email: args.email,
age: args.age
};
users.push(newUser);
return newUser;
},

// 사용자 수정
updateUser: (parent, args) => {
const user = users.find(u => u.id === args.id);
if (!user) return null;

if (args.name) user.name = args.name;
if (args.email) user.email = args.email;
if (args.age) user.age = args.age;

return user;
},

// 사용자 삭제
deleteUser: (parent, args) => {
const index = users.findIndex(u => u.id === args.id);
if (index === -1) return false;

users.splice(index, 1);
return true;
},

// 게시글 생성
createPost: (parent, args) => {
const newPost = {
id: posts.length + 1,
userId: args.userId,
title: args.title,
content: args.content
};
posts.push(newPost);
return newPost;
}
},

// ========== 관계 리졸버 ==========
User: {
// 사용자의 게시글 가져오기
posts: (parent) => {
return posts.filter(p => p.userId === parent.id);
}
},

Post: {
// 게시글의 작성자 가져오기
author: (parent) => {
return users.find(u => u.id === parent.userId);
}
}
};

// ========== 서버 실행 ==========
const server = new ApolloServer({ typeDefs, resolvers });

server.listen().then(({ url }) => {
console.log(`GraphQL 서버 실행: ${url}`);
});

GraphQL 클라이언트 사용

// ========== GraphQL 쿼리 예시 ==========

// 1. 이름만 조회 (Over-fetching 방지)
query {
user(id: 1) {
name
}
}
// 응답: { "data": { "user": { "name": "김철수" } } }

// 2. 사용자와 게시글 한 번에 조회 (Under-fetching 방지)
query {
user(id: 1) {
name
email
posts {
title
content
}
}
}
// 응답:
{
"data": {
"user": {
"name": "김철수",
"email": "kim@example.com",
"posts": [
{ "title": "REST API 소개", "content": "REST는..." },
{ "title": "GraphQL 소개", "content": "GraphQL은..." }
]
}
}
}

// 3. 여러 리소스 동시 조회
query {
users {
name
}
posts {
title
}
}

// 4. 변수 사용
query GetUser($userId: Int!) {
user(id: $userId) {
name
email
}
}
// 변수: { "userId": 1 }

// ========== Mutation (데이터 변경) ==========

// 1. 사용자 생성
mutation {
createUser(name: "박민수", email: "park@example.com", age: 28) {
id
name
email
}
}

// 2. 사용자 수정
mutation {
updateUser(id: 1, age: 26) {
id
name
age
}
}

// 3. 사용자 삭제
mutation {
deleteUser(id: 1)
}

// ========== JavaScript 클라이언트 ==========
async function fetchGraphQL(query, variables = {}) {
const response = await fetch('http://localhost:4000/graphql', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
query,
variables
})
});

return response.json();
}

// 사용자와 게시글 조회
const query = `
query GetUserWithPosts($userId: Int!) {
user(id: $userId) {
name
email
posts {
title
}
}
}
`;

const data = await fetchGraphQL(query, { userId: 1 });
console.log(data);

Apollo Client로 React 통합

// ========== Apollo Client 설정 ==========
import { ApolloClient, InMemoryCache, ApolloProvider, useQuery, useMutation, gql } from '@apollo/client';

// 클라이언트 생성
const client = new ApolloClient({
uri: 'http://localhost:4000/graphql',
cache: new InMemoryCache()
});

// 앱에 Provider 적용
function App() {
return (
<ApolloProvider client={client}>
<UserList />
</ApolloProvider>
);
}

// ========== 쿼리 사용 ==========
const GET_USERS = gql`
query GetUsers {
users {
id
name
email
posts {
title
}
}
}
`;

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

if (loading) return <p>로딩 중...</p>;
if (error) return <p>에러: {error.message}</p>;

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

// ========== Mutation 사용 ==========
const CREATE_USER = gql`
mutation CreateUser($name: String!, $email: String!, $age: Int) {
createUser(name: $name, email: $email, age: $age) {
id
name
email
}
}
`;

function CreateUserForm() {
const [createUser, { data, loading, error }] = useMutation(CREATE_USER);

const handleSubmit = (e) => {
e.preventDefault();
const formData = new FormData(e.target);

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

if (loading) return <p>생성 중...</p>;
if (error) return <p>에러: {error.message}</p>;
if (data) return <p>생성 완료: {data.createUser.name}</p>;

return (
<form onSubmit={handleSubmit}>
<input name="name" placeholder="이름" required />
<input name="email" type="email" placeholder="이메일" required />
<input name="age" type="number" placeholder="나이" />
<button type="submit">사용자 생성</button>
</form>
);
}

// ========== 캐싱 및 최적화 ==========
const GET_USER = gql`
query GetUser($id: Int!) {
user(id: $id) {
id
name
email
}
}
`;

function UserProfile({ userId }) {
const { loading, error, data, refetch } = useQuery(GET_USER, {
variables: { id: userId },
// 캐싱 정책
fetchPolicy: 'cache-first', // 캐시 우선
// fetchPolicy: 'network-only', // 항상 서버 요청
// fetchPolicy: 'cache-and-network', // 캐시 보여주고 업데이트
});

// 수동 새로고침
const handleRefresh = () => {
refetch();
};

if (loading) return <p>로딩 중...</p>;
if (error) return <p>에러: {error.message}</p>;

return (
<div>
<h2>{data.user.name}</h2>
<p>{data.user.email}</p>
<button onClick={handleRefresh}>새로고침</button>
</div>
);
}

🤔 자주 묻는 질문

Q1. REST API와 GraphQL 중 무엇을 선택해야 하나요?

A: 프로젝트 특성에 따라 선택하세요:

✅ REST API를 선택하는 경우:
├─ 간단한 CRUD 애플리케이션
├─ 리소스 구조가 명확함
├─ 캐싱이 중요함 (HTTP 캐싱 활용)
├─ 팀이 REST에 익숙함
├─ 파일 업로드/다운로드가 많음
└─ 예: 블로그, 전자상거래 기본 기능

✅ GraphQL을 선택하는 경우:
├─ 복잡한 데이터 관계
├─ 모바일 앱 (데이터 절약 중요)
├─ 클라이언트가 다양함 (웹, 앱, 태블릿)
├─ 빠른 프론트엔드 개발 원함
├─ 실시간 데이터 구독 필요
└─ 예: 소셜미디어, 대시보드, 실시간 앱

🤝 함께 사용도 가능:
├─ 주요 API는 GraphQL
├─ 파일 업로드는 REST
└─ 각 장점을 활용

Q2. 성능 차이는 어떤가요?

A:

// REST API 성능 특성
장점:
- HTTP 캐싱 활용 가능
GET /api/users/1
Cache-Control: max-age=3600

- CDN 캐싱 쉬움
- 단순한 엔드포인트는 빠름

단점:
- Over-fetching으로 불필요한 데이터 전송
GET /api/users/1
→ 모든 필드 반환 (100KB)

- Under-fetching으로 여러 요청 필요
GET /api/users/1 // 1번 요청
GET /api/users/1/posts // 2번 요청
GET /api/posts/1/comments // 3번 요청
// 총 3번 요청!

// GraphQL 성능 특성
장점:
- 정확히 필요한 데이터만 (10KB)
query {
user(id: 1) {
name
email
}
}

- 한 번의 요청으로 모든 데이터
query {
user(id: 1) {
name
posts {
title
comments { text }
}
}
}
// 1번 요청으로 끝!

단점:
- HTTP 캐싱 어려움 (POST 요청 사용)
- 복잡한 쿼리는 서버 부담
- N+1 문제 (DataLoader로 해결)

실제 벤치마크:
REST: 3번 요청, 총 200KB, 300ms
GraphQL: 1번 요청, 총 50KB, 150ms
→ 모바일 환경에서 GraphQL이 유리

Q3. GraphQL의 N+1 문제란?

A: DataLoader로 해결할 수 있습니다:

// ========== N+1 문제 ==========
// 10명의 사용자와 각자의 게시글 조회

// 문제: 비효율적인 쿼리
const resolvers = {
Query: {
users: () => {
return db.users.findAll(); // 1번 쿼리
}
},
User: {
posts: (user) => {
return db.posts.findByUserId(user.id); // 사용자마다 1번씩!
}
}
};

// 총 쿼리: 1 + 10 = 11번
// 1번: 사용자 조회
// 10번: 각 사용자의 게시글 조회

// ========== DataLoader로 해결 ==========
const DataLoader = require('dataloader');

// 배치로 게시글 조회
const postLoader = new DataLoader(async (userIds) => {
// 한 번에 모든 게시글 조회
const posts = await db.posts.findByUserIds(userIds);

// 사용자별로 그룹화
const postsByUser = {};
posts.forEach(post => {
if (!postsByUser[post.userId]) {
postsByUser[post.userId] = [];
}
postsByUser[post.userId].push(post);
});

// 각 사용자의 게시글 반환
return userIds.map(id => postsByUser[id] || []);
});

const resolvers = {
User: {
posts: (user) => {
return postLoader.load(user.id); // 배치로 처리!
}
}
};

// 총 쿼리: 1 + 1 = 2번
// 1번: 사용자 조회
// 1번: 모든 게시글 조회 (배치)
// → 성능 대폭 향상!

// ========== 실전 예시 ==========
const { ApolloServer } = require('apollo-server');
const DataLoader = require('dataloader');

const server = new ApolloServer({
typeDefs,
resolvers,
context: () => ({
// 요청마다 새 DataLoader 생성
loaders: {
postLoader: new DataLoader(batchGetPosts),
userLoader: new DataLoader(batchGetUsers)
}
})
});

// 리졸버에서 사용
const resolvers = {
User: {
posts: (user, args, context) => {
return context.loaders.postLoader.load(user.id);
}
}
};

Q4. REST API 버전 관리 vs GraphQL 스키마 진화?

A:

// ========== REST API 버전 관리 ==========

// 방법 1: URL에 버전 포함
GET /api/v1/users
GET /api/v2/users // 새 버전

// 방법 2: 헤더에 버전 명시
GET /api/users
Accept: application/vnd.myapp.v1+json

// 문제:
// - 여러 버전 유지보수
// - 클라이언트 업데이트 필요
// - 구버전 지원 부담

// 예시: 필드 이름 변경
// v1
{
"name": "김철수"
}

// v2
{
"fullName": "김철수" // name → fullName
}

// ========== GraphQL 스키마 진화 ==========

// 추가는 쉬움
type User {
id: Int!
name: String!
email: String! # 기존
phone: String # 추가 (새 필드)
address: Address # 추가 (새 타입)
}

// 변경: @deprecated 사용
type User {
id: Int!
name: String! @deprecated(reason: "Use fullName instead")
fullName: String! # 새 필드
email: String!
}

// 클라이언트는 점진적으로 업데이트
query {
user(id: 1) {
name # 여전히 작동 (deprecated)
fullName # 새 필드 사용
}
}

// 장점:
// - 하나의 엔드포인트
// - 버전 관리 불필요
// - 점진적 마이그레이션
// - 클라이언트가 원하는 시점에 업데이트

Q5. 실시간 데이터는 어떻게 처리하나요?

A:

// ========== REST API: 폴링 또는 SSE ==========

// 1. 폴링 (Polling)
setInterval(async () => {
const response = await fetch('/api/messages');
const messages = await response.json();
updateUI(messages);
}, 3000); // 3초마다 요청
// 단점: 비효율적, 서버 부담

// 2. Server-Sent Events (SSE)
const eventSource = new EventSource('/api/messages/stream');
eventSource.onmessage = (event) => {
const message = JSON.parse(event.data);
updateUI(message);
};
// 단점: 단방향 통신만 가능

// ========== GraphQL: Subscription ==========

// 서버 설정
const { PubSub } = require('graphql-subscriptions');
const pubsub = new PubSub();

const typeDefs = gql`
type Message {
id: Int!
text: String!
userId: Int!
createdAt: String!
}

type Subscription {
messageAdded: Message!
}

type Mutation {
addMessage(text: String!): Message!
}
`;

const resolvers = {
Mutation: {
addMessage: (parent, args) => {
const message = {
id: messages.length + 1,
text: args.text,
userId: 1,
createdAt: new Date().toISOString()
};
messages.push(message);

// 구독자들에게 알림
pubsub.publish('MESSAGE_ADDED', {
messageAdded: message
});

return message;
}
},

Subscription: {
messageAdded: {
subscribe: () => pubsub.asyncIterator(['MESSAGE_ADDED'])
}
}
};

// 클라이언트 (React)
import { useSubscription, gql } from '@apollo/client';

const MESSAGE_SUBSCRIPTION = gql`
subscription OnMessageAdded {
messageAdded {
id
text
userId
createdAt
}
}
`;

function ChatRoom() {
const { data, loading } = useSubscription(MESSAGE_SUBSCRIPTION);

useEffect(() => {
if (data) {
console.log('새 메시지:', data.messageAdded);
addMessageToUI(data.messageAdded);
}
}, [data]);

return <div>{/* UI */}</div>;
}

// 장점:
// - 실시간 양방향 통신
// - WebSocket 기반
// - 효율적

🎓 다음 단계

REST API와 GraphQL을 이해했다면, 다음을 학습해보세요:

  1. API란? (문서 작성 예정) - API 기본 개념
  2. JWT 토큰 (문서 작성 예정) - API 인증
  3. WebSocket이란? (문서 작성 예정) - 실시간 통신

실습해보기

# ========== REST API 실습 (Express) ==========
mkdir rest-api-demo
cd rest-api-demo
npm init -y
npm install express

# server.js 작성 후
node server.js

# 테스트
curl http://localhost:3000/api/users
curl -X POST http://localhost:3000/api/users \
-H "Content-Type: application/json" \
-d '{"name":"김철수","email":"kim@example.com"}'

# ========== GraphQL 실습 (Apollo Server) ==========
mkdir graphql-demo
cd graphql-demo
npm init -y
npm install apollo-server graphql

# server.js 작성 후
node server.js

# GraphQL Playground 열기
# http://localhost:4000
# 브라우저에서 쿼리 실행 가능!

# ========== React + GraphQL ==========
npx create-react-app my-app
cd my-app
npm install @apollo/client graphql

# Apollo Client 설정 후 실행
npm start

🎬 마무리

REST API와 GraphQL은 각각의 장단점이 있습니다:

  • REST API: 간단하고 익숙하며 캐싱이 쉬움
  • GraphQL: 유연하고 효율적이며 실시간 지원
  • 선택 기준: 프로젝트 복잡도, 팀 경험, 성능 요구사항
  • 하이브리드: 필요에 따라 두 방식을 함께 사용

올바른 API 설계로 훌륭한 애플리케이션을 만드세요! 🔄