GraphQL 공부: 개념과 예제 2

1. 스키마 설계 심화

GraphQL 스키마(Schema)는 서버가 제공하는 타입과 관계를 선언적으로 정의한다. 스키마가 탄탄해야 클라이언트가 안정적으로 데이터를 요청할 수 있다.

1.1 스칼라와 객체 타입

  • 스칼라(Scalar): Int, Float, String, Boolean, ID와 같이 더 이상 분해되지 않는 기본 타입.
  • 객체(Object): 여러 필드로 이루어진 타입. 예: type User { id: ID!, name: String! }

1.2 리스트와 널

  • [User]는 유저 목록을 의미한다.
  • User!는 값이 반드시 존재해야 함을 나타낸다.
  • [User!]!처럼 중첩하면 “목록도 널이 아니고, 각 항목도 널이 아님”을 표현한다.

1.3 입력 타입과 열거형

enum Role {
  USER
  ADMIN
}

input UserInput {
  name: String!
  role: Role = USER
}

type User {
  id: ID!
  name: String!
  role: Role!
}

입력 타입(Input Type)은 Mutation에서 주로 사용되며, 서버로 전달되는 데이터를 검증하는 데 도움을 준다.

2. Arguments와 Variables

필드는 인자(arguments)를 받을 수 있고, 클라이언트는 변수를 이용해 동적으로 값을 전달할 수 있다.

2.1 인자 정의

type Query {
  user(id: ID!): User
}

2.2 쿼리에서 직접 전달

{
  user(id: 1) {
    id
    name
  }
}

2.3 변수 사용법

query GetUser($userId: ID!) {
  user(id: $userId) {
    id
    name
  }
}

변수는 JSON 형태로 별도 전달한다.

{ "userId": 1 }

GraphiQL이나 Apollo Client에서는 QueryVariables 탭을 나눠 입력하면 된다.

3. Alias, Fragment, Directive

3.1 Alias로 결과 이름 변경

{
  admin: user(id: 1) {
    name
  }
  guest: user(id: 2) {
    name
  }
}

adminguest는 서버 타입과 무관하게 클라이언트가 원하는 이름을 지정할 수 있다.

3.2 Fragment로 필드 재사용

fragment UserFields on User {
  id
  name
  role
}

{
  me: user(id: 1) {
    ...UserFields
  }
  you: user(id: 2) {
    ...UserFields
  }
}

3.3 Directive로 조건부 요청

query ($withRole: Boolean!) {
  user(id: 1) {
    name
    role @include(if: $withRole)
  }
}

변수로 { "withRole": true }를 주면 role 필드가 포함되고, false면 제외된다. 반대로 @skip을 사용하면 특정 조건에서만 생략할 수도 있다.

4. Resolver와 Context

Resolver는 실제 데이터를 반환하는 함수다. 각 필드마다 Resolver를 정의하며, 필요 시 비동기 처리도 가능하다.

const root = {
  users: async (): Promise<User[]> => {
    return await db.selectAllUsers();
  },
  addUser: ({ input }: { input: UserInput }, context) => {
    if (!context.isAdmin) throw new Error('권한 없음');
    return db.insertUser(input);
  }
};

context는 인증 정보나 데이터베이스 커넥션 등 요청 전반에서 공유할 값을 담는다.

app.use(
  '/graphql',
  graphqlHTTP((req) => ({
    schema,
    rootValue: root,
    context: { isAdmin: req.headers['x-admin'] === '1' }
  }))
);

5. 예제: 사용자 관리 서버

아래는 앞서 설명한 내용을 조합한 간단한 서버 예제다.

import express from 'express';
import { graphqlHTTP } from 'express-graphql';
import { buildSchema } from 'graphql';

interface User {
  id: number;
  name: string;
  role: 'USER' | 'ADMIN';
}

const schema = buildSchema(`
  enum Role { USER ADMIN }
  input UserInput { name: String!, role: Role }
  type User { id: ID!, name: String!, role: Role! }
  type Query { users: [User!]!, user(id: ID!): User }
  type Mutation { addUser(input: UserInput!): User! }
`);

const users: User[] = [
  { id: 1, name: 'Alice', role: 'ADMIN' },
  { id: 2, name: 'Bob', role: 'USER' }
];

const root = {
  users: (): User[] => users,
  user: ({ id }: { id: number }): User | undefined =>
    users.find((u) => u.id == id),
  addUser: ({
    input
  }: {
    input: { name: string; role?: 'USER' | 'ADMIN' };
  }): User => {
    const newUser: User = {
      id: users.length + 1,
      name: input.name,
      role: input.role ?? 'USER'
    };
    users.push(newUser);
    return newUser;
  }
};

const app = express();
app.use('/graphql', graphqlHTTP({ schema, rootValue: root, graphiql: true }));
app.listen(4000, () => console.log('Server at http://localhost:4000/graphql'));

5.1 Mutation 실행 예시

curl -X POST -H "Content-Type: application/json" \
  -d '{"query":"mutation($name:String!){ addUser(input:{name:$name}) { id name role }}","variables":{"name":"Carol"}}' \
  http://localhost:4000/graphql

5.2 Alias와 Fragment 활용

{
  first: user(id: 1) {
    ...UserFields
  }
  second: user(id: 2) {
    ...UserFields
  }
}

fragment UserFields on User {
  id
  name
  role
}

6. 프런트엔드에서 GraphQL 요청하기

GraphQL은 프런트엔드에서도 간단하게 호출할 수 있다. 브라우저의 fetch API나 Apollo Client 같은 라이브러리를 활용하면 된다.

6.1 fetch API 사용 예시

const query = `
  query ($id: ID!) {
    user(id: $id) {
      id
      name
      role
    }
  }
`;

async function fetchUser(id) {
  const res = await fetch('/graphql', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      query,
      variables: { id }
    })
  });
  const { data } = await res.json();
  return data.user;
}

fetchUser(1).then(console.log);

6.2 Apollo Client로 관리형 상태 구성

npm install @apollo/client graphql


import { ApolloClient, InMemoryCache, gql } from '@apollo/client';

const client = new ApolloClient({
  uri: '/graphql',
  cache: new InMemoryCache()
});

client
  .query({
    query: gql`
      query ($id: ID!) {
        user(id: $id) {
          id
          name
          role
        }
      }
    `,
    variables: { id: 1 }
  })
  .then(({ data }) => console.log(data.user));

Reference

2025년 08월 21일에 수정됨
YUNSU BAE

YUNSU BAE

주니어 웹 개발자 배윤수 입니다!

예술의 영역을 동경하고 있어요. 🧑‍🎨