GraphQL과 Koa 프레임워크 통합하기

Koa 서버에서 GraphQL을 활용하려면 다음 단계별로 진행하면 된다.

1. 필수 패키지 설치

먼저 필요한 패키지들을 설치한다. graphql 라이브러리와 Koa 미들웨어가 필요하다.

npm install graphql koa-graphql koa-mount --save

2. 프로젝트 구조

전체 구조는 다음과 같이 구성한다:

├── app.js           # 메인 서버 파일
├── schema/
│   └── query.js     # GraphQL 스키마 정의
└── model/
    └── database.js  # MongoDB 유틸리티

3. 메인 서버 파일 구현

Koa 인스턴스를 생성하고 GraphQL 미들웨어를 설정한다.

// app.js
const Koa = require('koa');
const router = require('koa-router')();

const mount = require('koa-mount');
const graphqlHTTP = require('koa-graphql');

const GraphQLQuerySchema = require('./schema/query.js');

const app = new Koa();

// GraphQL 엔드포인트 설정
app.use(mount('/api/graphql', graphqlHTTP({
    schema: GraphQLQuerySchema,
    graphiql: true
})));

app.use(router.routes());
app.use(router.allowedMethods());
app.listen(5000);

console.log('GraphQL 서버가 http://localhost:5000/api/graphql 에서 실행 중');

4. GraphQL 스키마 정의

스키마 파일에서는 쿼리 타입과 리졸버 함수를 정의한다.

// schema/query.js
const Database = require('../model/database.js');

const {
    GraphQLObjectType,
    GraphQLString,
    GraphQLInt,
    GraphQLList,
    GraphQLSchema
} = require('graphql');

// 상품 스키마 타입 정의
const ProductType = new GraphQLObjectType({
    name: 'Product',
    fields: {
        id: {
            type: GraphQLString,
            resolve: (parent) => parent._id.toString()
        },
        name: {
            type: GraphQLString
        },
        price: {
            type: GraphQLInt
        },
        description: {
            type: GraphQLString
        },
        createdAt: {
            type: GraphQLString,
            resolve: (parent) => new Date(parent.add_time).toISOString()
        }
    }
});

// 루트 리졸버 정의
const RootResolver = new GraphQLObjectType({
    name: 'QueryRoot',
    fields: {
        // 전체 상품 목록 조회
        products: {
            type: GraphQLList(ProductType),
            async resolve() {
                const products = await Database.find('products', {});
                return products;
            }
        },
        // 단일 상품 조회
        product: {
            type: ProductType,
            args: {
                id: {
                    type: GraphQLString,
                    description: '상품 ID'
                }
            },
            async resolve(parent, args) {
                const [product] = await Database.find('products', {
                    _id: Database.createObjectId(args.id)
                });
                return product;
            }
        },
        // 카테고리별 상품 조회
        productsByCategory: {
            type: GraphQLList(ProductType),
            args: {
                category: {
                    type: GraphQLString
                }
            },
            async resolve(parent, args) {
                return await Database.find('products', {
                    category: args.category
                });
            }
        }
    }
});

// 스키마 생성 및エクスポート
module.exports = new GraphQLSchema({
    query: RootResolver
});

5. 데이터베이스 유틸리티 구현

MongoDB 연동을 위한 헬퍼 클래스를 생성한다. Singleton 패턴을 사용하여 데이터베이스 연결을 관리한다.

// model/database.js
const MongoDB = require('mongodb');
const MongoClient = MongoDB.MongoClient;
const ObjectID = MongoDB.ObjectID;

const Config = {
    dbUrl: 'mongodb://127.0.0.1:27017/',
    dbName: 'shop'
};

class Database {
    static getInstance() {
        if (!Database.instance) {
            Database.instance = new Database();
        }
        return Database.instance;
    }

    constructor() {
        this.dbClient = null;
        this.connect();
    }

    connect() {
        const self = this;
        return new Promise((resolve, reject) => {
            if (!self.dbClient) {
                MongoClient.connect(Config.dbUrl, { useNewUrlParser: true }, (err, client) => {
                    if (err) {
                        reject(err);
                        return;
                    }
                    self.dbClient = client.db(Config.dbName);
                    resolve(self.dbClient);
                });
            } else {
                resolve(self.dbClient);
            }
        });
    }

    find(collectionName, query, options = {}) {
        const { fields = {}, page = 1, pageSize = 100, sort = {} } = options;
        
        return new Promise((resolve, reject) => {
            this.connect().then((db) => {
                const cursor = db.collection(collectionName)
                    .find(query, { projection: fields })
                    .skip((page - 1) * pageSize)
                    .limit(pageSize)
                    .sort(sort);
                
                cursor.toArray((err, docs) => {
                    if (err) {
                        reject(err);
                        return;
                    }
                    resolve(docs);
                });
            });
        });
    }

    insert(collectionName, data) {
        return new Promise((resolve, reject) => {
            this.connect().then((db) => {
                db.collection(collectionName).insertOne(data, (err, result) => {
                    if (err) {
                        reject(err);
                        return;
                    }
                    resolve(result);
                });
            });
        });
    }

    update(collectionName, query, data) {
        return new Promise((resolve, reject) => {
            this.connect().then((db) => {
                db.collection(collectionName).updateOne(query, {
                    $set: data
                }, (err, result) => {
                    if (err) {
                        reject(err);
                        return;
                    }
                    resolve(result);
                });
            });
        });
    }

    delete(collectionName, query) {
        return new Promise((resolve, reject) => {
            this.connect().then((db) => {
                db.collection(collectionName).deleteOne(query, (err, result) => {
                    if (err) {
                        reject(err);
                        return;
                    }
                    resolve(result);
                });
            });
        });
    }

    createObjectId(id) {
        return new ObjectID(id);
    }

    count(collectionName, query) {
        return new Promise((resolve, reject) => {
            this.connect().then((db) => {
                db.collection(collectionName).countDocuments(query, (err, count) => {
                    if (err) {
                        reject(err);
                        return;
                    }
                    resolve(count);
                });
            });
        });
    }
}

module.exports = Database.getInstance();

6. GraphiQL 인터페이스 확인

서버를 실행한 후 브라우저에서 http://localhost:5000/api/graphql 에 접근하면 GraphiQL 쿼리 인터페이스를 사용할 수 있다. 다음과 같은 쿼리로 데이터를 테스트할 수 있다:

{
  products {
    id
    name
    price
  }
}

핵심 포인트 정리

  • koa-mount: 특정 경로에 미들웨어를 마운트하는 데 사용
  • koa-graphql: Koa에서 GraphQL 핸들러를 실행
  • GraphQLObjectType: 스키마의 타입을 정의
  • 리졸버: 실제 데이터 조회 로직을 구현
  • Singleton 패턴: 데이터베이스 연결을 효율적으로 관리

이제 Koa와 GraphQL이 통합된 API 서버를 구축할 수 있다. 더 복잡한 쿼리나 뮤테이션이 필요하다면 같은 구조에서 타입과 리졸버만 확장하면 된다.

태그: GraphQL koa Node.js MongoDB JavaScript

6월 16일 01:52에 게시됨