🔷 GraphQL0. GraphQL의 기초04 — Anatomy of a Query

04 — Anatomy of a Query

한 줄 답: 모든 GraphQL 쿼리는 operation type → name → variables → selection set의 4층으로 분해되며, 그 위에 alias·fragment·directive·introspection이 얹힌다.


Why — 왜 쿼리를 해부해야 하나

GraphQL을 처음 보는 사람은 흔히 다음과 같이 쓴다.

{
  user(id: "123") {
    name
  }
}

이건 동작하지만 production에 들어가면 거의 모든 부분에서 어긋난다 — 캐싱이 안 되고(operation name 없음), 보안 검토가 안 되고(variable 대신 inline literal), 재사용이 안 된다(fragment 없음). 쿼리의 문법 부품 하나하나를 알면, 어디서 무엇이 깨지는지 보인다.

이 문서는 production-ready 쿼리 한 개를 문법 부품 단위로 분해한다.


How — 쿼리의 4층 구조

표본 쿼리 — 모든 부품이 한 번씩 등장

query GetUserDashboard($userId: ID!, $includeDrafts: Boolean = false) {
  # ┌─ operation type
  # │       └─ operation name
  # │                          └─ variable definitions
 
  currentUser: user(id: $userId) {
    # │           └─ field with argument
    # └─ alias
 
    id
    name
 
    posts(first: 10) @include(if: $includeDrafts) {
      # │                └─ directive
      # └─ field with argument
 
      ...PostSummary
      # └─ fragment spread
    }
 
    ... on PremiumUser {
      # └─ inline fragment (type condition)
      subscriptionTier
    }
  }
 
  __typename  # introspection meta-field
}
 
fragment PostSummary on Post {
  # └─ fragment definition
  id
  title
  publishedAt
}

이 한 쿼리에 들어 있는 부품은 10가지다. 하나씩 본다.

1) Operation Type — 3개 중 하나

query   GetX { ... }      # 읽기 (병렬)
mutation DoY { ... }      # 쓰기 (순차)
subscription OnZ { ... }  # 스트림

생략하면 query로 간주된다 ({ user { name } } 형태). production에선 항상 명시 권장 — 도구·로깅이 쉬워진다.

2) Operation Name — 익명 금지

query GetUserDashboard { ... }   # ✅
query { ... }                    # ⚠️ anonymous
{ ... }                          # ⚠️ shorthand

이름이 있어야:

  • 로그에서 식별 가능 (Apollo Studio·Datadog 등)
  • persisted query로 hash 등록 가능
  • 복수 operation을 한 document에 둘 수 있음 (그땐 필수)

3) Variables — 절대 inline literal로 쓰지 말 것

# ❌ inline
query { user(id: "123") { name } }
 
# ✅ variable
query GetUser($id: ID!) {
  user(id: $id) { name }
}
// HTTP body
{
  "query": "query GetUser($id: ID!) { user(id: $id) { name } }",
  "variables": { "id": "123" }
}

Variable을 쓰면:

  • 쿼리 텍스트가 재사용 가능 → persisted query·캐시
  • 타입 검증을 서버가
  • SQL injection 같은 공격 surface가 줄어듦

타입 표기:

  • $id: ID!!non-null. 누락하면 validation 에러
  • $includeDrafts: Boolean = false — 기본값 지정 가능

4) Selection Set — 중괄호 안의 트리

{
  user {            ← selection set 시작
    namefield
    posts {         ← nested selection set
      titlefield
    }
  }
}

규칙: scalar 타입(Int/String/Boolean/ID/Float/Enum/custom scalar)은 selection set이 없어야 하고, object/interface/union 타입은 selection set이 반드시 있어야 한다. 위반하면 validation 에러.

5) Alias — 같은 필드를 다른 이름으로

{
  alice: user(id: "1") { name }
  bob: user(id: "2") { name }
}
{
  "data": {
    "alice": { "name": "Alice" },
    "bob": { "name": "Bob" }
  }
}

같은 user 필드를 두 번 호출하면서 응답에서 충돌 없이 둘 다 받는다. dashboard처럼 같은 타입을 여러 번 쿼리하는 화면에서 핵심.

6) Fragment — 재사용 가능한 selection set

Named fragment:

fragment PostSummary on Post {
  id
  title
  publishedAt
}
 
query {
  user { posts { ...PostSummary } }
  trending { ...PostSummary }
}

Inline fragment (타입 분기):

{
  search(q: "foo") {
    ... on User { name }
    ... on Post { title }
    ... on Comment { text }
  }
}

Union·interface 타입을 받을 때 타입별로 다른 필드를 쓰는 유일한 방법.

7) Directive — @include / @skip / @deprecated

GraphQL spec October 2021이 정의하는 built-in directive는 정확히 3개다.

Directive위치동작
@include(if: Boolean!)field, fragment spread, inline fragmenttrue일 때만 포함
@skip(if: Boolean!)field, fragment spread, inline fragmenttrue일 때 제외
@deprecated(reason: String)field/enum value definition (스키마)도구에 deprecated 표시
query Profile($includeEmail: Boolean!) {
  user {
    name
    email @include(if: $includeEmail)
  }
}

추가 directive(@cached, @auth, @connection 등)는 spec 외다 — 라이브러리·서버가 정의한 custom directive.

8) Field Arguments

posts(first: 10, after: "cursor123", orderBy: CREATED_DESC) { ... }

각 argument는 스키마에 타입과 함께 선언돼 있어야 하며, 기본값을 가질 수 있다. 위치 인자는 없다 — 항상 named다.

9) Introspection — 메타 필드 3개

GraphQL 서버는 자기 자신을 쿼리할 수 있어야 한다. spec이 정의하는 introspection 진입점은 정확히 세 개.

메타 필드어디서용도
__schemaQuery 타입의 root전체 스키마 메타데이터
__type(name: String!)Query 타입의 root특정 타입 조회
__typename모든 object/interface/union 위치런타임 타입 이름
# 전체 스키마 살펴보기
{
  __schema {
    types { name kind }
    queryType { name }
    mutationType { name }
    subscriptionType { name }
  }
}
 
# 한 타입 자세히
{
  __type(name: "User") {
    name
    fields { name type { name kind } }
  }
}
 
# Union/Interface에서 실제 타입 알아내기
{
  search(q: "foo") {
    __typename
    ... on User { name }
    ... on Post { title }
  }
}

GraphiQL·Apollo Sandbox 같은 도구가 자동완성과 문서를 보여주는 비결이 바로 이 introspection이다.

10) Comments

# 한 줄 주석 — '#'로 시작
query GetUser {
  user { name }  # 줄 끝에서도 가능
}

블록 주석은 spec에 없다. #만 있다.


What — 정확한 사양 (October 2021)

Document — 쿼리 파일 한 개의 단위

GraphQL spec에서 Document는 한 번에 보내는 텍스트 단위다. 다음을 포함할 수 있다.

# Document = N개의 ExecutableDefinition
 
query A { ... }              # operation definition 1
query B { ... }              # operation definition 2
mutation C { ... }           # operation definition 3
fragment X on T { ... }      # fragment definition
fragment Y on T { ... }      # fragment definition

여러 operation이 있는 document를 보낼 땐 어떤 operation을 실행할지 HTTP body에 명시.

{
  "query": "query A { ... } query B { ... }",
  "operationName": "A",
  "variables": {}
}

Validation 규칙 (spec §5에서 발췌)

규칙위반 시
Fields must exist on type존재하지 않는 필드 → 거부
Scalar leaf fieldsscalar에 { } 붙이면 거부
Object/interface non-leafobject에 { } 누락 시 거부
Argument types validargument 타입 mismatch 시 거부
Variables are input typesvariable이 object 타입이면 거부
Fragment spreads must form acyclic graphfragment가 순환 참조하면 거부
Operation name uniqueness같은 이름 operation 두 개면 거부

이 모두는 실행 전 정적으로 검증된다. resolver는 호출도 안 된다.

Response 구조

{
  "data": { ... },          // selection set과 동형 (실패 시 null 또는 부분)
  "errors": [               // 선택 — 실패한 필드들
    {
      "message": "Permission denied",
      "path": ["user", "email"],
      "locations": [{ "line": 3, "column": 5 }],
      "extensions": { "code": "FORBIDDEN" }
    }
  ],
  "extensions": { ... }     // 선택 — 서버가 자유롭게 추가 (trace 등)
}

세 최상위 키만 spec이 정의한다. 다른 키를 최상위에 두면 non-conformant.


What-if — 잘못 쓰면

1) Anonymous query만 쓴다

증상: 로그·메트릭에서 모든 쿼리가 같은 익명으로 집계. 원인: query { ... }로만 보냄. 대응: 모든 쿼리에 명사 형태의 이름 (GetUserDashboard, UpdateUserName).

2) Variable 대신 inline literal

증상: 쿼리 텍스트가 매번 다름 → cache miss, persisted query 불가. 원인: user(id: "123")처럼 값 박아넣음. 대응: 항상 $id: ID! variable.

3) Fragment 중복

증상: 여러 화면에서 같은 필드 셋을 복사붙여넣기. 원인: fragment를 안 쓰거나 너무 잘게 쪼갬. 대응: 화면 단위 fragment (예: UserCardFields, PostDetailFields).

4) @include/@skip 둘 다 거는 실수

field @include(if: $a) @skip(if: $b)

증상: 결과가 둘 다 만족해야 포함됨 — 직관에 어긋남. 대응: 둘을 동시에 쓰지 말고 boolean 식을 클라이언트에서 합성.

5) Introspection을 production에 열어둠

증상: 공격자가 __schema전체 스키마를 덤프 → 숨겨진 내부 필드 노출. 원인: GraphQL 라이브러리는 기본적으로 introspection을 켜둔다. 대응: production은 introspection 비공개 + persisted query만 허용. 개발/스테이징만 열기.

6) Subscription에 query만큼의 selection set

증상: subscription 응답이 거대 → WebSocket 트래픽 폭증. 원인: 이벤트 ID만 받으면 되는데 전체 객체를 받음. 대응: subscription은 알림 트리거만, 본 데이터는 별도 query로.


Insight — 흥미로운 이야기

”왜 __schema는 underscore 두 개로 시작하나”

GraphQL spec은 유저가 정의하는 모든 이름이 underscore 두 개로 시작하는 것을 금지한다 (__로 시작하는 이름은 spec 예약). 이 단순한 규칙 덕에 introspection 필드(__schema, __type, __typename)는 유저 정의 필드와 절대 충돌하지 않는다. 이름 충돌을 피하려고 접두사 네임스페이스를 따로 두지 않은 우아한 디자인.

”Fragment는 React에서 가져왔다”

Facebook 내부에서 GraphQL과 React는 같은 팀에서 같은 시기에 만들어졌다. Fragment 개념은 React의 컴포넌트 단위 데이터 의존성에서 직접 영감을 받았다 — 컴포넌트마다 자기가 필요한 필드 셋을 선언하고, 부모가 합성한다. Relay라는 client 라이브러리가 컴파일 타임에 fragment를 합성하는 패턴을 만들었고, 그것이 GraphQL 클라이언트의 표준 패턴이 됐다.

”Directive는 ‘GraphQL의 escape hatch’”

@include/@skip 외의 모든 directive는 spec 외다. 즉 GraphQL spec은 directive 문법만 정의하고 어떤 directive가 있을지는 라이브러리에 위임했다. 이 덕에 Apollo의 @key(Federation), Relay의 @connection(pagination), @defer/@stream(stream draft) 같은 후속 표준이 spec 변경 없이 추가될 수 있었다 — 언어는 작고, 생태계는 크다.

”변하지 않는 5가지 키워드”

10년이 지나도 GraphQL 쿼리의 핵심 키워드는 5개뿐이다 — query, mutation, subscription, fragment, on. 이 작은 어휘가 복잡한 데이터 그래프를 표현한다는 점이 GraphQL의 디자인 미학이다.


요약 + 다이어그램

모든 GraphQL 쿼리는 operation·variables·selection set이라는 3가지 핵심 부품 위에 alias·fragment·directive·introspection이 얹힌 트리다. Production에서는 anonymous 금지, inline literal 금지, fragment 적극 사용, introspection 비공개가 표준 hygiene.

다음 문서: 05-mental-model.mdx — 이 부품들의 를 한 줄로: GraphQL은 graph가 아니라 graph traversal이다.