들어가며: 왜 GraphQL인가?
2015년 Facebook이 오픈소스로 공개한 GraphQL은 이제 API 설계의 표준 선택지가 되었습니다. GitHub, Shopify, Netflix, Airbnb 등 주요 테크 기업들이 채택하며, REST의 한계를 넘어서는 새로운 패러다임을 보여주고 있습니다.
이 글에서는 GraphQL의 기초부터 Federation을 활용한 마이크로서비스 통합, 성능 최적화까지 실무에서 바로 활용할 수 있는 내용을 다룹니다.
1. REST vs GraphQL: 근본적인 차이
REST의 한계
REST API는 오랫동안 웹 API의 표준이었지만, 몇 가지 고질적인 문제가 있습니다.
**Over-fetching**: 필요하지 않은 데이터까지 응답에 포함됩니다. 사용자 이름만 필요한데 전체 프로필 정보가 옵니다.
**Under-fetching**: 하나의 화면을 그리기 위해 여러 엔드포인트를 호출해야 합니다. 사용자 정보 + 게시글 + 댓글 = 3번의 요청.
**엔드포인트 관리**: 클라이언트 요구사항이 변할 때마다 새 엔드포인트를 만들거나 기존 것을 수정해야 합니다.
GraphQL의 해결책
한 번의 요청으로 필요한 데이터만 정확히 가져옵니다
query GetUserWithPosts {
user(id: "123") {
name
posts(first: 5) {
title
commentCount
}
}
}
| 구분 | REST | GraphQL |
|------|------|---------|
| 엔드포인트 | 리소스마다 별도 URL | 단일 엔드포인트 |
| 데이터 양 | 서버가 결정 | 클라이언트가 결정 |
| 버전 관리 | v1, v2 URL 분기 | 스키마 진화(deprecation) |
| 타입 시스템 | Swagger/OpenAPI로 보강 | 내장 타입 시스템(SDL) |
| 실시간 | 별도 WebSocket 구현 | Subscription 내장 |
| 캐싱 | HTTP 캐시 활용 용이 | 별도 전략 필요 |
| 학습 곡선 | 낮음 | 중간~높음 |
핵심 차이: 선언적 데이터 요청
REST는 **어디서(WHERE)** 데이터를 가져올지 지정하고, GraphQL은 **무엇을(WHAT)** 가져올지 선언합니다.
2. 스키마 설계 (Schema Definition Language)
기본 타입 시스템
GraphQL의 스키마는 API의 계약서입니다. SDL을 사용하여 타입을 정의합니다.
스칼라 타입: String, Int, Float, Boolean, ID
! 는 Non-null을 의미합니다
type User {
id: ID!
name: String!
email: String!
age: Int
posts: [Post!]!
createdAt: DateTime!
}
type Post {
id: ID!
title: String!
content: String!
author: User!
comments: [Comment!]!
tags: [String!]
publishedAt: DateTime
}
type Comment {
id: ID!
body: String!
author: User!
post: Post!
createdAt: DateTime!
}
Query, Mutation, Subscription
GraphQL에는 세 가지 작업 타입이 있습니다.
type Query {
데이터 조회
user(id: ID!): User
users(filter: UserFilter, pagination: PaginationInput): UserConnection!
post(id: ID!): Post
searchPosts(query: String!): [Post!]!
}
type Mutation {
데이터 변경
createUser(input: CreateUserInput!): User!
updateUser(id: ID!, input: UpdateUserInput!): User!
deleteUser(id: ID!): Boolean!
createPost(input: CreatePostInput!): Post!
likePost(postId: ID!): Post!
}
type Subscription {
실시간 업데이트
postCreated: Post!
commentAdded(postId: ID!): Comment!
userStatusChanged(userId: ID!): User!
}
Input 타입과 Enum
input CreateUserInput {
name: String!
email: String!
age: Int
role: UserRole = USER
}
input UserFilter {
role: UserRole
nameContains: String
createdAfter: DateTime
}
input PaginationInput {
first: Int = 20
after: String
}
enum UserRole {
ADMIN
EDITOR
USER
GUEST
}
enum PostStatus {
DRAFT
PUBLISHED
ARCHIVED
}
Relay 스타일 커넥션 (Cursor-based Pagination)
type UserConnection {
edges: [UserEdge!]!
pageInfo: PageInfo!
totalCount: Int!
}
type UserEdge {
node: User!
cursor: String!
}
type PageInfo {
hasNextPage: Boolean!
hasPreviousPage: Boolean!
startCursor: String
endCursor: String
}
3. Resolver 패턴
기본 Resolver 구조
Resolver는 스키마의 각 필드를 실제 데이터와 연결합니다.
// resolver의 4가지 인자: parent, args, context, info
const resolvers = {
Query: {
user: async (parent, args, context, info) => {
const { id } = args;
const { dataSources, currentUser } = context;
return dataSources.userAPI.getUser(id);
},
users: async (_, { filter, pagination }, { dataSources }) => {
return dataSources.userAPI.getUsers(filter, pagination);
},
searchPosts: async (_, { query }, { dataSources }) => {
return dataSources.postAPI.search(query);
},
},
Mutation: {
createUser: async (_, { input }, { dataSources, currentUser }) => {
if (!currentUser?.isAdmin) {
throw new ForbiddenError('Admin access required');
}
return dataSources.userAPI.createUser(input);
},
likePost: async (_, { postId }, { dataSources, currentUser }) => {
if (!currentUser) {
throw new AuthenticationError('Login required');
}
return dataSources.postAPI.likePost(postId, currentUser.id);
},
},
// 타입 resolver - 관계 필드 해결
User: {
posts: async (user, _, { dataSources }) => {
return dataSources.postAPI.getPostsByAuthor(user.id);
},
},
Post: {
author: async (post, _, { dataSources }) => {
return dataSources.userAPI.getUser(post.authorId);
},
comments: async (post, _, { dataSources }) => {
return dataSources.commentAPI.getByPost(post.id);
},
},
};
Context 설정
const server = new ApolloServer({
typeDefs,
resolvers,
});
await server.start();
app.use(
'/graphql',
expressMiddleware(server, {
context: async ({ req }) => {
const token = req.headers.authorization || '';
const currentUser = await getUserFromToken(token);
return {
currentUser,
dataSources: {
userAPI: new UserDataSource(),
postAPI: new PostDataSource(),
commentAPI: new CommentDataSource(),
},
};
},
})
);
4. N+1 문제와 DataLoader
N+1 문제란?
게시글 목록을 가져온 뒤, 각 게시글의 작성자를 개별 쿼리로 가져오면 N+1 문제가 발생합니다.
Query: posts(first: 10) → 1번 쿼리 (게시글 10개)
Post.author (post 1) → 2번 쿼리
Post.author (post 2) → 3번 쿼리
...
Post.author (post 10) → 11번 쿼리
총 11번의 DB 쿼리가 실행됩니다.
DataLoader로 해결
DataLoader는 같은 이벤트 루프(tick) 내의 요청을 모아 배치 처리합니다.
// 배치 함수: ID 배열을 받아 같은 순서로 결과 반환
const userLoader = new DataLoader(async (userIds: string[]) => {
const users = await db.users.findMany({
where: { id: { in: userIds } },
});
// 요청된 ID 순서대로 결과를 매핑
const userMap = new Map(users.map((u) => [u.id, u]));
return userIds.map((id) => userMap.get(id) || null);
});
// Resolver에서 사용
const resolvers = {
Post: {
author: (post, _, { loaders }) => {
return loaders.userLoader.load(post.authorId);
},
},
};
**결과**: 11번의 쿼리가 2번으로 줄어듭니다.
Query: posts(first: 10) → 1번 쿼리
DataLoader batch: users([1,2,3,...,10]) → 2번 쿼리 (배치)
DataLoader 컨텍스트 패턴
// 요청마다 새로운 DataLoader 인스턴스를 생성합니다
function createLoaders() {
return {
userLoader: new DataLoader((ids) => batchGetUsers(ids)),
postLoader: new DataLoader((ids) => batchGetPosts(ids)),
commentLoader: new DataLoader((ids) => batchGetComments(ids)),
// 1:N 관계용 loader
postsByAuthorLoader: new DataLoader(async (authorIds) => {
const posts = await db.posts.findMany({
where: { authorId: { in: authorIds } },
});
const grouped = groupBy(posts, 'authorId');
return authorIds.map((id) => grouped[id] || []);
}),
};
}
// context에서 매 요청마다 새로 생성
context: async ({ req }) => ({
currentUser: await getUser(req),
loaders: createLoaders(),
}),
5. Apollo Server와 Client
Apollo Server 설정
const schema = makeExecutableSchema({
typeDefs,
resolvers,
});
const server = new ApolloServer({
schema,
plugins: [
// 인트로스펙션 제한 (프로덕션)
ApolloServerPluginLandingPageDisabled(),
// 사용량 리포팅
ApolloServerPluginUsageReporting(),
],
formatError: (formattedError, error) => {
// 에러 포맷팅 - 내부 정보 숨기기
if (formattedError.extensions?.code === 'INTERNAL_SERVER_ERROR') {
return { message: 'Internal server error' };
}
return formattedError;
},
});
const { url } = await startStandaloneServer(server, {
listen: { port: 4000 },
context: async ({ req }) => ({
currentUser: await authenticateUser(req),
loaders: createLoaders(),
}),
});
console.log(`Server ready at ${url}`);
Apollo Client (React)
ApolloClient,
InMemoryCache,
ApolloProvider,
useQuery,
useMutation,
gql,
} from '@apollo/client';
const client = new ApolloClient({
uri: 'https://api.example.com/graphql',
cache: new InMemoryCache({
typePolicies: {
Query: {
fields: {
posts: {
// Relay 스타일 커서 페이지네이션 머지
keyArgs: ['filter'],
merge(existing, incoming, { args }) {
if (!args?.after) return incoming;
return {
...incoming,
edges: [...(existing?.edges || []), ...incoming.edges],
};
},
},
},
},
},
}),
defaultOptions: {
watchQuery: {
fetchPolicy: 'cache-and-network',
},
},
});
// React 컴포넌트에서 사용
const GET_POSTS = gql`
query GetPosts($first: Int!, $after: String) {
posts(first: $first, after: $after) {
edges {
node {
id
title
author {
name
}
}
}
pageInfo {
hasNextPage
endCursor
}
}
}
`;
function PostList() {
const { data, loading, error, fetchMore } = useQuery(GET_POSTS, {
variables: { first: 10 },
});
if (loading) return <p>Loading...</p>;
if (error) return <p>Error occurred</p>;
return (
{data.posts.edges.map(({ node }) => (
))}
{data.posts.pageInfo.hasNextPage && (
onClick={() =>
fetchMore({
variables: { after: data.posts.pageInfo.endCursor },
})
}
>
Load More
)}
);
}
6. Apollo Federation v2
Federation이란?
Apollo Federation은 여러 개의 GraphQL 서비스(Subgraph)를 하나의 통합 API(Supergraph)로 합칩니다. 각 팀이 독립적으로 서비스를 개발하면서도 클라이언트에게는 하나의 GraphQL 엔드포인트를 제공합니다.
Subgraph 설계
--- Users Subgraph ---
type User @key(fields: "id") {
id: ID!
name: String!
email: String!
role: UserRole!
}
type Query {
user(id: ID!): User
me: User
}
--- Posts Subgraph ---
type Post @key(fields: "id") {
id: ID!
title: String!
content: String!
author: User!
publishedAt: DateTime
}
다른 subgraph의 User 타입 확장
type User @key(fields: "id") {
id: ID!
posts: [Post!]! # Posts 서비스에서 추가하는 필드
postCount: Int!
}
type Query {
post(id: ID!): Post
feed(first: Int = 20, after: String): PostConnection!
}
--- Reviews Subgraph ---
type Review @key(fields: "id") {
id: ID!
rating: Int!
comment: String
author: User!
post: Post!
}
type Post @key(fields: "id") {
id: ID!
reviews: [Review!]! # Reviews 서비스에서 추가
averageRating: Float
}
Router 설정 (Apollo Router)
router.yaml
supergraph:
listen: 0.0.0.0:4000
subgraphs:
users:
routing_url: http://users-service:4001/graphql
posts:
routing_url: http://posts-service:4002/graphql
reviews:
routing_url: http://reviews-service:4003/graphql
headers:
all:
request:
- propagate:
named: authorization
Subgraph의 Reference Resolver
// Posts Subgraph
const resolvers = {
User: {
// Federation이 User를 resolve할 때 호출
__resolveReference: async (user, { loaders }) => {
// user.id만 전달받음
return loaders.postsByUserLoader.load(user.id);
},
posts: async (user, _, { dataSources }) => {
return dataSources.postAPI.getByAuthor(user.id);
},
postCount: async (user, _, { dataSources }) => {
return dataSources.postAPI.countByAuthor(user.id);
},
},
};
7. Subscription (실시간 기능)
서버 설정
const pubsub = new PubSub();
const httpServer = createServer(app);
const wsServer = new WebSocketServer({
server: httpServer,
path: '/graphql',
});
useServer(
{
schema,
context: async (ctx) => {
const token = ctx.connectionParams?.authorization;
const user = await authenticateUser(token);
return { currentUser: user, pubsub };
},
},
wsServer
);
// Resolver
const resolvers = {
Subscription: {
commentAdded: {
subscribe: (_, { postId }) => {
return pubsub.asyncIterator(`COMMENT_ADDED_${postId}`);
},
},
postCreated: {
subscribe: () => pubsub.asyncIterator('POST_CREATED'),
},
},
Mutation: {
createComment: async (_, { input }, { pubsub, currentUser }) => {
const comment = await db.comments.create({
data: { ...input, authorId: currentUser.id },
});
// 구독자에게 알림
pubsub.publish(`COMMENT_ADDED_${input.postId}`, {
commentAdded: comment,
});
return comment;
},
},
};
클라이언트 Subscription
const COMMENT_SUBSCRIPTION = gql`
subscription OnCommentAdded($postId: ID!) {
commentAdded(postId: $postId) {
id
body
author {
name
}
createdAt
}
}
`;
function CommentFeed({ postId }) {
const { data, loading } = useSubscription(COMMENT_SUBSCRIPTION, {
variables: { postId },
});
// 새 댓글이 올 때마다 자동으로 data가 업데이트됩니다
if (data?.commentAdded) {
return <NewComment comment={data.commentAdded} />;
}
return null;
}
8. 인증(Authentication)과 인가(Authorization)
Directive 기반 인가
커스텀 directive 정의
directive @auth(requires: Role = USER) on FIELD_DEFINITION | OBJECT
enum Role {
ADMIN
EDITOR
USER
GUEST
}
type Query {
publicPosts: [Post!]!
me: User! @auth
adminDashboard: Dashboard! @auth(requires: ADMIN)
userManagement: [User!]! @auth(requires: ADMIN)
}
type Mutation {
createPost(input: CreatePostInput!): Post! @auth
deletePost(id: ID!): Boolean! @auth(requires: ADMIN)
updateProfile(input: UpdateProfileInput!): User! @auth
}
Directive 구현
function authDirectiveTransformer(schema) {
return mapSchema(schema, {
[MapperKind.OBJECT_FIELD]: (fieldConfig) => {
const authDirective = getDirective(schema, fieldConfig, 'auth')?.[0];
if (authDirective) {
const requiredRole = authDirective.requires || 'USER';
const originalResolve = fieldConfig.resolve || defaultFieldResolver;
fieldConfig.resolve = async (source, args, context, info) => {
const { currentUser } = context;
if (!currentUser) {
throw new AuthenticationError('Not authenticated');
}
if (!hasRole(currentUser, requiredRole)) {
throw new ForbiddenError('Insufficient permissions');
}
return originalResolve(source, args, context, info);
};
}
return fieldConfig;
},
});
}
9. 성능 최적화
Query Complexity 제한
const server = new ApolloServer({
schema,
validationRules: [
createComplexityLimitRule(1000, {
scalarCost: 1,
objectCost: 2,
listFactor: 10,
formatErrorMessage: (cost) =>
`Query too complex: cost ${cost} exceeds maximum 1000`,
}),
],
});
Depth Limiting
const server = new ApolloServer({
schema,
validationRules: [depthLimit(7)],
});
Persisted Queries (APQ)
// 서버
const server = new ApolloServer({
schema,
plugins: [ApolloServerPluginCacheControl({ defaultMaxAge: 60 })],
persistedQueries: {
cache: new KeyValueCache(), // Redis 등
},
});
// 클라이언트
const link = createPersistedQueryLink({ sha256 });
const client = new ApolloClient({
link: link.concat(httpLink),
cache: new InMemoryCache(),
});
캐시 제어
type Query {
posts: [Post!]! @cacheControl(maxAge: 300)
user(id: ID!): User @cacheControl(maxAge: 60)
me: User @cacheControl(maxAge: 0, scope: PRIVATE)
}
type Post @cacheControl(maxAge: 600) {
id: ID!
title: String!
author: User! @cacheControl(inheritMaxAge: true)
viewCount: Int! @cacheControl(maxAge: 30)
}
10. 캐싱 전략
서버 사이드 캐싱
const server = new ApolloServer({
schema,
cache: new KeyvAdapter(new Keyv({ store: new KeyvRedis('redis://localhost:6379') })),
});
// DataSource 레벨 캐싱
class CachedUserAPI {
private cache: RedisClient;
private ttl = 300; // 5분
async getUser(id: string): Promise<User> {
const cacheKey = `user:${id}`;
const cached = await this.cache.get(cacheKey);
if (cached) return JSON.parse(cached);
const user = await db.users.findUnique({ where: { id } });
await this.cache.set(cacheKey, JSON.stringify(user), 'EX', this.ttl);
return user;
}
}
클라이언트 사이드 캐싱 (Apollo Cache)
const cache = new InMemoryCache({
typePolicies: {
User: {
keyFields: ['id'],
fields: {
fullName: {
read(_, { readField }) {
const first = readField('firstName');
const last = readField('lastName');
return `${first} ${last}`;
},
},
},
},
Post: {
keyFields: ['id'],
},
Query: {
fields: {
post(_, { args, toReference }) {
return toReference({ __typename: 'Post', id: args?.id });
},
},
},
},
});
11. 테스팅
Resolver 단위 테스트
describe('User Resolvers', () => {
it('should return user by id', async () => {
const mockUser = { id: '1', name: 'Test User', email: 'test@example.com' };
const context = {
dataSources: {
userAPI: { getUser: vi.fn().mockResolvedValue(mockUser) },
},
};
const result = await resolvers.Query.user(null, { id: '1' }, context, null);
expect(result).toEqual(mockUser);
expect(context.dataSources.userAPI.getUser).toHaveBeenCalledWith('1');
});
it('should throw if not admin for createUser', async () => {
const context = {
currentUser: { role: 'USER' },
dataSources: { userAPI: { createUser: vi.fn() } },
};
await expect(
resolvers.Mutation.createUser(null, { input: {} }, context, null)
).rejects.toThrow('Admin access required');
});
});
통합 테스트
describe('GraphQL Integration', () => {
let server: ApolloServer;
beforeAll(async () => {
server = new ApolloServer({ typeDefs, resolvers });
});
it('should execute GetUser query', async () => {
const response = await server.executeOperation({
query: `
query GetUser($id: ID!) {
user(id: $id) {
id
name
}
}
`,
variables: { id: '1' },
});
expect(response.body.kind).toBe('single');
expect(response.body.singleResult.errors).toBeUndefined();
expect(response.body.singleResult.data?.user).toBeDefined();
});
});
12. GraphQL을 쓰지 말아야 할 때
모든 상황에 GraphQL이 적합하지는 않습니다.
**REST가 나은 경우:**
- 단순 CRUD API (리소스 구조가 명확할 때)
- 파일 업로드/다운로드가 주 기능인 서비스
- HTTP 캐싱이 핵심인 공개 API
- 팀 규모가 작고 러닝 커브를 감당하기 어려울 때
- 서버 간 내부 통신 (gRPC가 더 적합)
**GraphQL이 빛나는 경우:**
- 다양한 클라이언트(웹, 모바일, TV)가 같은 API를 사용
- 프론트엔드 팀이 빠르게 이터레이션해야 할 때
- 데이터 관계가 복잡한 도메인 (SNS, 이커머스)
- 마이크로서비스를 통합 API로 제공해야 할 때
- 실시간 기능이 필요한 애플리케이션
13. 실무 면접 질문 15선
기초 (1-5)
**Q1. GraphQL과 REST의 핵심 차이를 설명하세요.**
GraphQL은 클라이언트가 필요한 데이터의 구조를 직접 명시하는 쿼리 언어입니다. REST는 서버가 정의한 엔드포인트별로 고정된 응답을 반환하는 반면, GraphQL은 단일 엔드포인트에서 클라이언트가 원하는 필드만 선택적으로 요청합니다.
**Q2. Over-fetching과 Under-fetching을 설명하세요.**
Over-fetching은 API가 클라이언트에게 필요한 것 이상의 데이터를 반환하는 것이고, Under-fetching은 필요한 데이터를 얻기 위해 여러 번 API를 호출해야 하는 것입니다.
**Q3. SDL(Schema Definition Language)의 주요 타입들을 설명하세요.**
Object 타입(커스텀 필드 정의), Scalar 타입(String, Int, Float, Boolean, ID), Enum(열거형), Input(입력용 객체), Interface(공통 필드 정의), Union(여러 타입 중 하나)이 있습니다.
**Q4. Query, Mutation, Subscription의 차이를 설명하세요.**
Query는 데이터 조회(GET과 유사), Mutation은 데이터 변경(POST/PUT/DELETE와 유사), Subscription은 WebSocket을 통한 실시간 이벤트 스트림입니다.
**Q5. Non-null(!)과 리스트 타입의 조합을 설명하세요.**
`[String]`은 null 가능한 문자열의 null 가능한 배열, `[String!]`은 null 불가 문자열의 null 가능한 배열, `[String!]!`은 null 불가 문자열의 null 불가 배열입니다.
중급 (6-10)
**Q6. N+1 문제가 무엇이며 어떻게 해결하나요?**
부모 목록을 조회한 뒤 각 항목의 관계 데이터를 개별 쿼리로 가져오는 문제입니다. DataLoader로 같은 틱의 요청을 모아 한 번의 배치 쿼리로 실행합니다.
**Q7. Apollo Federation의 핵심 개념을 설명하세요.**
여러 Subgraph를 Router가 하나의 Supergraph로 합칩니다. @key로 엔티티를 식별하고, __resolveReference로 다른 서비스의 엔티티를 resolve합니다.
**Q8. GraphQL에서 인증과 인가를 어떻게 구현하나요?**
인증은 context에서 토큰을 검증하여 currentUser를 설정합니다. 인가는 커스텀 directive(@auth)나 resolver 내부에서 역할 기반 검사를 수행합니다.
**Q9. Persisted Queries의 장점은 무엇인가요?**
쿼리 문자열 대신 해시값을 전송하여 네트워크 페이로드를 줄이고, 서버에서 허용된 쿼리만 실행하여 보안을 강화합니다.
**Q10. Cursor-based vs Offset-based Pagination의 차이는?**
Offset은 페이지 번호 기반으로 중간 삽입/삭제 시 데이터가 중복/누락됩니다. Cursor는 특정 위치(opaque cursor) 기반으로 실시간 데이터에서도 안정적입니다.
고급 (11-15)
**Q11. Query Complexity와 Depth Limit을 설명하세요.**
Query Complexity는 각 필드에 비용을 부여해 총 비용이 한도를 초과하면 거부합니다. Depth Limit은 중첩 깊이를 제한하여 악의적인 재귀 쿼리를 방지합니다.
**Q12. Federation에서 Entity와 Reference Resolver의 동작을 설명하세요.**
Entity는 @key로 식별되는 타입입니다. Router가 다른 subgraph의 데이터가 필요할 때 __resolveReference를 호출하여 key 필드만으로 전체 엔티티를 resolve합니다.
**Q13. GraphQL의 캐싱이 REST보다 어려운 이유는?**
REST는 URL 기반 HTTP 캐시를 직접 활용하지만, GraphQL은 단일 엔드포인트에 POST 요청을 사용하므로 HTTP 캐시를 쓸 수 없습니다. APQ, CDN 캐시 키 설정, 정규화된 클라이언트 캐시 등 별도 전략이 필요합니다.
**Q14. Subscription의 프로덕션 스케일링은 어떻게 하나요?**
인메모리 PubSub 대신 Redis PubSub이나 Kafka를 사용합니다. 여러 서버 인스턴스 간 메시지를 공유하며, WebSocket 연결의 로드밸런싱에는 sticky session이나 별도 WebSocket 게이트웨이를 구성합니다.
**Q15. 스키마 진화(Evolution) 전략을 설명하세요.**
@deprecated directive로 필드를 폐기 예고하고, 새 필드를 추가합니다. 필드 제거 전 클라이언트 사용량을 모니터링하고, 스키마 레지스트리로 변경 호환성을 검증합니다. REST의 URL 버전 관리보다 유연합니다.
14. 실전 퀴즈 5문제
**정답: `[String!]!`**
`[String!]!` — 배열 자체도 null이 아니고(!), 배열 내 각 요소도 null이 아닙니다(String!). `[String]`은 배열도 null 가능, 요소도 null 가능합니다.
**정답: 같은 이벤트 루프 틱 내의 .load() 호출을 모아 배치 함수를 한 번 실행합니다.**
DataLoader는 요청을 큐에 쌓다가 현재 틱이 끝나면 모든 ID를 하나의 배치 함수에 전달합니다. 이를 통해 N+1 쿼리를 1+1 쿼리로 줄입니다.
**정답: 엔티티(Entity)를 고유하게 식별하는 필드를 지정합니다.**
`@key(fields: "id")`는 해당 타입이 Federation의 엔티티임을 선언하고, Router가 다른 subgraph에서 이 엔티티를 참조할 때 id 필드를 사용하여 resolve할 수 있게 합니다.
**정답: WebSocket**
Subscription은 HTTP 요청-응답이 아닌 WebSocket을 통한 양방향 연결을 사용합니다. graphql-ws 프로토콜로 서버에서 클라이언트로 실시간 이벤트를 push합니다.
**정답: 서버에 등록된 쿼리만 실행하여 임의의 쿼리(악의적 쿼리) 실행을 방지합니다.**
APQ(Automatic Persisted Queries)는 쿼리의 SHA256 해시를 전송합니다. 서버는 해시와 매핑된 쿼리만 실행하므로 공격자가 임의의 복잡한 쿼리를 보내 서버 자원을 소모하는 것을 막습니다.
참고 자료 (References)
1. [GraphQL 공식 문서](https://graphql.org/learn/)
2. [Apollo GraphQL 문서](https://www.apollographql.com/docs/)
3. [Apollo Federation 가이드](https://www.apollographql.com/docs/federation/)
4. [GraphQL Spec (2021)](https://spec.graphql.org/October2021/)
5. [DataLoader GitHub](https://github.com/graphql/dataloader)
6. [graphql-ws 프로토콜](https://github.com/enisdenjo/graphql-ws)
7. [How to GraphQL - 튜토리얼](https://www.howtographql.com/)
8. [Relay 공식 문서](https://relay.dev/)
9. [GraphQL Best Practices](https://graphql.org/learn/best-practices/)
10. [Production-Ready GraphQL (Marc-Andre Giroux)](https://book.productionreadygraphql.com/)
11. [The Guild - GraphQL Tools](https://the-guild.dev/graphql/tools)
12. [GraphQL Security (OWASP)](https://cheatsheetseries.owasp.org/cheatsheets/GraphQL_Cheat_Sheet.html)
13. [Apollo Client React 가이드](https://www.apollographql.com/docs/react/)
14. [Netflix: GraphQL at Scale](https://netflixtechblog.com/our-learnings-from-adopting-graphql-f712692c381)
15. [Shopify: GraphQL Design Tutorial](https://github.com/Shopify/graphql-design-tutorial)
16. [GitHub GraphQL API](https://docs.github.com/en/graphql)
현재 단락 (1/699)
2015년 Facebook이 오픈소스로 공개한 GraphQL은 이제 API 설계의 표준 선택지가 되었습니다. GitHub, Shopify, Netflix, Airbnb 등 주요 테...