쿼리 실행 과정

모든 GraphQL 쿼리는 parsed, validated, executed 과정을 통해 실행이 된다.

 

먼저 GraphQL 쿼리의 기본적인 형태는 다음과 같다.

 

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

 

이 쿼리를 실행시키기 전에 파싱하여 AST(Abstract Syntax Tree)로 만드는 작업을 거친다. Babel이나 EsLint도 해당 작업을 거친다. 파싱된 AST는 다음과 같다.

 

 

그 이후에는 해당 쿼리가 유효한지 validation 과정을 거친다. 스키마에서 정의된 필드가 없거나 잘못 됐을 경우 등을 검증하는 과정이다. 그리고 AST를 기반으로 트리의 루트부터 리졸버를 실행시킨다. 그리고 리졸버에서 리턴하는 결과물을 통해 JSON 형태로 만든다.

 

리졸버

리졸버는 스키마에 작성한 타입의 필드 값들을 정의한다. 즉, 특정 필드의 데이터를 반환하는 함수이다.

스키마는 객체를 리턴할 수도 있고, String, Int, Boolean 등과 같이 scalar 값들도 리턴할 수 있다. 객체가 리턴 될 경우에는 자식 필드(child field)로 체이닝 되면서 리졸버가 실행되고, scalar가 리턴 될 경우 실행이 완료된 것으로 간주한다. 또한 null이 리턴 되어도 종료된다.

또한 리졸버는 비동기적으로 실행이 되며, DB 등 여러 데이터 소스에 접근이 가능하다.

 

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

type User {
  id: ID!
  age: Int
  name: String
}

 

위와 같이 스키마가 정의되어 있다고 하자. 그리고 리졸버를 다음과 같이 작성할 수 있다.

 

const resolvers = {
  Query: {
    user(parent, args, context, info) {
      return users.find(user => user.id === args.id)
    }
  }
}

 

가장 기본적인 리졸버의 형태이다. 리졸버 함수를 살펴보면 네 개의 인자를 받고 있는 것을 볼 수 있다.

이처럼 모든 리졸버는 parent, args, context, info 네 개의 인자를 받는다. parent는 이전/부모 필드의 결과 값이 담겨 있고 이를 통해 체이닝이 일어난다. args는 필드로 넘겨진 인자 값들이 담겨 있으며 주로 비구조화 할당을 통해 사용한다. context는 모든 리졸버 함수에 제공되는 mutable한 객체가 담겨있다. 마지막으로 info는 쿼리와 관련된 특정 필드에 대한 정보가 담겨있다.

 

Default Resolver

const users = [
  {
    id: '1',
    age: 21,
    name: 'mori nana',
  },
  {
    id: '2',
    age: 23,
    name: 'hirose suzu',
  }
];

const resolver = {
  Query: {
    user(parent, args, context, info) {
      return users.find(user => user.id === args.id)
    }
  },
  User: {
    age(parent, args, context, info) {
      return parent.age
    },
    name(parent, args, context, info) {
      return parent.name
    },
  }
}

 

GraphQL 서버는 모든 필드에 대해 default resolver를 만들어 둔다. default resolver는 parent객체에서 필드와 같은 이름을 가진 프로퍼티를 찾아 리턴한다.

 

리졸버 체인

공식 문서에 있는 예시를 보면서 이해를 해보자.

 

const libraries = [
  {
    branch: 'downtown'
  },
  {
    branch: 'riverside'
  },
];

const books = [
  {
    title: 'The Awakening',
    author: 'Kate Chopin',
    branch: 'riverside'
  },
  {
    title: 'City of Glass',
    author: 'Paul Auster',
    branch: 'downtown'
  },
];

const typeDefs = gql`
  type Library {
    branch: String!
    books: [Book!]
  }

  type Book {
    title: String!
    author: Author!
  }

  type Author {
    name: String!
  }

  type Query {
    libraries: [Library]
  }
`;

const resolvers = {
  Query: {
    libraries() {
      return libraries;
    }
  },
  Library: {
    books(parent) {
      return books.filter(book => book.branch === parent.branch);
    }
  },
  Book: {
    author(parent) {
      return {
        name: parent.author
      };
    }
  }
};

 

libraries, books와 같이 필요한 데이터들은 하드코딩 했다. 스키마와 리졸버 위와 같은 상태에서 아래와 같은 쿼리를 실행시켜본다고 하자.

 

query GetBooksByLibrary {
  libraries {
    books {
      author {
        name
      }
    }
  }
}

 

쿼리가 실행이 되면 루트 쿼리에 대한 리졸버가 실행이 될 것이고, 위에서 libraries가 리턴이 될 것이다. 리턴된 libraries객체는 Library타입 리졸버의 parent에 담기게 되고, books필드에서 참조할 수 있다. 똑같은 방식으로 books에서 리턴한 객체는 Bookauthor필드에서 parent로 참조가 가능하다. 마지막으로 author필드에서 name프로퍼티를 가진 객체를 리턴하는 것을 볼 수 있는데, 이렇게 되면 위에서 설명한 Author타입의 name필드의 default resolver가 parent객체의 name프로퍼티 값을 리턴하게 된다. 그리고 String 형태인 Scalar 값이 리턴되었으므로 리졸버 체인이 종료된다.

 

 

 

참고

'GraphQL' 카테고리의 다른 글

[GraphQL] React + GraphCMS (react-apollo)  (0) 2020.07.10

생강강

,