[실습] GraphQL API 만들기 - 초기세팅

기운찬곰·2020년 9월 11일
0

GraphQL

목록 보기
5/6
post-thumbnail

시작하기에 앞서서...

지금까지 GraphQL이 무엇이고, 쿼리와 스키마에 대해 알아봤습니다. 이제 GraphQL API를 직접 만들어볼 준비는 다 끝난거 같습니다.


🚀 Apollo란?

공식문서 : https://www.apollographql.com/docs/

Apollo는 애플리케이션 클라이언트 (예 : React 및 iOS 앱)를 백엔드 서비스에 원활하게 연결하는 통신 계층인 데이터 그래프를 구축하기위한 플랫폼입니다.

크게 Apollo Server와 Apollo Client로 나눠지며 Client는 React, Android, iOS로 나눠서 문서화 되어있습니다. 저는 여기서 Apollo Server를 사용할 예정입니다.

Apollo Server


Apollo Server는 Apollo Client를 포함한 모든 GraphQL 클라이언트와 호환 되는 오픈 소스 사양 준수 GraphQL 서버 입니다 . 모든 소스의 데이터를 사용할 수있는 프로덕션 준비가 완료된 자체 문서화 GraphQL API를 구축하는 가장 좋은 방법입니다.


🪁 프로젝트 세팅

먼저 Apollo Server를 시작하려면 apollo-server및 graphql를 설치해야 합니다. 추가적으로 nodemon도 설치하겠습니다.

$ npm init -y
$ npm install apollo-server graphql nodemon

여기서는 생략했지만 import/export 구문을 사용하기 위해 바벨도 설치한다음 설정해줬습니다.


🌹 실습

리졸버(Resolver)

리졸버는 특정 필드의 데이터를 반환하는 함수입니다. 스키마에 정의된 타입과 형태에 따라 데이터를 반환합니다.

import { ApolloServer } from "apollo-server";

// 스키마
const typeDefs = `
    type Query {
        totalPhotos: Int!
    }
`;

// 리졸버
const resolvers = {
  Query: {
    totalPhotos: () => 42,
  },
};

// 서버 인스턴스 생성
const server = new ApolloServer({
  typeDefs,
  resolvers,
});

// 서버 구동
server
  .listen()
  .then(({ url }) => console.log(`GraphQL Service running on ${url}`));

쉽게 말해 스키마는 정의를 하는 부분이고, 실제 데이터를 반환하고 처리하는 일은 리졸버가 하게 됩니다. 그래서 직접 DB와 연동하거나 다른 API를 요청하거나 메모리에 접근하거나 해서 데이터를 가져오게 합니다. (참고로 스키마 타입과 리졸버 함수이름은 동일해야 합니다)

서버를 동작시키고 쿼리를 실행시켜보면 totalPhotos는 42를 반환하도록 리졸버에서 설정했기때문에 42가 출력되는것을 볼 수 있습니다.


루트리졸버 - 뮤테이션 사용

GraphQL API는 Query, Mutation, Subscription 루트 타입을 가집니다. 이 세 타입은 최상단 레벨에 위치하며 이들을 통해 사용가능한 모든 API 엔트리포인트를 표현할 수 있습니다.

방금처럼 Query 타입에 totalPhotos 필드를 추가했기때문에 해당 필드에 대한 쿼리 작업을 수행할 수 있었습니다. 이번에는 Mutation을 사용해보도록 하겠습니다.

const photos = [];

const typeDefs = `
    type Query {
        totalPhotos: Int!
    }
    
    type Mutation {
        postPhoto(name: String! description: String!): Boolean!
    }
`;

const resolvers = {
  Query: {
    totalPhotos: () => photos.length,
  },

  Mutation: {
    postPhoto: (parent, args) => {
      photos.push(args);
      return true;
    },
  },
};

역시나 마찬가지로 스키마를 정의한 다음 리졸버를 통해 구체적인 동작을 구현하게 됩니다. 여기서는 간단하게 구현하는 목적이기 때문에 DB까지는 쓰지 않고 photos라는 변수에다가 저장을 하도록 했습니다.

특별하게도 이번에는 parent, args 같은 함수 인자가 보입니다. 우선, 모든 GraphQL 리졸버 함수는 실제로 4개의 인자를 입력받는다는 사실을 꼭 기억하시기 바랍니다. 그 중에 두가지를 여기서 배워볼 예정입니다.

첫번째는 일단 건너뛰고 두번째 인자 args 부터 알아보겠습니다. args는 전달된 인자를 담고 있습니다. 지금의 경우라면 {name, description} 이 args가 되겠습니다. 실행시켜보니 추가가 잘 된것을 볼 수 있습니다.


변수 사용하기

mutation newPhoto($name: String!, $description: String!) {
  postPhoto(name: $name, description: $description)
}
{
  "name": "test",
  "description": "test photos"
}


타입 리졸버

let _id = 0;
const photos = [];

const typeDefs = `
    type Photo {
        id: ID!
        url: String!
        name: String!
        description: String
    }

    type Query {
        totalPhotos: Int!
        allPhotos: [Photo!]!
    }
    
    type Mutation {
        postPhoto(name: String! description: String!): Photo!
    }
`;

const resolvers = {
  Query: {
    totalPhotos: () => photos.length,
    allPhotos: () => photos,
  },

  Mutation: {
    postPhoto: (parent, args) => {
      _id += 1;
      const newPhoto = {
        id: _id,
        ...args,
      };
      photos.push(newPhoto);
      return newPhoto;
    },
  },
};

Photo 타입을 추가했습니다. allPhotos가 반환하는 타입은 Photo리스트이고, postPhoto 뮤테이션을 실행하고 난 반환타입도 Photo입니다. 앞으로는 이 규칙을 지켜서 반환해줘야 합니다.

mutation newPhoto($name: String!, $description: String!) {
  postPhoto(name: $name, description: $description) {
    id
    name
    description
  }
}

phostPhoto를 실행하고 난 다음 반환타입으로 url을 넣으면 어떻게 될까요? 아래처럼 에러메시지가 나옵니다. ! 이기 때문에 null인 값은 넘겨줄 수 없다는 뜻이 됩니다.

"message": "Cannot return null for non-nullable field Photo.url.",

해결방법은 리졸버에서 아래와 같이 추가해주면 됩니다. 원래 id, name, description도 적어줘야 하지만 생략해도 상관없습니다.

  Photo: {
    url: (parent) => `http://yoursite.com/img/${parent.id}.jpg`,
  }

위에서 parent (또는 root) 라고 불리는 첫번째 인자는 바로 직전 리졸버 실행 수준에서의 결과값이라고 했습니다.

첫번째 수준에서는 allPhotos 리졸버를 호출하고 photos에 포함된 모든 데이터를 반환합니다. 두번째 실행 수준에서는 각각의 항목들에 대하여 Photos 타입의 리졸버를 호출합니다. 따라서 parent는 첫번째 수준인 photos 리스트의 각 항목을 가리키게 됩니다.


인풋 & 열거 타입 사용하기

const typeDefs = `
    enum PhotoCategory {
        SELFIE
        PORTRAIT
        LANDSCAPE
        GRAPHIC
    }

    type Photo {
        id: ID!
        url: String!
        name: String!
        description: String
        category: PhotoCategory!
    }

    type Query {
        totalPhotos: Int!
        allPhotos: [Photo!]!
    }

    input PostPhotoInput {
        name: String!
        category: PhotoCategory=PORTRAIT
        description: String
    }
    
    type Mutation {
        postPhoto(input: PostPhotoInput!): Photo!
    }
`;

const resolvers = {
  Query: {
    totalPhotos: () => photos.length,
    allPhotos: () => photos,
  },

  Mutation: {
    postPhoto: (parent, args) => {
      _id += 1;
      const newPhoto = {
        id: _id,
        ...args.input,
      };
      photos.push(newPhoto);
      return newPhoto;
    },
  },

  Photo: {
    url: (parent) => `http://yoursite.com/img/${parent.id}.jpg`,
  },
};

PhotoCategory 라는 열거형 타입을 만들었는데 이는 category는 반드시 저 4가지 중에 한가지 값이 되게 됩니다.

input 은 입력받는 값이 많아질때 분리시키기 위해 사용합니다. 또한 이렇게 되면 재사용도 가능해지기 때문에 잘만 사용하면 유용하게 사용할 수 있습니다.

mutation newPhoto($input: PostPhotoInput!) {
  postPhoto(input: $input) {
    id
    name
    description
    category
  }
}
// Query Variables
{
  "input": {
    "name": "test",
    "description": "test photos"
  }
}


마침

일단 어떻게 쓰는지 실습을 해보니까 대략 감은 잡혔습니다. 양이 좀 많은 관계로 여기서 한번 끊고 다음시간에 이어서 진행해보도록 하죠.


참고자료 및 사이트

Hits

profile
배움을 좋아합니다. 새로운 것을 좋아합니다.

0개의 댓글