반응형
snowman95
코딩수련장
snowman95
전체 방문자
오늘
어제
  • 분류 전체보기 (229)
    • 앱테크 (3)
    • 옵시디언 (5)
    • 드라마, 영화 (1)
    • 개발자 이야기 (23)
    • 프로젝트 (10)
      • 프로젝트 방법론 (7)
      • 프로젝트 기록 (2)
      • Github (1)
    • 개발 지식 (0)
      • 디자인 패턴 (0)
    • 프론트엔드 개발 (5)
      • 테크트리 (2)
      • React.js (19)
      • ReactNative (2)
      • Next.js (6)
      • GraphQL (6)
      • 패키지 매니저 (2)
      • 라이브러리 (3)
      • 상태관리 라이브러리 (4)
      • Web 지식 (3)
      • HTML CSS (26)
      • Javascript (16)
      • 도구 (Tool) (3)
      • 성능 최적화 (1)
      • 디자인시스템 (0)
    • Python (53)
      • 모음집 (1)
      • 문법 (12)
      • 라이브러리 (15)
      • 알고리즘 (10)
      • 백준 문제풀이 (9)
      • 코딩테스트 (2)
      • 도구 (Tool) (3)
    • C++ (20)
      • 알고리즘 (6)
      • 삼성SW기출 (6)
      • 삼성 A형 (6)
    • 데이터사이언스 (1)
    • 인프라 (9)
      • 하드웨어 지식 (4)
      • Ansible (2)
      • Database (2)
      • 쉘스크립트 (1)
    • 주식 (0)
    • 취업 준비 (4)
      • 취업 이야기 (0)

블로그 메뉴

  • 홈
  • 태그

공지사항

인기 글

태그

  • 나의 해방일지
  • 백준
  • 전공요약
  • Next.js #graphql #tailwind.css
  • 오블완
  • A형
  • GraphQL
  • 면접
  • 삼성SDS
  • 공간복잡도
  • 전공 요약 #데이터베이스
  • 티스토리챌린지
  • 언어
  • 삼성SW역량테스트
  • 기계식키보드 #nuphy
  • C++
  • nextjs
  • 알고리즘
  • 전공 요약 #네트워크
  • 전공 요약 #운영체제

최근 댓글

최근 글

티스토리

hELLO · Designed By 정상우.
snowman95

코딩수련장

Apollo graphql 스키마와 리졸버에 대해 알아보자. 사용법
프론트엔드 개발/GraphQL

Apollo graphql 스키마와 리졸버에 대해 알아보자. 사용법

2023. 2. 12. 17:57
728x90
반응형

GraphQL

  • API용 쿼리 언어이며 데이터에 대해 정의한 유형 시스템을 사용하여 쿼리를 실행하기 위한 서버 측 런타임입니다. 
  • 특정 데이터베이스 또는 스토리지 엔진에 연결되지 않으며 대신 기존 코드 및 데이터로 지원됩니다.
  • 필요한 것을 요청하고 정확히 가져옵니다.
  • 단일 요청으로 많은 리소스 가져올 수 있습니다.

 

구조 (Apollo 사용)

일단 자세한 설명에 앞서서 전체 구조가 이런 식으로 돌아간다는 이해하시고 읽으면 이해가 더 잘 될 겁니다.

 

프론트엔드 : Apollo Client 를 통해 스키마의 앤드포인트에서 필요한 데이터를 GraphQL 쿼리문으로 요청

백엔드 : Apollo Server 에 스키마, 리졸버, 데이터소스 를 구성

  • 리졸버 : 데이터소스에서 데이터 가져와서 스키마 필드를 채워서 응답을 보냄
  • 스키마 : 클라이언트에서 접근할 수 있는 앤드포인트(Query, Mutation) 의 타입을 정의
  • 데이터 소스 : 데이터베이스 및 타 REST API 등의 데이터
 

스키마

  • 서버와 클라이언트 간의 계약 입니다.
  • 백엔드 구현 세부 정보를 숨기면서 소비자에게 유연성을 제공하는 추상화 층입니다.

스키마 정의 언어 (SDL)

  • 스키마는 필드를 포함하는 객체 타입의 모음입니다.
  • 각 필드에는 고유한 타입이 있습니다.
  • 필드 타입은 스칼라(Int 또는 String) 이거나 다른 사용자 정의 객체 타입일 수 있습니다.
  • 쉽게 생각하면 타입스크립트의 interface 나 Type 정도로 생각하면 됩니다.
    • 똑같이 ParscalCase 로 작성합니다.
    • 그러나 쉼표로 구분되지 않으며, null 허용하지 않는 경우 필드 뒤에 느낌표를 붙여야 합니다. non nullable
    • 자바스크립트 원시 타입인 string, number 이 아니라 String, Int 등을 사용해야 합니다.
  • 주석을 달 수도 있습니다.
"""
여러 줄 주석
"""
type SpaceCat { // ParscalCase 로 작성합니다
  "한 줄 주석"
  name : String! // Non Nullable
  age: Int
  missions: [Mission]
}

EntryPoint (진입점)

  • 클라이언트가 접근할 수 있는 필드 입니다.
  • 조회 요청은 Query, 쓰기 요청은 Mutation 안에 작성합니다.
    • Mutation 의 경우 이름은 add, delete, create 같은 동사로 시작하는게 좋습니다.
  • 그 외에 선언된 type 은 모두 커스텀 타입 정도로 생각하면 됩니다.
  • gql 은 무엇인가요 ?
    • 스키마 정의와 같은 GraphQL 문자열을 래핑하는 데 사용되는 태그가 지정된 템플릿 리터럴입니다.
    • GraphQL 문자열을 Apollo 가 작업 및 스키마로 작업할 때 예상하는 형식으로 변환하고 구문 강조도 활성화 합니다.
const { gql } = require("apollo-server");

const typeDefs = gql`
  type Query { // 조회 요청에 대한 진입점
    "Query to get tracks array for the homepage grid"
    tracksForHome: [Track!]! // 이것이 진입점
  }
  
  type Mutation { // 쓰기 요청에 대한 진입점
    "Increment the number of views of a given track, when the track card is clicked"
    incrementTrackViews(id: ID!): IncrementTrackViewsResponse!
  }
  
  "A track is a group of Modules that teaches about a specific topic"
  type Track { // 이것은 커스텀 타입 정도로 생각
    id: ID!
    "The track's title"
    title: String!
  }
`

module.exports = typeDefs;

Mutation 요청의 에러 처리

Mutation 요청의 응답에는 클라이언트에 유용한 정보를 담아서 반환값으로 던져주어야 합니다.

  • code: Int HTTP 상태 코드와 유사한 응답의 상태를 나타내는 입니다.
  • success: Boolean 변이가 담당하는 모든 업데이트가 성공했는지 여부를 나타내는 플래그입니다.
  • message: String 클라이언트 측에서 변형 결과에 대한 정보를 표시합니다. 이것은 돌연변이가 부분적으로만 성공했고 일반적인 오류 메시지가 전체 내용을 말할 수 없는 경우에 특히 유용합니다.
type AssignMissionResponse {
  code: Int!
  success: Boolean!
  message: String!
  spacecat: Spacecat
  misson: Misson
}

Mutation 오류 해결을 위해 try catch 문으로 감싸주어야 합니다.

incrementTrackViews: async (_, {id}, {dataSources}) => {
  try {
    const track = await dataSources.trackAPI.incrementTrackViews(id);
    return {
      code: 200,
      success: true,
      message: `Successfully incremented number of views for track ${id}`,
      track
    };
  } catch (err) {
    return {
      code: err.extensions.response.status,
      success: false,
      message: err.extensions.response.body,
      track: null
    };
  }
},

 

인수 argument

  • 인수란 쿼리의 특정 필드에 제공하는 값입니다.
  • 인수를 통해 여러가지를 할 수 있습니다.
    • 특정 개체를 검색
    • 개체 집합을 통해 필터링
    • 필드의 반환 값을 변환
    • 사용자가 제출한 검색어를 사용
  • 아래와 같이 함수 만들듯 인수 넣어주면 됩니다.
type Query {
  spacecat(id: ID!): SpaceCat
  missions(to: String, scheduled: Boolean): [Mission]
}

리졸버는 2번째 매개변수인 args 를 구조분해해서 접근할 수 있습니다.

const resolvers = {
  Query: {
    spaceCat: (_, {id}, {dataSources}) => {
      return dataSources.spaceCatsAPI.getSpaceCat(id)
    }
  }
}

 

Operation 변수

클라이언트 측에서는 아래와 같이 $ 기호를 사용하여 GraphQL 변수를 작성할 수 있습니다.

$변수명 : 유형

query GetTrack($trackId: ID!) {
  track(id: $trackId){
    id,
    title
    modules {
      id
      title
      length
    }
  }
}

그리고 Varaiables 에 trackId 추가 하면 인자 넘겨줘서 쿼리 가능합니다.

{
  "trackId": "c_0"
}

프론트에서는 useQuery 의 두번째 매개변수의 옵션에 variables 로 인수를 넘겨줄 수 있습니다.

const GET_SPACECAT = gql`
  query getSpacecat(&spaceCatId: ID!) {
    spacecat(id: &spaceCatId){
      name
    }
  }
`

const {loading, error, data} = useQuery(GET_SPACECAT, {
  variables: { spaceCatId }
})

쓰기 요청에는 useMutation 을 사용하면 됩니다. useQuery 와 달리 반환값이 배열이라는 점에 주목해야 합니다!

const INCREMENT_TRACK_VIEWS = gql`
  mutation IncrementTrackViews($incrementTrackViewsId: ID!) {
    incrementTrackViews(id: $incrementTrackViewsId) {
      code
      success
      message
      track {
        id
        numberOfViews
      }
    }
  }
`;

const [incrementTrackViews, {loading, error, data}] = useMutation(INCREMENT_TRACK_VIEWS, {
    variables: { incrementTrackViewsId: id },
    // to observe what the mutation response returns
    onCompleted: (data) => {
     console.log(data);
    },
 });

 

 

리졸버 (resolver)

  • GraphQL의 장점은 여러 데이터 소스를 혼합하여 클라이언트 앱의 요구 사항을 충족하는 API를 생성할 수 있다는 것입니다. 바로 이 역할을 리졸버가 해줍니다.
  • 리졸버는 DataSource 에서 데이터를 가져온 다음 해당 데이터를 클라이언트가 요구하는 모양으로 변환하고 스키마의 필드에 대한 데이터를 채우는 역할을 합니다.
    • DataSource 는 데이터베이스, 타사 API, 웹후크 등 모든 종류의 위치를 의미합니다.
  • 주의할 점으로는 리졸버의 필드명을 스키마에 작성된 필드이름과 똑같이 작성해줘야 합니다.

리졸버 생성 함수

필드명: (parent, args, context, info) => {},

  • parent: parent
    • 이 필드의 부모 리졸버의 반환 값입니다. 리졸버 체인을 다룰 때 유용합니다.
  •  args: args
    • 특정 항목(예: 모든 id args는 GraphQL 작업에 의해 필드에 제공된 모든 GraphQL 인수를 포함하는 개체입니다. 트랙 이 아닌 특정 트랙)을 쿼리할 때 클라이언트 영역에서 서버 영역에서 이 매개 변수를 통해 액세스할 수 있는 인수를 사용하여 쿼리를 만듭니다. 이 내용은 Lift-off III에서 더 자세히 다루겠습니다.
  • 컨텍스트: contextRESTDataSource
    • 특정 작업을 위해 실행 중인 모든 리졸버에서 공유되는 객체입니다.  리졸버는 상태 공유 (인증 정보, 데이터베이스 연결 ) 또는 RESTDataSource 경우를 위해 이 context가 필요합니다
  • info: info
    • 필드 이름, 루트에서 필드까지의 경로 등 작업의 실행 상태에 대한 정보를 포함합니다. 다른 것만큼 자주 사용되지는 않지만 확인자 수준에서 캐시 정책을 설정하는 것과 같은 고급 작업에 유용할 수 있습니다.

매개변수 필요 없는 경우에는 아래와 같이 언더바(_) 처리를 하여 스킵해줍니다.

  • 처음 생략할 매개변수는 _ 처리 합니다.
  • 그 다음 매개변수는 __ 처리 합니다.
  • 이게 매개변수 순서가 중요한게 아니라 등장한 순서에 맞춰 언더바(_) 를 써주면 됩니다.
const resolvers = {
  Query: {
    tracksForHome1: (parent, args, context, info) => {}, 
    
    tracksForHome2: (_, __, { dataSources }, info) => {}, 
    
    tracksForHome3: (parent, _, { dataSources }) => {}, // 2번째 매개변수지만 1번째 생략이기 때문에 언더바가 1개
  },
};

RESTDataSource

  • 왜 필요한가요? 상황을 가정하여 설명해보겠습니다.
    • /tracks 엔드포인트가 100개의 트랙을 반환합니다.
    • 각 트랙의 작성자 정보를 얻으려면 /tracks/:id 엔드포인트를 100번 호출해야 합니다.
    • 모든 트랙의 작성자가 모두 같은 저자라면 똑같은 엔드포인트를 100번 호출하는 것입니다.
  •  이런 비효율적인 문제를 해결하기 위해 캐싱 전략을 세워야 합니다.
    • Apollo 전용 DataSource클래스인 RESTDataSource 를 백엔드 서버에 구현하면 REST API 호출에 대한 리소스 캐싱 및 중복 제거를 처리해줍니다.
    • 또한 전용 클래스에서 데이터 가져오기 구현을 유지하고 리졸버를 단순하고 깨끗하게 유지하기 위해 사용합니다.
  • 설치 : npm install apollo-datasource-rest
const { RESTDataSource } = require("apollo-datasource-rest");

class TrackAPI extends RESTDataSource {
  constructor() {
    super();
    this.baseURL = "https://odyssey-lift-off-rest-api.herokuapp.com/";
  }
}
module.exports = TrackAPI;

리졸버와 데이터 소스 연결

// /server/src/index.js

const { ApolloServer } = require('apollo-server');
const typeDefs = require('./schema');
const resolvers = require('./resolvers');
const TrackAPI = require('./datasources/track-api');

const server = new ApolloServer({
  typeDefs,
  resolvers,
  dataSources: () => {
    return {
      trackAPI: new TrackAPI(),
    };
  },
});

에러가 발생하는 경우

  • 잘못된 Query 를 날리면 errors 배열이 반환됩니다.
  • 발생한 오류들이 담긴 배열입니다.
  • 오류코드 참고 https://www.apollographql.com/docs/apollo-server/v3/data/errors/#error-codes
const response = {
  "errors": [
    {
      "message": "Cannot query field \"numberOfViews\" on type \"Query\".",
      "extensions": {
        "code": "GRAPHQL_VALIDATION_FAILED",
        "exception": {
          "stacktrace": [
            "GraphQLError: Cannot query field \"numberOfViews\" on type \"Query\".",
            "    at Object.Field (/Users/mac-rent/Documents/GitHub/odyssey-lift-off-part2/server/node_modules/graphql/validation/rules/FieldsOnCorrectTypeRule.js:48:31)",
            "    at Object.enter (/Users/mac-rent/Documents/GitHub/odyssey-lift-off-part2/server/node_modules/graphql/language/visitor.js:323:29)",
            "    at Object.enter (/Users/mac-rent/Documents/GitHub/odyssey-lift-off-part2/server/node_modules/graphql/utilities/TypeInfo.js:370:25)",
            "    at visit (/Users/mac-rent/Documents/GitHub/odyssey-lift-off-part2/server/node_modules/graphql/language/visitor.js:243:26)",
            "    at validate (/Users/mac-rent/Documents/GitHub/odyssey-lift-off-part2/server/node_modules/graphql/validation/validate.js:69:24)",
            "    at validate (/Users/mac-rent/Documents/GitHub/odyssey-lift-off-part2/server/node_modules/apollo-server-core/dist/requestPipeline.js:186:39)",
            "    at processGraphQLRequest (/Users/mac-rent/Documents/GitHub/odyssey-lift-off-part2/server/node_modules/apollo-server-core/dist/requestPipeline.js:98:34)",
            "    at processTicksAndRejections (node:internal/process/task_queues:96:5)",
            "    at async processHTTPRequest (/Users/mac-rent/Documents/GitHub/odyssey-lift-off-part2/server/node_modules/apollo-server-core/dist/runHttpQuery.js:221:30)"
          ]
        }
      }
    }
  ]
}

 

리졸버 채이닝

/track/:id 호출해서 authorId 를 가져오고 이걸로 /author/:id 를 호출해서 작가의 name 을 가져온다면…

한 리졸버가 하는일이 너무 많습니다.

Query.track 리졸버의 결과가 Track.author 리졸버로 들어간다면 자연스럽게 역할이 분담되어 동작할 수 있지 않을까요?

실제로 이런 식으로 리졸버를 최대한 가볍게 분리를 시켜야 관리하기도 편하고 추후 확장하기도 좋다고 합니다.

 

아래는 조금 다르지만 리졸버 체이닝 예시입니다.

const resolvers = {
  Query: {
    // returns an array of Tracks that will be used to populate the homepage grid of our web client
    tracksForHome: (_, __, { dataSources }) => {
      return dataSources.trackAPI.getTracksForHome();
    },
  },
  Track: {
    author: ({ authorId }, _, { dataSources }) => {
      return dataSources.trackAPI.getAuthor(authorId);
    },
  },
};

module.exports = resolvers;
반응형
저작자표시 비영리 동일조건 (새창열림)

'프론트엔드 개발 > GraphQL' 카테고리의 다른 글

Apollo Client 와 Relay 를 비교해보자  (1) 2024.10.01
Next.js 의 ServerSide 에서 Apollo Client Cache 데이터가 모든 사용자 간에 공유되는 이슈  (0) 2023.12.19
Apollo Subscription 으로 GraphQL 서버와 WebSocket 통신하기  (0) 2023.04.17
React에서 Apollo Client 를 상태관리 라이브러리로 사용하기  (0) 2023.04.16
Apollo 오디세이(ODYSSEY) graphql 강의  (0) 2023.02.11
    '프론트엔드 개발/GraphQL' 카테고리의 다른 글
    • Next.js 의 ServerSide 에서 Apollo Client Cache 데이터가 모든 사용자 간에 공유되는 이슈
    • Apollo Subscription 으로 GraphQL 서버와 WebSocket 통신하기
    • React에서 Apollo Client 를 상태관리 라이브러리로 사용하기
    • Apollo 오디세이(ODYSSEY) graphql 강의
    snowman95
    snowman95
    (17~19) Unity/Unreal Engine 게임 프로그래머 (20~21) System Administrator ___________ (22~) React 웹 프론트앤드 개발자 __________ 깃헙 : https://github.com/snowman95

    티스토리툴바