resolver(리졸버)

차분한열정·2022년 3월 23일
0

GraphQL

목록 보기
3/7

1. 기초

A resolver is a function that's responsible for populating the data for a single field in your schema.

GraphQL의 핵심 개념이다. 프론트엔드에서 특정 필드를 요구할 때 해당 필드를 담당하는 리졸버가 해당 필드의 값을 구해오는 것이다.

리졸버는

(1) 해당 필드의 타입에 해당하는 값
또는
(2) (1)의 결과로 fulfilled될 Promise 객체
를 리턴한다.

리졸버의 함수 시그니처는 다음과 같다.

filedName: (parent, args, context, info) => data;

(1) parent: 이 필드의 부모 필드의 리졸버가 리턴한 값이다. (부모 필드에 대한 리졸버가 자식 필드에 대한 리졸버보다 먼저 실행된다. 예를 들어

query character(id: $id) {
	weapon {
    	name
        price
    }
}

이런 식으로 있고

  • character에 대한 리졸버
  • weapon에 대한 리졸버

가 있다고 할 때 weapon에 대한 리졸버의 parent에는 character에 대한 리졸버 함수가 리턴한 캐릭터 객체가 담기게 되는 것이다. 아래의 리졸버 체인의 관점에서 이야기하면 리졸버 체인에서 이전에 있는 리졸버가 리턴한 객체를 의미한다.

(2) args: 이 필드를 위해 제공된 GraphQL arguments 객체로 예를 들어 query{ user(id: "4") } 이런 오퍼레이션을 한다면 args 객체로는 { "id": "4" }가 설정된다.
(3) context: 모든 리졸버들 사이에서 공유되는 객체로 보통 각 작업마다의 상태 또는 인증 정보, 각 데이터 소스로의 접근(데이터로더 등) 등을 포함시켜서 사용한다. 리졸버는 이 context 객체를 굳이 수정하면 안 된다. ApolloServer에서는 context 초기화 함수를 그 생성자에 다음과 같이 집어넣어서 사용할 수 있다.


// Constructor
const server = new ApolloServer({
  typeDefs,
  resolvers,
  context: ({ req }) => ({
    authScope: getScope(req.headers.authorization)
  })
}));

// Example resolver
(parent, args, context, info) => {
  if(context.authScope !== ADMIN) throw new AuthenticationError('not admin');
  // Proceed
}

(4) info: 작업의 실행 상태에 대한 정보

예를 들어 이런 리졸버들이 있다고 할 때

const resolvers = {
  Query: {
    numberSix() {
      return 6;
    },
    numberSeven() {
      return 7;
    }
  }
}

이러한 리졸버들이 정의된 하나의 Javascript 객체를 resolver map(여기서는 resolvers 객체)이라고 한다. 보통 이렇게 하나의 resolver map으로 간단하게 끝나는 경우는 드물고, 실제로는 각각의 도메인별로 나눠진 파일에 resolver들을 정의하고 이것들을 하나의 resolver map으로 추출해서

const server = new ApolloServer({ typeDefs, resolvers });

이런 식으로 ApolloServer 생성자의 인수로 넣는다.

2. 필드 리졸버 정의하기

리졸버를 사용하다 보면 우리가 타입에서는 정의했지만 리졸버를 작성하지 않은 필드인데도 에러 없이 존재하는 필드만 응답에 잘 오는 것을 알 수 있는데 이것은 우리가 별도로 리졸버를 정의하지 않은 경우에는 Apollo Server가 default resolver를 실행하기 때문이다.

리졸버의 인자로 들어온 parent 객체가 특정 리졸버의 이름과 일치하는 property를 갖고 있는가?
(갖고있지 않음): undefined를 리턴
(갖고 있음): 해당 property의 값이 함수인가?

(함수가 아님): 그 값을 리턴
(함수임): 그 함수를 실행하고 해당 리턴값을 반환

따라서 우리는 보통 기본적으로 Query, Mutation 타입에 각각의 리졸버를 정의할 뿐만 아니라 흔히 그 리턴 타입의 각 필드에 대해서도 단순 값이 바로 있는 게 아니라 한번 더 뭔가 작업(추가 데이터 조회)을 해서 값을 할당해야하는 경우 별도의 리졸버를 정의하는 경우가 많다. 이를 보통 필드 리졸버(Field Resolver)라고 한다.

정리하면 필드 이름과 일치하는 리졸버 함수가 딱히 정의되지 않은 필드라면 해당 값을 그대로, 리졸버 함수가 있다면 해당 리졸버 함수가 리턴한 값을 세팅하는 것이다.

3. resolver chain

GraphQL 스키마에서 특정 필드를 보면 Object type -> Object type 등으로 계속 이어질 수가 있다. 이렇게 되면 가장 끝까지 resolve하게 되는데 다음 예시를 보면 바로 이해할 수 있다.

const { ApolloServer, gql } = require('apollo-server');

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

// The branch field of a book indicates which library has it in stock
const books = [
  {
    title: 'The Awakening',
    author: 'Kate Chopin',
    branch: 'riverside'
  },
  {
    title: 'City of Glass',
    author: 'Paul Auster',
    branch: 'downtown'
  },
];

// Schema definition
const typeDefs = gql`

# A library has a branch and books
  type Library {
    branch: String!
    books: [Book!]
  }

  # A book has a title and author
  type Book {
    title: String!
    author: Author!
  }

  # An author has a name
  type Author {
    name: String!
  }

  # Queries can fetch a list of libraries
  type Query {
    libraries: [Library]
  }
`;

// Resolver map
const resolvers = {
  Query: {
    libraries() {

      // Return our hardcoded array of libraries
      return libraries;
    }
  },
  Library: {
    books(parent) {

      // Filter the hardcoded array of books to only include
      // books that are located at the correct branch
      return books.filter(book => book.branch === parent.branch);
    }
  },
  Book: {

    // The parent resolver (Library.books) returns an object with the
    // author's name in the "author" field. Return a JSON object containing
    // the name, because this field expects an object.
    author(parent) {
      return {
        name: parent.author
      };
    }
  }

  // Because Book.author returns an object with a "name" field,
  // Apollo Server's default resolver for Author.name will work.
  // We don't need to define one.
};

// Pass schema definition and resolvers to the
// ApolloServer constructor
const server = new ApolloServer({ typeDefs, resolvers });

// Launch the server
server.listen().then(({ url }) => {
  console.log(`🚀  Server ready at ${url}`);
});

이런 서버가 실행될 때

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

이 operation을 수행하면 어떤 순서로 resolve가 될지 자연스럽게 그림을 그려볼 수 있다.

그런데 만약

query GetBooksByLibrary {
  libraries {
    books {
      title  // !!!
      author {
        name
      }
    }
  }
}

이런 식으로 title이라고 하는 필드가 추가되면 어떨까? 이렇게 되면 book 객체에서 title 값을 리졸브하는 동작과, author를 리졸브 하는 동작이 동시에(parallel) 실행된다.

4. 리졸버 관련 주의사항

(1) 만약 스키마에서 특정 필드가 nullable로 정의되어 있으면 null이 세팅되어 내려갈 수가 있는데 만약 해당 필드가 not nullable이라면 해당 필드로부터 시작해서 리졸버 체인을 쭉 따라 올라가면서 nullable인 필드를 만날 때까지 올라가서 해당 nullable 필드를 null로 설정해버린다. 이렇게 되면 응답의 errors 프로퍼티가 이것 관련된 에러 정보로 세팅된다.

(2) 리졸버는 특정 타입의 값이 아니라 그 값으로 resolve될 Promise 객체를 리턴해도 된다.

(3)

profile
성장의 기쁨

0개의 댓글

관련 채용 정보