Logo

[GraphQL/React] Apollo Hooks로 React 앱 개발하기

지난 포스팅에서는 Apollo Hooks라는 새로운 방법을 통해 React 앱에서 어떻게 GraphQL API를 호출할 수 있는지 간단히 살펴보았습니다. 이번 포스팅에서는 지난 포스팅에서 다뤘던 useQuery() 함수 뿐만 아니라 useMuation() 함수까지 사용해서 간단한 노트(Note) 앱을 React로 작성해보도록 하겠습니다.

Apollo Hooks가 생소하신 분들은 아래 포스팅를 통해 먼저 기본 개념을 잡으시고 이 포스팅로 돌아오시기를 추천드립니다.

기존에 React 앱에서 Apollo Client를 사용하여 GraphQL API를 호출하던 방법은 아래 포스팅를 참고 바랍니다.

React 앱에 Apollo Client 연결

우선, 앱의 최상위 컴포넌트를 @apollo/react-hooks 패키지에서 임포트한 <ApolloProvider/> 컴포넌트로 감싸줘야 합니다. 예를 들어, create-react-app으로 생성한 React 앱이라면 App.js 파일에서 이 작업을 해야합니다. <ApolloProvider/> 컴포넌트는 client prop을 통해 연결할 Apollo Client 객체를 받습니다.

예제에서는 SchemaLink를 사용하여 원격 서버 없이 스키마만을 이용해서 클라이언트 단에서 Apollo Client 객체를 생성하였습니다. Apollo Client 객체를 생성하는 방법은 이번 포스팅에서 다루고자 내용은 아니기 때문에 createApolloClient() 함수로 따로 빼내였습니다.

서버 없이도 클라이언트에서 GraphQL API를 호출할 수 있도록 도와주는 SchemaLink에 대한 자세한 설명은 아래 포스팅를 참고바랍니다.

import React from "react";
import { ApolloProvider } from "@apollo/react-hooks";
import createApolloClient from "./createApolloClient";

import NoteList from "./NoteList";
import NoteInput from "./NoteInput";

const client = createApolloClient();

function App() {
  return (
    <ApolloProvider client={client}>
      <h1>Notes with Apollo Hooks</h1>
      <NoteList />
      <NoteInput />
    </ApolloProvider>
  );
}

export default App;

노트 앱은 <NoteInput/>, <NoteList/> 크게 2개의 React 컴포넌트로 구성됩니다.

useQuery 사용한 NoteList 컴포넌트

<NoteList/> 컴포넌트는 노트의 목록을 보여주기 위한 함수 컴포넌트입니다. 노트의 목록은 notes라는 GraphQL 쿼리를 호출해서 가져와야 합니다. 해당 쿼리는 common.js 파일에 정의해놓고, <NoteList/> 컴포넌트에서 임포트합니다.

  • common.js
import gql from "graphql-tag";

export const GET_NOTES = gql`
  query getNotes {
    notes {
      id
      content
    }
  }
`;

GET_NOTES에 저장된 쿼리는, Apollo Hooks의 useQuery() 함수를 이용해서 호출할 수 있습니다. useQuery() 함수의 리턴값으로 부터 결과 데이터(data) 뿐만 아니라, 로딩 여부(loading)와 오류 데이터(error) 읽어올 수 있습니다.

쿼리가 실행 중이라서 아직 보여줄 데이터가 없는 동안에는 loading 값이 true이기 되기 때문에 로딩 중... 메시지를 보여줍니다. 쿼리가 호출이 실패한 경우에는 error에 오류 데이터가 저장되기 때문에, 오류 :( 메시지를 보여줍니다.

정상적으로 쿼리가 실행이 완료되어 노트 목록이 확보가 되었을 때는 loadingfalse, errorundefined이기 때문에 처음 2개의 if문은 모두 건너 뛰고 그 이후에 로직이 실행됩니다. 따라서 <ul/><li/> 태그를 이용해서 노트 목록이 화면에 랜더링 됩니다.

  • NoteList.js
import React from "react";
import { useQuery } from "@apollo/react-hooks";
import { GET_NOTES } from "./common";

function NoteList() {
  const { loading, error, data } = useQuery(GET_NOTES);
  if (loading) return <p>로딩 중...</p>;
  if (error) return <p>오류 :(</p>;
  const { notes } = data;
  return (
    <>
      <ul>
        {notes.map(({ id, content }) => (
          <li key={id}>{content}</li>
        ))}
      </ul>
    </>
  );
}

export default NoteList;

useMutation 사용한 NoteInput 컴포넌트

<NoteList/> 컴포넌트는 노트를 추가하기 위한 함수 컴포넌트입니다. 노트를 추가할 때는 addNote라는 GraphQL 뮤테이션(mutation)을 사용하며, 역시 common.js에 정의해놓고 <NoteList/> 컴포넌트에서 임포트합니다.

  • common.js
import gql from "graphql-tag";

export const ADD_NOTE = gql`
  mutation addNote($content: String) {
    addNote(content: $content) {
      id
      content
    }
  }
`;

ADD_NOTE에 저장된 mutation은, Apollo Hooks의 useMutation() 함수를 이용해서 호출할 수 있습니다. useMutation() 함수의 경우 useQuery()처럼 객체를 리턴하는 것이 아니라, 배열을 리턴합니다. 배열의 첫번째 원소는 mutation을 호출해야하는 순간에 호출해야하는 함수이고, 두번째 원소는 useQuery()가 리턴하던 것과 같은 형태의 객체입니다.

여기서는 첫번째 원소를 addNote에 할당해놓고, 추가 버튼이 클릭되었을 때 handleClick() 함수 내에서 호출되게 하였습니다. 유저는 입력 필드에 추가할 노트의 내용을 작성할 수 있으며, 이것은 content라는 state에 바인딩되어 있습니다. 그리고 이 contentaddNote() 함수를 호출할 때, 입력 변수로 넘어갑니다.

여기서 쿼리와 달리 mutation의 경우, loadingerror를 어디에다 써야할지 궁금하신 분들이 있으실텐데요. loading은 유저가 mutation 실행 중에 입력 필드나 버튼을 사용할 수 없도록 disabled처리할 때 쓸 수 있고, error는 mutation 실행이 실패했을 경우 입력 필드 주변에 오류 정보를 보여주기 위해서 쓸 수 있습니다.

  • NoteInput.js
import React, { useState } from "react";
import { useMutation } from "@apollo/react-hooks";
import { ADD_NOTE, GET_NOTES } from "./common";

function NoteInput() {
  const [content, setContent] = useState("");
  const [addNote, { loading, error }] = useMutation(ADD_NOTE);

  const handleClick = () => {
    addNote({ variables: { content } });
    setContent("");
  };

  return (
    <>
      <input
        value={content}
        onChange={({ target: { value } }) => setContent(value)}
        placeholder="new note"
        disabled={loading}
      />
      <button onClick={handleClick} disabled={loading}>
        추가
      </button>
      {error && <p style={{ color: "red" }}>Error :(</p>}
    </>
  );
}

export default NoteInput;

useMutation() 호출 후 캐시 업데이트

입력 필드에 새로운 노트를 입력하고 추가 버튼을 클릭하면 추가된 노트가 바로 목록이 뜨지 않을텐데요. 브라우저에서 새로 고침을 해줘야 비로서 추가된 노트가 목록에 추가되어 있음을 알 수 있으실 것입니다.

이는 useMutation()을 호출하여 추가된 데이터가 Apollo Cache는 반영이 되지 않아 생기는 문제인데요. 추가된 데이터가 바로 목록에 뜨기를 원한다면 useMutation()update 옵션을 이용해서 캐시를 직접 갱신해줘야 합니다.

const [addNote, { loading, error }] = useMutation(ADD_NOTE, {
  update(cache, { data: { addNote } }) {
    const { notes } = cache.readQuery({ query: GET_NOTES });
    cache.writeQuery({
      query: GET_NOTES,
      data: { notes: [...notes, addNote] },
    });
  },
});

노트 목록을 가져올 때 사용하는 쿼리가 저장되어 있는 GET_NOTES에 대한 캐시를 읽어온 후, 막 추가한 노트 데이터를 추가하여 캐시에 다시 저장해줍니다. 이렇게 한 후, 다시 노트를 추가해보면 추가한 노트가 바로 목록에 뜨는 것을 확인하실 수 있으실 것입니다.

useMutation() 호출 후 ID 피드백

유저에게 추가된 데이터의 ID를 피드백 해주고 싶다면, useMutation()onCompleted 옵션을 사용할 수 있습니다. onCompleted로 콜백 함수를 지정하는데, 막 추가된 노트 데이터를 인자로 넘어오기 때문에, 노트 ID를 읽어서 유저에게 피드백을 줄 수 있습니다.

const [addNote, { loading, error }] = useMutation(ADD_NOTE, {
  onCompleted({ addNote: { id } }) {
    alert(`노트가 추가되었습니다. (ID: ${id})`);
  },
});

전체 코드

마치면서

기존에 react-apollo 패키지에서 제공하는 <Query/><Mutation/>과 같은 HOC(Higher-Order Components)를 사용해서 React 앱을 개발해보신 분이라면, Apollo Hooks를 이용하면 얼마나 깨끗하게 코드를 작성할 수 있는지 느끼실 수 있으실 것입니다.