๐ก 06. Optimistic Updates (๋๊ด์ ์ ๋ฐ์ดํธ) ์ค์
๐ ๊ฐ์
์๋ฒ ์๋ต์ ๊ธฐ๋ค๋ฆฌ์ง ์๊ณ UI๋ฅผ ๋จผ์ ์ ๋ฐ์ดํธํ์ฌ ์ฑ์ ์ฒด๊ฐ ์๋(UX)๋ฅผ ๊ทน๋ํํ๋ ๊ธฐ๋ฒ๊ณผ ์๋ฌ ๋กค๋ฐฑ ๋งค์ปค๋์ฆ์ ๋ฐฐ์๋๋ค.
๐ ๋ชฉ์ฐจ
- โ๏ธ ์์ฒ ์ด์ ๊ณ ๋ฏผ: "์๋ฒ๊ฐ ๋๋ฆฌ๋ฉด ํ๋ฉด๋ ๋๋ ค์ผ ํ๋์?"
- ๐ค ์ ์์์ผ ํ๋๊ฐ: UX์ ์ง์ ๋์ฝ
- 1. ๋๊ด์ ์ ๋ฐ์ดํธ์ ํ์ ์ฝ๋ฐฑ 3์ด์ฌ
- 2. ์ค์ ์ฝ๋ ์ํฌ์ต: ์ข์์ ๊ธฐ๋ฅ ์๋ฒฝ ๊ตฌํ
- ๐ ๋ฐฐ์ด ๋ด์ฉ ์ ๊ฒํ๊ธฐ (Quiz)
"์์(UX ๋์์ด๋) ๋, ์ฌ์ฉ์๊ฐ ์ข์์ ๋ฒํผ ๋๋ฅผ ๋๋ง๋ค 0.5์ด์ฉ ๋ ๊ฑธ๋ฆฌ๋ ๊ฑฐ ๋ฐฉ๊ธ ๊ณ ์ณค์ต๋๋ค. ์ด์ ๋ฒ๊ฐ์ฒ๋ผ ๋ณํ ๊ฑฐ์์."
โ๏ธ ์์ฒ ์ด์ ๊ณ ๋ฏผ: "์๋ฒ๊ฐ ๋๋ฆฌ๋ฉด ํ๋ฉด๋ ๋๋ ค์ผ ํ๋์?"
(์์นจ ํ์ ์๊ฐ, ์์ ๋์์ด๋์ ํผ๋๋ฐฑ์ ๋ฃ๋ ์์ฒ )
๐จ ์์ (UX ๋์์ด๋): "์์ฒ ๋, ์ฐ๋ฆฌ ์ปค๋ฎค๋ํฐ ์ฑ ๋ง์ธ๋ฐ์... ์ธ์คํ๊ทธ๋จ์ด๋ ํธ์ํฐ๋ '์ข์์' ๋๋ฅด๋ฉด ๋ฐ๋ก ๋นจ๊ฐ๊ฒ ๋ณํ์์์? ๊ทผ๋ฐ ์ฐ๋ฆฌ ์ฑ์ ๋๋ฅด๊ณ ๋์ ํ 0.5์ด ๋ค์์ผ ์๊น์ด ๋ณํด์ ๋๊ฒ ๋ต๋ตํด์. ๊ฐ๋์ ๋ด๊ฐ ์ ๋๋ ๋ ์ถ์ด์ ๋ ๋๋ฅด๊ฒ ๋ผ์."
๐ฃ ์์ฒ : ์... ๊ทธ๊ฒ, ์ ํฌ ์๋ฒ ์๋ต ์๋๋ ๋คํธ์ํฌ ์ง์ฐ(Latency) ๋๋ฌธ์ ๋ฌผ๋ฆฌ์ ์ผ๋ก ์ด์ฉ ์๊ฐ ์๋ ๋ถ๋ถ์ด์์. ๋ฐฑ์๋์์ 200 OK ์ฌ์ธ์ด ๋จ์ด์ ธ์ผ ์ ๊ฐ ๋ง ๋๊ณ ํ๋ฉด ์๊น์ ๋ฐ๊ฟ์ค ์ ์๋ค๊ณ ์ ใ
ใ
๐ฆ ์ํธ: ์์ฒ ๋, ํ๋ก ํธ์๋๊ฐ ๋ฐฑ์๋ ์๋์ ํ๊ณ๋ฅผ ๋๋ฉด ์ ๋์ฃ . ๋ฐ์ดํฐ ๋ณ๊ฒฝ ์์ฒญ(Mutation)์ด ๋น์ฐํ ์ฑ๊ณตํ ๊ฒ์ด๋ผ ๋๊ด(Optimistic) ํ๊ณ ์ผ๋จ ํ๋ฉด๋ถํฐ ๋ ๋ค ๋ฐ๊ฟ์ฃผ๋ Optimistic Updates ๊ธฐ๋ฒ์ ์ฐ์๋ฉด ๋ฉ๋๋ค.
๐ค ์ ์์์ผ ํ๋๊ฐ: UX์ ์ง์ ๋์ฝ
์ธ์คํ๊ทธ๋จ, ๋
ธ์
, ํธ๋ ๋ก ๊ฐ์ ๊ธ๋ก๋ฒ ํํฐ์ด ์ฑ๋ค์ ๋ฒํผ์ ๋๋ฅด๋ ์๊ฐ 1๋ฐ๋ฆฌ์ด์ ์ง์ฐ๋ ์์ด ์ํธ์์ฉ์ด ์ผ์ด๋ฉ๋๋ค. ์ฌ์ค ๋ค์์๋ ์ฌ์ ํ ๋คํธ์ํฌ fetch ํต์ ์ด ๋์๊ฐ๊ณ ์์ง๋ง ๋ง์ด์ฃ .
React Query์์ ์คํ๋ผ์ธ ํ๊ฒฝ ๋ฐ ์ด์ ํ ์ธํฐ๋ท ํ๊ฒฝ์์๋ ์๋ฒฝํ ๋งค๋๋ฌ์ด UX๋ฅผ ์ ๊ณตํ๊ธฐ ์ํด ํ์์ ์ผ๋ก ์ฌ์ฉํ๋ ๊ธฐ์ ์ด ๋ฐ๋ก ๋๊ด์ ์ ๋ฐ์ดํธ์ ๋๋ค.
ํ์ง๋ง ๋ฌดํฑ๋๊ณ ํ๋ฉด๋ง ๋ฐ๊ฟจ๋ค๊ฐ ์ง์ง๋ก ์๋ฒ์์ ์ค๋ฅ(500 Error)๊ฐ ๋๋ฉด ์ด๋ป๊ฒ ๋ ๊น์? ์๋ชป ๋ฐ๋ ํ๋ฉด์ ์๋๋๋ก ๋๋๋ฆฌ๋ ๋กค๋ฐฑ(Rollback) ์์
์ด ์ ๊ตํ๊ฒ ๋๋ฐ๋์ด์ผ๋ง ํฉ๋๋ค. ์ด ์ฑํฐ์์๋ ์๋ฒ ์๋ฌ์ ๋์ฒํ๋ ์ฐ์ํ ๋กค๋ฐฑ ๋ก์ง์ TypeScript์ ํจ๊ป ์ค๊ณํด๋ด
๋๋ค.
1. ๋๊ด์ ์ ๋ฐ์ดํธ์ ํ์ ์ฝ๋ฐฑ 3์ด์ฌ
useMutation ํ
์์๋ ๋๊ด์ ์ํ๊ณ๋ฅผ ๊ตฌ์ฑํ๋ 3๊ฐ์ง ์๋ช
์ฃผ๊ธฐ(Lifecycle) ์ฝ๋ฐฑ์ด ์์ต๋๋ค.
onMutate: ๋์ฐ๋ณ์ด ์คํ ์ง์ ์ ๋ฐ๋! (์ฌ๊ธฐ์ ํ๋ฉด์ ๊ฐ์ง๋ก ๋ฐ๊พผ๋ค)onError: ์ง์ง๋ก ์๋ฒ ํต์ ํ๋๋ฐ ์๋ฌ ๊ฐ ๋ฌ์ ๋! (์ฌ๊ธฐ์ ํ๋ฉด์ ๋ค์ ์๋ ๋ก ๋๋๋ฆฐ๋ค)onSettled: ์ฑ๊ณตํ๋ ์คํจํ๋ ๋ง์ง๋ง ์ ๋ฌด์กฐ๊ฑด ์คํ! (์ด์จ๋ ๋๋ฌ์ผ๋ ์๋ฒ ์ต์ ๋ฐ์ดํฐ๋ก ์ต์ข ๋ฎ์ด์์ด๋ค)
๐ TypeScript ํ์ ์ ์ค์์ฑ
TypeScript ํ๊ฒฝ์์ ๋๊ด์ ์
๋ฐ์ดํธ๋ฅผ ์งค ๋๋ ์ ๋ค๋ฆญ(Generic) ํ์
์ ์ ํํ ๋ง์ถ๋ ๊ฒ์ด ํต์ฌ์
๋๋ค. ์๋ฌ ๋ฐ์ ์ onError๋ก ์๋ ๋ฐ์ดํฐ๋ฅผ ๋๊ฒจ์ฃผ๋ ค๋ฉด(Context), onMutate์์ ๋ฐํํ๋ ํ์
๊ณผ onError์์ ๋ฐ๋ Context์ ํ์
์ด ์ผ์นํด์ผ ํฉ๋๋ค.
// ๐ Mutation Context ํ์
ํ๊ธฐ
// ์ด์ ์บ์ ๋ฐ์ดํฐ๋ฅผ ์ ์ฅํด๋๋ค๊ฐ ์๋ฌ๊ฐ ๋๋ฉด ๋กค๋ฐฑํ ๋ ์ฐ๋ '์์ ์ฅ์น'
type TodoContext = {
previousTodos: Todo[] | undefined;
};2. ์ค์ ์ฝ๋ ์ํฌ์ต: ์ข์์ ๊ธฐ๋ฅ ์๋ฒฝ ๊ตฌํ
์์กฐ ๋ฆฌ๋๊ฐ ์ง์ ์ง์ฃผ๋ "์ ๋ ์คํจํ์ง ์๋ ๋๊ด์ ์ข์์ ๋ฒํผ" ์ฝ๋๋ฅผ ํ ์ค์ฉ ๋ฏ์ด๋ด ์๋ค.
import { useMutation, useQueryClient } from '@tanstack/react-query';
// DB์ ์ ์ฅ๋ Post ํ์
type Post = {
id: number;
title: string;
likes: number; // ์ข์์ ๊ฐ์
};
export function useLikePost() {
const queryClient = useQueryClient();
return useMutation({
// 1๏ธโฃ ์ง์ง ์๋ฒ ํต์
mutationFn: async (postId: number) => {
// (๊ฐ์ ) ์๊ฐ์ด 1์ด ๊ฑธ๋ฆฌ๋ ๋๋ฆฐ API
const res = await axios.post(`/posts/${postId}/like`);
return res.data;
},
// 2๏ธโฃ ๐ [ํต์ฌ] onMutate: ๋ฒํผ ํด๋ฆญ ์ฆ์ ๋ฐ๋! (์๋ฒ ๋๊ธฐ ์ ํจ)
onMutate: async (postId: number) => {
// 1. ํน์ ๋ฐฑ๊ทธ๋ผ์ด๋์์ ๋๊ณ ์๋ ๋ค๋ฅธ ํ์น๊ฐ ์๋ค๋ฉด ๊ฐ์ ๋ก ์ทจ์!
// (์ด ๊ฐ์ด ์ด์ ๊ฒฐ๊ณผ๋ก ๋ฎ์ด์์์ง๋ ์ต์
์ Race Condition ๋ฐฉ์ง)
await queryClient.cancelQueries({ queryKey: ['posts'] });
// 2. ์๋ฌ ๋ฌ์ ๋๋ฅผ ๋๋นํด์, ์
๋ฐ์ดํธ ์ '์๋ ๋ฐ์ดํฐ'๋ฅผ ๋ณ์์ ์ ๊น ๋ณด๊ด (์ค๋
์ท)
const previousPosts = queryClient.getQueryData<Post[]>(['posts']);
// 3. โจ ๋๋์ด ๋๊ด์ ์
๋ฐ์ดํธ ๋ฐ๋! ํ๋ฉด์ ๋ณด์ผ ์บ์ ๋ฐ์ดํฐ๋ฅผ ์๋์ผ๋ก ์์ ํ๋ค.
queryClient.setQueryData<Post[]>(['posts'], (old) => {
if (!old) return [];
return old.map(post =>
// ๋ด๊ฐ ์ง๊ธ ๋๋ฅธ ๊ฒ์๋ฌผ์ ์ข์์ ์๋ง ๊ฐ์ง๋ก +1 ์์ผ๋ฒ๋ฆผ (ํ๋ฉด ์ฆ์ ๋ฐ์)
post.id === postId ? { ...post, likes: post.likes + 1 } : post
);
});
// 4. ์๋ ๋ณด๊ดํด๋ ๋ฐ์ดํฐ๋ฅผ Context(ํผ๋์ฒ)์ ๋ด์์ ๋ฆฌํด! -> ์ด๊ฑด onError๋ก ์ ๋ฌ๋จ
return { previousPosts };
},
// 3๏ธโฃ ๐ฃ ๋ง์ฝ ์๋ฒ์์ 500 ์๋ฌ๊ฐ ๋ฌ๋ค๋ฉด? (๋กค๋ฐฑ)
onError: (err, postId, context) => {
// context์ ์๊น onMutate์์ ๋ฆฌํดํ๋ ์๋ ์ค๋
์ท์ด ๋ค์ด์๋ค.
if (context?.previousPosts) {
// ์๋ ๋ฐ์ดํฐ๋ก ๋ค์ ์บ์๋ฅผ ๋ณต๊ตฌ์์ผ๋ฒ๋ฆฐ๋ค! (Undo)
queryClient.setQueryData(['posts'], context.previousPosts);
}
},
// 4๏ธโฃ โ
์ฑ๊ณต์ด๋ ์คํจ๋ ๋ชจ๋ ๊ฒ ๋๋ฌ๋ค๋ฉด? (๊ฒ์ฆ)
onSettled: () => {
// ํน์ ๋ก์ปฌ ์กฐ์ ์ค ๊ผฌ์์์ง ๋ชจ๋ฅด๋ ์๋ฒ์์ ์ฐ ์ต์ ๋ฐ์ดํฐ๋ฅผ ๋ค์ ํ๋ฒ ๊น๋ํ๊ฒ ๋ถ์ด์จ๋ค.
queryClient.invalidateQueries({ queryKey: ['posts'] });
},
});
}์ด ํ๋ฆ์ ๊ฑฐ์ ๊ณต์(Boilerplate)์ ๊ฐ๊น์ต๋๋ค. ์ธ์ฅํ๋์ฒ๋ผ ๋ผ๋ค ๋ถ์ด์ ๋ ๋ ์ ๋๋ก ์๋ฒฝํ ์์ ์ฑ์ ์๋ํฉ๋๋ค.
๐ฃ ์์ฒ ์ด์ ํด๊ทผ ์ผ๊ธฐ
์, ์์งํ ์ฝ๋ ๋ณผ ๋ onMutate, cancelQueries, setQueryData ์ฃ๋ค ์์ฌ ์์ด์ ๋ณต์กํด ๋ณด์๋๋ฐ ์ง์ ํ์ ์ณ๋ณด๋๊น ์๋ฆฌ๊ฐ ๊ธฐ๊ฐ ๋งํ๋ค.
"1๋ฒ ๋ฉ์ถ๊ณ -> 2๋ฒ ๊ณผ๊ฑฐ ๋ฐฑ์
ํ๊ณ -> 3๋ฒ ์ต์ ์ธ ์ฒ ์์ฅํ๊ณ -> 4๋ฒ ๋งํ๋ฉด ๋ณต๊ตฌ / ์ฑ๊ณตํ๋ฉด ๋ฆฌ๋ด์ผ". ์ด ๋ค ๋ฐ์๊ฐ ์์ ์์ ์ด๋ค.
๐ก ์ค๋์ ๊ตํ: "์๋ฒ๊ฐ ๋๋ฆฌ๋ค๊ณ ๋ด ์ปดํฌ๋ํธ๊น์ง ๋๋ฆด ํ์๋ ์๋ค! ์ฌ์ฉ์์ ์ ๋ ฅ์ ๋ฏฟ๊ณ (Optimistic) ํ๋ฉด๋ถํฐ ๋ณด์ฌ์ค ๋ค, ๋ท๋จ์์ ์ฌ๊ณ ๋๋ฉด ์กฐ์ฉํ ๋กค๋ฐฑ(Rollback)ํด์ฃผ๋ ์ฐ์ํ ์ฌ๊ธฐ(?)๋ฅผ ์น์."
์์ ๋์์ด๋ ๋ํํ ์ด๊ฑฐ ์ ์ฉํ๊ณ ์์ฐ ๋ณด์ฌ๋๋ ธ๋๋ ๋์ด ๋ฅ๊ทธ๋์ ธ์ ์ปคํผ ์ฌ์ฃผ์ จ๋ค ใ ใ ใ . ๋ด์ผ์ ์ฅ๋ฐ๊ตฌ๋์ ์์ดํ ๋ด๋ ๊ฒ๋ ๋๊ด์ ์ ๋ฐ์ดํธ๋ก ๋ฐ๊ฟ๋์ผ์ง. (๋จ, ๊ฒฐ์ ๊ฐ์ ์น๋ช ์ ์ธ ๊ฑด ๋๊ด์ ์ผ๋ก ํ๋ฉด ํฐ์ผ๋๋ค๊ณ ์ํธ ๋์ด ๊ฒฝ๊ณ ํ์ จ๋ค... ํต์ฅ ์๊ณ 0์์ธ๋ฐ ๊ฒฐ์ ์๋ฃ ๋์ธ ๋ป ใ )
๐ ๋ฐฐ์ด ๋ด์ฉ ์ ๊ฒํ๊ธฐ (Quiz)
Q. ๋๊ด์ ์
๋ฐ์ดํธ๋ฅผ ๊ตฌํํ๋ onMutate ๋ธ๋ก์ ์ต์๋จ์์ ๊ฐ์ฅ ๋จผ์ await queryClient.cancelQueries({ queryKey: [...] }) ๋ฅผ ํธ์ถํ์ฌ ํ์ฑํ๋ ๋คํธ์ํฌ ์์ฒญ์ ์ทจ์(Cancel)ํ๋ ๊ทผ๋ณธ์ ์ธ ์ด์ ๋ ๋ฌด์์ธ๊ฐ์?
โ
์ ๋ต: ์ด์ ์ ์งํ ์ค์ด๋ ๋ฐฑ๊ทธ๋ผ์ด๋ ํ์น(Refetch)๊ฐ ํ๋ฐ ๋ฆ๊ฒ ๋์ฐฉํ์ฌ, ์ฐ๋ฆฌ๊ฐ ์๋์ผ๋ก ๊น์๋ ๋๊ด์ ์บ์(๊ฐ์ง ์ ๋ฐ์ดํฐ)๋ฅผ ๊ณผ๊ฑฐ์ ๋ก์ ๋ฐ์ดํฐ๋ก ๋ฎ์ด์์๋ฒ๋ฆฌ๋ ์ฐธ์ฌ(Race Condition)๋ฅผ ๋ง๊ธฐ ์ํด์์
๋๋ค.
๐ก ์์ธ ํด์ค:
- ์๋ฆฌ ์ค๋ช
: ๋ง์ฝ ์ฌ์ฉ์๊ฐ ์ข์์๋ฅผ ๋๋ฅด๊ธฐ 0.1์ด ์ ์ ๋ค๋ฅธ ์ด์ (์: ํญ ์ด๋)๋ก
['posts']์ฟผ๋ฆฌ๊ฐ ๋ฐฑ๊ทธ๋ผ์ด๋ ํจ์น๋ฅผ ์ด ๋ ์ํ์๋ค๊ณ ๊ฐ์ ํฉ์๋ค. ์ฌ์ฉ์๊ฐ ๋ฒํผ์ ๋๋ฌ ํํธ ๊ฐ์๋ฅผ ๊ฐ์ง๋ก+1ํด๋์๋๋ฐ, ๊ทธ ์งํ์ ์๊น ์๋ ๋ฐฑ๊ทธ๋ผ์ด๋ ํจ์น์ ์๋ต์ด ๋์ฐฉํด๋ฒ๋ฆฌ๋ฉด ์บ์๋ ๋ค์ ๊ณผ๊ฑฐ ๋ฐ์ดํฐ(ํํธ ๊ฐ์ -1) ์ํ๋ก ๋ฎ์ด์์์ ธ ๋ฒ๋ฆฌ๋ฉฐ ํ๋ฉด์ ํํธ๊ฐ ๊ป๋ป๊ป๋ป์ด๋ ๋ฒ๊ทธ๊ฐ ๋ฐ์ํฉ๋๋ค. - ์ค๋ต ํผ๋๋ฐฑ: "์์ฒ ๋, '์ด์ฐจํผ ๋๊ด์ ์ธ๋ฐ ๋ท๋จ์์ ๋ญฃํ๋ฌ ๋๋ ๊ฑฐ ์ทจ์ํด์?' ๋ผ๋จ! ์๋ ์กฐ์(setQueryData)์ ํ ๋ ์บ์์ ์์ ๊ถ์ ํ์ฌ ๊ฐ๋ฐ์์๊ฒ ์์ด์ผ ํฉ๋๋ค. ๊ณผ๊ฑฐ์ ๋ง๋ น(์ด์ ํ์นญ ๊ฒฐ๊ณผ)์ด ํจ๋ถ๋ก ์นจ๋ฒํ์ง ๋ชปํ๊ฒ ๋ฌธ์ ์ ๊ทธ๋(Cancel) ํ์๋ ๋๊ด์ ์ ๋ฐ์ดํธ์ ์ 1 ์์น์ ๋๋ค!"
- ๐ ํต์ฌ ๊ธฐ์ต๋ฒ: ๋๊ด์ ์๋ ๋ฎ์ด์ฐ๊ธฐ ์ ์ ๋ฌด์กฐ๊ฑด ๋จ์ ์์ฌ ์ฟผ๋ฆฌ๋ค
Cancel๋น ์๊ธฐ!