eatthefrog

Apollo Server로 GraphQL API 만들기 본문

백엔드 노트

Apollo Server로 GraphQL API 만들기

eater_forg 2025. 6. 16. 23:07

전체 구조 

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를 쉽고 빠르게 만들 수 있는 강력한 도구입니다! 🎯