GraphQL Yoga 패턴
빠른 안내:
createYoga+createSchema를 사용하여 Fetch API 호환 GraphQL 서버를 만들면 모든 JS 런타임에서 실행됩니다. Yoga v5는 Envelop을 사용한 플러그인 구성, SSE 기반 기본 구독, 내장 오류 마스킹, CORS 등을 기본 제공합니다. 의도적으로 클라이언트에 노출할 오류는graphql-yoga가 아닌graphql에서GraphQLError를 임포트하여 사용하세요. HTTP 수준 최적화를 위해 Envelop 플러그인보다는 Yoga 전용 플러그인을 선호하세요.
<critical_requirements>
중요: 이 스킬 사용 전에
모든 코드는 CLAUDE.md의 프로젝트 규칙을 따라야 합니다. (kebab-case, named exports, import 순서,
import type, named constants)
(GraphQLError는 반드시 'graphql'에서 임포트해야 합니다. 'graphql-yoga'에서 임포트하면 안 됩니다.)
(Yoga 전용 플러그인이 있다면 반드시 Envelop 플러그인보다 우선 사용해야 합니다. Yoga 플러그인은 HTTP 계층에서 작동하여 캐시되거나 유지된 결과에 대해 GraphQL 실행을 건너뛸 수 있습니다.)
(스키마 우선 접근 방식을 사용할 때는 반드시 'graphql-yoga'의 createSchema를 사용해야 합니다. v5에서는 createYoga에 직접 typeDefs/resolvers 객체를 전달하는 방식을 지원하지 않습니다.)
(모든 숫자 값(타임아웃, TTL, 포트 번호, 제한)은 반드시 named constants를 사용해야 합니다.)
</critical_requirements>
자동 감지 키워드: GraphQL Yoga, graphql-yoga, createYoga, createSchema, createPubSub, Envelop, useResponseCache, useCSRFPrevention, usePersistedOperations, GraphQL 구독 SSE, 오류 마스킹, maskedErrors, graphql-ws, Yoga 플러그인 훅, onRequest, onParams
사용 시기:
- Node.js, Bun, Deno, Cloudflare Workers에서 실행될 GraphQL 서버를 구축할 때
- SSE(기본) 또는 WebSocket을 통한 구독이 필요한 API
- Envelop 플러그인(캐싱, 인증, 로깅)으로 GraphQL 실행을 확장할 때
- GraphQL Multipart Request 스펙을 사용한 파일 업로드가 필요할 때
- 오류 마스킹, CORS, CSRF 보호가 필요한 프로덕션 API
사용하지 말아야 할 때:
- GraphQL 없이 REST API만 필요한 경우
- 프레임워크의 기본 라우트 핸들러로 충분한 단순 CRUD의 경우
- 페더레이션 게이트웨이가 필요한 경우 (전용 게이트웨이 솔루션 고려)
다루는 주요 패턴:
createYoga와createSchema를 사용한 서버 설정 (스키마 우선)createYoga<ServerContext>제네릭을 사용한 타입 안전 컨텍스트- Envelop 플러그인 시스템: 생명주기 훅, 커스텀 플러그인, Yoga 전용 플러그인
- 구독: SSE(기본),
graphql-ws를 통한 WebSocket, 내장 PubSub - 오류 마스킹과 의도적인
GraphQLError노출 - WHATWG
File스칼라를 사용한 파일 업로드 - 프로덕션 강화: CORS, CSRF 방어, GraphQL Armor, 로깅
- 크로스 런타임 배포: Node.js, Bun, Deno, Cloudflare Workers
상세 자료:
- examples/core.md - 서버 설정, 스키마, 컨텍스트, 리졸버, 크로스 런타임 배포
- examples/plugins.md - Envelop 플러그인, 커스텀 플러그인, 생명주기 훅
- examples/subscriptions.md - SSE, WebSocket, PubSub, 필터링
- examples/error-handling.md - 오류 마스킹, GraphQLError, 커스텀 마스킹
- examples/production.md - CORS, CSRF, 응답 캐싱, 유지된 작업(Persisted Operations), 로깅
- reference.md - 결정 프레임워크, 플러그인 참조, 프로덕션 체크리스트
철학
GraphQL Yoga는 필수 기능을 기본 탑재한 Fetch API 호환 GraphQL 서버입니다. 핵심은 WHATWG Fetch API(Request/Response)를 기반으로 하여 런타임에 구애받지 않습니다. 동일한 서버 코드를 Node.js, Bun, Deno, 엣지 런타임에 배포할 수 있습니다. Envelop 플러그인 시스템은 HTTP 계층과 GraphQL 실행 계층 모두에서 구성 가능한 미들웨어를 제공합니다.
스키마 접근 방식: Yoga는 스키마 라이브러리에 중립적입니다. createSchema(스키마 우선 SDL), Pothos(코드 우선), 또는 바닐라 graphql-js 등 GraphQLSchema를 생성하는 모든 방법을 사용할 수 있습니다.
플러그인 우선순위: 동일한 기능(캐싱, 유지된 작업, defer/stream)에 대해 Envelop 플러그인과 Yoga 전용 플러그인이 모두 존재하는 경우, 항상 Yoga 변형을 선택하세요. Yoga 플러그인은 HTTP 계층에 연결되어 GraphQL 실행이 시작되기 전에 단축 회로(short-circuit)를 통해 캐시되거나 유지된 결과에 대해 파싱과 검증을 건너뛸 수 있습니다.
오류 철학: 예상치 못한 오류는 프로덕션 환경에서 기본적으로 모두 마스킹됩니다. 의도적인 오류는 graphql 패키지의 GraphQLError로 던져집니다. 이 오류는 마스킹을 우회하여 메시지와 extensions이 그대로 클라이언트에 전달됩니다.
핵심 패턴
패턴 1: createYoga를 사용한 서버 설정
SDL 기반 스키마를 위해 createSchema와 함께 Yoga 인스턴스를 생성합니다. yoga 인스턴스 자체가 Fetch API 핸들러이므로, 모든 런타임의 HTTP 서버에 직접 전달할 수 있습니다.
import { createYoga, createSchema } from "graphql-yoga";
import { createServer } from "node:http";
const PORT = 4000;
const yoga = createYoga({
schema: createSchema({
typeDefs: /* GraphQL */ `
type Query {
greeting(name: String!): String!
}
`,
resolvers: {
Query: {
greeting: (_, { name }) => `Hello, ${name}!`,
},
},
}),
});
const server = createServer(yoga);
server.listen(PORT, () => {
console.info(`Server running on http://localhost:${PORT}/graphql`);
});
왜 좋은가: createSchema는 makeExecutableSchema를 래핑하며, yoga 인스턴스는 표준 Fetch 핸들러이므로 모든 런타임에서 작동합니다.
전체 설정, 크로스 런타임 배포, 타입 안전 컨텍스트는 examples/core.md를 참조하세요.
패턴 2: 타입 안전 컨텍스트
서버별 컨텍스트 타입을 위해 createYoga에 제네릭을 전달합니다. context 팩토리는 YogaInitialContext( request와 params 포함)를 받아 커스텀 컨텍스트를 반환합니다.
import { createYoga, type YogaInitialContext } from "graphql-yoga";
interface ServerContext {
user: { id: string } | null;
}
const yoga = createYoga<ServerContext>({
schema,
context: async ({ request }: YogaInitialContext) => {
const token = request.headers.get("authorization");
return { user: token ? await verifyToken(token) : null };
},
});
왜 좋은가: 제네릭 타입이 리졸버 컨텍스트 매개변수로 흘러가며, request는 표준 Fetch API(프레임워크별 req/res가 아님)를 사용합니다.
전체 컨텍스트 패턴은 examples/core.md를 참조하세요.
패턴 3: Envelop 플러그인 시스템
플러그인은 plugins 배열에 전달합니다. 가능하면 Yoga 전용 플러그인을 사용하세요. 이 플러그인은 HTTP 계층에서 작동하여 GraphQL 실행을 완전히 건너뛸 수 있습니다.
import { createYoga } from "graphql-yoga";
import { useResponseCache } from "@graphql-yoga/plugin-response-cache";
const CACHE_TTL_MS = 2_000;
const yoga = createYoga({
schema,
plugins: [
useResponseCache({
session: () => null,
ttl: CACHE_TTL_MS,
}),
],
});
왜 좋은가: Yoga 응답 캐시는 캐시된 결과에 대해 파싱/검증을 건너뜁니다(Envelop 버전은 불가능). 여러 플러그인이 충돌 없이 구성됩니다.
커스텀 플러그인, 생명주기 훅, 모든 Yoga 전용 플러그인은 examples/plugins.md를 참조하세요.
패턴 4: SSE(기본)를 사용한 구독
Yoga는 기본적으로 Server-Sent Events를 사용하여 구독을 지원합니다. WebSocket 설정이 필요 없습니다. 구독 리졸버에서 AsyncGenerator 문법을 사용하세요.
const schema = createSchema({
typeDefs: /* GraphQL */ `
type Subscription {
countdown(from: Int!): Int!
}
`,
resolvers: {
Subscription: {
countdown: {
subscribe: async function* (_, { from }) {
for (let i = from; i >= 0; i--) {
await new Promise((resolve) => setTimeout(resolve, 1_000));
yield { countdown: i };
}
},
},
},
},
});
왜 좋은가: WebSocket 인프라가 필요 없으며, HTTP 프록시와 로드 밸런서를 통과하여 작동합니다. 클라이언트는 graphql-sse 라이브러리를 사용합니다.
PubSub, WebSocket 설정, 필터링은 examples/subscriptions.md를 참조하세요.
패턴 5: 오류 마스킹과 GraphQLError
Yoga는 기본적으로 모든 예상치 못한 오류를 마스킹합니다. 의도적으로 클라이언트에 노출할 오류는 graphql에서 GraphQLError를 던지세요. 이 오류는 마스킹을 우회합니다.
import { GraphQLError } from "graphql";
const NOT_FOUND_CODE = "USER_NOT_FOUND";
throw new GraphQLError("User not found", {
extensions: { code: NOT_FOUND_CODE },
});
왜 좋은가: 예상치 못한 오류는 내부 정보(데이터베이스 세부 정보, 스택 트레이스)를 절대 유출하지 않으며, 의도적인 오류는 메시지와 extensions과 함께 통과합니다.
커스텀 마스킹, 마스킹 비활성화, 개발 모드는 examples/error-handling.md를 참조하세요.
패턴 6: 파일 업로드
Yoga는 GraphQL Multipart Request 스펙을 지원합니다. File 스칼라를 추가하고 리졸버에서 WHATWG File 객체를 받으세요.
const schema = createSchema({
typeDefs: /* GraphQL */ `
scalar File
type Mutation {
uploadFile(file: File!): Boolean!
}
`,
resolvers: {
Mutation: {
uploadFile: async (_, { file }: { file: File }) => {
const content = await file.arrayBuffer();
// 파일 내용 처리
return true;
},
},
},
});
왜 좋은가: 표준 WHATWG File API(브라우저와 동일)를 사용하며, 추가 패키지가 필요 없습니다. multipart: false로 비활성화할 수 있습니다.
전체 파일 업로드 패턴은 examples/core.md를 참조하세요.
<decision_framework>
결정 프레임워크
스키마 접근 방식
SDL에서 자동 생성된 타입이 필요한가?
+-- 예 --> createSchema (typeDefs + resolvers를 사용한 스키마 우선)
+-- 아니오 --> 스키마 정의에서 완전한 TypeScript 추론을 원하는가?
+-- 예 --> 코드 우선 라이브러리 (예: Pothos) -- 결과 GraphQLSchema를 Yoga에 전달
+-- 아니오 --> 바닐라 graphql-js GraphQLSchema
구독 전송
구독이 필요한가?
+-- 예 --> 클라이언트가 양방향 통신을 필요로 하는가?
| +-- 예 --> graphql-ws를 통한 WebSocket (ws + graphql-ws 패키지 추가)
| +-- 아니오 --> SSE (기본 설정, 구성 불필요, 프록시 통과 가능)
+-- 아니오 --> 구독 설정 불필요
플러그인 선택
원하는 기능에 Yoga 전용 플러그인이 존재하는가?
+-- 예 --> Yoga 플러그인 사용 (HTTP 수준 훅, 실행 생략 가능)
+-- 아니오 --> Envelop 플러그인 사용 (GraphQL 실행 수준 훅)
Yoga 전용 플러그인 (Envelop보다 선호)
| 플러그인 | 패키지 | Yoga 전용인 이유 |
|---|---|---|
| 응답 캐시 | @graphql-yoga/plugin-response-cache | 캐시된 쿼리에 대해 실행 생략 |
| 유지된 작업 | @graphql-yoga/plugin-persisted-operations | HTTP 계층에서 알 수 없는 작업 거부 |
| Defer/Stream | @graphql-yoga/plugin-defer-stream | HTTP 청크 인코딩을 통한 스트리밍 |
| CSRF 방어 | @graphql-yoga/plugin-csrf-prevention | 파싱 전에 커스텀 헤더 요구 |
| GraphQL SSE | @graphql-yoga/plugin-graphql-sse | 단일 연결 SSE 모드 |
</decision_framework>
<red_flags>
위험 신호
높은 우선순위:
GraphQLError를graphql-yoga에서 임포트하는 경우 (올바른 패키지:graphql) -- 잘못된 패키지이며 실패합니다.createYoga에createSchema없이typeDefs/resolvers객체를 직접 전달하는 경우 -- v5에서 지원되지 않습니다.- Yoga 전용 플러그인이 존재하는데 Envelop 플러그인을 사용하는 경우 -- HTTP 수준 최적화를 놓칩니다 (Yoga 응답 캐시는 파싱을 완전히 건너뛰지만 Envelop 버전은 불가능합니다).
- 리졸버에서 클라이언트가 메시지를 볼 것으로 기대하고 일반
Error를 던지는 경우 -- 프로덕션에서 "예기치 않은 오류가 발생했습니다."로 마스킹됩니다.
중간 우선순위:
- 프로덕션에서 CORS 출처를 설정하지 않는 경우 -- 기본값은
*이며, 이는 제한되어야 합니다. - 여러 서버 인스턴스에서 인메모리 PubSub을 사용하는 경우 -- 이벤트가 전파되지 않습니다 (Redis 기반
createRedisEventTarget사용). graphql피어 의존성이 누락된 경우 --graphql-yoga는graphql을 피어 의존성으로 필요로 하므로 둘 다 설치해야 합니다.- 스키마 없이
createSchema를 호출하는 경우 -- Yoga는 스키마를 필요로 하며, 추론하지 않습니다.
주의 사항 및 엣지 케이스:
YogaInitialContext.request는 Fetch APIRequest객체이지 Node.jsIncomingMessage가 아닙니다.req.headers가 아니라request.headers.get()을 사용하세요.- v5에서 플러그인 실행 순서가 변경되었습니다.
onPluginInit에서addPlugin으로 추가된 플러그인은 이제 추가하는 플러그인 바로 다음에 실행되며, 마지막에 실행되지 않습니다. useResponseCache의session콜백은 PRIVATE 범위를 위해 문자열(사용자 ID)을 반환하거나 PUBLIC을 위해null을 반환해야 합니다.undefined를 반환하면 캐싱이 중단됩니다.- SSE 구독은 HTTP(text/event-stream)를 통해 전달됩니다. 일부 프록시는 이벤트를 버퍼링할 수 있습니다. Nginx의 경우
X-Accel-Buffering: no를 설정하세요. - 업로드의
File스칼라는 WHATWGFile객체를 제공합니다..text(),.arrayBuffer(),.stream()메서드를 사용하세요(Node.jsBuffer직접 사용 금지). - Yoga의 내장 GraphiQL은 기본적으로 활성화되어 있습니다. 프로덕션에서는
graphiql: false로 비활성화하세요. maskedErrors를false로 설정하면 스택 트레이스를 포함한 모든 마스킹이 해제됩니다. 선택적 노출을 위해 대신 커스텀maskError함수를 사용하세요.origin: '*'와 함께credentials: true를 설정하면 Fetch 스펙에 따라 브라우저에서 거부됩니다. 정확한 출처를 지정하세요.
</red_flags>
<critical_reminders>
중요 알림
모든 코드는 CLAUDE.md의 프로젝트 규칙을 따라야 합니다.
(GraphQLError는 반드시 'graphql'에서 임포트해야 합니다. 'graphql-yoga'에서 임포트하면 안 됩니다.)
(Yoga 전용 플러그인이 있다면 반드시 Envelop 플러그인보다 우선 사용해야 합니다. Yoga 플러그인은 HTTP 계층에서 작동하여 캐시되거나 유지된 결과에 대해 GraphQL 실행을 건너뛸 수 있습니다.)
(스키마 우선 접근 방식을 사용할 때는 반드시 'graphql-yoga'의 createSchema를 사용해야 합니다. v5에서는 createYoga에 직접 typeDefs/resolvers 객체를 전달하는 방식을 지원하지 않습니다.)
(모든 숫자 값(타임아웃, TTL, 포트 번호, 제한)은 반드시 named constants를 사용해야 합니다.)
**이 규칙을 따르지 않으면 가져오기 오류, 마스킹 실패, 잘못된 동작이 발생합니다.
</critical_reminders>