GraphQL API 개발
이전 절에서 Node.js와 Express, 그리고 NestJS 프레임워크를 사용하여 RESTful API를 구축하는 방법을 살펴보았습니다. REST는 웹 서비스 개발의 주류 아키텍처이지만, 특정 사용 시나리오에서는 GraphQL이 더 효율적이고 유연한 대안이 될 수 있습니다.
GraphQL은 Facebook이 개발한 API를 위한 쿼리 언어이자 런타임입니다. 클라이언트가 필요한 데이터를 정확히 명시하여 서버로부터 가져올 수 있도록 하여, 데이터 과다 가져오기(over-fetching)나 부족하게 가져오기(under-fetching) 문제를 해결합니다. 타입스크립트는 GraphQL의 강력한 타입 시스템과 완벽하게 조화를 이루어, 더욱 견고하고 유지보수하기 쉬운 API를 개발할 수 있도록 돕습니다.
GraphQL 소개 및 REST와의 차이점
GraphQL은 기존 REST API의 여러 한계를 극복하기 위해 설계되었습니다.
REST API의 일반적인 문제점
- Over-fetching (과다 가져오기): 클라이언트가 필요한 데이터보다 더 많은 데이터를 서버로부터 받는 경우. 예를 들어, 사용자 이름만 필요한데 전체 사용자 객체를 받는 경우.
- Under-fetching (부족하게 가져오기): 클라이언트가 필요한 모든 데이터를 가져오기 위해 여러 번의 API 요청을 보내야 하는 경우. 예를 들어, 게시물과 해당 게시물의 댓글을 모두 가져오기 위해 두 번의 요청을 보내는 경우.
- 다중 엔드포인트: 리소스마다 고정된 엔드포인트(
GET /users
,GET /products/:id
)가 있어, 클라이언트 요구사항이 복잡해질수록 엔드포인트가 폭증할 수 있습니다. - 버전 관리: API 변경 시 v1, v2와 같이 버전을 관리하는 것이 복잡할 수 있습니다.
GraphQL의 특징 및 장점
- 단일 엔드포인트: 일반적으로
/graphql
과 같은 단일 엔드포인트를 통해 모든 데이터를 쿼리합니다. - 정확한 데이터 요청: 클라이언트가 필요한 필드만 정확하게 명시하여 요청할 수 있습니다. 서버는 요청된 필드만 응답하므로 Over-fetching 문제가 해결됩니다.
- 단일 요청으로 다중 리소스 획득: 여러 리소스에서 필요한 데이터를 단 한 번의 요청으로 가져올 수 있어 Under-fetching 문제가 해결되고 네트워크 오버헤드가 줄어듭니다.
- 강력한 타입 시스템: GraphQL은 스키마 정의 언어(Schema Definition Language, SDL)를 사용하여 API의 데이터 구조와 사용 가능한 쿼리/뮤테이션을 명확하게 정의합니다. 이는 API의 자체 문서화 역할도 합니다.
- 쉬운 버전 관리: 필드를 추가하거나 제거하여 API를 진화시키기 용이하며, 기존 클라이언트에 영향을 주지 않고 새로운 필드를 추가할 수 있습니다.
- 자동 완성 및 유효성 검사: GraphQL 스키마를 기반으로 클라이언트 측에서 쿼리에 대한 자동 완성 기능을 제공하고, 유효성 검사를 수행할 수 있습니다.
GraphQL 핵심 개념
GraphQL API를 개발하려면 다음 핵심 개념들을 이해해야 합니다.
-
스키마 (Schema): GraphQL API의 데이터 구조와 클라이언트가 수행할 수 있는 작업(쿼리, 뮤테이션, 구독)을 정의합니다. 모든 GraphQL 서비스는 스키마를 가집니다.
- 객체 타입 (Object Types): API가 노출하는 데이터 객체의 종류와 필드를 정의합니다.
- 스칼라 타입 (Scalar Types): GraphQL이 기본적으로 제공하는 가장 기본적인 데이터 타입(String, Int, Float, Boolean, ID)입니다. 커스텀 스칼라 타입도 정의할 수 있습니다.
- 쿼리 (Query): 데이터를 조회하는 작업입니다. REST의 GET과 유사합니다.
- 뮤테이션 (Mutation): 데이터를 생성, 수정, 삭제하는 작업입니다. REST의 POST, PUT, DELETE와 유사합니다.
- 구독 (Subscription): 실시간으로 데이터 변경 알림을 받는 작업입니다. 웹소켓을 기반으로 구현됩니다.
-
리졸버 (Resolvers): 스키마에 정의된 각 필드가 어떤 데이터를 반환해야 하는지 실제 로직을 구현하는 함수입니다. 쿼리, 뮤테이션, 구독 요청이 들어오면 해당 필드에 매핑된 리졸버가 실행됩니다.
Node.js와 TypeScript로 GraphQL API 구축
Node.js 환경에서 GraphQL API를 구축하는 데에는 여러 라이브러리가 있지만, Apollo Server와 TypeGraphQL의 조합은 타입스크립트 개발자에게 강력하고 생산적인 경험을 제공합니다.
- Apollo Server: 프로덕션 준비가 된(production-ready) GraphQL 서버 구현체입니다. HTTP 전송, 스키마 검증, 캐싱 등을 처리합니다.
- TypeGraphQL: 타입스크립트의 클래스와 데코레이터를 사용하여 GraphQL 스키마와 리졸버를 자동으로 생성해주는 프레임워크입니다. SDL을 수동으로 작성하는 대신, 타입스크립트 코드로 GraphQL 스키마를 정의할 수 있게 해줍니다.
프로젝트 초기 설정
-
새 프로젝트 초기화
mkdir my-graphql-ts-app cd my-graphql-ts-app npm init -y
-
필수 의존성 설치
npm install express apollo-server-express graphql reflect-metadata npm install --save-dev typescript @types/node @types/express @types/graphql ts-node nodemon npm install --save-dev type-graphql
reflect-metadata
: TypeGraphQL이 데코레이터를 통해 타입 정보를 추출하는 데 필요합니다.tsconfig.json
에 설정이 필요합니다.
-
tsconfig.json
설정:npx tsc --init
으로 생성 후 다음과 같이 수정합니다. 특히emitDecoratorMetadata
와experimentalDecorators
를true
로 설정하는 것이 중요합니다.// tsconfig.json { "compilerOptions": { "target": "es2018", "module": "commonjs", "lib": ["es2018"], "outDir": "./dist", "rootDir": "./src", "strict": true, "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true, "noImplicitAny": true, "emitDecoratorMetadata": true, // 필수: reflect-metadata와 데코레이터 메타데이터 방출 "experimentalDecorators": true, // 필수: 데코레이터 사용 "resolveJsonModule": true }, "include": ["src/**/*.ts"], "exclude": ["node_modules"] }
-
package.json
스크립트 추가// package.json { "name": "my-graphql-ts-app", "version": "1.0.0", "description": "", "main": "dist/index.js", "scripts": { "build": "tsc", "start": "node dist/index.js", "dev": "nodemon --exec ts-node src/index.ts" }, "keywords": [], "author": "", "license": "ISC", "dependencies": { "apollo-server-express": "^3.13.0", "express": "^4.19.2", "graphql": "^16.9.0", "reflect-metadata": "^0.2.2", "type-graphql": "^2.0.0-beta.6" // 또는 최신 안정 버전 }, "devDependencies": { "@types/express": "^4.17.21", "@types/graphql": "^14.5.0", "@types/node": "^20.14.9", "nodemon": "^3.1.4", "ts-node": "^10.9.2", "typescript": "^5.5.3" } }
GraphQL API 구현 예시
1. 타입 정의 (src/schemas/user.ts
)
GraphQL 스키마에 해당하는 타입을 타입스크립트 클래스로 정의합니다. @ObjectType()
, @Field()
데코레이터를 사용합니다.
import { ObjectType, Field, ID } from 'type-graphql';
@ObjectType() // GraphQL 객체 타입으로 선언
export class User {
@Field(() => ID) // GraphQL 필드로 선언, 타입은 ID
id!: number;
@Field() // GraphQL 필드로 선언, 타입은 String (타입스크립트 string에서 추론)
name!: string;
@Field()
email!: string;
// 비공개 필드는 @Field() 데코레이터를 붙이지 않습니다.
password?: string;
}
// 입력 타입 정의 (뮤테이션 등에서 사용)
import { InputType } from 'type-graphql';
@InputType()
export class CreateUserInput {
@Field()
name!: string;
@Field()
email!: string;
}
2. 리졸버 정의 (src/resolvers/user.resolver.ts
)
스키마에 정의된 쿼리와 뮤테이션에 대한 실제 로직을 구현합니다. @Resolver()
, @Query()
, @Mutation()
, @Args()
, @Arg()
데코레이터를 사용합니다.
import { Resolver, Query, Mutation, Arg } from 'type-graphql';
import { User, CreateUserInput } from '../schemas/user';
// 간단한 더미 데이터베이스
const users: User[] = [
{ id: 1, name: 'Alice', email: 'alice@example.com' },
{ id: 2, name: 'Bob', email: 'bob@example.com' },
];
let nextId = 3;
@Resolver(User) // User 타입에 대한 리졸버
export class UserResolver {
@Query(() => [User]) // 모든 사용자 조회 쿼리 (반환 타입: User 배열)
users(): User[] {
return users;
}
@Query(() => User, { nullable: true }) // 특정 사용자 조회 쿼리 (반환 타입: User 또는 null)
user(@Arg('id') id: number): User | undefined {
return users.find((user) => user.id === id);
}
@Mutation(() => User) // 사용자 생성 뮤테이션 (반환 타입: User)
async createUser(@Arg('input') input: CreateUserInput): Promise<User> {
const newUser: User = {
id: nextId++,
name: input.name,
email: input.email,
};
users.push(newUser);
return newUser;
}
}
3. Apollo Server 설정 및 실행 (src/index.ts
)
ApolloServer
를 Express 앱과 통합하여 GraphQL API를 노출합니다.
import 'reflect-metadata'; // TypeGraphQL 사용을 위해 최상단에 임포트
import { ApolloServer } from 'apollo-server-express';
import { buildSchema } from 'type-graphql';
import express from 'express';
import { UserResolver } from './resolvers/user.resolver';
async function bootstrap() {
// TypeGraphQL을 사용하여 스키마 빌드
const schema = await buildSchema({
resolvers: [UserResolver], // 모든 리졸버 등록
emitSchemaFile: true, // 생성된 스키마 파일 (schema.gql) 출력
});
// Apollo Server 인스턴스 생성
const server = new ApolloServer({
schema,
// Playground 활성화 (개발 환경에서 유용)
introspection: true,
playground: true,
});
// Express 앱 생성
const app = express();
// Apollo Server를 Express 앱에 미들웨어로 적용
await server.start(); // Apollo Server 3.x에서 start() 호출 필요
server.applyMiddleware({ app, path: '/graphql' });
const port = 4000;
app.listen(port, () => {
console.log(`Server started on http://localhost:${port}/graphql`);
console.log(`GraphQL Playground available at http://localhost:${port}/graphql`);
});
}
bootstrap().catch(console.error);
GraphQL API 사용해보기
서버를 실행 (npm run dev
) 한 후, 웹 브라우저에서 http://localhost:4000/graphql
로 접속하면 Apollo Studio (또는 GraphQL Playground) 인터페이스를 볼 수 있습니다. 여기서 직접 쿼리와 뮤테이션을 테스트해볼 수 있습니다.
쿼리 예시
query {
users {
id
name
email
}
}
query GetUserById {
user(id: 1) {
id
name
}
}
뮤테이션 예시
mutation CreateNewUser {
createUser(input: { name: "Charlie", email: "charlie@example.com" }) {
id
name
email
}
}
NestJS와 GraphQL 통합
NestJS는 GraphQL 모듈을 기본으로 제공하여 GraphQL API를 구축하는 것을 매우 간편하게 만듭니다. NestJS의 모듈 시스템, 의존성 주입, 데코레이터 등을 GraphQL 스키마 및 리졸버 정의와 완벽하게 통합할 수 있습니다.
NestJS에서 GraphQL을 사용하는 두 가지 주요 방식이 있습니다.
- Code-first (코드 우선): TypeGraphQL과 유사하게, 타입스크립트 클래스와 데코레이터를 사용하여 GraphQL 스키마를 자동으로 생성하는 방식입니다. NestJS의 기본 GraphQL 통합 방식입니다.
- Schema-first (스키마 우선):
.graphql
파일을 사용하여 SDL(Schema Definition Language)로 스키마를 직접 작성하고, NestJS가 이를 기반으로 타입과 리졸버를 생성하는 방식입니다.
대부분의 경우 Code-first 방식이 타입스크립트 개발자에게 더 자연스럽고 생산적입니다.
NestJS GraphQL 통합 예시 (Code-first)
-
NestJS 프로젝트 생성
nest new my-nestjs-graphql-app cd my-nestjs-graphql-app
-
GraphQL 관련 패키지 설치
npm install @nestjs/graphql @nestjs/apollo @apollo/server graphql # 또는 Nest CLI 사용 nest add graphql
-
AppModule에 GraphQLModule 설정 (
src/app.module.ts
)import { Module } from '@nestjs/common'; import { GraphQLModule } from '@nestjs/graphql'; import { ApolloDriver, ApolloDriverConfig } from '@nestjs/apollo'; import { UserResolver } from './user/user.resolver'; // 사용자 리졸버 임포트 @Module({ imports: [ GraphQLModule.forRoot<ApolloDriverConfig>({ driver: ApolloDriver, autoSchemaFile: 'src/schema.gql', // 스키마 파일을 자동으로 생성할 경로 sortSchema: true, // 생성된 스키마 정렬 playground: true, // GraphQL Playground 활성화 (개발용) }), ], providers: [UserResolver], // 리졸버 등록 }) export class AppModule {}
-
사용자 객체 타입 정의 (
src/user/models/user.model.ts
)import { Field, ID, ObjectType } from '@nestjs/graphql'; @ObjectType() export class User { @Field(() => ID) id: number; @Field() name: string; @Field() email: string; }
-
사용자 입력 타입 정의 (
src/user/dto/create-user.input.ts
)import { InputType, Field } from '@nestjs/graphql'; @InputType() export class CreateUserInput { @Field() name: string; @Field() email: string; }
-
사용자 리졸버 정의 (
src/user/user.resolver.ts
)import { Resolver, Query, Mutation, Args } from '@nestjs/graphql'; import { User } from './models/user.model'; import { CreateUserInput } from './dto/create-user.input'; const users: User[] = [ { id: 1, name: 'Alice', email: 'alice@example.com' }, { id: 2, name: 'Bob', email: 'bob@example.com' }, ]; let nextId = 3; @Resolver(() => User) export class UserResolver { @Query(() => [User]) findAllUsers(): User[] { return users; } @Query(() => User, { nullable: true }) findOneUser(@Args('id', { type: () => Number }) id: number): User | undefined { return users.find(user => user.id === id); } @Mutation(() => User) createUser(@Args('createUserInput') createUserInput: CreateUserInput): User { const newUser: User = { id: nextId++, ...createUserInput, }; users.push(newUser); return newUser; } }
NestJS의
@Args()
데코레이터는 GraphQL 인자를 타입 안전하게 가져올 수 있게 하며,type: () => Number
와 같이 GraphQL 타입을 명시할 수 있습니다.
이처럼 NestJS는 GraphQL 개발을 위한 강력한 통합과 구조를 제공하여, 대규모의 복잡한 GraphQL API를 효율적으로 개발할 수 있도록 돕습니다.
GraphQL API 개발의 이점
타입스크립트와 함께 GraphQL API를 개발하는 것은 다음과 같은 주요 이점을 제공합니다.
- 정확한 데이터 명세: 스키마는 API가 제공하는 모든 데이터와 기능을 명확하게 정의하며, 이는 프론트엔드 개발자와의 커뮤니케이션을 간소화하고 오류를 줄입니다.
- 클라이언트 유연성: 클라이언트가 필요한 데이터만 정확히 요청하고 받을 수 있어 네트워크 효율성이 극대화되고, 프론트엔드 변경에 백엔드 수정이 덜 필요합니다.
- 강력한 타입 안전성: 스키마, 리졸버, 입력 타입 등 모든 GraphQL 관련 코드가 타입스크립트의 타입 검사를 받으므로 런타임 오류가 현저히 줄어듭니다.
- 개발자 경험 향상: 자동 완성, 유효성 검사, Playground와 같은 도구들이 개발 생산성을 크게 높여줍니다.
- 풀 스택 타입 공유: 백엔드에서 정의한 GraphQL 스키마를 기반으로 프론트엔드에서 타입스크립트 타입을 자동으로 생성하여, 프론트엔드-백엔드 간의 타입 불일치 문제를 해결할 수 있습니다. (예:
graphql-codegen
)
결론
GraphQL은 현대 웹 애플리케이션의 데이터 요구사항에 맞춰 진화한 강력한 API 패러다임입니다. REST API의 한계를 극복하고 클라이언트와 서버 간의 데이터 교환을 더욱 효율적이고 유연하게 만듭니다. 타입스크립트와 GraphQL의 조합, 특히 TypeGraphQL이나 NestJS의 GraphQL 모듈과 함께 사용하면, 스키마 정의부터 리졸버 구현까지 모든 단계에서 강력한 타입 안전성을 확보하고 개발 생산성을 극대화할 수 있습니다. 복잡한 데이터 요구사항과 다양한 클라이언트를 가진 프로젝트라면 GraphQL API 개발을 적극적으로 고려해볼 가치가 있습니다.