GraphQL API 개발
GraphQL은 API를 위한 쿼리 언어로, 클라이언트가 필요한 데이터를 정확히 요청할 수 있게 해주는 강력한 도구입니다.
TypeScript와 결합하면 타입 안전성과 개발자 경험을 크게 향상시킬 수 있습니다.
GraphQL vs RESTful API
GraphQL은 단일 엔드포인트를 통해 필요한 데이터만 정확히 요청할 수 있어 오버페칭과 언더페칭 문제를 해결합니다.
RESTful API와 달리, 클라이언트가 데이터 구조를 결정합니다.
TypeScript와 GraphQL의 시너지
TypeScript의 정적 타입 시스템은 GraphQL 스키마와 자연스럽게 조화를 이룹니다.
이를 통해 런타임 이전에 많은 오류를 잡아낼 수 있으며, 자동완성과 타입 추론을 통해 개발 생산성을 높일 수 있습니다.
Apollo Server 설정
- 필요한 패키지 설치
npm install apollo-server graphql reflect-metadata type-graphql
- Apollo Server 설정
import { ApolloServer } from 'apollo-server';
import { buildSchema } from 'type-graphql';
import { UserResolver } from './resolvers/UserResolver';
async function bootstrap() {
const schema = await buildSchema({
resolvers: [UserResolver],
emitSchemaFile: true,
});
const server = new ApolloServer({
schema,
context: ({ req }) => ({ req })
});
const { url } = await server.listen(4000);
console.log(`Server is running, GraphQL Playground available at ${url}`);
}
bootstrap();
GraphQL 스키마 정의
Type-GraphQL을 사용한 코드 퍼스트 접근 방식
import { ObjectType, Field, ID } from 'type-graphql';
@ObjectType()
class User {
@Field(() => ID)
id: string;
@Field()
name: string;
@Field()
email: string;
}
이 접근 방식의 이점은 TypeScript 클래스와 GraphQL 스키마를 동시에 정의할 수 있어 코드 중복을 줄이고 타입 안전성을 보장한다는 것입니다.
리졸버 구현
타입 안전한 리졸버 함수
import { Resolver, Query, Mutation, Arg } from 'type-graphql';
import { User } from '../entities/User';
@Resolver(User)
class UserResolver {
@Query(() => [User])
async users(): Promise<User[]> {
// 데이터베이스에서 사용자 목록을 가져오는 로직
}
@Mutation(() => User)
async createUser(
@Arg('name') name: string,
@Arg('email') email: string
): Promise<User> {
// 새 사용자를 생성하는 로직
}
}
컨텍스트 객체 타입 정의
import { Request } from 'express';
interface Context {
req: Request;
user?: {
id: string;
roles: string[];
};
}
const server = new ApolloServer({
schema,
context: ({ req }): Context => ({ req })
});
쿼리, 뮤테이션, 서브스크립션 구현
- 쿼리
@Query(() => User, { nullable: true })
async user(@Arg('id') id: string): Promise<User | null> {
// 사용자를 찾는 로직
}
- 뮤테이션
@Mutation(() => Boolean)
async deleteUser(@Arg('id') id: string): Promise<boolean> {
// 사용자를 삭제하는 로직
}
- 서브스크립션
@Subscription(() => User, {
topics: 'NEW_USER'
})
newUser(@Root() user: User): User {
return user;
}
DataLoader를 사용한 N+1 문제 해결
import DataLoader from 'dataloader';
const createUserLoader = () =>
new DataLoader<string, User>(async (userIds) => {
const users = await User.findByIds(userIds);
const userMap = new Map(users.map(u => [u.id, u]));
return userIds.map(id => userMap.get(id));
});
// Context에 DataLoader 추가
interface Context {
userLoader: ReturnType<typeof createUserLoader>;
}
const server = new ApolloServer({
schema,
context: (): Context => ({
userLoader: createUserLoader()
})
});
// 리졸버에서 사용
@FieldResolver(() => User)
async author(@Root() book: Book, @Ctx() ctx: Context): Promise<User> {
return ctx.userLoader.load(book.authorId);
}
스키마 지시어(Directive) 구현
import { SchemaDirectiveVisitor } from 'apollo-server';
import { defaultFieldResolver, GraphQLField } from 'graphql';
class UppercaseDirective extends SchemaDirectiveVisitor {
visitFieldDefinition(field: GraphQLField<any, any>) {
const { resolve = defaultFieldResolver } = field;
field.resolve = async function(...args) {
const result = await resolve.apply(this, args);
if (typeof result === 'string') {
return result.toUpperCase();
}
return result;
};
}
}
// 스키마에 지시어 추가
const schema = makeExecutableSchema({
typeDefs,
resolvers,
schemaDirectives: {
upper: UppercaseDirective
}
});
인증과 권한 부여
import { createMethodDecorator } from 'type-graphql';
import { AuthenticationError } from 'apollo-server';
function Authorized(roles: string[] = []) {
return createMethodDecorator(async ({ context }, next) => {
if (!context.user) {
throw new AuthenticationError('Not authenticated');
}
if (roles.length > 0 && !roles.some(role => context.user.roles.includes(role))) {
throw new AuthenticationError('Not authorized');
}
return next();
});
}
// 리졸버에서 사용
@Query(() => [User])
@Authorized(['ADMIN'])
async users(): Promise<User[]> {
// 관리자만 접근 가능한 로직
}
GraphQL API 테스팅
import { gql } from 'apollo-server';
import { createTestClient } from 'apollo-server-testing';
import { server } from '../server';
const GET_USER = gql`
query GetUser($id: ID!) {
user(id: $id) {
id
name
email
}
}
`;
describe('User Query', () => {
it('should return a user', async () => {
const { query } = createTestClient(server);
const res = await query({ query: GET_USER, variables: { id: '1' } });
expect(res.data.user).toBeDefined();
expect(res.data.user.name).toBe('John Doe');
});
});
Best Practices와 구조화 전략
- 스키마 우선 설계 : API 설계를 스키마부터 시작하여 일관성을 유지합니다.
- 모듈화 : 기능별로 리졸버와 타입을 모듈화하여 관리합니다.
- 입력 유효성 검사 : class-validator를 활용하여 입력 데이터를 검증합니다.
- 에러 처리 : 일관된 에러 형식을 정의하고 사용합니다.
- 페이지네이션 : Relay 스타일의 커서 기반 페이지네이션을 구현합니다.
- 캐싱 전략 : Apollo Server의 캐싱 기능을 활용합니다.
- 성능 모니터링 : Apollo Studio를 사용하여 쿼리 성능을 모니터링합니다.
- 코드 생성 : GraphQL Code Generator를 사용하여 타입과 리졸버 시그니처를 자동 생성합니다.
- 버전 관리 : 스키마 변경을 신중히 관리하고 하위 호환성을 유지합니다.
- 문서화 : GraphQL 스키마 주석을 활용하여 자동으로 문서를 생성합니다.
GraphQL과 TypeScript를 함께 사용하면 타입 안전성, 개발자 경험, 그리고 API의 유연성을 크게 향상시킬 수 있습니다.
Apollo Server와 Type-GraphQL 같은 도구를 활용하면 강력하고 확장 가능한 API를 구축할 수 있습니다.
하지만 GraphQL API 개발 시 항상 보안을 최우선으로 고려해야 합니다.
쿼리 복잡도 제한, 적절한 인증 및 권한 부여 메커니즘 구현, 그리고 입력 데이터의 철저한 검증이 중요합니다.