GraphQL 로 Redux Local 동작 대체하기

sangminnn·2020년 4월 20일
0

GraphQL 사용기

목록 보기
2/2

요즘 관심이 가는 기술 중에서 REST API를 대체할 수 있는 기술이라고 하는

GraphQL에 대해서 공부해보았다.

아무래도 REST API를 사용할 때보다 Front-end 측의 자유도가 높아진다는 말을 듣고 흥미가 생겼는데

이보다 더 흥미가 생긴 점은 Redux를 대체할 수 있다 는 것이였다.

따라서 이전에 toy-project로 진행하던 Infinite-Scroll 실행 코드를 GraphQL로 대체해보기로 했다.

시작하며 ...

지금부터 소개하는 프로젝트의 구조는 지극히 개인적인 구조이며,

틀린 점이 있을 수 있을 수 있습니다.

(틀린 점이 있다면 누구든지 구원해주시길 ...)

한가지 더 당부하고 갈 내용은 Local 동작만을 제어해보는 것이다.

여기서 내가 생각하는 Local 동작으로는 기본 State 세팅 및 호출, State 변경 이다.

이제 밑밥은 그만깔고 바로 시작해보겠다.

해당 글은 GraphQL에 대한 기본 지식은 있는 상황을 가정하여 핵심 코드만 설명해보겠습니다.

프로젝트 구조

크게 중요한 코드로는 Apollo Client 를 세팅하는 index.js

그리고 기본 State 렌더링 및 infinite scroll 을 동작시키는 MainContainer.js 로 구성하였다.

++) 프로젝트 시작은 CRA를 이용하여 구성한 내용을 커스텀 한 내용입니다.

index

코드 구조를 먼저 보고 설명하도록 하겠다.

import gql from 'graphql-tag';

import { ApolloClient } from 'apollo-client';
import { InMemoryCache } from "apollo-cache-inmemory";
import { HttpLink } from "apollo-link-http";
import { ApolloProvider } from "react-apollo";

먼저 필요한 모듈들을 설치 및 import 해주는 부분이다.

Apollo Client 를 사용 및 세팅하기 위해 Apollo Boost 를 설치해주면 기본 Apollo Client 세팅을 위한 모듈은 거의 다 설치된다고 생각하면 된다.

그 중 세팅에 필요한 InMemoryCache, HttpLink, ApolloProvider, gql을 import 해준다.

const cache = new InMemoryCache();

const typeDefs = gql`
  type Picture {
    __typename: String!
    image: String!
    title: String!
  }

  type Query {
    pictures: [Picture!]!
  }

  type Mutation {
    addPicture(input: [Picture]): [Picture]!
  }
`;

위에서 import한 InMemoryCache를 이용하여 Local Storage 역할을 담당할 cache를 먼저 선언해주고

Apollo Client 세팅시에 필요한 typeDefs를 선언해준다.

로컬에 있는 파일들을 호출하기 위한 Picture Type과 기본 Query 및 Mutation을 선언해주었다.


const resolvers = {
  Mutation: {
    addPicture: (_, args, { cache }) => {
      
      const newthing = [
        { __typename: "Picture", image: 'prac1.jpg', title: 'hello'  },
        { __typename: "Picture", image: 'prac2.jpg', title: 'hello'  },
        { __typename: "Picture", image: 'prac3.jpg', title: 'hello'  },
        { __typename: "Picture", image: 'prac4.jpg', title: 'hello'  },
        { __typename: "Picture", image: 'prac5.jpg', title: 'hello'  },
        { __typename: "Picture", image: 'prac6.jpg', title: 'hello'  }
      ];

      const query = gql`
        query pictures {
          pictures @client {
            __typename
            image
            title
          }
        }
      `

      const previous = cache.readQuery({ query });
      const data = {
        pictures: [...previous.pictures, ...newthing]
      }

      cache.writeQuery({ query, data });
      return data;
    }
  }
};

처음에는 useMutation 을 사용하는 MainContainer에서 값을 추가해주려고했는데

원하는 타입인 Object들로 구성된 array를 arguments로 받으려고했지만 계속해서 넘어오지 않아 보류.

mutation의 실행만을 청취하기로 하고 resolver 에서 해당 동작을 정의해주기로 하였다.

scroll 이 내려간 동작에 따라 이벤트가 발생하였을 경우 mutation이 동작하도록 하고,

resolver에서는 새로 render해줄 데이터를 먼저 정의해주었다.

이후 기존의 local state(cache)에 있는 pictures 를 불러와주는 query를 선언해주고

cache의 메소드인 readQuery를 통해 기존의 pictures를 변수에 담아준다.

그 다음 기존의 pictures들과 새로 추가할 newthing 을 spread 를 사용해 하나의 배열로 만들어주고

이를 writeQuery 메소드를 사용해 cache에 다시 저장해준다.



const client = new ApolloClient({
  cache,
  link: new HttpLink({
    uri: 'http://localhost:4000'
  }),
  typeDefs,
  resolvers
});

cache.writeData({
  data: {
    pictures: [
      { __typename: "Picture", image: 'prac4.jpg', title: 'hello'  },
      { __typename: "Picture", image: 'prac2.jpg', title: 'hello'  },
      { __typename: "Picture", image: 'prac5.jpg', title: 'hello'  },
      { __typename: "Picture", image: 'prac4.jpg', title: 'hello'  },
      { __typename: "Picture", image: 'prac6.jpg', title: 'hello'  },
      { __typename: "Picture", image: 'prac6.jpg', title: 'hello'  }
    ]
  }
})

ReactDOM.render(
  <ApolloProvider client={client}>
    <BrowserRouter>
      <Route path="/" component={App} />
    </BrowserRouter>
  </ApolloProvider>, 
  document.getElementById('root')
);

위의 코드들은 Apollo Client에 미리 선언해둔 cache, typeDefs, resolvers를 연결해주고

현재 프로젝트에서 사용하지는 않지만 Apollo Client의 실행을 위해 서버단을 임의로 연결해준다.

이후 cache(local state)에 writeData 메소드를 사용해 기본 state를 정의해주고

ApolloProvider로 Apollo Client를 우리의 프로젝트에 연결해 주었다.

MainContainer


import gql from 'graphql-tag';
import { useQuery, useMutation } from "@apollo/react-hooks";

const GET_PICTURES = gql`
  query getPictures {
    pictures @client {  
      image
      title
    }
  }
`;

const ADD_PICTURES = gql`
  mutation addPicture {
    addPicture @client {
      __typename
      image
      title
    }
  }
`;

MainContainer에서는 사용할 gql, useQuery, useMutation을 import 후에

cache에서 기본 state를 가지고 올 GET_PICTURES 쿼리와

무한 스크롤링 이벤트 발생 시에 사진을 추가하는 ADD_PICTURES mutation을 정의해주었다.




function MainContainer() {
  
  // 1.
  
  const { data } = useQuery(GET_PICTURES);

  const [addPicture] = useMutation(ADD_PICTURES);


export default MainContainer;

이후 useQuery hooks 를 사용하여 cache의 기본 state를 data에 선언해주고

ADD_PICTURES mutation에 대한 트리거 함수를 addPicture로 선언해준다.

  const [target, setTarget] = useState(null);

  const [onload, setOnload] = useState(true);


  useEffect(() => {
    let observer;
    if (target) {
      observer = new IntersectionObserver(onIntersect, { threshold: 1 });
      observer.observe(target);
    }
  }, [target]);

  const onIntersect = ([ entry ]) => {
    if(entry.isIntersecting) {
      setOnload(false);
      addPicture();
      setOnload(true);
    }
  }

  return (
    <>
      <ImageList>
        {data.pictures.map(
          picture => (<Picture src={picture.image} title={picture.title}/>)
        )}
      </ImageList>
      {onload && <Target ref={setTarget}></Target>}
    </>
  )
};

그 다음 위의 코드는 IntersectionObserver를 사용하여

ImageList의 하단에 교차 이벤트를 감지할 tag를 만들고, 해당 tag에 setTarget을 연결해두어

해당 tag가 나타나면 target값에 해당 tag가 들어가도록 만들어준 다음

교차 이벤트 발생 시에 태그가 나타났다 사라졌다를 반복하여 매번 새로 addPicture를 연결해주어 계속해서 스크롤링 시에 이벤트가 계속 발생하도록 해주었다.

마치며 ..

개인적으로 공부해본 내용을 본인만의(?) 언어로 설명하여

이상한 부분이나 잘못된 부분이 있을 수 있습니다.

혹시나 잘못된 부분이 있다면 회초리 부탁드립니다 :)

++) 추가적으로 위와 같은 프로젝트 구조에서

MainContainer에서 ADDPICTURES의 인자로 pictures 객체배열? 과 같은 타입을 인자로 받고싶은 경우에 대한 해결방법을 아신다면 댓글로 가르침 부탁드리겠습니다. ( _)

profile
생각하며 코딩하려고 노력하는 개발자가 되겠습니다.

0개의 댓글