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

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

๐Ÿ“‹ ๊ฐœ์š”

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

๐Ÿ“‹ ๋ชฉ์ฐจ

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

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

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

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

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

๐Ÿฆ ์˜ํ˜ธ: ์˜์ฒ  ๋‹˜, ํ”„๋ก ํŠธ์—”๋“œ ๊ฐœ๋ฐœ์˜ ์‹ค๋ฌด์—์„œ ๊ฐ€์žฅ ๊นŒ๋‹ค๋กœ์šด ๋‘ ์˜์—ญ, ์—๋””ํ„ฐ(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) ์ƒํƒœ๋กœ ๋Œ€๊ธฐ ์ƒํƒœ๋กœ ๋จธ๋ญ…๋‹ˆ๋‹ค.

๊ทธ๋Ÿฐ๋ฐ ์—ฌ๊ธฐ์„œ ํ•จ์ • ์นด๋“œ!
๋ฐ์ดํ„ฐ๋ฅผ ์„œ๋ฒ„๋กœ ๋ณด๋‚ด๋Š”(์ˆ˜์ •ํ•˜๋Š”) 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 ์•„ํ‚คํ…์ฒ˜์ž…๋‹ˆ๋‹ค.


๐Ÿ“ ๋ฐฐ์šด ๋‚ด์šฉ ์ ๊ฒ€ํ•˜๊ธฐ (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)๋Š” ์ƒ๋ช… ์ฃผ๊ธฐ๊ฐ€ ๋‹ค๋ฆ…๋‹ˆ๋‹ค. ์ž…๋ ฅ์ฐฝ์€ ๋ณ„๋„ ์ปดํฌ๋„ŒํŠธ๋กœ ๋ถ„๋ฆฌํ•˜๊ณ , ํ•„์š”ํ•œ ๊ฒฝ์šฐ debounce๋œ ๊ฐ’๋งŒ ๋ถ€๋ชจ๋‚˜ query key๋กœ ์ „๋‹ฌํ•ด์•ผ ํ”„๋ ˆ์ž„ ์ €ํ•˜๋ฅผ ๋ง‰์„ ์ˆ˜ ์žˆ์–ด์š”."
  • ๐Ÿ“Œ ํ•ต์‹ฌ ๊ธฐ์–ต๋ฒ•: ์ž…๋ ฅ์ฐฝ(Form/Input)๊ณผ ์กฐํšŒ์ฐฝ(Query List)์€ ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋ถ„๋ฆฌํ•˜๋ผ! ๋ Œ๋”๋ง ๊ฐ„์„ญ ๊ธˆ์ง€!

๐Ÿ“ ๋งˆ๋ฌด๋ฆฌ ํ€ด์ฆˆ

Q1. React Query ๋ฐ์ดํ„ฐ์™€ React Hook Form ์ž…๋ ฅ๊ฐ’์„ ๊ฐ™์€ ์ƒ๋ช…์ฃผ๊ธฐ๋กœ ๋ฌถ์œผ๋ฉด ์–ด๋–ค ๋ฌธ์ œ๊ฐ€ ์ƒ๊ธฐ๋‚˜์š”?

โœ… ์ •๋‹ต: ๋ฐฑ๊ทธ๋ผ์šด๋“œ ๋ฆฌํŒจ์นญ์œผ๋กœ ์„œ๋ฒ„ ๋ฐ์ดํ„ฐ๊ฐ€ ๋ฐ”๋€Œ๋Š” ์ˆœ๊ฐ„ ์‚ฌ์šฉ์ž๊ฐ€ ์ž…๋ ฅ ์ค‘์ธ ํผ ๊ฐ’์ด ๋ฎ์ด๊ฑฐ๋‚˜ ๋ฆฌ์…‹๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๐Ÿ’ก ์ƒ์„ธ ํ•ด์„ค:
์„œ๋ฒ„ ๋ฐ์ดํ„ฐ๋Š” Query Cache๊ฐ€ ๊ณ„์† ์ตœ์‹ ์„ฑ์„ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. ๋ฐ˜๋ฉด ํผ ์ž…๋ ฅ์€ ์‚ฌ์šฉ์ž๊ฐ€ ์ง€๊ธˆ ํŽธ์ง‘ ์ค‘์ธ ํด๋ผ์ด์–ธํŠธ ์ƒํƒœ์ž…๋‹ˆ๋‹ค. ๋ถ€๋ชจ๊ฐ€ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜จ ๋’ค ์ž์‹ ํผ์— ์ดˆ๊ธฐ๊ฐ’์œผ๋กœ๋งŒ ๋„˜๊ธฐ๊ณ , ์ดํ›„์—๋Š” ํผ ์ƒํƒœ๋ฅผ ๋…๋ฆฝ์‹œํ‚ค๋Š” ์ด์œ ๊ฐ€ ์—ฌ๊ธฐ์— ์žˆ์Šต๋‹ˆ๋‹ค.

Q2. ์˜คํ”„๋ผ์ธ ํ™˜๊ฒฝ์—์„œ networkMode: 'offlineFirst'์™€ retry๋ฅผ ํ•จ๊ป˜ ๊ณ ๋ คํ•˜๋Š” ์ด์œ ๋Š” ๋ฌด์—‡์ธ๊ฐ€์š”?

โœ… ์ •๋‹ต: ๋„คํŠธ์›Œํฌ๊ฐ€ ๋Š๊ฒผ์„ ๋•Œ ๋ฎคํ…Œ์ด์…˜์„ ๊ณง๋ฐ”๋กœ ์‹คํŒจ๋กœ ๋๋‚ด์ง€ ์•Š๊ณ  ๋Œ€๊ธฐ์‹œํ‚จ ๋’ค, ์—ฐ๊ฒฐ์ด ํšŒ๋ณต๋˜๋ฉด ๋‹ค์‹œ ์‹œ๋„ํ•ด ์‚ฌ์šฉ์ž ์ž…๋ ฅ ์œ ์‹ค์„ ์ค„์ด๊ธฐ ์œ„ํ•ด์„œ์ž…๋‹ˆ๋‹ค.

๐Ÿ’ก ์ƒ์„ธ ํ•ด์„ค:
๋ชจ๋“  ์š”์ฒญ์„ ์˜คํ”„๋ผ์ธ ํ์— ๋„ฃ์–ด๋„ ๋˜๋Š” ๊ฒƒ์€ ์•„๋‹™๋‹ˆ๋‹ค. ์ข‹์•„์š”, ๋Œ“๊ธ€ ์ €์žฅ์ฒ˜๋Ÿผ ์žฌ์‹œ๋„ ๊ฐ€๋Šฅํ•œ ์ž‘์—…๊ณผ ๊ฒฐ์ œ์ฒ˜๋Ÿผ ์ค‘๋ณต ์‹คํ–‰์ด ์œ„ํ—˜ํ•œ ์ž‘์—…์„ ๋‚˜๋ˆ ์•ผ ํ•ฉ๋‹ˆ๋‹ค. ์˜ํ˜ธ๊ฐ€ ๋ณด๋Š” ๊ธฐ์ค€์€ UX๋ฟ ์•„๋‹ˆ๋ผ ์ž‘์—…์˜ ๋ฉฑ๋“ฑ์„ฑ๊ณผ ๋น„์ฆˆ๋‹ˆ์Šค ์œ„ํ—˜์ž…๋‹ˆ๋‹ค.

Q3. ์˜์ฒ ์ด์˜ ํ…Œ์ŠคํŠธ ํƒ€์ž„: ํšŒ์›์ •๋ณด ํผ์—์„œ ์‚ฌ์šฉ์ž๊ฐ€ ์ด๋ฆ„์„ ๊ณ ์น˜๋Š” ์ค‘์ธ๋ฐ 1๋ถ„๋งˆ๋‹ค ์„œ๋ฒ„ ๋ฆฌํŒจ์นญ์ด ์ผ์–ด๋‚ฉ๋‹ˆ๋‹ค. ์–ด๋–ค ๊ตฌ์กฐ๊ฐ€ ๊ฐ€์žฅ ์•ˆ์ „ํ•œ๊ฐ€์š”?

โœ… ์ •๋‹ต: ๋ถ€๋ชจ ์ปดํฌ๋„ŒํŠธ๋Š” useQuery๋กœ ์ดˆ๊ธฐ ๋ฐ์ดํ„ฐ๋ฅผ ์ค€๋น„ํ•˜๊ณ , ์ž์‹ ํผ์€ initialData๋ฅผ defaultValues๋กœ ํ•œ ๋ฒˆ๋งŒ ์‚ฌ์šฉํ•ด ํŽธ์ง‘ ์ƒํƒœ๋ฅผ ๋…๋ฆฝ์ ์œผ๋กœ ๊ด€๋ฆฌํ•ฉ๋‹ˆ๋‹ค.

๐Ÿ’ก ์ƒ์„ธ ํ•ด์„ค:
ํผ์€ ์‚ฌ์šฉ์ž์˜ ์ž„์‹œ ์ž‘์—… ๊ณต๊ฐ„์ž…๋‹ˆ๋‹ค. ์„œ๋ฒ„ ์บ์‹œ๊ฐ€ ๊ฐฑ์‹ ๋  ๋•Œ๋งˆ๋‹ค ํผ์„ ๋‹ค์‹œ ์ดˆ๊ธฐํ™”ํ•˜๋ฉด ์‚ฌ์šฉ์ž๊ฐ€ ์ž‘์„ฑํ•œ ๋‚ด์šฉ์ด ์‚ฌ๋ผ์ง‘๋‹ˆ๋‹ค. ์˜์ฒ ์ด ์ด์ œ ์„œ๋ฒ„ ์ƒํƒœ์˜ ์‹ ์„ ๋„์™€ ์‚ฌ์šฉ์ž์˜ ํŽธ์ง‘ ์ค‘ ์ƒํƒœ๋ฅผ ๋ถ„๋ฆฌํ•ด ๋ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

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

์˜คํ”„๋ผ์ธ ๋ชจ๋“œ์—์„œ ์‹คํŒจํ•œ ์š”์ฒญ์„ ๋ฐ”๋กœ ์‚ฌ์šฉ์ž ์ž…๋ ฅ ์†์‹ค๋กœ ์ด์–ด์ง€๊ฒŒ ๋งŒ๋“ค์ง€ ์•Š๊ณ , ์žฌ์‹œ๋„ ๊ฐ€๋Šฅํ•œ ์ž‘์—…์œผ๋กœ ๋ถ„๋ฅ˜ํ•ด ๋‚˜์ค‘์— ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ๋‹ค๋Š” ์ ์ด ์ธ์ƒ์ ์ด์—ˆ๋‹ค. ์˜ˆ์ „ ๊ฐ™์œผ๋ฉด localStorage์™€ online ์ด๋ฒคํŠธ๋ฅผ ์ง์ ‘ ์—ฎ์—ˆ์„ ํ…๋ฐ, ์ด์ œ๋Š” ์ž‘์—…์˜ ๋ฉฑ๋“ฑ์„ฑ๊ณผ ์žฌ์‹œ๋„ ๊ฐ€๋Šฅ์„ฑ์„ ๋จผ์ € ๋”ฐ์ ธ์•ผ ํ•œ๋‹ค.

๐Ÿ’ก ์˜ค๋Š˜์˜ ๊ตํ›ˆ: "Form ๋ฐ์ดํ„ฐ์™€ Query ์บ์‹œ๋Š” ๋ถ€๋ชจ/์ž์‹ ๊ด€๊ณ„๋กœ ๋ถ„๋ฆฌํ•ด ์ดˆ๊ธฐํ™” ๊ฐ„์„ญ์„ ๋Š์ž. ์˜คํ”„๋ผ์ธ ์žฌ์‹œ๋„๋Š” ์ž‘์—…์˜ ๋ฉฑ๋“ฑ์„ฑ๊ณผ ๋น„์ฆˆ๋‹ˆ์Šค ์œ„ํ—˜์„ ํ™•์ธํ•œ ๋’ค ์ผœ์ž."

์ด ๊ตฌ์กฐ๋ฅผ ์•ฑ์— ๋ฐ˜์˜ํ•˜๊ณ  ์˜์ˆ™ ๋””์ž์ด๋„ˆ ๋‹˜์—๊ฒŒ ์‹œ์—ฐํ–ˆ๋”๋‹ˆ, ํ„ฐ๋„ ๊ตฌ๊ฐ„์—์„œ๋„ ๋Œ“๊ธ€ ์ž…๋ ฅ์ด ๋ณด์กด๋˜๋Š”์ง€ QA ์‹œ๋‚˜๋ฆฌ์˜ค๋กœ ๋‹ค์‹œ ํ™•์ธํ•ด๋ณด์ž๊ณ  ํ–ˆ๋‹ค. ๋‚ด์ผ์€ ์˜คํ”„๋ผ์ธ ํ์— ๋„ฃ์–ด๋„ ๋˜๋Š” ์ž‘์—…๊ณผ ๋„ฃ์œผ๋ฉด ์•ˆ ๋˜๋Š” ์ž‘์—…์„ ํ‘œ๋กœ ์ •๋ฆฌํ•ด์•ผ๊ฒ ๋‹ค.