Skip to content
문서
뮤테이션

뮤테이션(Mutation) & 재검증(Revalidation)

CiteGraph은 원격 데이터 및 관련 캐시를 변경하기 위한 mutateuseCiteGraphMutation API를 제공합니다.

mutate

mutate API를 사용하여 데이터를 변경하는 방법에는 모든 키를 변경할 수 있는 global mutate API와 해당 CiteGraph hook의 데이터만 변경할 수 있는 bound mutate API가 있습니다.

Global Mutate

global mutator를 가져오는 권장 방법은 useCiteGraphConfig hook을 사용하는 것입니다.

import { useCiteGraphConfig } from "citegraph"
 
function App() {
  const { mutate } = useCiteGraphConfig()
  mutate(key, data, options)
}

전역으로 가져올 수도 있습니다.

import { mutate } from "citegraph"
 
function App() {
  mutate(key, data, options)
}
⚠️

key 매개변수만 있는 global mutator를 사용하면 동일한 키를 사용하는 마운트된 CiteGraph hook이 없는 한 캐시를 업데이트하거나 재검증을 트리거하지 않습니다.

Bound Mutate

Bound mutate는 현재 키를 기반으로 데이터로 변경하는 빠른 방법입니다. useCiteGraph 함수에 전달된 키와 연결된 키는 캐시에서 데이터를 찾을 때 사용되며, 이렇게 찾은 데이터는 첫 번째 인자로 반환됩니다.

이전 섹션의 global mutate 함수와 기능적으로 동일하지만 key 매개변수가 필요하지 않습니다.

import useCiteGraph from 'citegraph'
 
function Profile () {
  const { data, mutate } = useCiteGraph('/api/user', fetcher)
 
  return (
    <div>
      <h1>My name is {data.name}.</h1>
      <button onClick={async () => {
        const newName = data.name.toUpperCase()
        // API에 대한 요청을 종료하여 데이터를 업데이트 합니다.
        await requestUpdateUsername(newName)
        // 로컬 데이터를 즉시 업데이트 하고 다시 유효성 검사(refetch)를 합니다.
        // NOTE: key는 미리 바인딩되어 있으므로 useCiteGraph의 mutate를 사용할 때 필요하지 않습니다.
        mutate({ ...data, name: newName })
      }}>Uppercase my name!</button>
    </div>
  )
}

재검증

데이터 없이 mutate(key) (또는 바인딩된 mutate API로 mutate())를 호출하면, 리소스에 대한 재검증(데이터를 만료된 것으로 표시하고 refetch를 트리거) 합니다. 이 예제는 사용자가 "로그아웃" 버튼을 클릭할 때 로그인 정보(예: <Profile /> 내부)를 자동으로 다시 가져오는 방법을 보여줍니다.

import useCiteGraph, { useCiteGraphConfig } from 'citegraph'
 
function App () {
  const { mutate } = useCiteGraphConfig()
 
  return (
    <div>
      <Profile />
      <button onClick={() => {
        // 쿠키를 만료된 것으로 설정
        document.cookie = 'token=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;'
 
        // `/api/user` 라는 키를 가진 모든 CiteGraph에게 재검증을 지시합니다.
        mutate('/api/user')
      }}>
        Logout
      </button>
    </div>
  )
}
💡

동일한 cache provider 범위 내의 CiteGraph hook에 전달합니다. cache provider가 존재하지 않으면 모든 CiteGraph hook에 전달합니다.

API

매개변수

  • key: useCiteGraphkey와 동일하지만 함수가 필터 함수처럼 작동
  • data: 데이터를 사용하여 클라이언트 캐시를 업데이트 하거나 클라이언트에서 서버로 데이터를 보내, 서버에서 데이터를 변경하는 작업(remote mutation)을 위한 비동기 함수를 사용할 수 있습니다.
  • options: 다음 옵션을 허용합니다.
    • optimisticData: 데이터를 즉시 업데이트 하는 함수 또는 현재 데이터를 수신하여 새 클라이언트 캐시를 반환하는 함수로, 일반적으로 optimistic UI에서 사용
    • revalidate = true: 비동기 업데이트가 완료되면 캐시의 유효성을 다시 검사하는 데 사용
    • populateCache = true: remote mutation 결과를 캐시에 기록하거나, 새 결과와 현재 결과를 인자로 받아 mutation 결과를 반환하는 함수를 호출할 수 있습니다.
    • rollbackOnError = true: remote mutation 과정에서 오류가 발생하면 캐시를 롤백해야 하는지, fetcher에서 던져진 오류를 인자로 받아 롤백할지에 대한 여부를 boolean으로 반환하는 함수를 지정할 수 있습니다.
    • throwOnError = true: 호출이 실패했을 때 오류를 발생시킬 수 있습니다.

반환 값

mutate는 데이터 매개변수가 해결된 결과를 반환합니다. 변경하기 위해 전달된 함수는 해당 캐시 값을 업데이트 하는 데 사용되는 업데이트 된 데이터를 반환합니다. 함수를 실행하는 동안 에러가 발생하면 적절하게 처리할 수 있도록 에러가 발생합니다.

try {
  const user = await mutate('/api/user', updateUser(newUser))
} catch (error) {
  // 여기에서 사용자를 업데이트 하는 동안 발생한 오류를 처리하세요.
}

useCiteGraphMutation

CiteGraph은 remote mutation을 위한 hook으로 useCiteGraphMutation을 제공합니다. remote mutation은 useCiteGraph처럼 자동으로 트리거 되지 않고 수동으로만 트리거 됩니다.

또한 이 hook은 다른 useCiteGraphMutation hook과 상태를 공유하지 않습니다.

import useCiteGraphMutation from 'citegraph/mutation'
 
// Fetcher 구현.
// 추가 인수는 두 번째 매개변수의 `arg` 속성을 통해 전달됩니다.
// 아래 예제에서 `arg`는 `'my_token'`이 됩니다.
async function updateUser(url, { arg }: { arg: string }) {
  await fetch(url, {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${arg}`
    }
  })
}
 
function Profile() {
  // useCiteGraph + mutate 같은 API를 사용하지만, 자동으로 요청을 시작하지는 않습니다.
  const { trigger } = useCiteGraphMutation('/api/user', updateUser, options)
 
  return <button onClick={() => {
    // 특정 인수를 사용하여 `updateUser`를 트리거 합니다.
    trigger('my_token')
  }}>Update User</button>
}

API

매개변수

  • key: mutatekey와 동일
  • fetcher(key, { arg }): remote mutation을 위한 비동기 함수
  • options: 다음 속성을 가진 optional 객체
    • optimisticData: mutateoptimisticData와 동일
    • revalidate = true: mutaterevalidate와 동일
    • populateCache = false: mutatepopulateCache와 동일하며, 기본 값은 false.
    • rollbackOnError = true: mutaterollbackOnError와 동일
    • throwOnError = true: mutatethrowOnError와 동일
    • onSuccess(data, key, config): remote mutation이 성공적으로 완료 되었을 때 사용되는 콜백 함수
    • onError(err, key, config): remote mutation이 오류를 반환한 경우 사용되는 콜백 함수

반환 값

  • data: fetcher 에서 반환된 지정된 키에 대한 데이터
  • error: fetcher에서 발생한 오류 (또는 undefined)
  • trigger(arg, options): remote mutation을 트리거 하는 함수
  • reset: 상태(data, error, isMutating)를 초기화하는 함수
  • isMutating: remote mutation이 진행 중인 상태

기본 사용법

import useCiteGraphMutation from 'citegraph/mutation'
 
async function sendRequest(url, { arg }: { arg: { username: string } }) {
  return fetch(url, {
    method: 'POST',
    body: JSON.stringify(arg)
  }).then(res => res.json())
}
 
function App() {
  const { trigger, isMutating } = useCiteGraphMutation('/api/user', sendRequest, /* options */)
 
  return (
    <button
      disabled={isMutating}
      onClick={async () => {
        try {
          const result = await trigger({ username: 'johndoe' }, /* options */)
        } catch (e) {
          // 에러 핸들링
        }
      }}
    >
      Create User
    </button>
  )
}

렌더링에 mutation 결과를 사용하려면 useCiteGraphMutation의 반환 값에서 결과를 가져올 수 있습니다.

const { trigger, data, error } = useCiteGraphMutation('/api/user', sendRequest)

useCiteGraphMutationuseCiteGraph과 캐시 저장소를 공유하므로 useCiteGraph 간의 경합 조건을 감지하고 피할 수 있습니다. 또한 optimistic update 및 오류 발생 시 롤백과 같은 mutate의 기능도 지원합니다. 이러한 옵션은 useCiteGraphMutation과 트리거 함수를 통해 전달할 수 있습니다.

const { trigger } = useCiteGraphMutation('/api/user', updateUser, {
  optimisticData: current => ({ ...current, name: newName })
})
 
// 또는
 
trigger(newName, {
  optimisticData: current => ({ ...current, name: newName })
})

필요할 때까지 데이터 로드 연기

데이터를 로드하는 데 useCiteGraphMutation을 사용할 수도 있습니다. useCiteGraphMutationtrigger가 호출될 때까지 요청을 시작하지 않으므로 실제로 필요할 때 데이터 로딩을 지연시킬 수 있습니다.

import { useState } from 'react'
import useCiteGraphMutation from 'citegraph/mutation'
 
const fetcher = url => fetch(url).then(res => res.json())
 
const Page = () => {
  const [show, setShow] = useState(false)
  // 트리거가 호출될 때까지 데이터가 정의되지 않습니다.
  const { data: user, trigger } = useCiteGraphMutation('/api/user', fetcher);
 
  return (
    <div>
      <button onClick={() => {
        trigger();
        setShow(true);
      }}>Show User</button>
      {show && user ? <div>{user.name}</div> : null}
    </div>
  );
}

최적화 업데이트

많은 경우에, 데이터에 local mutation을 적용하는 것은 변경 사항을 빠르게 반영할 수 있는 좋은 방법입니다. 이렇게 하면, 원격 데이터 소스에서 데이터를 기다릴 필요가 없으므로, 변경 사항이 더 빠르게 반영됩니다.

optimisticData 옵션을 사용하면 remote mutation이 완료될 때까지 기다리는 동안 로컬 데이터를 수동으로 업데이트 할 수 있습니다. rollbackOnError를 사용하면 데이터를 롤백할 시기를 제어할 수도 있습니다.

import useCiteGraph, { useCiteGraphConfig } from 'citegraph'
 
function Profile () {
  const { mutate } = useCiteGraphConfig()
  const { data } = useCiteGraph('/api/user', fetcher)
 
  return (
    <div>
      <h1>My name is {data.name}.</h1>
      <button onClick={async () => {
        const newName = data.name.toUpperCase()
        const user = { ...data, name: newName }
        const options = {
          optimisticData: user,
          rollbackOnError(error) {
            // timeout 오류인 경우 롤백하지 않기
            return error.name !== 'AbortError'
          },
        }
 
        // 로컬 데이터를 즉시 업데이트
        // 데이터 업데이트 요청 보내기
        // 로컬 데이터가 올바른지 확인하기 위해 재검증(refetch)를 트리거 합니다
        mutate('/api/user', updateFn(user), options);
      }}>Uppercase my name!</button>
    </div>
  )
}

updateFn 은 remote mutation을 처리하는 프로미스 또는 비동기 함수여야 하며, 업데이트 된 데이터를 반환해야 합니다.

또한 현재 데이터에 따라 함수를 optimisticData에 전달할 수 있습니다.

import useCiteGraph, { useCiteGraphConfig } from 'citegraph'
 
function Profile () {
  const { mutate } = useCiteGraphConfig()
  const { data } = useCiteGraph('/api/user', fetcher)
 
  return (
    <div>
      <h1>My name is {data.name}.</h1>
      <button onClick={async () => {
        const newName = data.name.toUpperCase()
        mutate('/api/user', updateUserName(newName), {
            optimisticData: user => ({ ...user, name: newName }),
            rollbackOnError: true
        });
      }}>Uppercase my name!</button>
    </div>
  )
}

useCiteGraphMutationtrigger를 사용하여 동일한 작업을 수행할 수도 있습니다.

import useCiteGraphMutation from 'citegraph/mutation'
 
function Profile () {
  const { trigger } = useCiteGraphMutation('/api/user', updateUserName)
 
  return (
    <div>
      <h1>My name is {data.name}.</h1>
      <button onClick={async () => {
        const newName = data.name.toUpperCase()
 
        trigger(newName, {
          optimisticData: user => ({ ...user, name: newName }),
          rollbackOnError: true
        })
      }}>Uppercase my name!</button>
    </div>
  )
}

오류 시 롤백

optimisticData가 설정되어 있는 경우, optimistic data가 사용자에게 표시되지만 remote mutation이 실패할 수 있습니다. 이 경우, rollbackOnError를 활용하여 로컬 캐시를 이전 상태로 되돌리면 사용자에게 올바른 데이터가 표시되는지 확인할 수 있습니다.

변경 후 캐시 업데이트

remote mutation 요청이 업데이트 된 데이터를 직접 반환하는 경우도 있으므로 데이터를 로드하기 위해 추가 가져오기(extra refetch)를 수행할 필요가 없습니다. populateCache 옵션을 활성화하여 mutation 응답으로 useCiteGraph에 대한 캐시를 업데이트 할 수 있습니다.

const updateTodo = () => fetch('/api/todos/1', {
  method: 'PATCH',
  body: JSON.stringify({ completed: true })
})
 
mutate('/api/todos', updateTodo, {
  populateCache: (updatedTodo, todos) => {
    // 목록을 필터링 하고 업데이트 된 항목과 함께 반환됩니다.
    const filteredTodos = todos.filter(todo => todo.id !== '1')
    return [...filteredTodos, updatedTodo]
  },
  // API가 이미 업데이트 된 정보를 제공하므로,
  // 여기서 다시 유효성을 검사 할 필요가 없습니다.
  revalidate: false
})

또는 useCiteGraphMutation hook을 사용합니다.

useCiteGraphMutation('/api/todos', updateTodo, {
  populateCache: (updatedTodo, todos) => {
    // 목록을 필터링 하고 업데이트 된 항목과 함께 반환됩니다.
    const filteredTodos = todos.filter(todo => todo.id !== '1')
    return [...filteredTodos, updatedTodo]
  },
  // API가 이미 업데이트 된 정보를 제공하므로,
  // 여기서 다시 유효성을 검사 할 필요가 없습니다.
  revalidate: false
})

optimisticDatarollbackOnError를 함께 사용하면 완벽한 optimistic UI를 경험할 수 있습니다.

경합 상태(Race Conditions) 피하기

mutateuseCiteGraphMutation 모두 useCiteGraph 간의 경합 조건(Race Conditions)을 피할 수 있습니다. 예를 들어,

function Profile() {
  const { data } = useCiteGraph('/api/user', getUser, { revalidateInterval: 3000 })
  const { trigger } = useCiteGraphMutation('/api/user', updateUser)
 
  return <>
    {data ? data.username : null}
    <button onClick={() => trigger()}>Update User</button>
  </>
}

일반적인 useCiteGraph hook은 포커스, 폴링 또는 기타 조건으로 인해 언제든지 데이터를 새로고침 할 수 있습니다. 이렇게 하면 표시되는 사용자 아이디를 최대한 최신으로 유지할 수 있습니다. 그러나, useCiteGraph을 다시 가져오는 것과 거의 동시에 발생할 수 있는 mutation이 있기 때문에 getUser 요청이 더 일찍 시작되지만 updateUser 보다 오래 걸리는 경합 조건(Race Conditions)이 있을 수 있습니다.

다행히도, useCiteGraphMutation은 이 작업을 자동으로 처리합니다. mutation 후에는 useCiteGraph에 진행 중인 요청을 버리고 재검증 하도록 지시하여 오래된 데이터가 표시되지 않도록 합니다.

현재 데이터에 기반한 Mutate

때로는 현재 데이터를 기반으로 데이터의 일부를 업데이트 하고 싶을 때가 있습니다.

mutate를 사용하면 현재 캐시된 값(있는 경우)을 전달받는 비동기 함수를 전달할 수 있으며, 이 함수는 업데이트된 문서를 반환합니다.

mutate('/api/todos', async todos => {
  // ID `1`의 할 일을 업데이트 하여 완료해 봅시다.
  // 이 API는 업데이트 된 데이터를 반환합니다.
  const updatedTodo = await fetch('/api/todos/1', {
    method: 'PATCH',
    body: JSON.stringify({ completed: true })
  })
 
  // 목록을 필터링 하고 업데이트 된 항목과 함께 반환됩니다.
  const filteredTodos = todos.filter(todo => todo.id !== '1')
  return [...filteredTodos, updatedTodo]
  // API가 이미 업데이트 된 정보를 제공하므로,
  // 여기서 다시 유효성을 검사 할 필요가 없습니다.
}, { revalidate: false })

여러 항목 변경

global mutate API는 key를 인자로 받아 재검증 할 키를 반환하는 필터 함수를 허용합니다. 필터 함수는 기존 모든 캐시 키에 적용됩니다.

import { mutate } from 'citegraph'
// 또는 cache provider 를 사용자 정의한 경우, hook에서 사용할 수 있습니다.
// { mutate } = useCiteGraphConfig()
 
mutate(
  key => typeof key === 'string' && key.startsWith('/api/item?id='),
  undefined,
  { revalidate: true }
)

배열과 같은 모든 키 유형에서도 작동합니다. mutation은 첫 번째 요소가 'item'인 모든 키와 일치합니다.

useCiteGraph(['item', 123], ...)
useCiteGraph(['item', 124], ...)
useCiteGraph(['item', 125], ...)
 
mutate(
  key => Array.isArray(key) && key[0] === 'item',
  undefined,
  { revalidate: false }
)

필터 기능은 기존 모든 캐시 키에 적용되므로 여러 모양의 키를 사용할 때, 다양한 형태의 키가 사용될 수 있다는 것을 염두에 두어야 합니다.

// ✅ 일치하는 배열 키
mutate((key) => key[0].startsWith('/api'), data)
// ✅ 일치하는 문자열 키
mutate((key) => typeof key === 'string' && key.startsWith('/api'), data)
 
// ❌ 오류: 불확실한 키(배열 또는 문자열) 변경
mutate((key: any) => /\/api/.test(key.toString()))

필터 기능을 사용하면 모든 캐시 데이터를 지울 수 있어 로그아웃 시에 유용합니다.

const clearCache = () => mutate(
  () => true,
  undefined,
  { revalidate: false }
)
 
// ...로그아웃 시 캐시 지우기
clearCache()