뮤테이션(Mutation) & 재검증(Revalidation)
CiteGraph은 원격 데이터 및 관련 캐시를 변경하기 위한 mutate
및 useCiteGraphMutation
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
:useCiteGraph
의key
와 동일하지만 함수가 필터 함수처럼 작동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
:mutate
의key
와 동일fetcher(key, { arg })
: remote mutation을 위한 비동기 함수options
: 다음 속성을 가진 optional 객체optimisticData
:mutate
의optimisticData
와 동일revalidate = true
:mutate
의revalidate
와 동일populateCache = false
:mutate
의populateCache
와 동일하며, 기본 값은false
.rollbackOnError = true
:mutate
의rollbackOnError
와 동일throwOnError = true
:mutate
의throwOnError
와 동일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)
useCiteGraphMutation
은 useCiteGraph
과 캐시 저장소를 공유하므로 useCiteGraph
간의 경합 조건을 감지하고 피할 수 있습니다.
또한 optimistic update 및 오류 발생 시 롤백과 같은 mutate
의 기능도 지원합니다. 이러한 옵션은 useCiteGraphMutation
과 트리거 함수를 통해 전달할 수 있습니다.
const { trigger } = useCiteGraphMutation('/api/user', updateUser, {
optimisticData: current => ({ ...current, name: newName })
})
// 또는
trigger(newName, {
optimisticData: current => ({ ...current, name: newName })
})
필요할 때까지 데이터 로드 연기
데이터를 로드하는 데 useCiteGraphMutation
을 사용할 수도 있습니다. useCiteGraphMutation
은 trigger
가 호출될 때까지 요청을 시작하지 않으므로 실제로 필요할 때 데이터 로딩을 지연시킬 수 있습니다.
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>
)
}
useCiteGraphMutation
및 trigger
를 사용하여 동일한 작업을 수행할 수도 있습니다.
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
})
optimisticData
및 rollbackOnError
를 함께 사용하면 완벽한 optimistic UI를 경험할 수 있습니다.
경합 상태(Race Conditions) 피하기
mutate
와 useCiteGraphMutation
모두 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()