우리는 현재 수많은 데이터가 넘쳐흐르는 세상에서 살고 있습니다. 데이터는 주로 PC나 스마트폰과 같은 기기를 통해 생성되거나 혹은 조회되죠. 갈수록 높아지는 복잡도와 거대해져가는 크기의 데이터를 효율적으로 활용하기 위해 많은 노력과 시도들이 있었고, 이는 아직 끝나지 않은 현재진행형입니다.
일반적으로 데이터를 요청하는 과정은 API라는 클라이언트와 서버와의 규칙을 통해서 이루어집니다. 일반적으로 프로토콜, 경로, 요청 및 응답 형식 등이 API를 구성하는 데에 필요하며 현재는 대부분 HTTP 프로토콜 기반으로 작동하는 REST API를 활용하고 있습니다.
이번 포스트에서는 API를 구축하는 또 다른 방식인 GraphQL에 대해 알아보고 Node.js와 Postgres를 활용하여 실습을 진행해보겠습니다.
GraphQL은 API를 대상으로 데이터를 쿼리하기 위한 쿼리 언어이며, 2012년 Facebook(현재의 Meta)이 개발하였습니다. 당시 웹뷰(webview) 형태의 iOS/Android 모바일 앱을 네이티브로 재구축 하는 과정에서 단순 HTML 형식의 뉴스피드를 API 형태로 제공해야할 필요성이 대두되었고 여러 옵션을 검토하게 되었죠. 당시 검토했던 옵션에 만족하지 못했던 Facebook은 리소스 URL이나 보조키, 테이블 조인 등 기존 데이터 활용에 필요했던 요소에서 벗어나 객체 그래프 및 JSON과 같은 실제 어플리케이션에서 사용되는 모델을 중심으로 사고를 전환한 새로운 방식의 데이터 요청 시스템을 기획 및 개발하게 됩니다.
2015년 Facebook의 공식 블로그에 업로드 된 포스트 에서는 GraphQL을 다음과 같이 정의하고 있습니다.
GraphQL 쿼리는 서버로 전송되어 해석 및 처리되는 문자열이며, 서버는 다시 클라이언트로 JSON을 반환합니다.
GraphQL의 가장 큰 특징은 서버로부터 응답 받을 데이터의 형태를 클라이언트에서 쿼리로 지정할 수 있다는 것입니다. 쿼리 예시를 살펴보겠습니다.
GraphQL 쿼리 요청
query bookDetails {
bookById(id: "book-1") {
id
name
pageCount
author {
id
firstName
lastName
}
}
}
GraphQL 쿼리 응답
{
"data": {
"bookById": {
"id": "book-1",
"name": "Effective Java",
"pageCount": 416,
"author": {
"id": "author-1",
"firstName": "Joshua",
"lastName": "Bloch"
}
}
}
}
작성된 쿼리문의 형식에 맞게 데이터가 조회되며, 명시하지 않은 필드의 데이터는 조회하지 않습니다. 직관적으로 응답받을 데이터의 형태를 구성할 수 있어 클라이언트 측에서 어플리케이션을 설계하고 구현하는 것이 매우 간단해집니다.
GraphQL은 모든 세상이 그래프다(It’s Graphs All the Way Down) 라는 문구를 통해 그래프가 현실 세상을 반영하기에 적합한 데이터 구조라는 것을 시사하고 있습니다.
노드와 간선으로 구성된 자료구조인 그래프는 객체 간의 연결 관계를 표현할 수 있으며 인간의 신경망과도 유사한 특징을 가진다고 알려져 있습니다. GraphQL은 모델링된 데이터 및 관계를 클라이언트에서 손쉽게 활용할 수 있도록 설계되었으며, 스키마에서 객체 타입의 이름을 통해 관계를 설정할 수 있습니다.
REST API가 가지는 불편함이나 한계를 극복하기 위해 등장한 GraphQL은 HTTP 프로토콜 상에서 작동한다는 것과 서버-클라이언트 간의 무상태성(Stateless)을 유지한다는 것은 REST 방식과 동일하지만 데이터를 요청 및 반환하는 형식에서 차이가 있습니다.
GraphQL의 크게 스키마, 쿼리/뮤테이션, 리졸버라는 구성 요소로 이뤄져 있습니다.
스키마(Schema)는 클라이언트에서 얻고자 하는 데이터의 설계를 가리킵니다. 예시는 아래와 같습니다.
type Query {
bookById(id: ID): Book
}
type Book {
id: ID
name: String
pageCount: Int
author: Author
}
type Author {
id: ID
firstName: String
lastName: String
}
Query
, Book
, Author
라는 타입의 데이터가 정의되어 있으며, Book
타입의 데이터를 정의할 때 author: Author
와 같이 다른 타입의 객체를 참조할 수 있습니다.
데이터를 조회하는 쿼리(Query)와 생성/수정/삭제하는 뮤테이션(Mutation)은 스키마에서 다른 객체와 동일한 방식으로 정의되며, 프로그래밍 언어의 맵핑 방식에 따라 메서드로 활용됩니다.
리졸버(Resolver)는 클라이언트의 요청에 맞게 필드 별로 데이터를 처리하는 서버 측 도구입니다. 모든 타입의 모든 필드에 대해서 리졸버를 설정하는 것이 가능하며, 데이터를 반환하는 형식을 지정하고 관계를 형성하고 있는 다른 타입의 객체에서 선택적으로 데이터를 가져올 수 있습니다.
실습은 아래의 Node.js 어플리케이션을 통해 진행됩니다. 저장소를 clone 하거나 fork 해주세요.
클라우드타입에 로그인 후 우측 네비바의 ➕ 버튼을 눌러 새 프로젝트 창을 띄우고 프로젝트 이름과 표시 이름을 입력한 뒤 생성하기 버튼을 누릅니다.
가운데 ➕ 버튼을 누르고 PostgreSQL 템플릿을 선택합니다. 루트 패스워드에 희망하는 값을 입력하고 Database Name에 적절한 값을 작성 후 배포하기를 클릭합니다.
연결 탭에 생성된 주소 중 svc.sel5.cloudtype.app
로 시작되는 주소는 내외부에서 모두 TCP 프로토콜로 통신이 가능한 Public address이며, 외부에서 연결을 하는 경우 프로젝트 설정에서 TCP 접근을 허용해 주어야 합니다. 방법은 여기를 참고해주세요. 프로젝트 내에 배포된 서비스는 같은 네트워크에 위치하므로 PostgreSQL 접속을 위한 주소로 postgresql:5432
를 사용하겠습니다.
저장소 내에서 GraphQL 스키마를 담당하고 있는 graphql/schema.js
파일의 내용은 다음과 같습니다.
const {
GraphQLSchema,
GraphQLObjectType,
GraphQLString,
GraphQLList,
GraphQLNonNull
} = require("graphql");
const pool = require("../config/database");
const UserType = new GraphQLObjectType({
name: "User",
fields: {
id: { type: GraphQLString },
name: { type: GraphQLString },
email: { type: GraphQLString },
},
});
const RootQueryType = new GraphQLObjectType({
name: "Query",
fields: {
users: {
type: GraphQLList(UserType),
resolve: async () => {
const { rows } = await pool.query("SELECT * FROM users");
return rows;
},
},
},
});
const RootMutationType = new GraphQLObjectType({
name: "Mutation",
fields: {
addUser: {
type: UserType,
args: {
name: { type: GraphQLNonNull(GraphQLString) },
email: { type: GraphQLNonNull(GraphQLString) },
},
resolve: async (_, args) => {
const { name, email } = args;
const { rows } = await pool.query(
"INSERT INTO users (name, email) VALUES ($1, $2) RETURNING *",
[name, email]
);
return rows[0];
},
},
updateUser: {
type: UserType,
args: {
id: { type: GraphQLNonNull(GraphQLString) },
name: { type: GraphQLString },
email: { type: GraphQLString },
},
resolve: async (_, args) => {
const { id, name, email } = args;
const { rows } = await pool.query(
"UPDATE users SET name=$1, email=$2 WHERE id=$3 RETURNING *",
[name, email, id]
);
return rows[0];
},
},
deleteUser: {
type: UserType,
args: {
id: { type: GraphQLNonNull(GraphQLString) },
},
resolve: async (_, args) => {
const { id } = args;
const { rows } = await pool.query(
"DELETE FROM users WHERE id=$1 RETURNING *",
[id]
);
return rows[0];
},
},
},
});
module.exports = new GraphQLSchema({
query: RootQueryType,
mutation: RootMutationType,
});
UserType
은 GraphQL로 다뤄지는 User 객체에 대한 각 필드를 정의하고 있습니다.API 서버의 실행을 담당하는 server.js
파일의 내용은 다음과 같습니다.
const express = require("express");
const { graphqlHTTP } = require("express-graphql");
const schema = require("./graphql/schema");
const pool = require("./config/database");
const app = express();
(async () => {
try {
await pool.connect();
const dropUserTableQuery = `DROP TABLE IF EXISTS users;`;
const createUserTableQuery = `
CREATE TABLE IF NOT EXISTS users (
id SERIAL PRIMARY KEY,
name VARCHAR(255),
email VARCHAR(255)
);
`;
await pool.query(dropUserTableQuery);
await pool.query(createUserTableQuery);
console.log("데이터베이스 연결 및 테이블 생성 완료");
app.use(
"/graphql",
graphqlHTTP({
schema: schema,
graphiql: true,
})
);
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => console.log(`Server started on port ${PORT}`));
} catch (error) {
console.error("Error starting server:", error);
}
})();
app.use()
함수의 첫 번째 인자는 API 접속 경로, 두 번째 인자는 GraphQL 스키마 및 GUI 활성화를 설정합니다.클라우드타입의 프로젝트 페이지에서 ➕ 버튼을 누르고 Node.js를 선택한 후, 미리 fork 해놓은 nodejs-graphql 를 선택합니다. 기타 설정은 아래를 참고하여 입력한 후 배포하기 버튼을 클릭합니다.
버전: v18
환경변수(Environment Variables)
NODE_ENV
: productionPOSTGRES_USER
: rootPOSTGRES_PASSWORD
: 열쇠 아이콘 => postgresql-root-passwordPOSTGRES_HOST
: postgresqlPOSTGRES_PORT
: 5432POSTGRES_DATABASE
: PostgrSQL 생성 시 입력했던 Database Namehttps://
로 시작되는 URL을 확인합니다. 이 URL에 /graphql
경로를 붙인 것이 프론트엔드에서 API를 호출하는 주소로 사용됩니다.배포된 서비스의 URL에 /graphql
을 붙여 브라우저에서 접속합니다.
다음의 graphql 쿼리를 입력하고 실행 버튼을 눌러 User 객체를 신규 생성합니다.
mutation {
addUser(name: "admin", email: "admin@example.com") {
id
name
email
}
}
이어서 다음 쿼리를 입력하고 실행 버튼을 눌러 새로운 객체를 생성합니다.
mutation {
addUser(name: "staff", email: "staff@example.com") {
id
name
email
}
}
다음 쿼리를 입력하고 실행하여 전체 User 객체를 조회합니다.
query {
users {
id
name
email
}
}
쿼리의 id
필드에 주석을 설정하고 다시 실행합니다. 객체의 모든 필드를 가져오지 않고 필요한 필드에 대해서 선택적으로 fetch가 가능합니다.
query {
users {
# id
name
email
}
}
다음의 쿼리를 실행하여 객체의 정보가 수정되는지 확인합니다.
mutation {
updateUser(id: "2", name: "guest", email: "guest@example.com") {
id
name
email
}
}
다음의 쿼리를 실행하여 객체가 삭제되는지 확인합니다.
mutation {
deleteUser(id: "2") {
id
name
email
}
}
Postman 실행 후 New 버튼을 누르고 GraphQL을 클릭합니다.
클라우드타입에서 배포한 GraphQL 서버의 URL에 /graphql
경로를 붙여서 주소창에 넣으면 자동으로 스키마를 인식하여 Postman에 정보가 표시됩니다.
users
의 name
, email
을 선택하고 Query 버튼을 누르면 GUI에서 쿼리를 직접 실행했던 것과 같은 결과를 확인할 수 있습니다. 기타 Query 및 Mutation도 같은 방법으로 테스트 할 수 있습니다.