GraphQL(Apollo) + Angular 상태관리 및 사용기

노요셉·2020년 4월 20일
3

앵귤러는 컴포넌트로 페이지나 작게는 셀렉트박스같은 것들을 "Component"라는 단위로 만들어서 관리합니다.

이러한 컴포넌트는 컴포넌트 트리 형태로 관리됩니다.
최상위 App컴포넌트가 있고, 새로운 컴포넌트가 생성되면 그 하위 컴포넌트로 연결되어 관리됩니다.

apollo란 Grapqhl를 사용하여 client, server를 생성할 수 있고, apollo롤 query( read ), mutate( create, update, delete)를 도와주는 라이브러리라고 볼 수 있습니다.

오늘은 흔히 리덕스라는 통합 상태관리를 얘기할겁니다.
redux는 흔히 복잡한 컴포넌트 트리관계에서 두 컴포넌트 이상에서 관리할 데이터를 한 곳에 몰아두고 사용하자는 겁니다.

Figure 1 : Without Redux and with Redux application state behavior (Source: https://www.slideshare.net/binhqdgmail/006-react-redux-framework)

angular에서 redux를 사용한 방법을 써봤어요.
ngrx라는 상태관리 라이브러리를 사용했습니다.
ngrx 사용기

오늘은 graphql만으로 redux처럼 Apollo Cache를 이용한 single source of truth를 사용해볼거에요.

이제는 graphql이란 용어 대신 apollo로 얘기할거에요. 구현체라고 생각하시면 됩니다. 다른 구현체로는 relay가 있다고 합니다.
https://www.apollographql.com/docs/tutorial/local-state/

apollo + graphql 개발환경 세팅은 공식 홈페이지에 잘 나와있고, 포스팅은 추후에 해야겠네요.
https://www.apollographql.com/docs/angular/basics/setup/

어떻게 redux처럼 관리할건가?

보통 apollo client를 이용해 프론트엔드 앱에 apollo server와 통신할 설정을 해놓습니다.


위 그림에서 graphql은 말그대로 백엔드 서버를 말하고, schema는 graphql를 이용해 쿼리및 쿼리에서 사용할 데이터 타입을 설정하는 것을 말해요.

graphql에게 이러이러한 쿼리와 이러이러한 데이터를 주고 받을거란걸 알려줘야하니까요.

어떻게 redux처럼 상태(데이터)를 관리할 것인가를 요약하면,

서버에 요청하듯이 apollo client에 요청하자! 단 @client 디렉티브를 붙여서!

우리는 redux처럼 상태를 관리하는 apollo cache를 이용할건데요.
흔히 client - server - db 로 데이터가 흘렀다면,
client ( angular - apollo client - apollo cache ) angular 앱 내부에서 server에 graphql query를 요청하듯이, client에 query를 하는겁니다.

백엔드에 요청할때와 대응되게 억지로 끼워맞추긴 했는데요.
서버에 요청하듯이 apollo client에 요청하자! 단 @client 디렉티브를 붙여서!

./schema.graphql

type Query {
  datas: [Data]!
  data(name: String!): Data
  update(data: Data): [Data]
}

type Data {
  id: Int!
  name: String!
  age: Int!
  gender: String!
}

resolvers 는 쿼리에서 수행될 엔드포인트를 구현해주는 곳입니다. 여기선 graphql server인 백엔드에 다음과 같은 작업들이 들어갈건데요. 아래에는 설명을 위해 만든 구조라 데이터와 모델과 endpoint가 같이 사용되고 있어요. 구현하실때는 데이터와 모델을 나눠서 구현하시면 됩니다.
resolvers.ts

interface Data {
  id: number;
  name: string;
  age: number;
  gender: string;
}

...

// datas: Data[] 타입의 배열

const getById = (id: number) => {
  const filteredData = datas.filter((data) => id === data.id);
  return filteredData[0];
};

const updateData = (id: number, name: string) => {
  const filteredData = datas.filter((data) => id !== data.id);
  const selectedData = datas.filter((data) => id === data.id);
  const newData = { ...selectedData[0], name };
  return [...filteredData, newData];
};

export const resolvers = {
  Query: {
    datas: () => datas,
    data: (_, { id }) => {
      console.log('id', id);

      return getById(id);
    },
    update: (_, { id, name }) => {
      console.log('id:', id);
      console.log('name:', name);
      return updateData(id, name);
    },
  },
};

query를 수행하기 위해 선행조건이 뭐가 있나?

typeDefs, resolvers를 apollo client를 셋업할때 프로퍼티로 넣어주시면 됩니다.

typeDefs는 뭔가? 위에 봤던 schema.graphql
resolvers는? 위에서 언급했던 resolvers.ts 엔드포인트를 말합니다.

import { NgModule } from '@angular/core';
import { ApolloModule, APOLLO_OPTIONS } from 'apollo-angular';
import { HttpLinkModule, HttpLink } from 'apollo-angular-link-http';
import { InMemoryCache } from 'apollo-cache-inmemory';
import { setContext } from 'apollo-link-context';
import { ApolloLink } from 'apollo-link';
import {resolvers} from './resolvers';


const uri = 'url/graphql'; //<-- add the URL of the GraphQL server here

export function provideApollo(httpLink: HttpLink) {
  const basic = setContext((operation, context) => ({
    headers: {
      Accept: 'charset=utf-8',
    },
  }));

  // Get the authentication token from local storage if it exists
  const token = localStorage.getItem('token');
  const auth = setContext((operation, context) => ({
    headers: {
      ...
    },
  }));

  const link = ApolloLink.from([basic, auth, httpLink.create({ uri })]);
  const cache = new InMemoryCache();

  return {
    link,
    cache,
    typeDefs: './schema.graphql',
    resolvers,
    defaultOptions: {
      watchQuery: {
        fetchPolicy: 'no-cache',
        errorPolicy: 'all',
      },
    },
  };
}

@NgModule({
  exports: [ApolloModule, HttpLinkModule],
  providers: [
    {
      provide: APOLLO_OPTIONS,
      useFactory: provideApollo,
      deps: [HttpLink],
    },
  ],
})
export class GraphQLModule {}

query를 통해 내부 상태를 관리합니다.

resolvers에 data 엔드포인트를 이용해 내부 상태를 받아옵니다.

query에 @client를 붙여둡니다.

  testLocal() {
    this.apollo
      .watchQuery<any>({
        query: gql`
          {
            datas @client {
              id
            }
          }
        `,
      })
      .valueChanges.subscribe(({ data, loading }) => {
        console.log(data);
      });
  }

내부 상태를 업데이트 하고싶어요.

내부상태를 업데이트한다해서 mutate를 쓰지 않고 query를 씁니다.

리스트에 새로운 값을 추가한다고 해볼게요.

  • resolvers에 엔드포인트를 만들어줍니다.
  • 내부 상태를 업데이트 하는 로직을 구현해줍니다.
  • schema에 엔드포인트를 추가해줍니다.
  • query를 통해 내부 상태를 업데이트합니다.

resolvers에 엔드포인트를 만들어줍니다.

addData라는 엔드포인트를 만들어주고, 내부상태를 업데이트하는 로직도 구현해줍니다.

resolvers.ts

const addData = (name: string, age: number, gender: string) => {
  const newData = {
    id: datas.length,
    name,
    age,
    gender,
  };
  datas.push(newData);
  return datas;
};

export const resolvers = {
  Query: {
    ...
    addData: (_, { name, age, gender }) => {
      console.log(`name: ${name}, age: ${age}, gender: ${gender}`);
      return addData(name, age, gender);
    },
  },
};

schema에 엔드포인트를 추가해줍니다.

schema.graphql

type Query {
  ...
  addData(name: String!, age: Int!, gender: String!): Data!
}

query를 통해 내부 상태를 업데이트합니다.

tset.component.ts

testAdd() {
    this.apollo
      .watchQuery<any>({
        query: gql`
          {
            addData(name: $name, age: $age, gender: $gender) @client {
              id
              name
              age
              gender
            }
          }
        `,
        variables: {
          name: 'hanna',
          age: 20,
          gender: 'female',
        },
      })
      .valueChanges.subscribe(
        ({ data }) => {
          console.log('got data', data);
        },
        (error) => {
          console.log('there was an error sending the query', error);
        }
      );
  }

업데이트 전
ngOnit에서 내부 데이터를 읽어본후

업데이트 후
클릭 이벤트를 통해 업데이트를 하는 로직을 넣어줬습니다.

요약

graphql로 redux처럼 상태 관리하고 싶으면?
서버에 요청하듯이 apollo client에 요청합니다!

apollo fetch policy

https://medium.com/@galen.corey/understanding-apollo-fetch-policies-705b5ad71980

apollo client를 셋업할때 fetch policy를 설정하실 수가 있습니다.

cache-first - default

cache-and-network

network-only

no-cache

cache-only

fetch policy이 중요해?

https://github.com/kamilkisiela/apollo-angular/blob/master/docs/source/features/cache-updates.md
중요합니다.

inboxPostsByCategory라는 엔드포인트는 블로그 포스트 데이터를 배열로 응답해줍니다.

this.querySubscription = this.apollo
      .watchQuery<any>({
        query: gql`
        {
          inboxPostsByCategory(category: "", size: 9, page: ${this.page}) {
            posts {
              id
              postCtgr
              postDate
              postTime
              postTitle
              postThumbnailUrl
              postContent
              postAddData
            }
            total
            size
            page
          }
        }
      `,
      })
      .valueChanges.subscribe(({ data, loading }) => {

        const result = data.inboxPostsByCategory;

        const newItems = result.posts.map((post) => { // <- here
          const link = `/insight/${post.postCtgr}/${post.id}`;

          return {
            ...post,
            link,
          };
        });

      });

응답받은 데이터를 result.posts.map()으로 새로운 데이터로 가공해서 사용해요.
그렇다면 웃긴 상황이 발생하는데 inboxPostsByCategory 똑같은 쿼리가 다른 컴포넌트에서 graphql를 할때도 또 동작하는거에요. fetch-policy가 default였기 때문에 cache-first 일텐데요.
이걸 no-cache로 바꾼 순간 똑같은 쿼리가 수행되는 결과가 없어졌어요.

다시 확인해본 결과 fetchpolicy문제가 아닌, 같은 쿼리를 두번 처리하는 상황이 일어났기 때문에 분기처리가 필요한 부분이었음.

fetch-policy의 중요성은 계속해서 연구해봐야할듯

profile
서로 아는 것들을 공유해요~

0개의 댓글