apollo에서 비동기 상태 관리하는 방식 알아보기

date
Jun 29, 2022
slug
apollo-gettings-started-mutate
status
Published
tags
Next.js
GraphQL
Apollo
summary
useMutation을 사용해보고, 실제 비동기 상태를 처리하는 방식에 대해서 살펴보자
type
Post
Updated At
Aug 1, 2022 01:18 AM
Created At
Jun 29, 2022 04:43 AM

시작하며

이전 글에서는 useQuery hook을 이용해 데이터를 조회해 보았으니, 이번에는 useMutation를 통해 mutation를 다뤄보자.

개발 환경 설정

개발환경 설정에 대해서는 이전 글을 참고해 보면 좋을 것 같다. 해당 글은 개발 환경 설정이 완료 되었다고 가정하고 이전글에 이어서 작성 해본다.

useMutation

해당 hook에 mutation을 넘겨주면 실제 mutate를 요청하는 mutateFunction과 useQuery처럼 해당 결과 값에 대한 상태값을 전달해준다. 해당 mutateFunction을 통해 mutation을 요청할 수 있다.
아래 예시는 useMutation을 이용해 Item을 생성하는 mutation을 요청하는 컴포넌트이다.
//.....

const initialInputs = {
  title: '',
  description: '',
}

const CREATE_ITEM = gql`
mutation CreateItem($input: CreateItemInput!) {
  createItem(input: $input) {
    id
    title
    description
    compleated
  }
}
`

function CreateForm({ data }: Props) {
  const classes = useStyles()
  const [createItem] = useMutation(CREATE_ITEM)
  const [inputs, setInputs] = useState(initialInputs)

  const handleChange: ChangeEventHandler<
    HTMLTextAreaElement | HTMLInputElement
  > = (e) => {
    const { name, value } = e.target
    setInputs({
      ...inputs,
      [name]: value,
    })
  }

  const handleOnSubmit: FormEventHandler<HTMLFormElement> = async (e) => {
    e.preventDefault()

    try {
      if (inputs.title && inputs.description && data?.list?.id) {
        createItem({
          variables: {
            input: {
              listId: listId,
              ...inputs,
            },
          },
          refetchQueries: [GetTodoListDocument],
        })
    } catch (error) {
      console.log(error)
    }
  }

  return (
    <form className={classes.wrapper} onSubmit={handleOnSubmit}>
      <h1>할 일 등록하기</h1>
      <TextField
        name="title"
        value={inputs.title}
        onChange={handleChange}
        label="Title"
        variant="outlined"
      />
      <TextField
        name="description"
        value={inputs.description}
        onChange={handleChange}
        label="Description"
        multiline
        minRows={10}
        maxRows={10}
        variant="outlined"
      />
      <Button variant="contained" type="submit">
        등록
      </Button>
    </form>
  )
}

export default CreateForm

Local에 cache된 데이터를 업데이트하기

useMutation을 사용할 때에는 useQuery와 다르게 고려해야하는 점이 한가지 있다. 바로 mutation을 통해 back-end 데이터를 수정하는 요청을 하게 되면 기존에 local에 cache된 데이터 또한 실제 백엔드에 변경된 데이터에 맞게 수정을 해주어야 한다는 점이다. (그렇지 않으면 새로고침을 통해 다시 데이터를 조회해야 수정된 부분이 보일 것이다.)

기존에 cache 되어있는 데이터를 수정한 경우 (update)

기존에 cache 되어있는 데이터를 수정하는 경우에는 mutation을 요청했을 경우 response되는 데이터에 __typenameid 가 있다면 자동으로 해당 데이터를 수정 해준다. (이부분은 redux 에서는 직접 구현해주어야하는 것에 비해 편하게 느껴졌다.)

cache가 되어 있지 않은 데이터를 수정한 경우 (create, delete)

앞서 말했듯이useMutation은 서버로 mutation을 요청할 뿐 local에 남아있는 cache 되어있는 상태를 어떻게 업데이트해야하는지 알 수 없기에 cache된 데이터를 수정하는것이 필요하다.

Refetching queries 를 사용해 cache 갱신하기

cache된 데이터를 갱신하는 방법중 가장 일반적인 방법은 해당 데이터를 조회하는 통신을 통해 갱신된 데이터를 다시 받아오는 방법이다. apollo client에서는 refetchingQueries를 통해 이를 구현할 수 있다. 위의 코드에서 createItem을 호출하는 부분만 살펴보자.
//...


const GetTodoListDocument = gql`
    query getTodoList($listId: ID!) {
  list(id: $listId) {
    id
    title
    items {
      id
      title
      description
      compleated
    }
  }
}
`


createItem({
  variables: {
    input: {
      listId: listId,
      ...inputs,
    },
  },
  refetchQueries: [GetTodoListDocument],
})

//...
다음과같이 refetchQueries 속성에 해당 query를 넘겨주면 mutation이 정상적으로 진행된 뒤에, 해당 쿼리를 실행하여 cache된 데이터를 갱신시켜줄 수 있다.

update 함수를 통해 cache 직접 갱신하기

위의 방식으로 진행하게 된다면 mutation 이외에 하나의 통신 과정이 더 발생하게 된다. update function을 이용하면 mutation의 response 여부에 따라 cache를 변경된 결과에 맞게 본인이 직접 수정하는 방법도 가능하다. 아래 예시를 확인해보자.
createItem({
  variables: {
    input: {
      listId: listId,
      ...inputs,
    },
  },
  update(cache, result) {
    cache.modify({
      id: cache.identify(data?.list as StoreObject),
      fields: {
        items(cachedItemRefs: StoreObject[], { readField }) {
          const newItemRef = cache.writeFragment({
            data: result.data?.createItem,
            fragment: gql`
              fragment NewItem on Item {
                id
                title
                description
                compleated
              }
            `,
          })
          if (
            cachedItemRefs.some(
              (ref) =>
                readField('id', ref) === result.data?.createItem?.id
            )
          ) {
            return cachedItemRefs
          }

          return [...cachedItemRefs, newItemRef]
        },
      },
    })
  },
})
update 함수에는 우리가 처음에 apollo client 인스턴스를 생성할 때 넣어준 InMemeryCache 인스턴스와 그 결과 값을 넘겨주고 있다. 해당 인자를 활용하여 api 문서를 보고 알맞게(?) 수정해주면 된다.

OptimisticResponse를 통해 미리 갱신하기

위의 두가지 방법을 사용하면 통신이 끝난 뒤에야 캐시가 변경되기 때문에 사용자입장에서 느리게 반영되는 것처럼 느껴질 수 있다. 따라서 예상되는 형태로 미리 변경되는 상태값을 반영해두고, 실제로 mutation이 정상적으로 수행 되었다면 그대로 두고, 실패 하였다면 되돌려 주는 형태를 Optimistic UI라고 하는데, apollo client에서는 이를 OptimisticResponse 속성을 통해 쉽게 구현할 수 있다.
createItem({
  variables: {
    input: {
      listId: data?.list?.id,
      ...inputs,
    },
  },
 optimisticResponse: {
   createItem: {
     id: 'id-temp',
     __typename: 'Item',
     title: inputs.title,
     description: inputs.description,
     compleated: false,
   },
 },
  update(cache, result) {
    cache.modify({
      id: cache.identify(data?.list as StoreObject),
      fields: {
        items(cachedItemRefs: StoreObject[], { readField }) {
          const newItemRef = cache.writeFragment({
            data: result.data?.createItem,
            fragment: gql`
              fragment NewItem on Item {
                id
                title
                description
                compleated
              }
            `,
          })
          if (
            cachedItemRefs.some(
              (ref) =>
                readField('id', ref) === result.data?.createItem?.id
            )
          ) {
            return cachedItemRefs
          }

          return [...cachedItemRefs, newItemRef]
        },
      },
    })
  },
})
실제 update 함수는 동일하게 적용하고, optimisticResponse 속성에 예상되는 결과값을 적어두면 응답이 이루어지기 전에 미리 예상되는 결과값으로 cache를 업데이트 해준다.

마치며

사실 어떠한 통신(REST API, GraphQL)을 이용하거나 어떠한 상태관리 라이브러리(Redux, Recoil, React-query, Apollo 등등등…)를 사용하던지 비동기 상태관리에 대해서 명확하게 이해하고 있다면 금방 이해하고 적용할 수 있는 것 같다. 그중 우리는 GraphQL을 이용한 통신 중에서 apollo client를 이용해 상태관리를 하고 있는 것일 뿐… 항상 본질을 이해하려 노력하고 상황에 맞는 기술을 사용할 수 있는 개발자가 되는 것이 중요한 것 같다.