GraphQL API

GraphQL은 API를 위한 쿼리 언어이자 서버 측에서 이러한 쿼리를 실행하기 위한 런타임이다. 2015년 Facebook(현 Meta)에서 공개한 후 오픈 소스 프로젝트로 발전했으며, API 개발 방식에 혁신을 가져왔다.

가장 단순하게 설명하자면, GraphQL은 클라이언트가 서버에게 “정확히 필요한 데이터만” 요청할 수 있게 해주는 기술이다. 이는 전통적인 REST API와는 다른 접근 방식을 제공한다.

여기서 ‘Graph’는 그래프 데이터 구조를 의미한다. GraphQL은 데이터를 노드(객체)와 엣지(관계)로 구성된 그래프로 모델링한다. ‘QL’은 Query Language의 약자로, 이 그래프 구조에 대한 쿼리를 작성하는 언어를 의미한다.

왜 GraphQL이 필요한가?

전통적인 REST API의 한계

REST API는 오랫동안 웹 서비스의 표준이었지만, 몇 가지 주요 한계가 있다:

  1. 오버페칭(Overfetching): 필요 이상의 데이터를 가져오는 문제이다. 예를 들어, 사용자 이름만 필요한데 전체 사용자 정보를 받게 되는 상황.
  2. 언더페칭(Underfetching): 필요한 모든 데이터를 얻기 위해 여러 번의 API 호출이 필요한 상황이다. 예를 들어, 사용자와 그 사용자의 게시물을 가져오려면 두 번의 별도 요청이 필요할 수 있다.
  3. 엔드포인트 증가: 다양한 클라이언트 요구를 충족하기 위해 점점 더 많은 엔드포인트가 생기는 문제이다.
  4. 버전 관리의 어려움: API가 변경되면 새로운 버전을 만들고 이전 버전과의 호환성을 유지해야 하는 복잡성이 있다.

GraphQL의 핵심 원칙

  1. 단일 엔드포인트: REST와 달리 GraphQL은 일반적으로 단일 엔드포인트(예: /graphql)를 사용한다.
  2. 클라이언트 주도적 데이터 요청: 클라이언트가 필요한 데이터만 정확히 요청할 수 있다.
  3. 강력한 타입 시스템: 모든 데이터 타입이 명확하게 정의되어 있다.

GraphQL의 해결책

GraphQL은 이러한 문제들을 해결하기 위해 설계되었다:

  1. 필요한 데이터만 요청: 클라이언트가 정확히 필요한 필드만 명시하여 요청할 수 있다.
  2. 단일 요청으로 여러 리소스 가져오기: 하나의 쿼리로 여러 관련 데이터를 한 번에 가져올 수 있다.
  3. 단일 엔드포인트: 일반적으로 /graphql이라는 하나의 엔드포인트만 사용한다.
  4. 타입 시스템: 강력한 타입 시스템을 통해 API 변경 사항을 더 명확하게 관리할 수 있다.

GraphQL의 주요 구성 요소

스키마 (Schema)

GraphQL API의 핵심은 스키마이다. 스키마는 API에서 사용 가능한 모든 데이터 타입과 관계, 그리고 클라이언트가 실행할 수 있는 작업들을 정의한다.

간단한 블로그 API 스키마 예시:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
type Post {
  id: ID!
  title: String!
  content: String!
  author: User!
  comments: [Comment!]!
}

type User {
  id: ID!
  name: String!
  email: String!
  posts: [Post!]!
}

type Comment {
  id: ID!
  text: String!
  author: User!
  post: Post!
}

type Query {
  getPost(id: ID!): Post
  getUser(id: ID!): User
  getPosts: [Post!]!
}

type Mutation {
  createPost(title: String!, content: String!, authorId: ID!): Post!
  createComment(text: String!, postId: ID!, authorId: ID!): Comment!
}

이 스키마에서:

타입 시스템

GraphQL은 강력한 타입 시스템을 가지고 있으며 다음과 같은 기본 타입들이 있다:

작업 유형 (Operation Types)

GraphQL에는 세 가지 주요 작업 유형이 있다:

  1. 쿼리(Query): 데이터 읽기 작업 (GET과 유사)
  2. 뮤테이션(Mutation): 데이터 변경 작업 (POST, PUT, DELETE와 유사)
  3. 구독(Subscription): 실시간 데이터 업데이트 (웹소켓과 유사)

리졸버 (Resolver)

리졸버는 GraphQL 필드의 데이터를 어떻게 가져올지 정의하는 함수이다.
각 필드마다 연결된 리졸버가 있다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// 리졸버 예제 (JavaScript)
const resolvers = {
  Query: {
    user: (parent, args, context, info) => {
      // args.id를 사용하여 사용자 검색
      return database.findUserById(args.id);
    }
  },
  User: {
    posts: (parent, args, context, info) => {
      // parent.id를 사용하여 사용자의 게시물 검색
      return database.findPostsByAuthorId(parent.id);
    }
  }
};

기타 특징

인트로스펙션(Introspection)

GraphQL은 인트로스펙션이라는 강력한 기능을 제공한다.
이를 통해 API 스스로가 자신이 제공하는 타입과 쿼리에 대한 정보를 노출할 수 있다.

1
2
3
4
5
6
7
8
{
  __schema {
    types {
      name
      description
    }
  }
}

이런 쿼리를 통해 API의 모든 타입 목록을 가져올 수 있으며, 이는 문서화와 개발 도구에서 매우 유용하다.

프래그먼트(Fragments)

프래그먼트는 재사용 가능한 필드 집합으로, 쿼리를 단순화하는 데 도움이 된다:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
fragment UserDetails on User {
  id
  name
  email
}

{
  getUser(id: "1") {
    ...UserDetails
    posts {
      title
    }
  }
}

별칭(Aliases)

같은 필드를 다른 인자로 여러 번 쿼리하려면 별칭을 사용할 수 있다:

1
2
3
4
5
6
7
8
{
  firstPost: getPost(id: "1") {
    title
  }
  secondPost: getPost(id: "2") {
    title
  }
}

디렉티브(Directives)

쿼리의 실행 방식을 동적으로 변경하는 주석과 같은 기능이다:

1
2
3
4
5
6
7
{
  getUser(id: "1") {
    name
    email @include(if: $includeEmail)
    phoneNumber @skip(if: $hidePhoneNumber)
  }
}

여기서 @include@skip은 내장 디렉티브로, 조건에 따라 필드를 포함하거나 제외한다.

GraphQL 쿼리 작성법

기본 쿼리

1
2
3
4
5
6
7
8
9
query {
  user(id: "123") {
    name
    email
    posts {
      title
    }
  }
}

변수와 인자

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
query GetUser($id: ID!) {
  user(id: $id) {
    name
    email
  }
}

# 변수
{
  "id": "123"
}

뮤테이션

1
2
3
4
5
6
7
8
9
mutation CreatePost($title: String!, $content: String!, $authorId: ID!) {
  createPost(title: $title, content: $content, authorId: $authorId) {
    id
    title
    author {
      name
    }
  }
}

GraphQL 고급 기능

페이지네이션

대량의 데이터를 처리할 때 페이지네이션은 필수적이다.
GraphQL에서는 주로 두 가지 접근 방식을 사용한다:

  1. 오프셋 기반 페이지네이션:

    1
    2
    3
    4
    5
    6
    
    {
      getPosts(offset: 0, limit: 10) {
        id
        title
      }
    }
    
  2. 커서 기반 페이지네이션:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    
    {
      getPosts(first: 10, after: "cursor_value") {
        edges {
          node {
            id
            title
          }
          cursor
        }
        pageInfo {
          hasNextPage
          endCursor
        }
      }
    }
    

데이터 캐싱

GraphQL 클라이언트(Apollo Client, Relay 등)는 쿼리 결과를 효율적으로 캐싱하는 기능을 제공한다:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// Apollo Client의 캐시 설정
const client = new ApolloClient({
  uri: 'http://localhost:4000/graphql',
  cache: new InMemoryCache({
    typePolicies: {
      User: {
        keyFields: ['id'], // 개체 식별을 위한 필드
      }
    }
  })
});

실시간 업데이트: 구독(Subscriptions)

WebSocket을 통한 실시간 데이터 업데이트를 위해 GraphQL은 구독을 제공한다:

1
2
3
4
5
6
7
8
9
subscription {
  newComment(postId: "123") {
    id
    text
    author {
      name
    }
  }
}

서버 측 구현:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const { PubSub } = require('graphql-subscriptions');
const pubsub = new PubSub();

const resolvers = {
  Subscription: {
    newComment: {
      subscribe: (_, { postId }) => 
        pubsub.asyncIterator([`NEW_COMMENT_${postId}`])
    }
  },
  
  Mutation: {
    createComment: (_, { text, postId, authorId }) => {
      // 댓글 생성 로직...
      
      // 새 댓글 이벤트 발생
      pubsub.publish(`NEW_COMMENT_${postId}`, { 
        newComment: newComment 
      });
      
      return newComment;
    }
  }
};

배치 요청

여러 쿼리를 배치 처리할 수 있어 네트워크 요청 수를 줄일 수 있다:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// Apollo Client에서의 배치 요청
import { BatchHttpLink } from '@apollo/client/link/batch';

const link = new BatchHttpLink({
  uri: 'http://localhost:4000/graphql',
  batchMax: 5, // 최대 배치 크기
  batchInterval: 20 // 배치 간격 (ms)
});

const client = new ApolloClient({
  link,
  cache: new InMemoryCache()
});

GraphQL 보안 및 최적화

N+1 문제

GraphQL에서 자주 발생하는 성능 문제 중 하나는 N+1 쿼리 문제이다.
예를 들어, 10개의 게시물을 가져오는 쿼리에서 각 게시물의 작성자 정보를 가져오기 위해 추가로 10번의 데이터베이스 쿼리가 발생할 수 있다.

이 문제를 해결하기 위한 방법으로는 다음과 같은 것들이 있다:

  1. 데이터로더(DataLoader): 중복 요청을 배치 처리하여 효율성을 높인다.

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    
    const DataLoader = require('dataloader');
    
    // 사용자 로더 생성
    const userLoader = new DataLoader(async (userIds) => {
      console.log('로드할 사용자 ID들:', userIds);
    
      // 단일 쿼리로 여러 사용자 로드
      const users = await UserModel.find({ _id: { $in: userIds } });
    
      // DataLoader는 userIds와 동일한 순서로 결과를 반환해야 함
      const userMap = {};
      users.forEach(user => {
        userMap[user._id] = user;
      });
    
      return userIds.map(id => userMap[id]);
    });
    
    // 리졸버에서 사용
    const resolvers = {
      Post: {
        author: async (post, _, { loaders }) => {
          return await loaders.user.load(post.authorId);
        }
      }
    };
    
  2. 쿼리 최적화: 특정 쿼리 패턴에 대해 최적화된 데이터베이스 쿼리를 작성한다.

인증 및 권한 부여

GraphQL API에서 인증과 권한 관리는 중요한 부분이이다:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// Apollo Server에서의 인증 컨텍스트
const server = new ApolloServer({
  typeDefs,
  resolvers,
  context: ({ req }) => {
    // 요청 헤더에서 토큰 추출
    const token = req.headers.authorization || '';
    
    // 토큰 검증 및 사용자 정보 추출
    const user = validateToken(token);
    
    return { user };
  }
});

// 리졸버에서 권한 확인
const resolvers = {
  Mutation: {
    createPost: (_, { title, content }, { user }) => {
      // 인증 확인
      if (!user) {
        throw new AuthenticationError('로그인이 필요합니다');
      }
      
      // 권한 확인
      if (!user.hasPermission('create:post')) {
        throw new ForbiddenError('게시물 생성 권한이 없습니다');
      }
      
      // 게시물 생성 로직...
    }
  }
};

쿼리 복잡성 제한

악의적이거나 비효율적인 쿼리로부터 서버를 보호하기 위해 쿼리 복잡성을 제한할 수 있다:

1
2
3
4
5
6
7
8
9
const { createComplexityLimitRule } = require('graphql-validation-complexity');

const server = new ApolloServer({
  typeDefs,
  resolvers,
  validationRules: [
    createComplexityLimitRule(1000) // 최대 쿼리 복잡성 제한
  ]
});

속도 제한(Rate Limiting)

API 남용을 방지하기 위한 속도 제한도 구현할 수 있다:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
const { createRateLimitRule } = require('graphql-rate-limit');

const server = new ApolloServer({
  typeDefs,
  resolvers,
  validationRules: [
    createRateLimitRule({
      identifyContext: (context) => context.user?.id,
      windowMs: 60 * 1000, // 1분
      max: 100 // 최대 100 요청
    })
  ]
});

REST API vs. GraphQL

REST APIGraphQL
여러 엔드포인트단일 엔드포인트
오버페칭/언더페칭 문제필요한 데이터만 요청 가능
버전 관리 필요점진적 진화 가능
캐싱 내장수동 캐싱 구현 필요

GraphQL vs. REST: 실제 예시로 비교하기

REST와 GraphQL의 차이를 실제 상황을 통해 이해해보자.
블로그 앱에서 특정 게시물, 그 작성자 정보, 그리고 해당 게시물의 댓글들을 가져오고 싶다고 가정해보면:

REST 방식

REST API를 사용하면 다음과 같은 여러 요청이 필요할 수 있다:

  1. 게시물 가져오기: GET /posts/123
  2. 작성자 정보 가져오기: GET /users/456
  3. 댓글 가져오기: GET /posts/123/comments

각 요청은 해당 리소스의 모든 필드를 반환할 수 있으며, 이 중 일부만 필요한 경우에도 전체 데이터를 받게 된다.

GraphQL 방식

GraphQL을 사용하면 단일 요청으로 필요한 모든 데이터를 가져올 수 있다:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
{
  getPost(id: "123") {
    title
    content
    author {
      name
      email
    }
    comments {
      text
      author {
        name
      }
    }
  }
}

이 하나의 쿼리로 게시물, 작성자, 댓글 정보 중 필요한 특정 필드만 한 번에 가져올 수 있다.

주요 GraphQL 도구 및 라이브러리

GraphQL 생태계는 다양한 도구와 라이브러리를 제공한다.

서버 측 라이브러리

  1. Apollo Server: 가장 인기 있는 GraphQL 서버 구현체로, Express, Fastify 등 다양한 Node.js 프레임워크와 통합된다.
  2. Graphql-js: Facebook에서 개발한 공식 JavaScript 참조 구현체이다.
  3. TypeGraphQL: TypeScript로 GraphQL 스키마를 정의할 수 있게 해주는 프레임워크이다.
  4. Nexus: 코드 우선 접근 방식의 GraphQL 스키마 정의 라이브러리이다.

클라이언트 측 라이브러리

  1. Apollo Client: 가장 널리 사용되는 GraphQL 클라이언트로, React, Vue, Angular 등 다양한 프레임워크와 통합된다.
  2. Relay: Facebook에서 개발한 GraphQL 클라이언트로, 성능 최적화에 중점을 둔다.
  3. urql: 경량화된 GraphQL 클라이언트로, 커스터마이징이 쉽다.

개발 도구

  1. GraphQL Playground/GraphiQL: API를 탐색하고 테스트할 수 있는 브라우저 기반 IDE.
  2. Apollo Studio (이전 Apollo Graph Manager): GraphQL API 개발, 모니터링, 분석을 위한 통합 플랫폼.
  3. PostGraphile: PostgreSQL 데이터베이스로부터 자동으로 GraphQL API를 생성한다.
  4. Hasura: PostgreSQL, MS SQL Server 등의 데이터베이스에서 실시간 GraphQL API를 자동 생성한다.
  5. Prisma: 데이터베이스 ORM으로, GraphQL API 개발을 단순화한다.

GraphQL 실제 구현 방법

GraphQL API를 구현하는 방법을 단계별로 살펴보자.
(JavaScript와 Node.js 환경을 기준)

1. 서버 설정 (Node.js + Express + Apollo Server)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
const express = require('express');
const { ApolloServer } = require('apollo-server-express');
const { typeDefs } = require('./schema');
const { resolvers } = require('./resolvers');

async function startServer() {
  const app = express();
  
  const server = new ApolloServer({
    typeDefs,
    resolvers,
  });
  
  await server.start();
  server.applyMiddleware({ app });
  
  app.listen({ port: 4000 }, () =>
    console.log(`서버 실행 중: http://localhost:4000${server.graphqlPath}`)
  );
}

startServer();

2. 스키마 정의

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// schema.js
const { gql } = require('apollo-server-express');

const typeDefs = gql`
  type Post {
    id: ID!
    title: String!
    content: String!
    author: User!
    comments: [Comment!]!
  }

  type User {
    id: ID!
    name: String!
    email: String!
    posts: [Post!]!
  }

  type Comment {
    id: ID!
    text: String!
    author: User!
    post: Post!
  }

  type Query {
    getPost(id: ID!): Post
    getUser(id: ID!): User
    getPosts: [Post!]!
  }

  type Mutation {
    createPost(title: String!, content: String!, authorId: ID!): Post!
    createComment(text: String!, postId: ID!, authorId: ID!): Comment!
  }
`;

module.exports = { typeDefs };

3. 리졸버 구현

리졸버는 GraphQL 쿼리에 대한 실제 데이터를 가져오는 함수:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
// resolvers.js
// 간단한 예시를 위한 인메모리 데이터
const posts = [
  { id: '1', title: 'GraphQL 소개', content: 'GraphQL은 API를 위한 쿼리 언어입니다...', authorId: '1' }
];

const users = [
  { id: '1', name: '홍길동', email: 'hong@example.com' }
];

const comments = [
  { id: '1', text: '좋은 글이네요!', authorId: '1', postId: '1' }
];

const resolvers = {
  // 객체 간 관계 해결
  Post: {
    author: (post) => users.find(user => user.id === post.authorId),
    comments: (post) => comments.filter(comment => comment.postId === post.id)
  },
  
  User: {
    posts: (user) => posts.filter(post => post.authorId === user.id)
  },
  
  Comment: {
    author: (comment) => users.find(user => user.id === comment.authorId),
    post: (comment) => posts.find(post => post.id === comment.postId)
  },
  
  // 쿼리 리졸버
  Query: {
    getPost: (_, { id }) => posts.find(post => post.id === id),
    getUser: (_, { id }) => users.find(user => user.id === id),
    getPosts: () => posts
  },
  
  // 뮤테이션 리졸버
  Mutation: {
    createPost: (_, { title, content, authorId }) => {
      const newPost = { 
        id: String(posts.length + 1), 
        title, 
        content, 
        authorId 
      };
      posts.push(newPost);
      return newPost;
    },
    
    createComment: (_, { text, postId, authorId }) => {
      const newComment = { 
        id: String(comments.length + 1), 
        text, 
        authorId, 
        postId 
      };
      comments.push(newComment);
      return newComment;
    }
  }
};

module.exports = { resolvers };

4. 클라이언트 측 구현 (React + Apollo Client)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
// App.js
import React from 'react';
import { ApolloClient, InMemoryCache, ApolloProvider, useQuery, gql } from '@apollo/client';

// Apollo 클라이언트 설정
const client = new ApolloClient({
  uri: 'http://localhost:4000/graphql',
  cache: new InMemoryCache()
});

// 게시물 쿼리 정의
const GET_POST = gql`
  query GetPost($id: ID!) {
    getPost(id: $id) {
      title
      content
      author {
        name
      }
      comments {
        text
        author {
          name
        }
      }
    }
  }
`;

// 게시물 컴포넌트
function Post({ id }) {
  const { loading, error, data } = useQuery(GET_POST, {
    variables: { id }
  });

  if (loading) return <p>로딩 ...</p>;
  if (error) return <p>오류: {error.message}</p>;

  const post = data.getPost;
  
  return (
    <div>
      <h1>{post.title}</h1>
      <p>작성자: {post.author.name}</p>
      <div>{post.content}</div>
      
      <h2>댓글</h2>
      {post.comments.map(comment => (
        <div key={comment.id}>
          <p>{comment.text}</p>
          <small>작성자: {comment.author.name}</small>
        </div>
      ))}
    </div>
  );
}

// 앱 컴포넌트
function App() {
  return (
    <ApolloProvider client={client}>
      <div className="App">
        <Post id="1" />
      </div>
    </ApolloProvider>
  );
}

export default App;

용어 정리

용어설명
오버페칭(Overfetching)오버페칭은 클라이언트가 요청한 데이터보다 불필요하게 많은 데이터를 서버로부터 받아오는 상황을 의미한다. 이로 인해 네트워크 리소스가 낭비되고 클라이언트 측에서 불필요한 데이터를 처리해야 하므로 성능 저하가 발생할 수 있다.

예시:
- 클라이언트가 사용자 이름만 필요하지만 API가 사용자 이름, 이메일, 주소 등 모든 정보를 반환하는 경우.
- 예를 들어, GET /users 요청 시 각 사용자에 대한 이름, 나이, 주소, 전화번호 등이 반환되지만 클라이언트는 이름과 이메일만 필요한 상황.

문제점:
- 네트워크 비용 증가: 불필요한 데이터 전송으로 인해 네트워크 트래픽이 증가.
- 응답 시간 증가: 과도한 데이터 처리로 인해 응답 시간이 길어짐.
- 클라이언트 처리 부담: 사용하지 않는 데이터를 필터링하거나 무시해야 하는 추가 작업 필요.
언더페칭(Underfetching)언더페칭은 클라이언트가 요청한 데이터가 충분하지 않아 추가적인 요청을 해야 하는 상황을 의미한다. 이는 하나의 엔드포인트로 필요한 모든 데이터를 가져오지 못해 여러 번의 API 호출이 필요하게 된다.

예시:
- 클라이언트가 사용자 이름과 친구 목록을 동시에 필요로 하지만 API가 사용자 이름만 반환하는 경우. 추가적으로 친구 목록을 가져오기 위해 별도의 요청이 필요.
- 예를 들어, GET /user 요청으로 사용자 ID만 받고, 친구 목록을 얻기 위해 추가적으로 GET /user/:id/friends를 호출해야 하는 상황.

문제점:
- 추가 요청 증가: 여러 번의 API 호출로 인해 네트워크 트래픽이 증가.
- 응답 지연: 필요한 데이터를 모두 얻기까지 시간이 더 오래 걸림.
- 복잡한 코드 작성 필요: 클라이언트 측에서 여러 요청을 관리하고 결합해야 함.
인트로스펙션(Introspection)GraphQL 서버가 자신의 스키마(schema), 타입(types), 필드(fields), 그리고 관계를 클라이언트가 쿼리할 수 있도록 하는 기능을 의미한다. 이를 통해 개발자는 API의 구조를 탐색하고 필요한 데이터를 효율적으로 요청할 수 있다. 인트로스펙션은 GraphQL의 핵심적인 특징 중 하나로, API를 **자체 문서화(self-documenting)**하고 개발자 경험을 크게 향상시킨다.

참고 및 출처