๐Ÿ“ 06. [์‹ค๋ฌด] ํผ(Form) ์—ฐ๋™ ๋ฐ ์˜คํ”„๋ผ์ธ-First ์•ฑ ์„ค๊ณ„

2026๋…„ 3์›” 4์ผ ์ˆ˜์ •๋จ

๐Ÿ“‹ ๊ฐœ์š”

React Hook Form๊ณผ React Query๋ฅผ ์ถฉ๋Œ ์—†์ด ๊ฒฐํ•ฉํ•˜๋Š” ๋…ธํ•˜์šฐ์™€, ๋„คํŠธ์›Œํฌ๊ฐ€ ๋Š๊ธด ์˜คํ”„๋ผ์ธ ์ƒํ™ฉ์—์„œ๋„ ๋™์ž‘ํ•˜๋Š” ์˜คํ”„๋ผ์ธ ํผ์ŠคํŠธ(Offline-First) UX๋ฅผ ์„ค๊ณ„ํ•ฉ๋‹ˆ๋‹ค.

๐Ÿ“‹ ๋ชฉ์ฐจ

"์˜์ฒ  ๋‹˜, ์œ ์ €๊ฐ€ ์‚ฐ์†์—์„œ ํ„ฐ๋„ ์ง€๋‚  ๋•Œ ๊ธ€ ์ž‘์„ฑ ๋ฒ„ํŠผ ๋ˆŒ๋ €๋”๋‹ˆ ์—๋Ÿฌ ๋‚˜๊ณ  ๋ฐฉ๊ธˆ ์“ด ๊ธ€ ํ•œ ๋ฐ”๋‹ฅ ๋‹ค ๋‚ ์•„๊ฐ”๋‹ค๋Š”๋ฐ์š”?"

โ˜•๏ธ ์˜์ฒ ์ด์˜ ๊ณ ๋ฏผ: "์ž…๋ ฅ์ฐฝ ๋ฐ์ดํ„ฐ๋ž‘ ์บ์‹œ ๋ฉ”๋ชจ๋ฆฌ๊ฐ€ ์ž๊พธ ์‹ธ์›Œ์š”!"

(๋ชฉ์š”์ผ ์˜คํ›„, ํผ(Form) ํ™”๋ฉด์˜ ๋ฒ„๊ทธ ๋ฆฌํฌํŠธ๋ฅผ ์ฝ๊ณ  ๋จธ๋ฆฌ๋ฅผ ์ฅ์–ด๋œฏ๋Š” ์˜์ฒ )

๐Ÿฃ ์˜์ฒ : ๋ฆฌ๋“œ ๋‹˜, React Hook Form ๋„์ž…ํ•ด์„œ ํšŒ์›์ •๋ณด ์ˆ˜์ • ์ฐฝ ๋งŒ๋“ค์—ˆ๊ฑฐ๋“ ์š”?
๊ทผ๋ฐ ์ฒซ ํ™”๋ฉด ๋„์šธ ๋•Œ useQuery๋กœ ๋ฐ›์•„์˜จ ์œ ์ € ์ •๋ณด๋ฅผ ํผ ์ดˆ๊ธฐ๊ฐ’์œผ๋กœ ๋„ฃ์–ด์ฃผ๊ณ , ์œ ์ €๊ฐ€ ๋ง‰ ํƒ€์ดํ•‘ํ•˜๋‹ค๊ฐ€ 1๋ถ„์ด ์ง€๋‚˜๋‹ˆ๊นŒ... React Query ๋ฐฑ๊ทธ๋ผ์šด๋“œ ๋ฆฌํŒจ์นญ์ด ๋Œ๋ฉด์„œ useQuery ๋ฐ์ดํ„ฐ๊ฐ€ ์ƒˆ๋กœ ๋“ค์–ด์˜ค๊ณ  ์ œ ํผ ์ž‘์„ฑ ๋‚ด์šฉ์ด ์‹น ๋‹ค ๋ฆฌ์…‹๋ผ๋ฒ„๋ ค์š”! ์œ ์ €๋“ค ํ•ญ์˜๊ฐ€ ๋น—๋ฐœ์นฉ๋‹ˆ๋‹ค ใ… ใ… .

๊ทธ๋ฆฌ๊ณ  ํ•˜๋‚˜ ๋”์š”! ์™€์ดํŒŒ์ด ์‚ด์ง ๋Š๊ฒผ์„ ๋•Œ "์ €์žฅ" ๋ฒ„ํŠผ ๋ˆ„๋ฅด์‹  ๋ถ„๋“ค์€ Network Error ๋œจ๋ฉด์„œ ์ž‘์„ฑ ๋‚ด์šฉ์ด ํ—ˆ๊ณต์œผ๋กœ ์ฆ๋ฐœํ•ด๋ฒ„๋ ธ๋Œ€์š”. ๋ฏธ์น˜๊ฒ ์–ด์š”.

๐Ÿฆ ์˜ํ˜ธ: ์˜์ฒ  ๋‹˜, ํ”„๋ก ํŠธ์—”๋“œ ๊ฐœ๋ฐœ์˜ ์ง„์ •ํ•œ ์ง€์˜ฅ 2๊ฐ€์ง€, ์—๋””ํ„ฐ(Form) ์™€ ๊ฒฐ์† ๋„คํŠธ์›Œํฌ(Offline) ๋ฅผ ๋™์‹œ์— ๋งž์œผ์…จ๊ตฐ์š”.
์ฒซ ๋ฒˆ์งธ ๋ฌธ์ œ๋Š” Server State์™€ Client State๋ฅผ ํ•œ ๊ณต๊ฐ„์— ์„ž์–ด๋†”์„œ ๋ฒŒ์–ด์ง„ ์ฐธ์‚ฌ๊ณ (TkDodo #14),
๋‘ ๋ฒˆ์งธ ๋ฌธ์ œ๋Š” React Query v5์— ๋‚ด์žฅ๋œ ์˜คํ”„๋ผ์ธ ๋Œ€์‘ ๋ฎคํ…Œ์ด์…˜(Offline Mutation) ์˜ต์…˜์„ ์•ˆ ์ผœ๋‘ฌ์„œ ๊ทธ๋ ‡์Šต๋‹ˆ๋‹ค (TkDodo #13).
์‹ค์ „์˜ ๋ฒฝ, ํผ๊ณผ ์˜คํ”„๋ผ์ธ์„ ์šฐ์•„ํ•˜๊ฒŒ ์ •๋ณตํ•ด ๋ด…์‹œ๋‹ค.


๐Ÿค” ์™œ ์•Œ์•„์•ผ ํ•˜๋Š”๊ฐ€: ๊ทนํ•œ์˜ UX ๋ฐฉ์–ด์„ 

์›น ์•ฑ์ด ๊ฐ€์žฅ ๋งŽ์ด ๋ฐ•์‚ด๋‚˜๋Š” ๊ณณ์€ ์œ ์ €๊ฐ€ ํ…์ŠคํŠธ๋ฅผ ์ž…๋ ฅํ•˜๋Š” ํผ(Form) ํ™”๋ฉด์ž…๋‹ˆ๋‹ค.
์„œ๋ฒ„์—์„œ ๋ฐ์ดํ„ฐ๋ฅผ ์ฝ์–ด์™€์„œ(Read), ์‚ฌ์šฉ์ž๊ฐ€ ๊ทธ๊ฑธ ๊ณ ์น˜๊ณ (Update), ๋‹ค์‹œ ์„œ๋ฒ„์— ์ด์•ผ ํ•˜์ฃ (Mutation).

์—ฌ๊ธฐ์„œ React Query(์„œ๋ฒ„์˜ ์ง„์‹ค)์™€ React Hook Form(์œ ์ €์˜ ์ž…๋ ฅ๊ฐ’)์˜ ํƒ€์ด๋ฐ์„ ์—„๊ฒฉํžˆ ํ†ต์ œํ•˜์ง€ ๋ชปํ•˜๋ฉด ์˜์ฒ ์ด์ฒ˜๋Ÿผ ํผ์ด ๊ฐ•์ œ ๋ฆฌ์…‹๋˜๋Š” ๋Œ€์ฐธ์‚ฌ๊ฐ€ ํ„ฐ์ง‘๋‹ˆ๋‹ค.

๋˜ํ•œ ๋ชจ๋ฐ”์ผ ๋ธŒ๋ผ์šฐ์ € ํ™˜๊ฒฝ์€ ์–ธ์ œ ํ„ฐ๋„์ด๋‚˜ ์ง€ํ•˜์ฒ ์— ๋“ค์–ด๊ฐ€ ์ธํ„ฐ๋„ท์ด 10์ดˆ๊ฐ„ ๋Š๊ธธ์ง€ ๋ชจ๋ฆ…๋‹ˆ๋‹ค. ์ด๋•Œ "์ธํ„ฐ๋„ท์ด ๋Š๊ฒผ์Šต๋‹ˆ๋‹ค" ํ•˜๊ณ  ํŠ•๊ฒจ๋‚ด๋Š” ์•ฑ๊ณผ, "์ผ๋‹จ ์ €์žฅํ•ด ๋‘˜๊ฒŒ์š”!" ํ•˜๊ณ  ์ธํ„ฐ๋„ท์ด ์—ฐ๊ฒฐ๋˜๋Š” ์ˆœ๊ฐ„ ๋’ค์—์„œ ๋ชฐ๋ž˜ ์ด์ฃผ๋Š” ์•ฑ์˜ ํด๋ž˜์Šค ์ฐจ์ด๋Š” ํ•˜๋Š˜๊ณผ ๋•… ์ฐจ์ด์ž…๋‹ˆ๋‹ค.


1. ํผ(Form) ์—ฐ๋™์˜ ํ™ฉ๊ธˆ๋ฅ : "์ดˆ๊นƒ๊ฐ’์€ ๋ถ€๋ชจ๊ฐ€ ๋„˜๊ธฐ๊ณ  ์†์ ˆํ•˜๋ผ"

์˜์ฒ ์ด์˜ ํผ ์ดˆ๊ธฐํ™” ์—๋Ÿฌ ์›์ธ์€ Basic 08๊ฐ•์—์„œ ์‚ด์ง ๋‹ค๋ฃจ์—ˆ์Šต๋‹ˆ๋‹ค.
useQuery์—์„œ ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ›์•„์™€์„œ ํผ์˜ defaultValues์— ์ฐ”๋Ÿฌ ๋„ฃ์—ˆ๋Š”๋ฐ, 1๋ถ„ ๋’ค ๋ฐฑ๊ทธ๋ผ์šด๋“œ์—์„œ ์บ์‹œ๊ฐ€ ๊ฐฑ์‹ ๋˜์ž ๊ทธ ๋ฐ์ดํ„ฐ๊ฐ€ ๋‹ค์‹œ ํผ์„ ๋•Œ๋ ค๋ฒ„๋ฆฐ ๊ฑฐ์ฃ .

์ด๋ฅผ ๋ง‰์œผ๋ ค๋ฉด ๋ทฐ(View)๋ฅผ ๋ถ„๋ฆฌํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค!
๋ฐ์ดํ„ฐ๋ฅผ ํผ์˜ค๊ธฐ๋งŒ ํ•˜๋Š” ๋ถ€๋ชจ(Data Fetcher) ์™€ ํผ๋งŒ ๊ฐ€์ง€๊ณ  ๋…ธ๋Š” ์ž์‹(Form Editor) ์œผ๋กœ ๋ง์ด์ฃ .

// ๐Ÿ“ ๋ถ€๋ชจ ์ปดํฌ๋„ŒํŠธ: ์˜ค์ง ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜ค๊ณ  ๋„˜๊ฒจ์ฃผ๊ธฐ๋งŒ ํ•œ๋‹ค.
function UserProfilePage() {
  const { data: user } = useQuery({ queryKey: ['user'], queryFn: fetchUser });
  
  if (!user) return <Spinner />;
 
  // ๐Ÿš€ ํ™ฉ๊ธˆ๋ฅ  1: ๋ฐ์ดํ„ฐ๊ฐ€ ์™„์ „ํžˆ ์ค€๋น„๋œ ์ƒํƒœ์—์„œ๋งŒ ์ž์‹ ํผ์„ ๊ทธ๋ฆฐ๋‹ค.
  return <UserProfileForm initialData={user} />
}
// ๐Ÿ“ ์ž์‹ ์ปดํฌ๋„ŒํŠธ: React Hook Form (์˜ค์ง ํด๋ผ์ด์–ธํŠธ ์ƒํƒœ)
import { useForm } from 'react-hook-form';
 
type FormValues = { name: string; age: number };
 
function UserProfileForm({ initialData }: { initialData: User }) {
  const { register, handleSubmit } = useForm<FormValues>({
    // ๐Ÿš€ ํ™ฉ๊ธˆ๋ฅ  2: ๋ถ€๋ชจ๊ฐ€ ์ค€ ๋ฐ์ดํ„ฐ๋ฅผ "์ตœ์ดˆ์˜ ์ดˆ๊นƒ๊ฐ’"์œผ๋กœ๋งŒ ์“ฐ๊ณ  ๋’ค๋„ ๋Œ์•„๋ณด์ง€ ์•Š๋Š”๋‹ค.
    defaultValues: { name: initialData.name, age: initialData.age },
  });
 
  const mutation = useMutation({
    mutationFn: updateUser,
    onSuccess: () => queryClient.invalidateQueries({ queryKey: ['user'] }),
  });
 
  const onSubmit = (values: FormValues) => {
    mutation.mutate(values); 
  };
 
  return <form onSubmit={handleSubmit(onSubmit)}>...</form>
}

์ด๋ ‡๊ฒŒ ํŠธ๋ฆฌ๋ฅผ ์ชผ๊ฐœ๋ฉด ๋ถ€๋ชจ ์ชฝ React Query๊ฐ€ 1๋ถ„๋งˆ๋‹ค ๋ฐฑ๊ทธ๋ผ์šด๋“œ ํ†ต์‹ ์„ ํ•˜๋“  ๋ง๋“ , ์ž์‹ ์ชฝ์˜ useForm์€ ์ฒ˜์Œ ๋„˜๊ฒจ๋ฐ›์€ ์ดˆ๊นƒ๊ฐ’๋งŒ ์ฅ๊ณ  ์ž๊ธฐ ๊ฐˆ ๊ธธ์„ ๊ฐ€๊ธฐ ๋•Œ๋ฌธ์— ์œ ์ €๊ฐ€ ํƒ€์ดํ•‘ํ•˜๋˜ ๊ธ€์ž๊ฐ€ ๋ฆฌ์…‹๋˜๋Š” ์ผ์ด 0%๊ฐ€ ๋ฉ๋‹ˆ๋‹ค.


2. ์˜คํ”„๋ผ์ธ ํผ์ŠคํŠธ(Offline-First) ์„ค๊ณ„: "์ธํ„ฐ๋„ท์ด ๋Š๊ฒจ๋„ ์•ฑ์€ ์‚ด์•„์•ผ ํ•œ๋‹ค"

์ž, ๋‘ ๋ฒˆ์งธ ๋ฌธ์ œ! ์œ ์ €๊ฐ€ ํผ์„ ๋‹ค ์ž‘์„ฑํ•˜๊ณ  "์ €์žฅ" ๋ฒ„ํŠผ์„ ๋ˆŒ๋ €๋Š”๋ฐ ํ•˜ํ•„ ์ง€ํ•˜์ฒ  ์™€์ดํŒŒ์ด๊ฐ€ ๋Š๊ฒผ์Šต๋‹ˆ๋‹ค. ๋ฐ”๋‹๋ผ fetch ์˜€๋‹ค๋ฉด ์ฆ‰์‹œ Uncaught Network Error๊ฐ€ ๋– ์„œ ๋นจ๊ฐ„ ์•Œ๋ฆผ์ฐฝ์„ ๋ฟœ์—ˆ๊ฒ ์ฃ .

ํ•˜์ง€๋งŒ React Query์—๋Š” ๋ฌด์‹œ๋ฌด์‹œํ•œ ๊ธฐ๋ณธ๊ฐ’์ด ์ˆจ์–ด์žˆ์Šต๋‹ˆ๋‹ค.
๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜ค๋Š” useQuery ๋Š” ํ†ต์‹ ์ด ์‹คํŒจํ•˜๋ฉด ์ž๋™์œผ๋กœ 3๋ฒˆ๊นŒ์ง€ ์žฌ์‹œ๋„(Retry) ํ•˜๊ณ , ์˜คํ”„๋ผ์ธ ์ƒํƒœ์ผ ๋•Œ๋Š” ์ธํ„ฐ๋„ท์ด ์—ฐ๊ฒฐ๋  ๋•Œ๊นŒ์ง€ ์ผ์‹œ ์ •์ง€(Pause) ์ƒํƒœ๋กœ ์ˆจ์ฃฝ์ด๊ณ  ๊ธฐ๋‹ค๋ฆฝ๋‹ˆ๋‹ค (TkDodo ํ”ผ์…œ, ์ตœ๊ณ ์˜ ๋ฏธ๋•).

๊ทธ๋Ÿฐ๋ฐ ์—ฌ๊ธฐ์„œ ํ•จ์ • ์นด๋“œ!
๋ฐ์ดํ„ฐ๋ฅผ ์„œ๋ฒ„๋กœ ๋ณด๋‚ด๋Š”(์ˆ˜์ •ํ•˜๋Š”) useMutation ์€ ๊ธฐ๋ณธ์ ์œผ๋กœ Retry๊ฐ€ 0๋ฒˆ(๊บผ์ ธ์žˆ์Œ)์ž…๋‹ˆ๋‹ค. ์™œ๋ƒํ•˜๋ฉด ๊ฒฐ์ œ ๋ฒ„ํŠผ์„ 3๋ฒˆ ์—ฐ๋‹ฌ์•„ ๋ˆ„๋ฅด๋Š” ์‚ฌ๊ณ ๊ฐ€ ํ„ฐ์งˆ๊นŒ ๋ด ๋ณด์ˆ˜์ ์œผ๋กœ ์„ธํŒ…๋œ ๊ฑฐ๋‹ˆ๊นŒ์š”.

์˜คํ”„๋ผ์ธ ํ(Offline Mutation Queue) ํ•ด์ œ๋ฒ•

๋Œ“๊ธ€ ์ž‘์„ฑ, ์ข‹์•„์š”, ์ผ๋ฐ˜์ ์ธ ๊ธ€ ์ˆ˜์ • ๊ฐ™์€ ์•ˆ์ „ํ•œ(Idempotentํ•œ ๋А๋‚Œ์˜) Mutation์— ํ•œํ•˜์—ฌ, ์šฐ๋ฆฌ๋Š” ์ด๋“ค์„ ์ผ์‹œ ์ •์ง€(Pause)์‹œ์ผœ๋’€๋‹ค๊ฐ€ ์ธํ„ฐ๋„ท์ด ๋ณต๊ตฌ๋˜๋Š” ์ˆœ๊ฐ„ ์ผ์ œํžˆ ํˆฌํ•˜ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค!

// ๐Ÿ“ ์ปค์Šคํ…€ Mutation ํ›…
export const useSavePost = () => {
  const queryClient = useQueryClient();
 
  return useMutation({
    mutationFn: savePostApi,
    // ๐Ÿš€ ๋น„๊ธฐ 1: "์˜คํ”„๋ผ์ธ์ผ ๋• ์—๋Ÿฌ๋กœ ํŠ•๊ฒจ๋‚ด์ง€ ๋ง๊ณ , ์ผ์‹œ ์ •์ง€(Pause)์‹œ์ผœ์„œ ํ์— ๋‹ด์•„๋ผ!"
    networkMode: 'offlineFirst', 
    
    // ๐Ÿš€ ๋น„๊ธฐ 2: "๋„คํŠธ์›Œํฌ ๋Š๊ฒจ์„œ ์‹คํŒจํ–ˆ์–ด? ๊ทธ๋Ÿผ ์ธํ„ฐ๋„ท ๋ณต๊ตฌ๋  ๋•Œ๊นŒ์ง€ 3๋ฒˆ์€ ๋‹ค์‹œ ์‹œ๋„ํ•ด๋ด!"
    retry: 3, 
 
    onMutate: async (newPostInfo) => {
      // ๐Ÿš€ ๋‚™๊ด€์  ์—…๋ฐ์ดํŠธ ๋ฐœ๋™!
      // ์ธํ„ฐ๋„ท์ด ๋Š๊ฒจ์„œ ์„œ๋ฒ„๊ฐ€ "์‘๋‹ต"์„ ์•ˆ ์คฌ์Œ์—๋„ ๋ถˆ๊ตฌํ•˜๊ณ  
      // ํ™”๋ฉด์—๋Š” ๋ฌด์กฐ๊ฑด ์ƒˆ๋กœ ์“ด ๊ธ€์„ ๋‹น์žฅ ๊ทธ๋ ค์„œ ์œ ์ € ๋งˆ์Œ์„ ํŽธ์•ˆํ•˜๊ฒŒ ๋งŒ๋“ญ๋‹ˆ๋‹ค.
      await queryClient.cancelQueries({ queryKey: ['posts'] });
      
      /* ... ์•ž์˜ 06๊ฐ•์ฒ˜๋Ÿผ setQueryData ๋กœ ์บ์‹œ ์กฐ์ž‘ ... */
    },
  });
};

์ด๋ ‡๊ฒŒ ์„ธํŒ…ํ•˜๊ณ  ์œ ์ €๊ฐ€ ๋น„ํ–‰๊ธฐ ๋ชจ๋“œ์—์„œ ํผ์„ ์ „์†ก ์ณ๋ณด์„ธ์š”.

  1. ๋‚™๊ด€์  ์—…๋ฐ์ดํŠธ ๋•๋ถ„์— ํ™”๋ฉด์—๋Š” ๋‚ด ๊ธ€์ด ๋ฉ‹์ง€๊ฒŒ ๋“ฑ๋ก๋œ ๊ฒƒ์ฒ˜๋Ÿผ ๋ณด์ž…๋‹ˆ๋‹ค.
  2. ๋’ท๋‹จ์—์„œ Mutation์€ ์‹คํŒจํ•˜์ง€๋งŒ ํ„ฐ์ง€์ง€ ์•Š๊ณ  ๋Œ€๊ธฐ์—ด(Queue) ์— ๋“ค์–ด๊ฐ€ ์ˆ™๋ฉด์„ ์ทจํ•ฉ๋‹ˆ๋‹ค. (isPaused === true)
  3. 30๋ถ„ ๋’ค ์œ ์ €๊ฐ€ ์™€์ดํŒŒ์ด ์กด์— ๋“ค์–ด์™€ ํ•ธ๋“œํฐ ๋ฐ์ดํ„ฐ๊ฐ€ ๋‹ค์‹œ ์—ฐ๊ฒฐ(online ์ด๋ฒคํŠธ ๋ฐœ๋™)๋ฉ๋‹ˆ๋‹ค!
  4. React Query ํ์— ์ž ๋“ค์–ด์žˆ๋˜ Mutation์ด ์ฆ‰๊ฐ์ ์œผ๋กœ ์„œ๋ฒ„๋กœ ๋ฐœ์‚ฌ(Retry)๋˜๋ฉฐ DB์— ์ •๋ง๋กœ ์ €์žฅ๋ฉ๋‹ˆ๋‹ค!

์ด ํ๋ฆ„์ด์•ผ๋ง๋กœ ๋„ทํ”Œ๋ฆญ์Šค, ์ธ์Šคํƒ€๊ทธ๋žจ์—์„œ๋‚˜ ๋ณผ ๋ฒ•ํ•œ ์ดˆ์ผ๋ฅ˜ ์˜คํ”„๋ผ์ธ ํผ์ŠคํŠธ(Offline-First) UX ์•„ํ‚คํ…์ฒ˜์ž…๋‹ˆ๋‹ค.


๐Ÿฃ ์˜์ฒ ์ด์˜ ํ‡ด๊ทผ ์ผ๊ธฐ

์™€, ์˜คํ”„๋ผ์ธ ๋ชจ๋“œ์—์„œ ์—๋Ÿฌ ์•ˆ ๋‚˜๊ณ  ๋ฐฑ๊ทธ๋ผ์šด๋“œ ํ(Queue)์— ์Œ“์•„ ๋†จ๋‹ค๊ฐ€ ๋‚˜์ค‘์— ์ด์ฃผ๋Š” ๊ฑฐ ์ง„์งœ ์ปฌ์ฒ˜์‡ผํฌ๋‹ค. ์ด๊ฑฐ ๋‚ด๊ฐ€ ์ˆ˜๋™์œผ๋กœ ์งฐ์œผ๋ฉด ๋กœ์ปฌ์Šคํ† ๋ฆฌ์ง€์— ๋ฐ์ดํ„ฐ ์บ์‹ฑํ•˜๊ณ , window.addEventListener('online', ...) ๋‹ฌ๊ณ  ๋ฐฑ๋‚  ์ƒ์‘ˆ๋ฅผ ํ–ˆ์„ ํ…๋ฐ, ์˜ต์…˜ ๋‘ ๊ฐœ(networkMode, retry)๋กœ ๋๋‚œ๋‹ค๊ณ ...?

๐Ÿ’ก ์˜ค๋Š˜์˜ ๊ตํ›ˆ: "Form ๋ฐ์ดํ„ฐ์™€ Query ์บ์‹œ๋Š” ์ฒ ์ €ํžˆ ๋ถ€๋ชจ/์ž์‹ ๊ด€๊ณ„๋กœ ์ชผ๊ฐœ์„œ ์ดˆ๊ธฐํ™” ๊ฐ„์„ญ์„ ๋Š์–ด๋ผ! ๊ทธ๋ฆฌ๊ณ  ๊ฒฐ์ œ์ฒ˜๋Ÿผ ๋ฏผ๊ฐํ•œ ๊ฒŒ ์•„๋‹Œ ๋ชจ๋“  Mutation(์ข‹์•„์š”, ๋ฆฌ์ŠคํŠธ ์ˆ˜์ • ๋“ฑ)์€ ๊ณผ๊ฐํ•˜๊ฒŒ ์˜คํ”„๋ผ์ธ Retry ํ๋ฅผ ์—ด์–ด๋‘ฌ์„œ ๊ฒฐ์† ๋„คํŠธ์›Œํฌ UX๋ฅผ ์™„๋ฒฝํžˆ ๋ง‰์•„๋ผ!"

์ด๊ฑฐ ์•ฑ์— ๋ฐ˜์˜ํ•˜๊ณ  ์ง€ํ•˜์ฒ  ์ถœํ‡ด๊ทผ๋Ÿฌ(์˜์ˆ™ ๋””์ž์ด๋„ˆ ๋‹˜)ํ•œํ…Œ ์‹œ์—ฐ ๋ณด์—ฌ๋“œ๋ ธ๋”๋‹ˆ ๊ธฐ๋ฆฝ๋ฐ•์ˆ˜ ๋ฐ›์•˜๋‹ค. "์˜์ฒ  ๋‹˜, ํ„ฐ๋„ ์ง€๋‚˜๊ฐˆ ๋•Œ๋งˆ๋‹ค ์šฐ๋ฆฌ ์•ฑ ๋งจ๋‚  ํ„ฐ์ ธ์„œ ์งœ์ฆ ๋‚ฌ๋Š”๋ฐ ์ด์ œ ๋Œ“๊ธ€ ๋ง˜๋Œ€๋กœ ์จ์ ธ์š” ใ… ใ… " ์บฌ.. ๊ฐœ๋ฐœ์ž ํ•  ๋ง› ๋‚œ๋‹ค ใ…‹ใ…‹ใ…‹ ๋‚ด์ผ์€ ๋“œ๋””์–ด ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ ๋งŒ์ง€๋Š” ๋งˆ์ง€๋ง‰ ๊ฐ•์ขŒ๋‹ค. ํŒŒ์ดํŒ…!


๐Ÿ“ ๋ฐฐ์šด ๋‚ด์šฉ ์ ๊ฒ€ํ•˜๊ธฐ (Quiz)

Q. ์˜์ฒ ์ด๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์ด useQuery์—์„œ ๋ฐ›์•„์˜จ ๋ฐ์ดํ„ฐ์™€ ์ปดํฌ๋„ŒํŠธ ๋‚ด๋ถ€์˜ ๋กœ์ปฌ useState (ํ•„ํ„ฐ์šฉ ์ž…๋ ฅ ์ƒํƒœ)๋ฅผ ํ•˜๋‚˜์˜ ํŒŒ์ผ ์•ˆ์— ๋ชจ๋‘ ๋„ฃ์–ด๋‘์—ˆ์Šต๋‹ˆ๋‹ค. ๋‹ค์Œ ์ค‘, ์ด ์ปดํฌ๋„ŒํŠธ๊ฐ€ ์„ฑ๋Šฅ์ƒ ๋ฌธ์ œ๋ฅผ ์ผ์œผํ‚ค๊ณ  (๋˜๋Š” ๋ฒ„๊ทธ์˜ ์˜จ์ƒ์ด ๋˜๊ณ ) ์žˆ๋Š” ์ด์œ ๋ฅผ Form/์ปดํฌ๋„ŒํŠธ ๋ถ„๋ฆฌ ์›์น™์— ์ž…๊ฐํ•˜์—ฌ ๊ฐ€์žฅ ์ •ํ™•ํ•˜๊ฒŒ ์งš์€ ๊ฒƒ์€ ๋ฌด์—‡์ž…๋‹ˆ๊นŒ?

function BadSearchPage() {
  const { data } = useQuery({ queryKey: ['orders'], queryFn: fetchOrders });
  const [filterText, setFilterText] = useState("");
 
  const filteredData = data?.filter(item => item.name.includes(filterText));
 
  return (
    <div>
      <input value={filterText} onChange={e => setFilterText(e.target.value)} />
      <ul>{filteredData?.map(...)}</ul>
    </div>
  )
}
  • A) useState๊ฐ€ ์ž…๋ ฅ๋  ๋•Œ๋งˆ๋‹ค (ํ‚ค๋ณด๋“œ ์น  ๋•Œ๋งˆ๋‹ค) useQuery ๋˜ํ•œ ๋งค๋ฒˆ ๋ฆฌ๋ Œ๋”๋ง๋˜์–ด ์„œ๋ฒ„์— ๋ฐฑ๊ทธ๋ผ์šด๋“œ ์ค‘๋ณต Fetch๋ฅผ ์˜๊ฒŒ ๋˜๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค.
  • B) filterText ํƒ€์ดํ•‘์œผ๋กœ ์ธํ•ด ์ปดํฌ๋„ŒํŠธ๊ฐ€ ๋‹ค์‹œ ๋ Œ๋”๋ง๋  ๋•Œ๋งˆ๋‹ค, ๊ตณ์ด ์•ˆ ๋ฐ”๋€Œ์–ด๋„ ๋˜๋Š” ๊ป๋ฐ๊ธฐ ๋กœ์ง๋“ค์ด ์žฌ์‹คํ–‰๋˜๊ณ , ๋ฐ˜๋Œ€๋กœ ์„œ๋ฒ„ data๊ฐ€ ๋„์ฐฉํ•˜๊ฑฐ๋‚˜ ๋ฐฑ๊ทธ๋ผ์šด๋“œ ๋ฆฌํŒจ์นญ ๋  ๋•Œ๋งˆ๋‹ค ํƒ€์ดํ•‘ ์ค‘์ด๋˜ UI๊ฐ€ ๋ Œ๋” ๋ฒ„๋ฒ…์ž„์„ ์ผ์œผํ‚ค๋Š” "์ƒํƒœ ๋ฒ”์œ„ ์นจ๋ฒ”" ์ด ๋ฐœ์ƒํ•˜๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค.
  • C) useState ๋Œ€์‹  ๋ฐ˜๋“œ์‹œ ์ „์—ญ ์ƒํƒœ(Zustand)๋ฅผ ์จ์•ผ๋งŒ ํ•„ํ„ฐ๊ฐ€ ์ ์šฉ๋˜๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค.

โœ… ์ •๋‹ต: B

๐Ÿ’ก ์ƒ์„ธ ํ•ด์„ค:

  • ์›๋ฆฌ ์„ค๋ช…: React Query๋Š” ๊ธฐ๋ณธ์ ์œผ๋กœ A๋ฒˆ์ฒ˜๋Ÿผ ์ปดํฌ๋„ŒํŠธ๊ฐ€ ๋ฆฌ๋ Œ๋”๋œ๋‹ค๊ณ  ํ•ด์„œ ๋„คํŠธ์›Œํฌ ์š”์ฒญ์„ ๋ฌด์ง€์„ฑ์œผ๋กœ ๋‹ค์‹œ ์˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค (์บ์‹œ๊ฐ€ ์ œ์–ดํ•จ). ์ง„์งœ ์‹ฌ๊ฐํ•œ ๋ฌธ์ œ๋Š” B๋ฒˆ์ž…๋‹ˆ๋‹ค! ์‚ฌ์šฉ์ž๊ฐ€ ํ‚ค๋ณด๋“œ๋ฅผ 'A', 'p', 'p', 'l', 'e' ์น  ๋•Œ๋งˆ๋‹ค setFilterText๊ฐ€ ํ„ฐ์ง€๋ฉฐ ์ € ๊ฑฐ๋Œ€ํ•œ ์ปดํฌ๋„ŒํŠธ๋ฅผ 5๋ฒˆ ์žฌ๋ Œ๋”ํ•ฉ๋‹ˆ๋‹ค. ๋งŒ์•ฝ ํ™”๋ฉด ํ•˜๋‹จ์— ๋ฌด๊ฑฐ์šด ๋กœ์ง์ด ์žˆ๋‹ค๋ฉด ํ‚ค๋ณด๋“œ ํƒ€์ดํ•‘๋งˆ๋‹ค ์•ฑ ์ „์ฒด๊ฐ€ ๋Š๊น๋‹ˆ๋‹ค.
  • ์˜ค๋‹ต ํ”ผ๋“œ๋ฐฑ: "์˜์ฒ  ๋‹˜, ํด๋ผ์ด์–ธํŠธ ์ž…๋ ฅ ์ƒํƒœ(input)์™€ ์„œ๋ฒ„ ๋ Œ๋” ์ƒํƒœ(data list)๋Š” ์ƒ๋ช… ์ฃผ๊ธฐ(Lifecycle)๊ฐ€ ์™„์ „ํžˆ ๋‹ค๋ฆ…๋‹ˆ๋‹ค! Input ์ฐฝ์€ ๋ณ„๋„์˜ ์Šค๋งˆํŠธ ์ปดํฌ๋„ŒํŠธ๋กœ ๋นผ์„œ(์ž์‹์œผ๋กœ ๋ถ„๋ฆฌ) ์ž…๋ ฅ๊ฐ’๋งŒ debounce ํƒœ์›Œ์„œ ์œ„๋กœ ์ด์ฃผ๋˜๊ฐ€, ์•„๋‹ˆ๋ฉด ๋ฆฌ์ŠคํŠธ ์ปดํฌ๋„ŒํŠธ ์ž์ฒด๋ฅผ ์ž์‹์œผ๋กœ ๋ถ„๋ฆฌํ•ด์„œ ๋ Œ๋”๋ง ์ถฉ๋Œ์„ ์ชผ๊ฐœ์•ผ ํ”„๋ ˆ์ž„ ์ €ํ•˜๋ฅผ ๋ง‰์„ ์ˆ˜ ์žˆ์–ด์š”."
  • ๐Ÿ“Œ ํ•ต์‹ฌ ๊ธฐ์–ต๋ฒ•: ์ž…๋ ฅ์ฐฝ(Form/Input)๊ณผ ์กฐํšŒ์ฐฝ(Query List)์€ ์ปดํฌ๋„ŒํŠธ๋ฅผ ์ฐข์–ด๋ผ! ๋ Œ๋”๋ง ๊ฐ„์„ญ ๊ธˆ์ง€!