Node.js와 Apollo Server를 활용해 GraphQL API를 처음부터 구축하는 방법을 친절하게 안내합니다. 스키마 정의부터 데이터 연동까지, 실전 예제로 완벽 마스터하세요!
안녕하세요, 개발자 여러분! 혹시 API 개발을 진행하다가 이런 경험 없으신가요? 클라이언트에서 필요한 데이터는 딱 3개인데, REST API는 100개를 다 던져줘서 데이터 낭비가 심하거나, 반대로 필요한 데이터가 여러 엔드포인트에 흩어져 있어서 API를 여러 번 호출해야 했던 경험 말이에요. 이런 상황들이 쌓이면 개발 생산성이 떨어지고, 결국 유지보수도 어려워지기 십상이죠.
이런 문제들을 해결하고자 등장한 것이 바로 GraphQL입니다. GraphQL은 클라이언트가 서버에 필요한 데이터를 정확히 '요청'할 수 있게 해주는 강력한 API 쿼리 언어이자 런타임이거든요. 오늘은 Node.js 환경에서 Apollo Server를 활용하여 GraphQL API를 처음부터 끝까지 구축하는 실전 가이드를 준비해봤습니다. 복잡하게만 느껴졌던 GraphQL, 저와 함께라면 쉽고 재미있게 마스터할 수 있을 거예요!
📑 목차
- GraphQL, 왜 지금 주목할까요? REST API와 비교하며 알아보기
- REST API의 한계와 GraphQL의 등장
- GraphQL의 핵심 장점들
- Node.js와 Apollo Server, 환상의 짝꿍 소개
- Node.js, 비동기 처리에 강한 백엔드 런타임
- Apollo Server, GraphQL API 구축의 표준
- GraphQL API 구축, 첫걸음 떼기: 개발 환경 설정
- 프로젝트 초기화 및 필수 패키지 설치
- GraphQL 스키마 설계와 Resolver 구현
- GraphQL 스키마 정의 언어 (SDL) 이해하기
- 데이터 조회 (Query)와 변경 (Mutation) Resolver 작성
- 데이터베이스 연동과 실제 데이터 처리
- 간단한 인메모리 데이터베이스 구축
- Resolver에서 데이터 처리 로직 구현
- Apollo Server 실행 및 API 테스트
- Apollo Server 인스턴스 생성 및 시작
- Apollo Sandbox를 활용한 API 테스트
- 마무리하며: GraphQL 여정의 다음 단계
Image by Hans on Pixabay
GraphQL, 왜 지금 주목할까요? REST API와 비교하며 알아보기
우리가 오랫동안 사용해왔던 REST API는 분명 강력하고 유용한 아키텍처 스타일이지만, 특정 상황에서는 몇 가지 한계를 보이기도 합니다. 그리고 이 한계를 극복하기 위해 GraphQL이 등장했죠.
REST API의 한계와 GraphQL의 등장
REST API는 '자원(Resource)' 중심의 아키텍처로, 각 자원에 대해 고정된 형태의 데이터를 제공하는 것이 일반적입니다. 예를 들어, 사용자 정보를 가져오는 API는 항상 사용자 ID, 이름, 이메일, 주소 등 모든 정보를 반환하죠. 여기서 문제가 생기는데요.
- Over-fetching (과다 조회): 클라이언트가 필요한 데이터보다 더 많은 데이터를 받는 경우입니다. 예를 들어, 사용자 이름만 필요한데 API는 주소, 전화번호 등 모든 정보를 반환한다면 불필요한 네트워크 대역폭을 소모하게 됩니다. 모바일 환경에서는 특히 큰 문제가 될 수 있죠.
- Under-fetching (과소 조회): 반대로, 클라이언트가 하나의 화면을 구성하기 위해 여러 개의 REST API를 호출해야 하는 경우입니다. 게시글 목록을 보여주는데, 각 게시글의 작성자 정보와 댓글 개수를 보여줘야 한다면, 게시글 API, 작성자 API, 댓글 API를 각각 호출해야 할 수도 있습니다. 이는 네트워크 왕복 시간을 증가시켜 성능 저하를 일으킬 수 있습니다.
- 잦은 버전 관리: 클라이언트 요구사항이 변경될 때마다 새로운 API 엔드포인트를 만들거나 기존 엔드포인트의 응답 구조를 변경해야 하는 경우도 많습니다. 이는 API 버전을 관리하는 데 어려움을 더하죠.
이러한 문제들에 대한 해결책으로 Facebook에서 개발한 것이 바로 GraphQL입니다. GraphQL은 클라이언트가 정확히 필요한 데이터만을 요청할 수 있도록 설계되었으며, 단일 엔드포인트를 통해 모든 데이터를 가져올 수 있게 해줍니다.
GraphQL의 핵심 장점들
GraphQL은 위에서 언급된 REST API의 한계를 극복하며 다음과 같은 주요 장점들을 제공합니다.
- 데이터 과다/과소 조회 문제 해결: 클라이언트가 필요한 필드를 명시적으로 요청하기 때문에, 서버는 요청된 필드에 해당하는 데이터만 반환합니다. 덕분에 네트워크 효율성이 크게 향상되죠.
- 단일 엔드포인트: 일반적으로 GraphQL API는 하나의 HTTP 엔드포인트(예:
/graphql)를 통해 모든 데이터 요청을 처리합니다. 클라이언트는 이 단일 엔드포인트에 GraphQL 쿼리를 보내 필요한 데이터를 가져올 수 있어요. - 강력한 타입 시스템: GraphQL은 스키마 정의 언어(SDL)를 사용하여 API의 모든 데이터를 타입으로 정의합니다. 이는 API의 '청사진' 역할을 하며, 클라이언트와 서버 개발자 모두에게 API가 어떤 데이터를 제공하는지 명확하게 알려주죠. 덕분에 개발 과정에서 발생할 수 있는 오류를 줄이고, 자동 완성 기능을 활용할 수 있게 해줍니다.
- 빠른 개발 속도: 클라이언트와 서버 개발자가 독립적으로 작업할 수 있는 환경을 제공합니다. 스키마가 정의되면, 클라이언트 개발자는 스키마에 맞춰 데이터를 요청하고, 서버 개발자는 해당 스키마에 맞춰 리졸버를 구현하면 되거든요.
REST API와 GraphQL의 특징을 한눈에 비교해볼까요?
| 특징 | REST API | GraphQL |
|---|---|---|
| 데이터 요청 방식 | 고정된 리소스 형태, 여러 엔드포인트 | 클라이언트가 필요한 필드를 직접 요청, 단일 엔드포인트 |
| 데이터 과다/과소 조회 | 발생 가능성 높음 | 클라이언트 요청에 따라 정확한 데이터 반환 |
| 엔드포인트 수 | 다수 (리소스별) | 하나 (일반적으로 /graphql) |
| 타입 시스템 | 명시적 정의 필요 없음 (문서화 의존) | 강력한 스키마 기반 타입 시스템 |
| 버전 관리 | 새로운 엔드포인트 또는 URI 버전 추가 | 스키마에 필드 추가/삭제로 유연하게 관리 |
Node.js와 Apollo Server, 환상의 짝꿍 소개
GraphQL의 장점을 이해했다면, 이제 실제 구현을 위한 도구들을 알아봐야겠죠? 저희는 Node.js와 Apollo Server라는 강력한 조합을 활용할 예정입니다.
Node.js, 비동기 처리에 강한 백엔드 런타임
Node.js는 자바스크립트를 웹 브라우저 밖, 즉 서버 사이드에서 실행할 수 있게 해주는 런타임 환경입니다. Google Chrome의 V8 자바스크립트 엔진을 기반으로 구축되어 매우 빠른 성능을 자랑하죠. 특히 논블로킹 I/O와 이벤트 기반 아키텍처 덕분에 수많은 동시 요청을 효율적으로 처리할 수 있어서, 실시간 데이터 처리가 중요한 API 서버 구축에 아주 적합하답니다. 개발자들이 프론트엔드와 백엔드를 모두 자바스크립트로 개발할 수 있게 해주어 생산성을 크게 높여주는 장점도 있고요.
Apollo Server, GraphQL API 구축의 표준
그럼 Node.js 위에서 GraphQL API를 어떻게 만들까요? 바로 Apollo Server가 그 해답인데요. Apollo Server는 GraphQL 명세에 따라 API를 쉽게 구축할 수 있도록 도와주는 오픈소스 라이브러리예요. 강력하고 유연하며, 다음과 같은 특징들을 제공합니다.
- 쉬운 스키마 정의: GraphQL 스키마를 간단하게 정의할 수 있도록 지원합니다.
- 리졸버 구현 지원: 스키마에 정의된 필드에 대한 데이터 처리 로직(리졸버)을 쉽게 작성할 수 있어요.
- 미들웨어 통합: Express, Koa 등 다양한 Node.js 프레임워크와 쉽게 통합됩니다.
- 개발자 도구: API 테스트를 위한 내장 UI인 Apollo Sandbox(이전에는 GraphQL Playground)를 제공하여 개발 및 디버깅 과정을 훨씬 편리하게 만들어줍니다.
- 확장성: 캐싱, 인증, 에러 처리 등 다양한 기능을 플러그인 형태로 확장할 수 있습니다.
Node.js의 빠르고 효율적인 비동기 처리 능력과 Apollo Server의 편리한 GraphQL 구현 기능을 결합하면, 안정적이고 성능 좋은 GraphQL API를 빠르게 개발할 수 있을 거예요.
GraphQL API 구축, 첫걸음 떼기: 개발 환경 설정
자, 이제 이론은 충분히 살펴봤으니, 본격적으로 GraphQL API를 구축해볼 시간입니다. 먼저 기본적인 개발 환경을 설정해볼까요?
프로젝트 초기화 및 필수 패키지 설치
가장 먼저 새로운 Node.js 프로젝트를 생성하고 필요한 패키지들을 설치해야 합니다. 터미널을 열고 다음 명령어를 입력해주세요.
# 1. 새로운 프로젝트 디렉토리 생성 및 이동
mkdir my-graphql-api
cd my-graphql-api
# 2. Node.js 프로젝트 초기화 (-y 옵션은 기본값으로 package.json 파일을 생성합니다)
npm init -y
# 3. Apollo Server와 GraphQL 패키지 설치
npm install apollo-server graphql
위 명령어를 실행하면 package.json 파일이 생성되고, node_modules 디렉토리 안에 apollo-server와 graphql 패키지가 설치될 거예요. package.json 파일은 프로젝트의 메타데이터와 의존성 정보를 담고 있죠. 설치가 완료되면 다음과 같은 내용이 포함된 package.json을 확인할 수 있습니다.
{
"name": "my-graphql-api",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"apollo-server": "^3.13.0",
"graphql": "^16.8.1"
}
}
이제 프로젝트의 루트 디렉토리에 index.js라는 이름의 파일을 생성해주세요. 이 파일이 바로 우리 GraphQL 서버의 메인 진입점이 될 겁니다.
Image by Elchinator on Pixabay
GraphQL 스키마 설계와 Resolver 구현
GraphQL API의 핵심은 스키마와 리졸버입니다. 스키마는 API가 제공할 데이터의 '형태'를 정의하고, 리졸버는 그 형태에 맞춰 실제 데이터를 '반환'하는 로직을 담당하죠.
GraphQL 스키마 정의 언어 (SDL) 이해하기
GraphQL 스키마는 스키마 정의 언어(Schema Definition Language, SDL)를 사용하여 작성됩니다. SDL은 인간이 읽기 쉽고 직관적인 문법으로 데이터 모델을 정의할 수 있게 해주는데요. 예를 들어, 책(Book) 정보를 다루는 API를 만든다면 다음과 같이 스키마를 정의할 수 있습니다.
# index.js 파일 내부에 작성할 내용
const typeDefs = `
type Book {
id: ID!
title: String!
author: String
publishedYear: Int
}
type Query {
books: [Book!]!
book(id: ID!): Book
}
type Mutation {
addBook(title: String!, author: String, publishedYear: Int): Book!
updateBook(id: ID!, title: String, author: String, publishedYear: Int): Book
deleteBook(id: ID!): Boolean!
}
`;
위 코드를 하나씩 살펴볼까요?
type Book:Book이라는 커스텀 객체 타입을 정의했습니다.id,title,author,publishedYear와 같은 필드를 가집니다. 필드 뒤에 붙은!는 해당 필드가 필수(non-nullable)임을 의미해요.ID!,String!,Int: GraphQL이 기본으로 제공하는 스칼라(Scalar) 타입입니다.ID는 고유 식별자를 나타내고,String은 문자열,Int는 정수형 데이터를 의미하죠. 이 외에도Float(실수),Boolean(논리값) 등이 있습니다.type Query: 데이터를 조회(Read)하는 모든 요청을 정의하는 특별한 타입입니다.books는 모든 책 목록을 반환하는 쿼리이고,book(id: ID!)는 특정 ID의 책 하나를 반환하는 쿼리입니다. 대괄호[]는 배열을 의미하며,[Book!]!는 'Book타입의 필수 요소들로 이루어진 필수 배열'이라는 뜻이에요.type Mutation: 데이터를 변경(Create, Update, Delete)하는 모든 요청을 정의하는 특별한 타입입니다.addBook,updateBook,deleteBook과 같은 작업들을 정의했죠.
이렇게 스키마를 정의하는 것만으로도 API가 어떤 데이터를 제공하고 어떤 작업을 수행할 수 있는지 명확하게 파악할 수 있답니다.
데이터 조회 (Query)와 변경 (Mutation) Resolver 작성
스키마에서 정의한 타입과 필드에 대해 실제 데이터를 반환하거나 처리하는 함수를 리졸버(Resolver)라고 부르는데요. 리졸버는 스키마의 각 필드에 매핑되어 해당 필드를 요청했을 때 어떤 데이터를 어떻게 가져올지 결정합니다. index.js 파일에 다음과 같이 리졸버를 작성해볼까요?
# index.js 파일 내부에 작성할 내용
const books = [
{ id: '1', title: 'The Great Gatsby', author: 'F. Scott Fitzgerald', publishedYear: 1925 },
{ id: '2', title: 'To Kill a Mockingbird', author: 'Harper Lee', publishedYear: 1960 },
{ id: '3', title: '1984', author: 'George Orwell', publishedYear: 1949 },
];
const resolvers = {
Query: {
books: () => books,
book: (parent, { id }) => books.find(book => book.id === id),
},
Mutation: {
addBook: (parent, { title, author, publishedYear }) => {
const newBook = {
id: String(books.length + 1), // 간단하게 ID 생성
title,
author,
publishedYear,
};
books.push(newBook);
return newBook;
},
updateBook: (parent, { id, title, author, publishedYear }) => {
const bookIndex = books.findIndex(book => book.id === id);
if (bookIndex === -1) return null;
const updatedBook = { ...books[bookIndex], title, author, publishedYear };
books[bookIndex] = updatedBook;
return updatedBook;
},
deleteBook: (parent, { id }) => {
const initialLength = books.length;
books = books.filter(book => book.id !== id);
return books.length < initialLength; // 삭제 성공 여부 반환
},
},
};
리졸버 함수는 일반적으로 4개의 인자를 받습니다: (parent, args, context, info).
parent: 이전 리졸버의 반환 값입니다. 루트 리졸버에서는 사용되지 않아요.args: GraphQL 쿼리나 뮤테이션에서 전달된 인자(arguments) 객체입니다. 예를 들어book(id: ID!)쿼리에서id값은args.id로 접근할 수 있죠.context: 모든 리졸버에서 공유되는 객체입니다. 데이터베이스 연결, 인증 정보 등을 담을 때 유용하게 사용됩니다.info: 현재 쿼리에 대한 실행 상태 정보가 담긴 객체입니다. 고급 사용 시에 활용됩니다.
위 예시에서는 parent와 info는 사용하지 않고, args만 활용하여 데이터를 조회하거나 변경하는 로직을 구현했습니다. 특히 Mutation에서는 새로운 책을 추가하거나, 기존 책 정보를 수정하고, 책을 삭제하는 로직을 간단하게 구현해봤어요.
데이터베이스 연동과 실제 데이터 처리
지금까지는 메모리 내의 배열에 데이터를 저장했지만, 실제 애플리케이션에서는 데이터베이스와 연동하여 데이터를 영구적으로 저장하고 관리해야 합니다. 여기서는 데이터베이스 연동의 개념과 리졸버에서 실제 데이터를 처리하는 방식을 설명해 드릴게요.
간단한 인메모리 데이터베이스 구축
실제 서비스에서는 MongoDB, PostgreSQL, MySQL 등 다양한 데이터베이스를 사용하겠지만, 이 튜토리얼에서는 복잡성을 줄이기 위해 앞서 정의한 books 배열을 인메모리 데이터베이스처럼 활용할게요. 이 배열은 서버가 재시작되면 초기화되지만, GraphQL API의 동작 원리를 이해하기에는 충분할 거예요.
let books = [ // let으로 변경하여 Mutation에서 배열 재할당 가능하게 함
{ id: '1', title: 'The Great Gatsby', author: 'F. Scott Fitzgerald', publishedYear: 1925 },
{ id: '2', title: 'To Kill a Mockingbird', author: 'Harper Lee', publishedYear: 1960 },
{ id: '3', title: '1984', author: 'George Orwell', publishedYear: 1949 },
];
이 books 배열이 우리 서버의 '데이터' 역할을 할 겁니다. 각 책은 고유한 id를 가지고 있죠. 실제 데이터베이스를 사용한다면, 이 books 배열 대신 데이터베이스 클라이언트(예: Mongoose for MongoDB, Sequelize for SQL)를 통해 데이터를 조회하거나 저장하는 로직이 리졸버 안에 들어가게 됩니다.
Resolver에서 데이터 처리 로직 구현
리졸버는 단순히 데이터를 반환하는 것을 넘어, 데이터베이스에서 데이터를 가져오거나, 외부 API를 호출하거나, 복잡한 비즈니스 로직을 수행하는 역할을 합니다. 위에서 작성한 리졸버 코드를 다시 한번 보면서 데이터 처리 로직을 좀 더 자세히 알아볼까요?
const resolvers = {
Query: {
// 모든 책 목록을 반환합니다. 실제 DB라면 `db.books.find()`와 같은 호출이 들어갈 수 있겠죠.
books: () => books,
// 특정 ID의 책을 찾아 반환합니다. `db.books.findById(id)`와 유사한 역할입니다.
book: (parent, { id }) => books.find(book => book.id === id),
},
Mutation: {
// 새로운 책을 추가합니다. 실제 DB라면 `db.books.create(newBook)`와 같이 데이터를 삽입합니다.
addBook: (parent, { title, author, publishedYear }) => {
const newBook = {
id: String(books.length + 1), // ID는 실제 DB에서 자동 생성될 수 있습니다.
title,
author,
publishedYear,
};
books.push(newBook); // 인메모리 배열에 추가
return newBook;
},
// 기존 책 정보를 업데이트합니다. `db.books.updateOne({ id }, { $set: updateData })`와 유사합니다.
updateBook: (parent, { id, title, author, publishedYear }) => {
const bookIndex = books.findIndex(book => book.id === id);
if (bookIndex === -1) return null; // 책을 찾지 못하면 null 반환
// 기존 데이터와 전달받은 데이터를 병합하여 업데이트
const updatedBook = { ...books[bookIndex], title, author, publishedYear };
books[bookIndex] = updatedBook; // 인메모리 배열 업데이트
return updatedBook;
},
// 특정 책을 삭제합니다. `db.books.deleteOne({ id })`와 유사합니다.
deleteBook: (parent, { id }) => {
const initialLength = books.length;
books = books.filter(book => book.id !== id); // 인메모리 배열에서 삭제
return books.length < initialLength; // 삭제 성공 여부 (배열 길이가 줄었는지 확인)
},
},
};
보시는 것처럼, 각 리졸버 함수 안에는 마치 실제 데이터베이스와 상호작용하는 것과 같은 로직이 담겨 있습니다. 이 로직은 동기적으로 실행될 수도 있고, 비동기적으로 데이터베이스 쿼리를 날리거나 외부 API를 호출할 수도 있습니다. Node.js의 비동기 처리 강점을 살려 async/await 문법을 사용하면, 데이터베이스 작업과 같은 비동기 로직도 깔끔하게 처리할 수 있답니다.
예를 들어, MongoDB와 연동한다면 addBook 리졸버는 다음과 같이 바뀔 수 있을 거예요.
# MongoDB 연동 시 addBook 리졸버 예시
// ... (MongoDB 연결 설정 코드)
// const BookModel = mongoose.model('Book', bookSchema);
Mutation: {
addBook: async (parent, { title, author, publishedYear }) => {
// const newBook = new BookModel({ title, author, publishedYear });
// await newBook.save();
// return newBook;
// ... 실제 MongoDB 연동 코드가 들어갈 자리
},
// ...
}
이렇게 리졸버는 데이터 소스와 GraphQL 스키마 사이의 '다리' 역할을 하며, 어떤 데이터 소스든 GraphQL API를 통해 서비스할 수 있게 해줍니다.
Image by RiaanMarais on Pixabay
Apollo Server 실행 및 API 테스트
스키마와 리졸버를 모두 작성했다면, 이제 Apollo Server 인스턴스를 생성하고 실행할 차례입니다. 그리고 서버가 잘 작동하는지 Apollo Sandbox를 통해 직접 테스트해볼 거예요.
Apollo Server 인스턴스 생성 및 시작
index.js 파일에 지금까지 작성한 typeDefs와 resolvers를 사용하여 Apollo Server를 설정하고 시작하는 코드를 추가합니다.
// index.js 파일 전체 내용
const { ApolloServer } = require('apollo-server');
// 인메모리 데이터 (데이터베이스 역할)
let books = [
{ id: '1', title: 'The Great Gatsby', author: 'F. Scott Fitzgerald', publishedYear: 1925 },
{ id: '2', title: 'To Kill a Mockingbird', author: 'Harper Lee', publishedYear: 1960 },
{ id: '3', title: '1984', author: 'George Orwell', publishedYear: 1949 },
];
// GraphQL 스키마 정의 (SDL)
const typeDefs = `
type Book {
id: ID!
title: String!
author: String
publishedYear: Int
}
type Query {
books: [Book!]!
book(id: ID!): Book
}
type Mutation {
addBook(title: String!, author: String, publishedYear: Int): Book!
updateBook(id: ID!, title: String, author: String, publishedYear: Int): Book
deleteBook(id: ID!): Boolean!
}
`;
// 리졸버 함수들
const resolvers = {
Query: {
books: () => books,
book: (parent, { id }) => books.find(book => book.id === id),
},
Mutation: {
addBook: (parent, { title, author, publishedYear }) => {
const newBook = {
id: String(books.length + 1),
title,
author,
publishedYear,
};
books.push(newBook);
return newBook;
},
updateBook: (parent, { id, title, author, publishedYear }) => {
const bookIndex = books.findIndex(book => book.id === id);
if (bookIndex === -1) return null;
const updatedBook = { ...books[bookIndex], title, author, publishedYear };
books[bookIndex] = updatedBook;
return updatedBook;
},
deleteBook: (parent, { id }) => {
const initialLength = books.length;
books = books.filter(book => book.id !== id);
return books.length < initialLength;
},
},
};
// Apollo Server 인스턴스 생성
const server = new ApolloServer({ typeDefs, resolvers });
// 서버 시작
server.listen().then(({ url }) => {
console.log(`🚀 Server ready at ${url}`);
console.log(`Explore your API at ${url}`);
});
모든 코드를 작성했다면, 터미널에서 다음 명령어를 실행하여 서버를 시작해보세요.
node index.js
서버가 성공적으로 실행되면 터미널에 다음과 비슷한 메시지가 출력될 겁니다.
🚀 Server ready at http://localhost:4000/
Explore your API at http://localhost:4000/
기본적으로 Apollo Server는 4000번 포트에서 실행됩니다. 다른 포트를 사용하고 싶다면 server.listen({ port: 5000 })와 같이 지정할 수 있어요.
Apollo Sandbox를 활용한 API 테스트
서버가 실행 중인 상태에서 브라우저를 열고 터미널에 출력된 URL(예: http://localhost:4000/)로 접속해보세요. 그러면 Apollo Sandbox라는 강력한 개발자 도구 UI가 나타날 겁니다. 이 도구는 GraphQL 쿼리, 뮤테이션을 작성하고 실행하며, API의 스키마를 탐색하는 데 아주 유용해요.
1. 모든 책 조회 (Query)
왼쪽 패널에 다음 쿼리를 입력하고 실행 버튼을 눌러보세요.
query GetBooks {
books {
id
title
author
}
}
오른쪽 패널에 다음과 같은 결과가 나타날 겁니다. 클라이언트가 요청한 id, title, author 필드만 정확히 반환되는 것을 확인할 수 있죠.
{
"data": {
"books": [
{ "id": "1", "title": "The Great Gatsby", "author": "F. Scott Fitzgerald" },
{ "id": "2", "title": "To Kill a Mockingbird", "author": "Harper Lee" },
{ "id": "3", "title": "1984", "author": "George Orwell" }
]
}
}
2. 특정 책 조회 (Query with Arguments)
이번에는 ID를 사용하여 특정 책을 조회해볼까요?
query GetBookById {
book(id: "2") {
id
title
publishedYear
}
}
{
"data": {
"book": {
"id": "2",
"title": "To Kill a Mockingbird",
"publishedYear": 1960
}
}
}
3. 새로운 책 추가 (Mutation)
데이터를 변경하는 뮤테이션도 테스트해볼 수 있습니다.
mutation AddNewBook {
addBook(title: "The Hitchhiker's Guide to the Galaxy", author: "Douglas Adams", publishedYear: 1979) {
id
title
author
}
}
뮤테이션 실행 후 다시 GetBooks 쿼리를 날려보면 새로 추가된 책이 목록에 포함된 것을 확인할 수 있을 거예요!
4. 책 정보 업데이트 (Mutation)
이번에는 기존 책의 정보를 업데이트 해보죠.
mutation UpdateExistingBook {
updateBook(id: "1", title: "The Great Gatsby (Revised Edition)", publishedYear: 2020) {
id
title
publishedYear
}
}
5. 책 삭제 (Mutation)
마지막으로 책을 삭제하는 뮤테이션입니다.
mutation DeleteOneBook {
deleteBook(id: "3")
}
{
"data": {
"deleteBook": true
}
}
deleteBook 쿼리 후 GetBooks를 다시 실행하면 '1984' 책이 사라진 것을 확인할 수 있을 겁니다.
Apollo Sandbox는 스키마를 기반으로 자동 완성 기능을 제공하고, 에러 메시지도 상세하게 보여주기 때문에 GraphQL API를 개발하고 디버깅하는 데 정말 큰 도움이 된답니다!
마무리하며: GraphQL 여정의 다음 단계
축하드립니다! Node.js와 Apollo Server를 활용하여 기본적인 GraphQL API를 성공적으로 구축하고 테스트까지 완료하셨습니다. 스키마를 정의하고, 리졸버를 구현하며, Apollo Sandbox를 통해 쿼리와 뮤테이션을 실행하는 전 과정을 직접 경험해보셨죠? 이 과정에서 GraphQL이 어떻게 데이터 과다/과소 조회 문제를 해결하고, Node.js의 효율적인 비동기 처리 능력과 Apollo Server의 편리함이 만나 시너지를 내는지 이해하셨을 거라 생각합니다.
물론, 이번 튜토리얼은 GraphQL API 구축의 아주 기본적인 단계만을 다루었습니다. 실제 프로덕션 환경에서는 인증/인가 처리, 데이터베이스 연동(MongoDB, PostgreSQL 등), 에러 핸들링, 파일 업로드, 실시간 통신을 위한 Subscription 구현, 성능 최적화 등 더 많은 고려사항들이 존재합니다. 하지만 이 가이드를 통해 GraphQL의 핵심 개념과 Apollo Server 사용법을 탄탄하게 익히셨으니, 이제 어떤 복잡한 요구사항도 자신감을 가지고 도전하실 수 있을 거예요.
이 글이 여러분의 GraphQL 학습 여정에 작은 도움이 되었기를 바랍니다. 혹시 더 궁금한 점이 있으시거나, 다음에 다루었으면 하는 주제가 있다면 언제든지 댓글로 남겨주세요! 여러분의 피드백은 저에게 큰 힘이 된답니다. 다음에도 더 유익한 정보로 찾아뵙겠습니다!
📌 함께 읽으면 좋은 글
- [개발 도구] 개발자 터미널 환경 최적화: Zsh, Tmux, 플러그인으로 생산성 극대화
- [튜토리얼] PostgreSQL 성능 최적화: 인덱싱과 쿼리 튜닝 실전 가이드
- [튜토리얼] GitHub Actions 활용 웹 애플리케이션 CI/CD 파이프라인 구축 실전 가이드
이 글이 도움이 되셨다면 공감(♥)과 댓글로 응원해 주세요!
궁금한 점이나 다루었으면 하는 주제가 있다면 댓글로 남겨주세요.
'튜토리얼' 카테고리의 다른 글
| Nx 모노레포 실전 가이드: 풀스택 TypeScript 개발 효율 높이기 (0) | 2026.06.28 |
|---|---|
| Docker Compose 활용 다중 서비스 로컬 개발 환경 구축 실전 가이드 (0) | 2026.06.27 |
| GitHub Actions 활용 웹 애플리케이션 CI/CD 파이프라인 구축 실전 가이드 (0) | 2026.06.26 |
| Docker Compose Nginx Node.js PostgreSQL 로컬 개발 환경 구축: 실전 가이드 (0) | 2026.06.25 |
| PostgreSQL 성능 최적화: 인덱싱과 쿼리 튜닝 실전 가이드 (0) | 2026.06.22 |