Files
2nd/10_Wiki/Topics/Coding/Backend_GraphQL_Server_Patterns.md
T
2026-05-09 21:08:02 +09:00

4.5 KiB

id, title, category, status, source_trust_level, verification_status, created_at, updated_at, tags, tech_stack, applied_in, aliases
id title category status source_trust_level verification_status created_at updated_at tags tech_stack applied_in aliases
backend-graphql-server-patterns GraphQL Server — Schema / Resolver / DataLoader Coding draft B conceptual 2026-05-09 2026-05-09
backend
graphql
dataloader
n+1
vibe-coding
language applicable_to
TS / Apollo / Pothos / Yoga
Backend
Apollo
Yoga
Pothos
schema-first
code-first
N+1

GraphQL Server

클라이언트가 필요한 필드만 요청. N+1 문제 = DataLoader 가 답. Schema-first(SDL) vs Code-first(Pothos). REST 와 공존 — 단순 CRUD 는 REST 가 종종 낫다.

📖 핵심 개념

  • Resolver: field 마다 lazy 로 호출.
  • N+1: list resolver → 각 item resolver → DB 100번 hit.
  • DataLoader: 같은 tick 안 batch + cache.
  • Persisted query: 클라가 hash 만 보내 — 페이로드 줄임 + allowlist 보안.

💻 코드 패턴

Pothos (code-first, type-safe)

import SchemaBuilder from '@pothos/core';

const builder = new SchemaBuilder<{
  Context: { db: Db; loaders: Loaders };
}>({});

builder.objectType('User', {
  fields: t => ({
    id: t.exposeID('id'),
    email: t.exposeString('email'),
    posts: t.field({
      type: ['Post'],
      resolve: (user, _, ctx) => ctx.loaders.postsByUser.load(user.id),
    }),
  }),
});

builder.queryType({
  fields: t => ({
    me: t.field({
      type: 'User',
      resolve: (_, __, ctx) => ctx.db.getUser(ctx.userId),
    }),
  }),
});

export const schema = builder.toSchema();

DataLoader — N+1 해결

import DataLoader from 'dataloader';

function makeLoaders(db: Db) {
  return {
    postsByUser: new DataLoader<string, Post[]>(async (userIds) => {
      const posts = await db.posts.where('userId', 'in', userIds);
      const byUser = new Map<string, Post[]>();
      for (const p of posts) {
        const arr = byUser.get(p.userId) ?? [];
        arr.push(p);
        byUser.set(p.userId, arr);
      }
      return userIds.map(id => byUser.get(id) ?? []);
    }),
  };
}

// 매 request 마다 새로 — cache 가 cross-request 누수 안 되게
app.use((req, res, next) => { req.loaders = makeLoaders(db); next(); });

Mutations

builder.mutationType({
  fields: t => ({
    createPost: t.field({
      type: 'Post',
      args: {
        input: t.arg({ type: CreatePostInput, required: true }),
      },
      resolve: async (_, { input }, ctx) => {
        if (!ctx.userId) throw new Error('UNAUTHORIZED');
        return ctx.db.posts.insert({ ...input, userId: ctx.userId });
      },
    }),
  }),
});

Subscription (real-time)

builder.subscriptionType({
  fields: t => ({
    postCreated: t.field({
      type: 'Post',
      subscribe: (_, __, ctx) => ctx.pubsub.subscribe('POST_CREATED'),
      resolve: (payload) => payload,
    }),
  }),
});

Server (Yoga)

import { createYoga } from 'graphql-yoga';
import { useResponseCache } from '@graphql-yoga/plugin-response-cache';

const yoga = createYoga({
  schema,
  context: ({ request }) => ({
    db, userId: getUserId(request), loaders: makeLoaders(db),
  }),
  plugins: [
    useResponseCache({ ttl: 1_000 }),
  ],
});

Persisted query (보안 + 성능)

// 클라이언트 빌드 시 query → hash 매핑 .json 생성 → 서버에 동기화
// 서버는 hash 만 받고 등록된 query 만 실행

Error handling

import { GraphQLError } from 'graphql';

throw new GraphQLError('Not found', { extensions: { code: 'NOT_FOUND' } });

🤔 의사결정 기준

상황 추천
다양한 클라 (web/mobile/admin) GraphQL
단순 CRUD 5개 endpoint REST 충분
실시간 Subscription (WS) 또는 SSE
파일 업로드 REST (multipart) — GraphQL 어색
마이크로서비스 통합 Federation (Apollo / Mesh)
강력 type safety Pothos / GraphQL-Codegen

안티패턴

  • DataLoader 안 씀: N+1 으로 100ms → 5s.
  • DataLoader cross-request 공유: 권한/cache leak.
  • Resolver 깊이 무제한: query depth 제한 (e.g. 10) + cost analysis.
  • Internal error 그대로 노출: stack trace 노출.
  • Auth resolver 안에서 검사: 쉽게 까먹음. Auth directive 또는 plugin.
  • Mutation 이 read 도: side-effect 명시 분리.
  • Schema 자동 노출 prod: introspection off + persisted query.

🤖 LLM 활용 힌트

  • Pothos + Yoga + DataLoader 디폴트.
  • 매 요청 loaders 새로.
  • Persisted query 권장.

🔗 관련 문서