eatthefrog
Apollo Server로 GraphQL API 만들기 본문
전체 구조
Apollo Server GraphQL API 구축
├── 🏗️ 기본 설정
│ ├── 설치 & 초기화
│ ├── 스키마 정의
│ └── 서버 실행
├── 📝 스키마 설계
│ ├── 타입 정의 (TypeDefs)
│ ├── 리졸버 함수
│ └── 데이터 관계 설정
└── 🔧 고급 기능
├── 데이터베이스 연동
├── 인증/권한
└── 실시간 기능
🏗️ 기본 설정
핵심 개념
- Apollo Server: GraphQL 서버를 쉽게 만드는 도구
- 스키마 우선: 데이터 구조를 먼저 정의
- 타입 안전성: 명확한 데이터 타입 정의
설치부터 실행까지
# 1. 필수 패키지 설치
npm install apollo-server graphql
# 2. 추가 도구들
npm install @apollo/server graphql-tag
최소 코드로 서버 만들기
// server.js - 가장 간단한 Apollo Server
const { ApolloServer } = require('apollo-server');
// 📝 1. 스키마 정의 (어떤 데이터?)
const typeDefs = `
type User {
id: ID!
name: String!
email: String!
}
type Query {
users: [User!]!
user(id: ID!): User
}
`;
// 🔧 2. 리졸버 정의 (어떻게 가져올?)
const resolvers = {
Query: {
users: () => [
{ id: '1', name: '김철수', email: 'kim@example.com' },
{ id: '2', name: '이영희', email: 'lee@example.com' }
],
user: (parent, { id }) => {
return { id, name: '김철수', email: 'kim@example.com' };
}
}
};
// 🚀 3. 서버 생성 및 실행
const server = new ApolloServer({ typeDefs, resolvers });
server.listen().then(({ url }) => {
console.log(`🚀 서버 실행: ${url}`);
});
📝 스키마 설계
TypeDefs (타입 정의) 핵심
- 스키마 언어: GraphQL만의 특별한 문법
- 타입 시스템: 강력한 타입 검증
- 느낌표(!): 필수 필드 표시
- 대괄호[]: 배열 표시
기본 타입들
# 스칼라 타입 (기본 데이터)
scalar Date
type User {
id: ID! # 고유 식별자 (필수)
name: String! # 문자열 (필수)
age: Int # 정수 (선택)
height: Float # 실수 (선택)
isActive: Boolean # 불린 (선택)
createdAt: Date # 커스텀 타입
}
# 배열과 관계
type Post {
id: ID!
title: String!
author: User! # 단일 관계
tags: [String!]! # 문자열 배열
comments: [Comment!]! # 객체 배열
}
쿼리/뮤테이션/구독 정의
type Query {
# 데이터 조회
users: [User!]!
user(id: ID!): User
posts(limit: Int, offset: Int): [Post!]!
}
type Mutation {
# 데이터 변경
createUser(input: CreateUserInput!): User!
updateUser(id: ID!, input: UpdateUserInput!): User!
deleteUser(id: ID!): Boolean!
}
type Subscription {
# 실시간 업데이트
userAdded: User!
postUpdated(userId: ID!): Post!
}
# 입력 타입
input CreateUserInput {
name: String!
email: String!
age: Int
}
🔧 리졸버 함수
핵심 개념
- 리졸버: 실제 데이터를 가져오는 함수
- 4개 인자: parent, args, context, info
- 체이닝: 중첩된 데이터 처리
리졸버 구조
const resolvers = {
// 🔍 Query 리졸버 (데이터 조회)
Query: {
// users 필드를 요청하면 실행
users: async (parent, args, context, info) => {
// 데이터베이스에서 사용자 목록 조회
return await User.findAll();
},
// user(id: "123") 요청하면 실행
user: async (parent, { id }, context) => {
return await User.findById(id);
}
},
// ✏️ Mutation 리졸버 (데이터 변경)
Mutation: {
createUser: async (parent, { input }, context) => {
// 새 사용자 생성
const newUser = await User.create(input);
return newUser;
}
},
// 🔗 중첩 데이터 리졸버
User: {
// User의 posts 필드가 요청되면 실행
posts: async (parent, args, context) => {
// parent는 User 객체
return await Post.findByUserId(parent.id);
}
}
};
리졸버 인자 이해하기
const resolvers = {
Query: {
user: (parent, args, context, info) => {
// parent: 부모 객체 (Query에서는 보통 undefined)
// args: 클라이언트에서 전달한 인수 { id: "123" }
// context: 요청 전반에 공유되는 데이터 (DB, 인증 등)
// info: 쿼리 정보 (고급 사용)
console.log('인수:', args.id);
console.log('컨텍스트:', context.user);
return getUserById(args.id);
}
}
};
🗄️ 데이터베이스 연동
MongoDB 연동 예시
const mongoose = require('mongoose');
// 📋 MongoDB 스키마 정의
const UserSchema = new mongoose.Schema({
name: String,
email: String,
createdAt: { type: Date, default: Date.now }
});
const User = mongoose.model('User', UserSchema);
// 🔧 리졸버에서 MongoDB 사용
const resolvers = {
Query: {
users: async () => {
return await User.find();
},
user: async (parent, { id }) => {
return await User.findById(id);
}
},
Mutation: {
createUser: async (parent, { input }) => {
const user = new User(input);
return await user.save();
}
}
};
// 🚀 서버 시작 시 DB 연결
mongoose.connect('mongodb://localhost:27017/myapp')
.then(() => console.log('📊 MongoDB 연결 완료'));
MySQL/PostgreSQL 연동
const { Sequelize, DataTypes } = require('sequelize');
// 📊 데이터베이스 연결
const sequelize = new Sequelize('database', 'username', 'password', {
host: 'localhost',
dialect: 'mysql' // 또는 'postgres'
});
// 📋 모델 정의
const User = sequelize.define('User', {
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true
},
name: DataTypes.STRING,
email: DataTypes.STRING
});
// 🔧 리졸버에서 사용
const resolvers = {
Query: {
users: () => User.findAll(),
user: (parent, { id }) => User.findByPk(id)
},
Mutation: {
createUser: (parent, { input }) => User.create(input)
}
};
🔐 인증/권한 키워드
Context를 통한 인증
const jwt = require('jsonwebtoken');
// 🔑 인증 미들웨어
const getUser = async (token) => {
try {
if (!token) return null;
const decoded = jwt.verify(token, 'secret-key');
return await User.findById(decoded.userId);
} catch (error) {
return null;
}
};
// 🚀 서버 설정에 context 추가
const server = new ApolloServer({
typeDefs,
resolvers,
context: async ({ req }) => {
// 헤더에서 토큰 추출
const token = req.headers.authorization?.replace('Bearer ', '');
const user = await getUser(token);
return {
user, // 인증된 사용자
dataSources // 데이터 소스들
};
}
});
// 🔒 리졸버에서 권한 확인
const resolvers = {
Query: {
myProfile: (parent, args, { user }) => {
if (!user) throw new Error('로그인이 필요합니다');
return user;
}
},
Mutation: {
createPost: (parent, { input }, { user }) => {
if (!user) throw new Error('로그인이 필요합니다');
return Post.create({ ...input, authorId: user.id });
}
}
};
🎛️ 실시간 기능 (Subscription) 키워드
기본 구독 설정
const { PubSub } = require('apollo-server');
// 📡 이벤트 발행/구독 시스템
const pubsub = new PubSub();
const typeDefs = `
type Subscription {
messageAdded: Message!
userOnline: User!
}
type Message {
id: ID!
text: String!
user: String!
}
`;
const resolvers = {
Subscription: {
// 📨 새 메시지 구독
messageAdded: {
subscribe: () => pubsub.asyncIterator(['MESSAGE_ADDED'])
}
},
Mutation: {
// 💬 메시지 생성 시 이벤트 발행
addMessage: (parent, { text }, { user }) => {
const message = {
id: Date.now(),
text,
user: user.name
};
// 🔊 이벤트 발행
pubsub.publish('MESSAGE_ADDED', { messageAdded: message });
return message;
}
}
};
🛠️ 실무 완성형 서버 구조
프로젝트 폴더 구조
src/
├── schema/
│ ├── typeDefs.js # 스키마 정의
│ └── resolvers.js # 리졸버 함수
├── models/
│ ├── User.js # 사용자 모델
│ └── Post.js # 게시글 모델
├── utils/
│ ├── auth.js # 인증 관련
│ └── database.js # DB 연결
└── server.js # 메인 서버 파일
완성형 서버 코드
// server.js
const { ApolloServer } = require('apollo-server');
const mongoose = require('mongoose');
const typeDefs = require('./schema/typeDefs');
const resolvers = require('./schema/resolvers');
const { getUser } = require('./utils/auth');
// 📊 데이터베이스 연결
mongoose.connect('mongodb://localhost:27017/graphql-app');
// 🚀 Apollo Server 생성
const server = new ApolloServer({
typeDefs,
resolvers,
context: async ({ req }) => {
const token = req.headers.authorization?.replace('Bearer ', '');
const user = await getUser(token);
return {
user,
isAuthenticated: !!user
};
},
// 🛡️ 에러 처리
formatError: (error) => {
console.error('GraphQL Error:', error);
return {
message: error.message,
code: error.extensions?.code
};
}
});
// 🎯 서버 실행
server.listen({ port: 4000 }).then(({ url }) => {
console.log(`🚀 GraphQL Server ready at ${url}`);
console.log(`🎮 GraphQL Playground available`);
});
키워드 정리
필수 개념 4가지
1️⃣ TypeDefs = "데이터 구조 정의" (스키마)
2️⃣ Resolvers = "데이터 가져오는 함수" (로직)
3️⃣ Context = "요청 공통 데이터" (인증, DB 등)
4️⃣ Apollo Server = "모든 걸 연결하는 도구"
개발 순서
📝 1. 스키마 설계 (어떤 데이터?)
🔧 2. 리졸버 작성 (어떻게 가져올?)
🗄️ 3. 데이터베이스 연동
🔐 4. 인증/권한 추가
🚀 5. 서버 실행 및 테스트
실무 팁
- 스키마 우선: 데이터 구조부터 명확히
- 모듈화: 파일별로 기능 분리
- 에러 처리: 명확한 에러 메시지
- 성능: N+1 문제 주의 (DataLoader 사용)
결론: Apollo Server는 GraphQL API를 쉽고 빠르게 만들 수 있는 강력한 도구입니다! 🎯
'백엔드 노트' 카테고리의 다른 글
| GraphQL 핵심 요약 (2) | 2025.06.18 |
|---|---|
| GraphQL: A query language for your API 공식문서 읽기 (2) | 2025.06.18 |
| REST API의 한계와 GraphQL (2) | 2025.06.16 |
| 백엔드 개발자들이 실제로 회사에서 하는 일 (3) | 2024.12.16 |
| API 서버 (1) | 2024.12.16 |