๐Ÿช 22. ์ปค์Šคํ…€ ํ›… ์„ค๊ณ„ ์ฒ ํ•™: ๋กœ์ง์˜ ์บก์Аํ™”์™€ ํ•ฉ์„ฑ

๐Ÿ“‹ ๊ฐœ์š”

๋ณต๋ถ™ ์ง€์˜ฅ์—์„œ ํƒˆ์ถœํ•˜๋Š” ์ปค์Šคํ…€ ํ›… ์ถ”์ถœ ๊ธฐ์ค€๋ถ€ํ„ฐ, useAsyncยทuseLocalStorageยทuseDebounce ๋“ฑ ์‹ค๋ฌด ํ›… ์„ค๊ณ„ ํŒจํ„ด๊นŒ์ง€. 'ํ›…์ด์–ด์•ผ ํ•˜๋Š”๊ฐ€, ์ปดํฌ๋„ŒํŠธ์—ฌ์•ผ ํ•˜๋Š”๊ฐ€'์˜ ํŒ๋‹จ ๊ธฐ์ค€์„ ๋ช…ํ™•ํžˆ ๋‹ค์ง‘๋‹ˆ๋‹ค.

๐Ÿ“‹ ๋ชฉ์ฐจ


๐Ÿ“Œ ์ด ๋ฌธ์„œ๋ฅผ ์ฝ๊ธฐ ์ „์—

โฑ๏ธ ์˜ˆ์ƒ ์ฝ๊ธฐ ์‹œ๊ฐ„: 15๋ถ„ (์ „์ฒด) / ํ•ต์‹ฌ ํŒŒํŠธ๋งŒ: 8๋ถ„

๐Ÿ—บ๏ธ ์ด ๋ฌธ์„œ์˜ ํ๋ฆ„
[๋ณต๋ถ™ ๋ฌธ์ œ ์ธ์‹] โ†’ [์ถ”์ถœ ๊ธฐ์ค€ ์ฒด๋“] โ†’ [์‹ค๋ฌด ํ›… 4์ข… ์ง์ ‘ ๋งŒ๋“ค๊ธฐ] โ†’ [ํ›… ํ•ฉ์„ฑ ํŒจํ„ด] โ†’ [ํ›… vs ์ปดํฌ๋„ŒํŠธ ๊ฒฝ๊ณ„]

๐ŸŽฏ ์ด ๋ฌธ์„œ๋ฅผ ๋‹ค ์ฝ์œผ๋ฉด ํ•  ์ˆ˜ ์žˆ๋Š” ๊ฒƒ

  • "์ด ๋กœ์ง์ด ํ›…์œผ๋กœ ์ถ”์ถœ๋˜์–ด์•ผ ํ•˜๋Š”๊ฐ€?" ๋ฅผ 5์ดˆ ์•ˆ์— ํŒ๋‹จํ•  ์ˆ˜ ์žˆ๋‹ค
  • useAsync, useLocalStorage, useDebounce ๋ฅผ ์ง์ ‘ ์„ค๊ณ„ํ•  ์ˆ˜ ์žˆ๋‹ค
  • ํ›… ๋ฐ˜ํ™˜๊ฐ’์„ ๋ฐฐ์—ด vs ๊ฐ์ฒด๋กœ ์–ธ์ œ ํ•ด์•ผ ํ•˜๋Š”์ง€ ์„ค๋ช…ํ•  ์ˆ˜ ์žˆ๋‹ค

๐Ÿ—บ๏ธ ์ด ๋ฌธ์„œ์˜ ๋ฐฐ๊ฒฝ ์„ธ๊ณ„๊ด€: '์˜์ˆ˜๋„ค ์ปค๋ฎค๋‹ˆํ‹ฐ'

  • ์˜์ฒ (์‹ ์ž…): "๊ฒŒ์‹œ๊ธ€ ๋ชฉ๋ก, ์œ ์ € ๋ชฉ๋ก, ๋Œ“๊ธ€ ๋ชฉ๋ก... ์„ธ ์ปดํฌ๋„ŒํŠธ์— useState + useEffect + fetch ํŒจํ„ด์„ ์™„์ „ํžˆ ๋˜‘๊ฐ™์ด ๋ณต๋ถ™ํ•ด๋†จ๋Š”๋ฐ, ๋กœ๋”ฉ ๋กœ์ง์— ๋ฒ„๊ทธ๊ฐ€ ๋‚˜์„œ ์„ธ ๊ตฐ๋ฐ๋ฅผ ๋‹ค ๊ณ ์ณ์•ผ ํ•ด์š” ใ… ใ… "
  • ์˜ํ˜ธ(๋ฆฌ๋“œ): "์˜์ฒ  ๋‹˜, ๋ณต๋ถ™ํ•œ ์ˆœ๊ฐ„ ๊ทธ ์ฝ”๋“œ๋Š” ์„ธ ๊ฐœ์˜ ๋…๋ฆฝ๋œ ๋ฒ„๊ทธ ํ›„๋ณด๊ฐ€ ๋ฉ๋‹ˆ๋‹ค. ๊ทธ ํŒจํ„ด, ์ง€๊ธˆ ๋‹น์žฅ ์ปค์Šคํ…€ ํ›… ํ•˜๋‚˜๋กœ ๋นจ์•„๋“ค์ด๊ฒ ์Šต๋‹ˆ๋‹ค."

๐Ÿค” ์™œ ์•Œ์•„์•ผ ํ•˜๋Š”๊ฐ€: ๋ณต๋ถ™ ์ง€์˜ฅ์˜ ๊ณตํฌ

๐ŸŽฏ ์ด ์„น์…˜์„ ์ฝ๊ณ  ๋‚˜๋ฉด:

  • ๋ณต๋ถ™์ด ์™œ ๊ธฐ์ˆ  ๋ถ€์ฑ„์˜ ์”จ์•—์ธ์ง€ ์„ค๋ช…ํ•  ์ˆ˜ ์žˆ๋‹ค
  • ์ปค์Šคํ…€ ํ›…์ด "์ปดํฌ๋„ŒํŠธ ๋ถ„๋ฆฌ"์™€ ๋ฌด์—‡์ด ๋‹ค๋ฅธ์ง€ ๊ตฌ๋ถ„ํ•  ์ˆ˜ ์žˆ๋‹ค

'์˜์ˆ˜๋„ค ์ปค๋ฎค๋‹ˆํ‹ฐ' ์•ฑ์ด ์ปค์ง€๋ฉด์„œ ์˜์ฒ ์ด๋Š” ๋ฐ์ดํ„ฐ ํŒจ์นญ ์ฝ”๋“œ๋ฅผ ์„ธ ๊ตฐ๋ฐ์— ๋ณต๋ถ™ํ–ˆ์–ด์š”.

// โŒ PostList.tsx โ€” ์˜์ฒ ์ด์˜ ๋ณต๋ถ™ 1๋ฒˆ
function PostList() {
  const [posts, setPosts] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
 
  useEffect(() => {
    fetchPosts()
      .then(data => {
        setPosts(data);
        setLoading(false);
      })
      .catch(err => {
        setError(err);
        setLoading(false);
      });
  }, []);
 
  if (loading) return <Spinner />;
  if (error) return <ErrorMessage />;
  return <ul>{posts.map(p => <PostItem key={p.id} post={p} />)}</ul>;
}
 
// โŒ UserList.tsx โ€” ๋ณต๋ถ™ 2๋ฒˆ (loading ๋ฆฌ์…‹์„ finally๋กœ ์•ˆ ์“ด ๊ฒƒ ์ฃผ๋ชฉ)
function UserList() {
  const [users, setUsers] = useState([]);
  const [loading, setLoading] = useState(true);   // ๐Ÿ› ๋ฒ„๊ทธ ์”จ์•—: loading ์ดˆ๊ธฐํ™” ํƒ€์ด๋ฐ ๋‹ค๋ฆ„
  const [error, setError] = useState(null);
 
  useEffect(() => {
    fetchUsers()
      .then(data => {
        setUsers(data);
        setLoading(false);
      })
      .catch(err => {
        setError(err);
        // ๐Ÿ’ฃ ์—ฌ๊ธฐ์„œ setLoading(false) ๋น ๋œจ๋ฆผ! โ†’ ์—๋Ÿฌ๋‚˜๋ฉด ์˜์›ํžˆ ๋กœ๋”ฉ ์ค‘ ํ‘œ์‹œ
      });
  }, []);
  // ...
}

์ด ์ฝ”๋“œ์˜ ๋ฌธ์ œ์ ์€ ๋ถ„๋ช…ํ•ด์š”:

  1. ๋ฒ„๊ทธ ํ•˜๋‚˜, ์ˆ˜์ • ์„ธ ๊ณณ: loading ๋ฆฌ์…‹ ๋ฒ„๊ทธ๊ฐ€ UserList ์—๋งŒ ์žˆ์–ด๋„, ๋‚˜์ค‘์—” CommentList ์—๋„ ๊ฐ™์€ ๋ฒ„๊ทธ๊ฐ€ ์ƒ๊ธธ ๊ฐ€๋Šฅ์„ฑ์ด ๋†’์•„์š”
  2. ํ…Œ์ŠคํŠธ ๋ถˆ๊ฐ€๋Šฅ: useEffect ๊ฐ€ ์ปดํฌ๋„ŒํŠธ ์•ˆ์— ๋ฐ•ํ˜€์žˆ์œผ๋ฉด ๋‹จ๋…์œผ๋กœ ํ…Œ์ŠคํŠธํ•  ์ˆ˜ ์—†์–ด์š”
  3. ์ง„ํ™” ๋น„์šฉ ์ฆ๊ฐ€: ๋‚˜์ค‘์— "์š”์ฒญ ์ทจ์†Œ(cancel)" ๊ธฐ๋Šฅ์„ ์ถ”๊ฐ€ํ•  ๋•Œ, ์„ธ ๊ตฐ๋ฐ ๋ชจ๋‘ ์ˆ˜์ •ํ•ด์•ผ ํ•ด์š”

๐Ÿ’ก ํ•œ ์ค„๋กœ ๊ธฐ์–ตํ•˜๊ธฐ
"๋ณต๋ถ™์€ ๋ฒ„๊ทธ์˜ ์”จ์•—์„ ์„ธ ๊ตฐ๋ฐ์— ์‹ฌ๋Š” ๊ฒƒ์ด๋‹ค. ๊ฐ™์€ ๋กœ์ง์€ ๋ฐ˜๋“œ์‹œ ํ•˜๋‚˜์˜ ํ›…์œผ๋กœ ์‚ด์•„์•ผ ํ•œ๋‹ค."


๐Ÿ—๏ธ ๋น„์œ ๋กœ ๋จผ์ € ์ดํ•ดํ•˜๊ธฐ

๐Ÿง’ 5์‚ด์—๊ฒŒ ์„ค๋ช…ํ•œ๋‹ค๋ฉด?

์˜์ˆ˜๋„ค ์ปค๋ฎค๋‹ˆํ‹ฐ ํŒ€์ด ์ƒˆ ๊ธฐ๋Šฅ์„ ๋งŒ๋“ค ๋•Œ๋งˆ๋‹ค ๋งค๋ฒˆ ์ƒˆ ์„œ๋ฒ„๋ฅผ ์ฒ˜์Œ๋ถ€ํ„ฐ ์กฐ๋ฆฝํ•œ๋‹ค๊ณ  ์ƒ์ƒํ•ด๋ด.
CPU ๋ผ์šฐ๊ณ , ๋ฉ”๋ชจ๋ฆฌ ๊ฝ‚๊ณ , ์ „์› ์—ฐ๊ฒฐํ•˜๊ณ ... ์„ธ ๋ฒˆ ๋ฐ˜๋ณตํ•˜๋ฉด ์„ธ ๊ฐœ์˜ ์„œ๋ฒ„๊ฐ€ ์ƒ๊ธฐ๋Š”๋ฐ, ๋‚˜์ค‘์— ์ „์› ์ผ€์ด๋ธ” ๊ทœ๊ฒฉ์ด ๋ฐ”๋€Œ๋ฉด ์„ธ ๊ฐœ๋ฅผ ๋‹ค ๋ถ„ํ•ดํ•ด์•ผ ํ•ด.

์ปค์Šคํ…€ ํ›…์€ "์กฐ๋ฆฝ ๋งค๋‰ด์–ผ ํŒŒ์ผ ํ•œ ์žฅ" ์ด์•ผ.
๋งค๋‰ด์–ผ์„ ์—…๋ฐ์ดํŠธํ•˜๋ฉด, ๊ทธ ๋งค๋‰ด์–ผ๋Œ€๋กœ ๋งŒ๋“ค์–ด์ง„ ์„œ๋ฒ„๋“ค์€ ์ž๋™์œผ๋กœ ์ตœ์‹  ๋ฐฉ๋ฒ•์œผ๋กœ ๋™์ž‘ํ•ด.
๋ณต๋ถ™์€ "๋งค๋‰ด์–ผ ๋ณต์‚ฌ๋ณธ ์„ธ ์žฅ"์ด๊ณ , ํ›…์€ "์›๋ณธ ๋งค๋‰ด์–ผ ํ•˜๋‚˜๋ฅผ ์„ธ ๊ณณ์—์„œ ์ฐธ์กฐํ•˜๋Š” ๊ฒƒ"์ด์•ผ.


๐Ÿ” ์ปค์Šคํ…€ ํ›… ์ถ”์ถœ ํŒ๋‹จ ๊ธฐ์ค€ ๐ŸŸข

๐ŸŽฏ ์ด ์„น์…˜์„ ์ฝ๊ณ  ๋‚˜๋ฉด:

  • 5์ดˆ ์•ˆ์— "ํ›…์œผ๋กœ ๋บ„๊นŒ, ์ปดํฌ๋„ŒํŠธ๋กœ ๋บ„๊นŒ, ๊ทธ๋ƒฅ ์œ ํ‹ธ ํ•จ์ˆ˜๋กœ ๋บ„๊นŒ"๋ฅผ ํŒ๋‹จํ•  ์ˆ˜ ์žˆ๋‹ค

๐Ÿค” ์ž ๊น, ๋จผ์ € ์ƒ๊ฐํ•ด๋ด
์•„๋ž˜ ์„ธ ๊ฐ€์ง€ ์ค‘ ์–ด๋–ค ๊ฒŒ ์ปค์Šคํ…€ ํ›…์œผ๋กœ ์ถ”์ถœ๋˜์–ด์•ผ ํ• ๊นŒ?

  1. useState + useEffect ๋กœ API๋ฅผ ํ˜ธ์ถœํ•˜๋Š” 20์ค„ ๋กœ์ง
  2. <ul> ํƒœ๊ทธ๋กœ ์•„์ดํ…œ์„ ๋ Œ๋”๋งํ•˜๋Š” JSX
  3. ๋ฐฐ์—ด์„ ์ •๋ ฌํ•˜๋Š” sortByDate(items) ํ•จ์ˆ˜

์ปค์Šคํ…€ ํ›… ์ถ”์ถœ ๊ฒฐ์ • ํŠธ๋ฆฌ:

์ด ์ฝ”๋“œ๋ฅผ ์žฌ์‚ฌ์šฉํ•˜๊ณ  ์‹ถ์€๊ฐ€?
  โ”‚
  โ”œโ”€ JSX(UI)๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค
  โ”‚   โ””โ”€ โ†’ ์ปดํฌ๋„ŒํŠธ๋กœ ์ถ”์ถœ
  โ”‚
  โ”œโ”€ React Hook(useState, useEffect ๋“ฑ)์„ ์‚ฌ์šฉํ•œ๋‹ค
  โ”‚   โ””โ”€ โ†’ ์ปค์Šคํ…€ ํ›…์œผ๋กœ ์ถ”์ถœ
  โ”‚
  โ””โ”€ Hook์„ ์ „ํ˜€ ์‚ฌ์šฉํ•˜์ง€ ์•Š๋Š” ์ˆœ์ˆ˜ ๋กœ์ง์ด๋‹ค
      โ””โ”€ โ†’ ์œ ํ‹ธ ํ•จ์ˆ˜๋กœ ์ถ”์ถœ (hooks/ ํด๋” ์•„๋‹˜, utils/ ํด๋”)

ํŒ๋‹จ ๊ธฐ์ค€ ์š”์•ฝํ‘œ:

์ƒํ™ฉ์„ ํƒ์ด์œ 
useState + useEffect ์กฐํ•ฉ์ด 2๊ณณ ์ด์ƒ์ปค์Šคํ…€ ํ›…๋กœ์ง ์žฌ์‚ฌ์šฉ
๊ฐ™์€ UI(JSX)๊ฐ€ 2๊ณณ ์ด์ƒ์ปดํฌ๋„ŒํŠธUI ์žฌ์‚ฌ์šฉ
ํ›… ์—†๋Š” ์ˆœ์ˆ˜ ๊ณ„์‚ฐ ํ•จ์ˆ˜์œ ํ‹ธ ํ•จ์ˆ˜ํ›… ๋ถˆํ•„์š”
์ปดํฌ๋„ŒํŠธ ์•ˆ์— ์กฐ๊ฑด๋ฌธ์ด ๋„ˆ๋ฌด ๋ณต์žก์ปค์Šคํ…€ ํ›…๋กœ์ง ๋ถ„๋ฆฌ

โš ๏ธ ์ฃผ์˜: ํ›…์ด JSX๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋ฉด ์•ˆ ๋ผ

// โŒ ํ›…์ฒ˜๋Ÿผ ์ƒ๊ฒผ์ง€๋งŒ ํ›…์ด ์•„๋‹˜ โ€” ์ปดํฌ๋„ŒํŠธ์ž„
function useUserProfile(userId: string) {
  const user = useUser(userId);
  return <div>{user.name}</div>; // JSX ๋ฐ˜ํ™˜ โ†’ ์ด๊ฑด ํ›…์ด ์•„๋‹ˆ๋ผ ์ปดํฌ๋„ŒํŠธ!
}
 
// โœ… ํ›…์€ ๋ฐ์ดํ„ฐ/๋กœ์ง๋งŒ ๋ฐ˜ํ™˜
function useUserProfile(userId: string) {
  const [user, setUser] = useState(null);
  // ... ๋กœ์ง
  return { user, loading, error }; // ๋ฐ์ดํ„ฐ๋งŒ ๋ฐ˜ํ™˜
}

๐Ÿ’ก ํ•œ ์ค„๋กœ ๊ธฐ์–ตํ•˜๊ธฐ
"ํ›…์€ ๋กœ์ง์„ ์บก์Аํ™”ํ•˜๊ณ , ์ปดํฌ๋„ŒํŠธ๋Š” UI๋ฅผ ์บก์Аํ™”ํ•œ๋‹ค. JSX๊ฐ€ ๋‚˜์˜ค๋ฉด ํ›…์ด ์•„๋‹ˆ๋‹ค."


โšก ์‹ค๋ฌด ํ›… ํŒจํ„ด 1: useAsync ๐ŸŸก

๐ŸŽฏ ์ด ์„น์…˜์„ ์ฝ๊ณ  ๋‚˜๋ฉด:

  • ๋น„๋™๊ธฐ ๋ฐ์ดํ„ฐ ํŒจ์นญ ๋กœ์ง ์ „์ฒด๋ฅผ ํ›… ํ•˜๋‚˜๋กœ ์ถ”์ƒํ™”ํ•  ์ˆ˜ ์žˆ๋‹ค
  • ๊ฒฝ์Ÿ ์กฐ๊ฑด(Race Condition) ๋ฒ„๊ทธ๋ฅผ ํด๋ฆฐ์—… ํ•จ์ˆ˜๋กœ ๋ฐฉ์ง€ํ•  ์ˆ˜ ์žˆ๋‹ค

useAsync ๋Š” ๋ชจ๋“  ๋น„๋™๊ธฐ ํ˜ธ์ถœ์— ๊ณตํ†ต์œผ๋กœ ํ•„์š”ํ•œ loading / data / error ์ƒํƒœ๋ฅผ ํ†ตํ•ฉ ๊ด€๋ฆฌํ•ด์š”.

// hooks/useAsync.ts
import { useState, useEffect } from 'react';
 
interface AsyncState<T> {
  data: T | null;
  loading: boolean;
  error: Error | null;
}
 
// T: ๋น„๋™๊ธฐ ํ•จ์ˆ˜๊ฐ€ ๋ฐ˜ํ™˜ํ•˜๋Š” ๋ฐ์ดํ„ฐ์˜ ํƒ€์ž…
function useAsync<T>(
  asyncFn: () => Promise<T>,  // ์‹คํ–‰ํ•  ๋น„๋™๊ธฐ ํ•จ์ˆ˜
  deps: React.DependencyList = []  // useEffect ์˜์กด์„ฑ ๋ฐฐ์—ด
): AsyncState<T> {
  const [state, setState] = useState<AsyncState<T>>({
    data: null,
    loading: true,
    error: null,
  });
 
  useEffect(() => {
    let cancelled = false; // ์–ธ๋งˆ์šดํŠธ ํ›„ setState ํ˜ธ์ถœ ๋ฐฉ์ง€์šฉ ํ”Œ๋ž˜๊ทธ
 
    // ๋งค ์‹คํ–‰๋งˆ๋‹ค ์ดˆ๊ธฐ ์ƒํƒœ๋กœ ๋ฆฌ์…‹
    setState({ data: null, loading: true, error: null });
 
    asyncFn()
      .then(data => {
        if (!cancelled) {  // ์–ธ๋งˆ์šดํŠธ๋์œผ๋ฉด ์ƒํƒœ ์—…๋ฐ์ดํŠธ ๊ฑด๋„ˆ๋œ€
          setState({ data, loading: false, error: null });
        }
      })
      .catch(error => {
        if (!cancelled) {
          setState({ data: null, loading: false, error });
        }
      });
 
    return () => {
      cancelled = true; // ํด๋ฆฐ์—…: deps ๋ณ€๊ฒฝ or ์–ธ๋งˆ์šดํŠธ ์‹œ ์ด์ „ ์š”์ฒญ ์ทจ์†Œ
    };
  }, deps);
 
  return state;
}
 
export default useAsync;

์‚ฌ์šฉ ์˜ˆ์‹œ โ€” ๋ณต๋ถ™ ์ง€์˜ฅ ํƒˆ์ถœ:

// โœ… PostList.tsx โ€” ์ด์ œ ๋‹จ ๋‘ ์ค„๋กœ ๋ฐ์ดํ„ฐ ํŒจ์นญ ๋
function PostList() {
  const { data: posts, loading, error } = useAsync(fetchPosts, []);
 
  if (loading) return <Spinner />;
  if (error) return <ErrorMessage message={error.message} />;
 
  return <ul>{posts!.map(p => <PostItem key={p.id} post={p} />)}</ul>;
}
 
// โœ… UserList.tsx โ€” ์™„์ „ํžˆ ๋™์ผํ•œ ํŒจํ„ด, ๋ฒ„๊ทธ ์—†์Œ
function UserList() {
  const { data: users, loading, error } = useAsync(fetchUsers, []);
  // ...
}
 
// โœ… deps๊ฐ€ ์žˆ๋Š” ๊ฒฝ์šฐ: userId ๋ฐ”๋€Œ๋ฉด ์ž๋™ ์žฌ์š”์ฒญ
function UserProfile({ userId }: { userId: string }) {
  const { data: user, loading } = useAsync(
    () => fetchUser(userId),  // userId ํด๋กœ์ € ์บก์ฒ˜
    [userId]                  // userId ๋ฐ”๋€Œ๋ฉด ์žฌ์‹คํ–‰
  );
}

๊ฒฝ์Ÿ ์กฐ๊ฑด(Race Condition) ๋ฐฉ์ง€ ์›๋ฆฌ:

// โŒ cancelled ํ”Œ๋ž˜๊ทธ ์—†๋Š” ๊ฒฝ์šฐ์˜ ์žฌ์•™
// userId=1 ์š”์ฒญ(๋А๋ฆผ) โ†’ userId=2 ์š”์ฒญ(๋น ๋ฆ„) โ†’ userId=2 ๊ฒฐ๊ณผ ํ‘œ์‹œ
//                                             โ†’ userId=1 ๊ฒฐ๊ณผ ๋Šฆ๊ฒŒ ๋„์ฐฉ โ†’ userId=1 ๋ฎ์–ด์”€!
// ํ™”๋ฉด์—๋Š” userId=2์ธ๋ฐ ๋ฐ์ดํ„ฐ๋Š” userId=1 โ†’ ๋ฒ„๊ทธ!
 
// โœ… cancelled ํ”Œ๋ž˜๊ทธ๊ฐ€ ์žˆ์œผ๋ฉด:
// userId=2 ๋กœ deps ๋ณ€๊ฒฝ โ†’ ํด๋ฆฐ์—… ์‹คํ–‰(cancelled=true)
// โ†’ userId=1 ์š”์ฒญ์ด ๋’ค๋Šฆ๊ฒŒ ์™„๋ฃŒ๋ผ๋„ setState ๋ฌด์‹œ๋จ โ†’ ์•ˆ์ „!

์‹ค์Šต ํ›„ ์ฒดํฌ๋ฆฌ์ŠคํŠธ:

  • useAsync ์˜ cancelled ํ”Œ๋ž˜๊ทธ๊ฐ€ ์—†์œผ๋ฉด ์–ด๋–ค ๋ฒ„๊ทธ๊ฐ€ ์ƒ๊ธฐ๋Š”์ง€ ์„ค๋ช…ํ•  ์ˆ˜ ์žˆ๋‹ค
  • deps ๋ฐฐ์—ด์˜ ์—ญํ• ์„ useEffect ์˜ ๊ทธ๊ฒƒ๊ณผ ๋™์ผํ•˜๊ฒŒ ์ดํ•ดํ•˜๊ณ  ์žˆ๋‹ค
  • useAsync(fetchPosts, []) ๋ฅผ useAsync(fetchPosts) ๋กœ ๋ฐ”๊พธ๋ฉด ์–ด๋–ป๊ฒŒ ๋‹ค๋ฅธ์ง€ ์•ˆ๋‹ค

๐Ÿ’ก ํ•œ ์ค„๋กœ ๊ธฐ์–ตํ•˜๊ธฐ
"useAsync ๋Š” ๋ชจ๋“  ๋ฐ์ดํ„ฐ ํŒจ์นญ์˜ ๋ณด์ผ๋Ÿฌํ”Œ๋ ˆ์ดํŠธ(loading, error, data)๋ฅผ ๋‹จ ํ•œ ์ค„๋กœ ์••์ถ•ํ•ด์ค€๋‹ค. ํด๋ฆฐ์—…์˜ cancelled ํ”Œ๋ž˜๊ทธ๊ฐ€ ๊ฒฝ์Ÿ ์กฐ๊ฑด์„ ๋ง‰๋Š” ํ•ต์‹ฌ์ด์•ผ."


๐Ÿ’พ ์‹ค๋ฌด ํ›… ํŒจํ„ด 2: useLocalStorage ๐ŸŸก

๐ŸŽฏ ์ด ์„น์…˜์„ ์ฝ๊ณ  ๋‚˜๋ฉด:

  • localStorage ์™€ React ์ƒํƒœ๋ฅผ ๋™๊ธฐํ™”ํ•˜๋Š” ํ›…์„ ์„ค๊ณ„ํ•  ์ˆ˜ ์žˆ๋‹ค
  • ๊ฒŒ์œผ๋ฅธ ์ดˆ๊ธฐํ™”(Lazy Initialization)์˜ ํ•„์š”์„ฑ์„ ์ดํ•ดํ•œ๋‹ค

useLocalStorage ๋Š” useState ์ฒ˜๋Ÿผ ์“ฐ์ง€๋งŒ, ๊ฐ’์ด ๋ธŒ๋ผ์šฐ์ € ์ƒˆ๋กœ๊ณ ์นจ ํ›„์—๋„ ์œ ์ง€๋ผ์š”.

// hooks/useLocalStorage.ts
import { useState } from 'react';
 
function useLocalStorage<T>(key: string, initialValue: T) {
  // ๊ฒŒ์œผ๋ฅธ ์ดˆ๊ธฐํ™”(Lazy Init): ํ•จ์ˆ˜๋ฅผ ๋„˜๊ธฐ๋ฉด ์ดˆ๊ธฐ ๋ Œ๋”๋ง ๋•Œ๋งŒ ์‹คํ–‰๋จ
  // โ†’ ๋งค ๋ Œ๋”๋งˆ๋‹ค localStorage๋ฅผ ์ฝ์ง€ ์•Š์•„๋„ ๋จ (์„ฑ๋Šฅ ์ตœ์ ํ™”)
  const [storedValue, setStoredValue] = useState<T>(() => {
    try {
      const item = window.localStorage.getItem(key);
      return item ? (JSON.parse(item) as T) : initialValue;
    } catch {
      // localStorage ์ ‘๊ทผ ๋ถˆ๊ฐ€(SSR, ํ”„๋ผ์ด๋น— ๋ชจ๋“œ) ์‹œ ์ดˆ๊ธฐ๊ฐ’ ์‚ฌ์šฉ
      return initialValue;
    }
  });
 
  // useState์˜ setter์ฒ˜๋Ÿผ ํ•จ์ˆ˜ํ˜• ์—…๋ฐ์ดํŠธ๋„ ์ง€์›
  const setValue = (value: T | ((val: T) => T)) => {
    try {
      const valueToStore =
        value instanceof Function ? value(storedValue) : value;
      setStoredValue(valueToStore);
      window.localStorage.setItem(key, JSON.stringify(valueToStore));
    } catch {
      console.warn(`localStorage์— '${key}' ์ €์žฅ ์‹คํŒจ`);
    }
  };
 
  return [storedValue, setValue] as const; // useState์ฒ˜๋Ÿผ [๊ฐ’, setter] ๋ฐฐ์—ด ๋ฐ˜ํ™˜
}
 
export default useLocalStorage;

์‚ฌ์šฉ ์˜ˆ์‹œ:

// โœ… ๋‹คํฌ๋ชจ๋“œ ์„ค์ • ์˜๊ตฌ ์ €์žฅ
function ThemeToggle() {
  const [isDark, setIsDark] = useLocalStorage('theme', false);
  //    useState์™€ ์™„์ „ํžˆ ๋™์ผํ•œ ์ธํ„ฐํŽ˜์ด์Šค!
 
  return (
    <button onClick={() => setIsDark(prev => !prev)}>
      {isDark ? 'โ˜€๏ธ ๋ผ์ดํŠธ ๋ชจ๋“œ' : '๐ŸŒ™ ๋‹คํฌ ๋ชจ๋“œ'}
    </button>
  );
}
 
// โœ… ๊ฒ€์ƒ‰ ํžˆ์Šคํ† ๋ฆฌ ์ €์žฅ
function SearchHistory() {
  const [history, setHistory] = useLocalStorage<string[]>('search-history', []);
 
  const addSearch = (query: string) => {
    setHistory(prev => [query, ...prev.slice(0, 9)]); // ์ตœ๋Œ€ 10๊ฐœ ์œ ์ง€
  };
}

์™œ ๊ฒŒ์œผ๋ฅธ ์ดˆ๊ธฐํ™”๊ฐ€ ์ค‘์š”ํ•œ๊ฐ€:

// โŒ ๊ฒŒ์œผ๋ฅธ ์ดˆ๊ธฐํ™” ์—†์ด (๋‚˜์œ ์˜ˆ)
const [value, setValue] = useState(localStorage.getItem(key)); // ๋งค ๋ Œ๋”๋งˆ๋‹ค ์‹คํ–‰!
// โ†’ ์ปดํฌ๋„ŒํŠธ๊ฐ€ 1์ดˆ์— 60๋ฒˆ ๋ Œ๋”๋˜๋ฉด localStorage๋„ 60๋ฒˆ ์ฝ์Œ
 
// โœ… ๊ฒŒ์œผ๋ฅธ ์ดˆ๊ธฐํ™” ์‚ฌ์šฉ (์ข‹์€ ์˜ˆ)
const [value, setValue] = useState(() => localStorage.getItem(key)); // ์ฒ˜์Œ ํ•œ ๋ฒˆ๋งŒ ์‹คํ–‰

๐Ÿ’ก ํ•œ ์ค„๋กœ ๊ธฐ์–ตํ•˜๊ธฐ
"useLocalStorage ๋Š” useState ์˜ ์™„์ „ํ•œ ๋Œ€์ฒด์ œ์•ผ. ์ธํ„ฐํŽ˜์ด์Šค๊ฐ€ ๋™์ผํ•˜๋‹ˆ๊นŒ ๊ธฐ์กด ์ฝ”๋“œ๋ฅผ ๋ฐ”๊พธ์ง€ ์•Š๊ณ  ์˜์†์„ฑ(Persistence)๋งŒ ์ถ”๊ฐ€ํ•  ์ˆ˜ ์žˆ์–ด."


โฑ๏ธ ์‹ค๋ฌด ํ›… ํŒจํ„ด 3: useDebounce ๐ŸŸก

๐ŸŽฏ ์ด ์„น์…˜์„ ์ฝ๊ณ  ๋‚˜๋ฉด:

  • ๋””๋ฐ”์šด์Šค๊ฐ€ ์™œ ํ•„์š”ํ•œ์ง€, ์–ด๋–ป๊ฒŒ ๊ตฌํ˜„ํ•˜๋Š”์ง€ ์„ค๋ช…ํ•  ์ˆ˜ ์žˆ๋‹ค
  • useDebounce ์™€ useThrottle ์˜ ์ฐจ์ด๋ฅผ ๊ตฌ๋ถ„ํ•  ์ˆ˜ ์žˆ๋‹ค

๊ฒ€์ƒ‰์ฐฝ์— ํƒ€์ดํ•‘ํ•  ๋•Œ๋งˆ๋‹ค API๋ฅผ ํ˜ธ์ถœํ•˜๋ฉด ์„œ๋ฒ„๊ฐ€ ํญ๊ฒฉ ๋งž์•„์š”. ๋””๋ฐ”์šด์Šค(Debounce) ๋Š” "๋งˆ์ง€๋ง‰ ์ž…๋ ฅ์œผ๋กœ๋ถ€ํ„ฐ N์ดˆ ํ›„์— ์‹คํ–‰"ํ•˜๋Š” ํŒจํ„ด์ด์—์š”.

// hooks/useDebounce.ts
import { useState, useEffect } from 'react';
 
function useDebounce<T>(value: T, delay: number): T {
  const [debouncedValue, setDebouncedValue] = useState<T>(value);
 
  useEffect(() => {
    // delay ํ›„์— ๊ฐ’์„ ์—…๋ฐ์ดํŠธํ•˜๋Š” ํƒ€์ด๋จธ ์„ค์ •
    const timer = setTimeout(() => {
      setDebouncedValue(value);
    }, delay);
 
    // ํด๋ฆฐ์—…: ์ƒˆ value๊ฐ€ ๋“ค์–ด์˜ค๋ฉด ์ด์ „ ํƒ€์ด๋จธ ์ทจ์†Œ โ†’ ์‚ฌ์‹ค์ƒ ํƒ€์ด๋จธ ๋ฆฌ์…‹
    return () => clearTimeout(timer);
  }, [value, delay]);
 
  return debouncedValue; // delay๊ฐ€ ์ง€๋‚œ ํ›„์—์•ผ ๋ฐ”๋€Œ๋Š” ๊ฐ’
}
 
export default useDebounce;

๋™์ž‘ ์›๋ฆฌ ํƒ€์ž„๋ผ์ธ:

์‚ฌ์šฉ์ž ์ž…๋ ฅ:      "๋ฆฌ" โ†’ "๋ฆฌ์•ก" โ†’ "๋ฆฌ์•กํŠธ" โ†’ (500ms ์นจ๋ฌต) โ†’ ์‹ค์ œ API ํ˜ธ์ถœ
                   โ†‘       โ†‘        โ†‘
               ํƒ€์ด๋จธ ์ทจ์†Œ  ํƒ€์ด๋จธ ์ทจ์†Œ   ํƒ€์ด๋จธ ์‹œ์ž‘!

์‚ฌ์šฉ ์˜ˆ์‹œ:

// โœ… ์‹ค์‹œ๊ฐ„ ๊ฒ€์ƒ‰ โ€” ํƒ€์ดํ•‘ ๋ฉˆ์ถ”๊ณ  500ms ํ›„ API ํ˜ธ์ถœ
function SearchBar() {
  const [query, setQuery] = useState('');
  const debouncedQuery = useDebounce(query, 500); // ์‹ค์ œ API ํ˜ธ์ถœ์— ์“ธ ๊ฐ’
 
  // debouncedQuery๊ฐ€ ๋ฐ”๋€” ๋•Œ๋งŒ API ํ˜ธ์ถœ (ํƒ€์ดํ•‘ ์ค‘์—” ํ˜ธ์ถœ ์•ˆ ํ•จ)
  const { data: results, loading } = useAsync(
    () => searchPosts(debouncedQuery),
    [debouncedQuery]
  );
 
  return (
    <div>
      <input
        value={query}
        onChange={e => setQuery(e.target.value)} // ์ฆ‰์‹œ ๋ฐ˜์‘ (UI๋Š” ๋น ๋ฅด๊ฒŒ)
        placeholder="๊ฒŒ์‹œ๊ธ€ ๊ฒ€์ƒ‰..."
      />
      {loading && <Spinner />}
      {results?.map(post => <PostItem key={post.id} post={post} />)}
    </div>
  );
}

๋””๋ฐ”์šด์Šค vs ์“ฐ๋กœํ‹€(Throttle) ๋น„๊ต:

๋””๋ฐ”์šด์Šค(Debounce)์“ฐ๋กœํ‹€(Throttle)
๋™์ž‘๋งˆ์ง€๋ง‰ ์ž…๋ ฅ ํ›„ N์ดˆ ๋’ค ์‹คํ–‰N์ดˆ๋งˆ๋‹ค ์ตœ๋Œ€ 1๋ฒˆ ์‹คํ–‰
์šฉ๋„๊ฒ€์ƒ‰์–ด ์ž…๋ ฅ, ํผ ์ž๋™์ €์žฅ์Šคํฌ๋กค ์ด๋ฒคํŠธ, ๋ฆฌ์‚ฌ์ด์ฆˆ
ํŠน์ง•์—ฐ์† ์ž…๋ ฅ ์ค‘์—” ์‹คํ–‰ ์•ˆ ๋จ์ค‘๊ฐ„์ค‘๊ฐ„ ์‹คํ–‰๋จ

๐Ÿ’ก ํ•œ ์ค„๋กœ ๊ธฐ์–ตํ•˜๊ธฐ
"๋””๋ฐ”์šด์Šค๋Š” '๋งˆ์ง€๋ง‰ ์ด์•Œ๋งŒ ๋ฐœ์‚ฌ'๊ณ , ์“ฐ๋กœํ‹€์€ '์ผ์ • ๊ฐ„๊ฒฉ์œผ๋กœ ๋ฐœ์‚ฌ'. ๊ฒ€์ƒ‰์ฐฝ์—” ๋””๋ฐ”์šด์Šค, ์Šคํฌ๋กค์—” ์“ฐ๋กœํ‹€์ด ์ •์„์ด์•ผ."


๐Ÿ‘๏ธ ์‹ค๋ฌด ํ›… ํŒจํ„ด 4: useIntersectionObserver ๐ŸŸก

๐ŸŽฏ ์ด ์„น์…˜์„ ์ฝ๊ณ  ๋‚˜๋ฉด:

  • IntersectionObserver API๋ฅผ React ํ›…์œผ๋กœ ๊ฐ์‹ธ๋Š” ๋ฐฉ๋ฒ•์„ ์•Œ ์ˆ˜ ์žˆ๋‹ค
  • ๋ฌดํ•œ ์Šคํฌ๋กค(Infinite Scroll) ํŠธ๋ฆฌ๊ฑฐ๋ฅผ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ๋‹ค

๋ฌดํ•œ ์Šคํฌ๋กค ์€ "๋ชฉ๋ก ๋งจ ์•„๋ž˜ ์š”์†Œ๊ฐ€ ํ™”๋ฉด์— ๋ณด์ด๋ฉด ๋‹ค์Œ ํŽ˜์ด์ง€๋ฅผ ๋กœ๋“œ"ํ•˜๋Š” ํŒจํ„ด์ด์—์š”. IntersectionObserver โ€” ๊ต์ฐจ ๊ด€์ฐฐ์ž(๊ต์ฐจ ๊ฐ์‹œ์›) โ€” ๋ฅผ ํ›…์œผ๋กœ ์ถ”์ƒํ™”ํ•ด์š”.

๐Ÿ“– ์šฉ์–ด: IntersectionObserver โ€” ํŠน์ • DOM ์š”์†Œ๊ฐ€ ๋ทฐํฌํŠธ(ํ™”๋ฉด)์™€ ๊ต์ฐจ(๊ฒน์น˜๋Š”์ง€)๋ฅผ ๊ฐ์‹œํ•˜๋Š” ๋ธŒ๋ผ์šฐ์ € ๋‚ด์žฅ API. ์Šคํฌ๋กค ์ด๋ฒคํŠธ๋ณด๋‹ค ํ›จ์”ฌ ์„ฑ๋Šฅ์ด ์ข‹์•„์š”.

// hooks/useIntersectionObserver.ts
import { useEffect, useRef, useState } from 'react';
 
interface UseIntersectionObserverOptions {
  threshold?: number;   // 0~1: ์š”์†Œ๊ฐ€ ์–ผ๋งˆ๋‚˜ ๋ณด์—ฌ์•ผ ๊ต์ฐจ๋กœ ์ธ์‹ํ• ์ง€ (0.1 = 10%)
  rootMargin?: string;  // ๋ทฐํฌํŠธ ์—ฌ๋ฐฑ (ex: '0px 0px 100px' = ์•„๋ž˜์ชฝ 100px ๋ฏธ๋ฆฌ ๊ฐ์ง€)
}
 
function useIntersectionObserver(options: UseIntersectionObserverOptions = {}) {
  const [isIntersecting, setIsIntersecting] = useState(false);
  const ref = useRef<HTMLDivElement>(null); // ๊ฐ์‹œํ•  DOM ์š”์†Œ์— ๋ถ™์ผ ref
 
  useEffect(() => {
    const element = ref.current;
    if (!element) return;
 
    const observer = new IntersectionObserver(([entry]) => {
      setIsIntersecting(entry.isIntersecting); // ํ™”๋ฉด์— ๋ณด์ด๋ฉด true
    }, options);
 
    observer.observe(element); // ๊ฐ์‹œ ์‹œ์ž‘
 
    return () => observer.unobserve(element); // ํด๋ฆฐ์—…: ๊ฐ์‹œ ํ•ด์ œ
  }, []);
 
  return { ref, isIntersecting };
}
 
export default useIntersectionObserver;

๋ฌดํ•œ ์Šคํฌ๋กค ๊ตฌํ˜„ ์˜ˆ์‹œ:

// โœ… ๊ฒŒ์‹œ๊ธ€ ๋ฌดํ•œ ์Šคํฌ๋กค
function PostFeed() {
  const [page, setPage] = useState(1);
  const [allPosts, setAllPosts] = useState<Post[]>([]);
  const { ref: bottomRef, isIntersecting } = useIntersectionObserver({
    threshold: 0.1,       // 10%๋งŒ ๋ณด์—ฌ๋„ ๊ฐ์ง€
    rootMargin: '0px 0px 200px', // ๋ฐ”๋‹ฅ 200px ์ „์— ๋ฏธ๋ฆฌ ๊ฐ์ง€ (๋ถ€๋“œ๋Ÿฌ์šด ๋กœ๋”ฉ)
  });
 
  const { data: newPosts, loading } = useAsync(
    () => fetchPosts({ page }),
    [page]
  );
 
  // ์ƒˆ ๋ฐ์ดํ„ฐ ๋„์ฐฉ ์‹œ ๊ธฐ์กด ๋ชฉ๋ก์— ์ถ”๊ฐ€
  useEffect(() => {
    if (newPosts) {
      setAllPosts(prev => [...prev, ...newPosts]);
    }
  }, [newPosts]);
 
  // ํ•˜๋‹จ ์š”์†Œ๊ฐ€ ํ™”๋ฉด์— ๋ณด์ด๋ฉด ๋‹ค์Œ ํŽ˜์ด์ง€ ๋กœ๋“œ
  useEffect(() => {
    if (isIntersecting && !loading) {
      setPage(prev => prev + 1);
    }
  }, [isIntersecting, loading]);
 
  return (
    <div>
      {allPosts.map(post => <PostItem key={post.id} post={post} />)}
      {loading && <Spinner />}
      <div ref={bottomRef} style={{ height: 1 }} /> {/* ๊ฐ์‹œ ๋Œ€์ƒ (๋ˆˆ์— ์•ˆ ๋ณด์ž„) */}
    </div>
  );
}

๐Ÿ’ก ํ•œ ์ค„๋กœ ๊ธฐ์–ตํ•˜๊ธฐ
"useIntersectionObserver ๋Š” ์Šคํฌ๋กค ์ด๋ฒคํŠธ์˜ ๋Œ€์ฒด์ž์•ผ. '์ด ์š”์†Œ๊ฐ€ ํ™”๋ฉด์— ๋ณด์ด๋Š”๊ฐ€?' ๋ฅผ ์„ฑ๋Šฅ ์ข‹๊ฒŒ ๊ฐ์ง€ํ•˜๊ณ , ๋ฌดํ•œ ์Šคํฌ๋กคยท๋ ˆ์ด์ง€ ์ด๋ฏธ์ง€ ๋กœ๋”ฉ์— ๋‘๋ฃจ ์“ฐ์—ฌ."


๐Ÿ“ ํ›… ๋ฐ˜ํ™˜๊ฐ’ API ์„ค๊ณ„ ๐ŸŸก

๐ŸŽฏ ์ด ์„น์…˜์„ ์ฝ๊ณ  ๋‚˜๋ฉด:

  • ํ›… ๋ฐ˜ํ™˜๊ฐ’์„ ๋ฐฐ์—ด๋กœ ํ• ์ง€, ๊ฐ์ฒด๋กœ ํ• ์ง€ ํŒ๋‹จ ๊ธฐ์ค€์„ ๊ฐ–๊ฒŒ ๋œ๋‹ค

๋ฐฐ์—ด ๋ฐ˜ํ™˜ (Tuple):

// ๋ฐฐ์—ด ๋ฐ˜ํ™˜: useState ์Šคํƒ€์ผ โ€” ์ด๋ฆ„์„ ์ž์œ ๋กญ๊ฒŒ ๋ฐ”๊ฟ€ ์ˆ˜ ์žˆ์Œ
const [theme, setTheme] = useLocalStorage('theme', 'dark');
const [query, setQuery] = useLocalStorage('query', '');
// ์ด๋ฆ„ ์ถฉ๋Œ ์—†์ด ๋‘ ๋ฒˆ ์“ธ ์ˆ˜ ์žˆ๋‹ค๋Š” ๊ฒŒ ๋ฐฐ์—ด ๋ฐ˜ํ™˜์˜ ๊ฐ•์ 

๊ฐ์ฒด ๋ฐ˜ํ™˜ (Named):

// ๊ฐ์ฒด ๋ฐ˜ํ™˜: ์ด๋ฆ„์ด ๊ณ ์ •๋จ, ํ•„์š”ํ•œ ๊ฒƒ๋งŒ ๊ตฌ์กฐ๋ถ„ํ•ด ๊ฐ€๋Šฅ
const { data, loading, error } = useAsync(fetchPosts);
const { data: user } = useAsync(() => fetchUser(id)); // ์ด๋ฆ„ ์ถฉ๋Œ ์‹œ ๋ณ„์นญ ์‚ฌ์šฉ

์–ธ์ œ ์–ด๋А ์ชฝ์„ ์“ธ๊นŒ?

์ƒํ™ฉ์„ ํƒ์ด์œ 
๋ฐ˜ํ™˜๊ฐ’์ด 2๊ฐœ (๊ฐ’ + setter)๋ฐฐ์—ดuseState ์ฒ˜๋Ÿผ ์ด๋ฆ„ ์ž์œ 
๋ฐ˜ํ™˜๊ฐ’์ด 3๊ฐœ ์ด์ƒ๊ฐ์ฒด์ด๋ฆ„ ๋ช…์‹œ๋กœ ๊ฐ€๋…์„ฑ ํ–ฅ์ƒ
๊ฐ™์€ ํ›…์„ ํ•œ ์ปดํฌ๋„ŒํŠธ์—์„œ 2๋ฒˆ ์‚ฌ์šฉ๋ฐฐ์—ด๋ณ„์นญ ์—†์ด ์ž์œ ๋กœ์šด ์ด๋ฆ„
๋ฐ˜ํ™˜๊ฐ’ ์ผ๋ถ€๋งŒ ์„ ํƒํ•ด์„œ ์“ธ ๋•Œ๊ฐ์ฒด๊ตฌ์กฐ๋ถ„ํ•ด๋กœ ํ•„์š”ํ•œ ๊ฒƒ๋งŒ

๐Ÿ’ก ํ•œ ์ค„๋กœ ๊ธฐ์–ตํ•˜๊ธฐ
"๋ฐ˜ํ™˜๊ฐ’์ด 2๊ฐœ๋ฉด ๋ฐฐ์—ด(์ด๋ฆ„ ์ž์œ ), 3๊ฐœ ์ด์ƒ์ด๋ฉด ๊ฐ์ฒด(๋ช…์‹œ์„ฑ). ๊ฐ™์€ ํ›…์„ ๋‘ ๋ฒˆ ์จ์•ผ ํ•˜๋ฉด ๋ฌด์กฐ๊ฑด ๋ฐฐ์—ด."


๐Ÿ”— ํ›… ํ•ฉ์„ฑ ํŒจํ„ด ๐Ÿ”ด

๐ŸŽฏ ์ด ์„น์…˜์„ ์ฝ๊ณ  ๋‚˜๋ฉด:

  • ์ž‘์€ ํ›…๋“ค์„ ์กฐํ•ฉํ•ด ๋” ๋ณต์žกํ•œ ํ›…์„ ๋งŒ๋“œ๋Š” ํ•ฉ์„ฑ ํŒจํ„ด์„ ์ ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค

ํ›…์€ ๋‹ค๋ฅธ ํ›…์„ ํ˜ธ์ถœํ•  ์ˆ˜ ์žˆ์–ด์š”. ์ด ํŠน์„ฑ์„ ์ด์šฉํ•ด ์ž‘์€ ํ›…๋“ค์„ ์Œ“์•„ ๋ณต์žกํ•œ ๊ธฐ๋Šฅ์„ ๋งŒ๋“œ๋Š” ๊ฒŒ ํ›… ํ•ฉ์„ฑ(Hook Composition) ์ด์—์š”.

// โœ… ๋ ˆ์ด์–ด๋ณ„ ํ›… ํ•ฉ์„ฑ: ๊ฒ€์ƒ‰ ๊ธฐ๋Šฅ ์ „์ฒด๋ฅผ ํ•œ ํ›…์œผ๋กœ
function useSearch(initialQuery = '') {
  // Layer 1: ๊ฒ€์ƒ‰์–ด ์ƒํƒœ (localStorage์— ์˜๊ตฌ ์ €์žฅ)
  const [query, setQuery] = useLocalStorage('last-search', initialQuery);
 
  // Layer 2: ๋””๋ฐ”์šด์Šค ์ ์šฉ (ํƒ€์ดํ•‘ ๋ฉˆ์ถ”๊ณ  300ms ํ›„ ์‹ค์ œ ๊ฒ€์ƒ‰)
  const debouncedQuery = useDebounce(query, 300);
 
  // Layer 3: ์‹ค์ œ API ํ˜ธ์ถœ (๋””๋ฐ”์šด์Šค๋œ ๊ฐ’์œผ๋กœ)
  const { data: results, loading, error } = useAsync(
    () => debouncedQuery ? searchPosts(debouncedQuery) : Promise.resolve([]),
    [debouncedQuery]
  );
 
  return {
    query,        // ์ฆ‰์‹œ ๋ฐ˜์‘ํ•˜๋Š” ์ž…๋ ฅ๊ฐ’ (UI ํ‘œ์‹œ์šฉ)
    setQuery,     // ๊ฒ€์ƒ‰์–ด ๋ณ€๊ฒฝ
    results,      // ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ
    loading,
    error,
  };
}
 
// ์‚ฌ์šฉ: ๋ณต์žกํ•œ ๋กœ์ง์ด ํ•œ ์ค„๋กœ
function SearchPage() {
  const { query, setQuery, results, loading } = useSearch();
 
  return (
    <div>
      <input value={query} onChange={e => setQuery(e.target.value)} />
      {loading ? <Spinner /> : results?.map(p => <PostItem key={p.id} post={p} />)}
    </div>
  );
}

์ด๋ ‡๊ฒŒ useLocalStorage โ†’ useDebounce โ†’ useAsync ๋ฅผ ์กฐํ•ฉํ•˜๋ฉด, ๊ฐ ํ›…์€ ๋…๋ฆฝ์ ์œผ๋กœ ํ…Œ์ŠคํŠธ ๊ฐ€๋Šฅํ•˜๋ฉด์„œ๋„ useSearch ๋ผ๋Š” ๊ณ ์ˆ˜์ค€ ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ๋งŒ๋“ค ์ˆ˜ ์žˆ์–ด์š”.

๐Ÿ’ก ํ•œ ์ค„๋กœ ๊ธฐ์–ตํ•˜๊ธฐ
"ํ›… ํ•ฉ์„ฑ์€ ๋ ˆ๊ณ  ์กฐ๋ฆฝ์ด์•ผ. ์ž‘์€ ํ›…(๋ธ”๋ก)๋“ค์„ ๋…๋ฆฝ์ ์œผ๋กœ ๋งŒ๋“ค๊ณ , ํฐ ํ›…(๊ตฌ์กฐ๋ฌผ)์€ ๊ทธ๊ฒƒ๋“ค์„ ์Œ“์•„ ๋งŒ๋“ค์–ด. ๊ฐ ๋ธ”๋ก์€ ๋”ฐ๋กœ ํ…Œ์ŠคํŠธ ๊ฐ€๋Šฅํ•ด."


โš”๏ธ ํ›… vs ์ปดํฌ๋„ŒํŠธ ๊ฒฝ๊ณ„์„  ๐ŸŸข

๐ŸŽฏ ์ด ์„น์…˜์„ ์ฝ๊ณ  ๋‚˜๋ฉด:

  • "์ด๊ฑธ ํ›…์œผ๋กœ ๋งŒ๋“ค๊นŒ, ์ปดํฌ๋„ŒํŠธ๋กœ ๋งŒ๋“ค๊นŒ"๋ฅผ ๋ช…ํ™•ํ•˜๊ฒŒ ํŒ๋‹จํ•  ์ˆ˜ ์žˆ๋‹ค

๊ฐ€์žฅ ํ”ํ•œ ํ˜ผ๋™์€ "๋กœ์ง ์žฌ์‚ฌ์šฉ = ํ›…, UI ์žฌ์‚ฌ์šฉ = ์ปดํฌ๋„ŒํŠธ" ๋ผ๋Š” ์›์น™์„ ๋ฌด์‹œํ•  ๋•Œ ๋ฐœ์ƒํ•ด์š”.

// โŒ ์ž˜๋ชป๋œ ์˜ˆ: ํ›…์ด JSX๋ฅผ ๋ฐ˜ํ™˜ํ•จ
function useLoadingState(isLoading: boolean) {
  if (isLoading) {
    return <div className="spinner">Loading...</div>; // ํ›…์ด JSX ๋ฐ˜ํ™˜ โ†’ ๊ทœ์น™ ์œ„๋ฐ˜!
  }
  return null;
}
 
// โœ… ์˜ฌ๋ฐ”๋ฅธ ๋ถ„๋ฆฌ
// 1) ๋กœ์ง(์ƒํƒœ)์€ ํ›…์œผ๋กœ
function useLoadingState() {
  const [isLoading, setIsLoading] = useState(false);
  return { isLoading, setIsLoading };
}
 
// 2) UI๋Š” ์ปดํฌ๋„ŒํŠธ๋กœ
function LoadingSpinner({ visible }: { visible: boolean }) {
  if (!visible) return null;
  return <div className="spinner">Loading...</div>;
}
 
// 3) ์ปดํฌ๋„ŒํŠธ์—์„œ ์กฐํ•ฉ
function MyPage() {
  const { isLoading } = useLoadingState();
  return <LoadingSpinner visible={isLoading} />;
}

"Render Props vs Custom Hook" ๊ฒฝ๊ณ„:

// ๐Ÿ’ก Render Prop ํŒจํ„ด (๊ตฌํ˜•, ํ›… ์ด์ „ ์‹œ๋Œ€)
<DataLoader render={data => <PostList data={data} />} />
 
// ๐Ÿ’ก ์ปค์Šคํ…€ ํ›… ํŒจํ„ด (ํ˜„๋Œ€์ , ๋” ๋‹จ์ˆœ)
const { data } = useData();
return <PostList data={data} />;
// โ†’ ๋™์ผํ•œ ๋กœ์ง ์žฌ์‚ฌ์šฉ์ด์ง€๋งŒ ํ›…์ด ํ›จ์”ฌ ์ฝ๊ธฐ ์‰ฌ์›€

๐Ÿ’ก ํ•œ ์ค„๋กœ ๊ธฐ์–ตํ•˜๊ธฐ
"ํ›…์€ ๋กœ์ง(How), ์ปดํฌ๋„ŒํŠธ๋Š” UI(What). ํ›…์ด return <div> ๋ฅผ ํ•œ๋‹ค๋ฉด ๊ทธ๊ฑด ์ปดํฌ๋„ŒํŠธ์•ผ."


๐Ÿ’ฅ ์—๋Ÿฌ ํ•ด๊ฒฐ ์นดํƒˆ๋กœ๊ทธ

์—๋Ÿฌ ๋ฉ”์‹œ์ง€๊ฐ€ ๋œจ๋ฉด Ctrl+F ๋กœ ๋ฉ”์‹œ์ง€ ์ผ๋ถ€๋ฅผ ๊ฒ€์ƒ‰ํ•ด๋ด. ๋Œ€๋ถ€๋ถ„ ์—ฌ๊ธฐ ์žˆ์–ด.


โŒ React Hook "useXxx" is called conditionally

์–ธ์ œ ๋‚˜์˜ค๋Š”๊ฐ€?

function MyComponent({ isLoggedIn }) {
  if (!isLoggedIn) return null; // โ† ์—ฌ๊ธฐ์„œ early return
  const { data } = useUserData(); // ๐Ÿ’ฅ ์กฐ๊ฑด๋ฌธ ์ดํ›„์— ํ›… ํ˜ธ์ถœ โ†’ ๊ทœ์น™ ์œ„๋ฐ˜
}

์›์ธ: ํ›…์€ ํ•ญ์ƒ ๊ฐ™์€ ์ˆœ์„œ๋กœ ํ˜ธ์ถœ๋˜์–ด์•ผ ํ•ด์š”. ์กฐ๊ฑด๋ฌธ/๋ฐ˜๋ณต๋ฌธ ์•ˆ์— ํ›…์ด ๋“ค์–ด๊ฐ€๋ฉด ์•ˆ ๋ผ์š”.

ํ•ด๊ฒฐ์ฑ…:

// โœ… ํ›…์„ ์กฐ๊ฑด๋ฌธ ์ด์ „์— ํ˜ธ์ถœ, ์กฐ๊ฑด ์ฒ˜๋ฆฌ๋Š” ๊ทธ ํ›„์—
function MyComponent({ isLoggedIn }) {
  const { data } = useUserData(); // ํ•ญ์ƒ ํ˜ธ์ถœ
 
  if (!isLoggedIn) return null; // ๊ทธ ๋‹ค์Œ์— ์กฐ๊ฑด ์ฒ˜๋ฆฌ
  return <div>{data?.name}</div>;
}

โŒ Warning: Can't perform a React state update on an unmounted component

์–ธ์ œ ๋‚˜์˜ค๋Š”๊ฐ€?

useEffect(() => {
  fetchData().then(data => {
    setState(data); // ๐Ÿ’ฅ ์ด๋ฏธ ์–ธ๋งˆ์šดํŠธ๋๋Š”๋ฐ setState ํ˜ธ์ถœ!
  });
}, []);

์›์ธ: ์ปดํฌ๋„ŒํŠธ๊ฐ€ ์–ธ๋งˆ์šดํŠธ๋œ ํ›„ ๋น„๋™๊ธฐ ์ฝœ๋ฐฑ์ด ์™„๋ฃŒ๋˜์–ด setState ๋ฅผ ํ˜ธ์ถœํ•  ๋•Œ ๋ฐœ์ƒํ•ด์š”.

ํ•ด๊ฒฐ์ฑ…: useAsync ํ›…์˜ cancelled ํ”Œ๋ž˜๊ทธ ํŒจํ„ด์„ ์‚ฌ์šฉํ•ด์š”:

useEffect(() => {
  let cancelled = false;
  fetchData().then(data => {
    if (!cancelled) setState(data); // ์–ธ๋งˆ์šดํŠธ ํ›„๋ฉด ๊ฑด๋„ˆ๋œ€
  });
  return () => { cancelled = true; };
}, []);

โŒ useLocalStorage ์—์„œ SSR ์—๋Ÿฌ: window is not defined

์–ธ์ œ ๋‚˜์˜ค๋Š”๊ฐ€?

ReferenceError: window is not defined

์›์ธ: Next.js ๊ฐ™์€ SSR ํ™˜๊ฒฝ์—์„œ๋Š” ์„œ๋ฒ„์—์„œ window ๊ฐ์ฒด๊ฐ€ ์—†์–ด์š”.

ํ•ด๊ฒฐ์ฑ…:

const [value, setValue] = useState<T>(() => {
  if (typeof window === 'undefined') return initialValue; // SSR ํ™˜๊ฒฝ ์ฒดํฌ
  try {
    const item = window.localStorage.getItem(key);
    return item ? JSON.parse(item) : initialValue;
  } catch {
    return initialValue;
  }
});

๐Ÿ ์ด๋ฒˆ์— ๋ฐฐ์šด ๋‚ด์šฉ ์ด์ •๋ฆฌ

์˜ค๋Š˜ ๋ฐฐ์šด ํ•ต์‹ฌ์„ ํ•œ๋ˆˆ์— ์ •๋ฆฌํ•ด๋ณผ๊นŒ? ์‹ค๋ฌด์—์„œ ๊ธธ์„ ์žƒ์—ˆ์„ ๋•Œ ์ด๊ฒƒ๋งŒ ๋ด๋„ ๋ผ.

๐Ÿ“‹ ํ›… ์ถ”์ถœ ํŒ๋‹จ ๊ธฐ์ค€

์ƒํ™ฉ์„ ํƒ
useState + useEffect ์กฐํ•ฉ์ด 2๊ณณ ์ด์ƒ ๋ณต๋ถ™๋จโœ… ์ปค์Šคํ…€ ํ›…
๊ฐ™์€ JSX ๊ตฌ์กฐ๊ฐ€ 2๊ณณ ์ด์ƒ ๋ณต๋ถ™๋จโœ… ์ปดํฌ๋„ŒํŠธ
ํ›… ์—†๋Š” ์ˆœ์ˆ˜ ํ•จ์ˆ˜ (๊ณ„์‚ฐ, ๋ณ€ํ™˜)โœ… ์œ ํ‹ธ ํ•จ์ˆ˜
JSX๋ฅผ ๋ฐ˜ํ™˜ํ•˜๊ณ  ์‹ถ์€ ํ›…โŒ ํ›… ์•„๋‹˜, ์ปดํฌ๋„ŒํŠธ๋กœ

๐Ÿ“‹ ์‹ค๋ฌด ํ›… 4์ข… ์š”์•ฝ

ํ›…์—ญํ• ๋ฐ˜ํ™˜
useAsync(fn, deps)๋น„๋™๊ธฐ ๋ฐ์ดํ„ฐ ํŒจ์นญ{ data, loading, error }
useLocalStorage(key, init)์˜์†์„ฑ ์žˆ๋Š” ์ƒํƒœ[value, setter] (๋ฐฐ์—ด)
useDebounce(value, delay)๋น ๋ฅธ ๊ฐ’ ๋ณ€ํ™”๋ฅผ ์ง€์—ฐdebouncedValue
useIntersectionObserver()DOM ๊ฐ€์‹œ์„ฑ ๊ฐ์ง€{ ref, isIntersecting }

โš ๏ธ ์ ˆ๋Œ€ ํ•˜์ง€ ๋ง ๊ฒƒ

โŒ ๋‚˜์œ ์˜ˆโœ… ์ข‹์€ ์˜ˆ์ด์œ 
ํ›… ์•ˆ์—์„œ JSX ๋ฐ˜ํ™˜์ปดํฌ๋„ŒํŠธ๋กœ ๋ถ„๋ฆฌํ›… ๊ทœ์น™ ์œ„๋ฐ˜
์กฐ๊ฑด๋ฌธ ์•ˆ์—์„œ ํ›… ํ˜ธ์ถœํ›… ๋จผ์ € ํ˜ธ์ถœ ํ›„ ์กฐ๊ฑด ์ฒ˜๋ฆฌํ›… ์ˆœ์„œ ๊ทœ์น™ ์œ„๋ฐ˜
ํด๋ฆฐ์—… ์—†๋Š” ๋น„๋™๊ธฐ ํ›…cancelled ํ”Œ๋ž˜๊ทธ ์‚ฌ์šฉ๋ฉ”๋ชจ๋ฆฌ ๋ˆ„์ˆ˜/๋ฒ„๊ทธ
๊ฒŒ์œผ๋ฅธ ์ดˆ๊ธฐํ™” ์—†์ด localStorage ์ฝ๊ธฐuseState(() => ...) ์‚ฌ์šฉ๋งค ๋ Œ๋”๋งˆ๋‹ค IO ๋ฐœ์ƒ

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

๊ฒŒ์‹œ๊ธ€, ์œ ์ €, ๋Œ“๊ธ€ ๋ชฉ๋ก ์„ธ ๊ตฐ๋ฐ์„œ ์ „๋ถ€ setLoading(false)๊ฐ€ ๊ผฌ์—ฌ์„œ ์•ผ๊ทผํ–ˆ๋˜ ๋‚ ์ด ๋– ์˜ค๋ฅธ๋‹ค. ํ›…์€ ๊ทธ๋ƒฅ '๋ณต๋ถ™ ์—†์• ๊ธฐ' ์šฉ๋„์ธ ์ค„ ์•Œ์•˜๋Š”๋ฐ, ๋กœ์ง ์ž์ฒด๋ฅผ ์บก์А ์•ˆ์—์„œ ์•ˆ์ „ํ•˜๊ฒŒ ๋ณดํ˜ธํ•ด ์ฃผ๋Š” ๊ฑฐ์˜€๋‹ค.

๐Ÿ’ก "๋กœ์ง(How)์„ ์บก์А์— ๋‹ด์œผ๋ฉด ์ปค์Šคํ…€ ํ›…์ด๊ณ , UI(What)๋ฅผ ์บก์А์— ๋‹ด์œผ๋ฉด ์ปดํฌ๋„ŒํŠธ๋‹ค. ํ›…์ด ํƒœ๊ทธ(JSX)๋ฅผ ๋ฑ‰์–ด๋‚ด๋Š” ์ˆœ๊ฐ„ ๊ทœ์น™ ์œ„๋ฐ˜!"

useAsync ํ•˜๋‚˜์— ์บ”์Šฌ ํ”Œ๋ž˜๊ทธ(ํด๋ฆฐ์—…) ์„ธํŒ…์„ ํ•ด ๋†“์œผ๋‹ˆ ๋” ์ด์ƒ ๊ฒฝ์Ÿ ์กฐ๊ฑด(Race Condition)์„ ์‹ ๊ฒฝ ์“ธ ํ•„์š”๊ฐ€ ์—†์–ด์ง„ ๊ฒŒ ์ œ์ผ ์‹œ์›ํ•˜๋‹ค. ์กฐ๋ฆฝ ๋งค๋‰ด์–ผ ๋น„์œ ๊ฐ€ ๋”ฑ์ด๋‹ค. ๋‚ด์ผ ๋‹น์žฅ ํšŒ์‚ฌ ์ฝ”๋“œ์— ๋„๋ ค ์žˆ๋Š” ๋”๋Ÿฌ์šด useEffect + fetch ํŠธ๋ฆฌ๋“ค์„ ๋ชจ์กฐ๋ฆฌ ํ›… ํ•˜๋‚˜๋กœ ๋นจ์•„๋“ค์—ฌ ๋ฒ„๋ฆฌ๊ฒ ๋‹ค.


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

Q1. ์•„๋ž˜ ์ค‘ ์ปค์Šคํ…€ ํ›…์œผ๋กœ ์ถ”์ถœํ•ด์•ผ ํ•˜๋Š” ๊ฒƒ์€?

  • A) <UserCard> ์ปดํฌ๋„ŒํŠธ์—์„œ ์“ฐ์ด๋Š” ์‚ฌ์šฉ์ž ์ด๋ฆ„ ํ‘œ์‹œ JSX
  • B) 3๊ฐœ ์ปดํฌ๋„ŒํŠธ์—์„œ ๋™์ผํ•˜๊ฒŒ ๋ฐ˜๋ณต๋˜๋Š” useState + useEffect + fetch ํŒจํ„ด
  • C) ๋‚ ์งœ ํ˜•์‹์„ ๋ณ€ํ™˜ํ•˜๋Š” formatDate(date) ํ•จ์ˆ˜
  • D) ๋ฒ„ํŠผ ํด๋ฆญ ์‹œ API๋ฅผ ํ˜ธ์ถœํ•˜๋Š” ์ด๋ฒคํŠธ ํ•ธ๋“ค๋Ÿฌ ํ•จ์ˆ˜

โœ… ์ •๋‹ต: B

  • A: JSX โ†’ ์ปดํฌ๋„ŒํŠธ๋กœ ์ถ”์ถœ
  • B: useState + useEffect ๋ฐ˜๋ณต ํŒจํ„ด โ†’ ์ปค์Šคํ…€ ํ›…์œผ๋กœ ์ถ”์ถœ โœ…
  • C: ํ›… ์—†๋Š” ์ˆœ์ˆ˜ ํ•จ์ˆ˜ โ†’ ์œ ํ‹ธ ํ•จ์ˆ˜
  • D: ์ด๋ฒคํŠธ ํ•ธ๋“ค๋Ÿฌ๋Š” ํ›… ์—†์œผ๋ฉด ์ผ๋ฐ˜ ํ•จ์ˆ˜๋กœ ์ถฉ๋ถ„

๐Ÿ“Œ ํ•ต์‹ฌ ๊ธฐ์–ต๋ฒ•: "Hook์ด ๋“ค์–ด๊ฐ€๋Š” ๋กœ์ง ์žฌ์‚ฌ์šฉ = ํ›…. JSX ์žฌ์‚ฌ์šฉ = ์ปดํฌ๋„ŒํŠธ. Hook ์—†๋Š” ๋กœ์ง = ์œ ํ‹ธ ํ•จ์ˆ˜."


Q2. ์•„๋ž˜ ๋นˆ์นธ์„ ์ฑ„์›Œ๋ณด์ž.

function useDebounce<T>(value: T, delay: number): T {
  const [debouncedValue, setDebouncedValue] = useState<T>(value);
 
  useEffect(() => {
    const timer = ________(() => {    // 1๋ฒˆ ๋นˆ์นธ
      setDebouncedValue(value);
    }, delay);
 
    return () => __________(timer);   // 2๋ฒˆ ๋นˆ์นธ (ํด๋ฆฐ์—…)
  }, [value, delay]);
 
  return debouncedValue;
}

โœ… ์ •๋‹ต: 1๋ฒˆ: setTimeout, 2๋ฒˆ: clearTimeout

ํ•ด์„ค: setTimeout ์œผ๋กœ ์ง€์—ฐ ์‹คํ–‰์„ ์˜ˆ์•ฝํ•˜๊ณ , ํด๋ฆฐ์—…์—์„œ clearTimeout ์œผ๋กœ ์ด์ „ ํƒ€์ด๋จธ๋ฅผ ์ทจ์†Œํ•ด์š”. ์ƒˆ value ๊ฐ€ ๋“ค์–ด์˜ฌ ๋•Œ๋งˆ๋‹ค ์ด์ „ ํƒ€์ด๋จธ๊ฐ€ ์ทจ์†Œ๋˜์–ด ๊ฒฐ๊ณผ์ ์œผ๋กœ "๋งˆ์ง€๋ง‰ ๊ฐ’๋งŒ" ์ฒ˜๋ฆฌ๋ผ์š”.

๐Ÿ“Œ ํ•ต์‹ฌ ๊ธฐ์–ต๋ฒ•: "๋””๋ฐ”์šด์Šค = setTimeout + clearTimeout. ํƒ€์ด๋จธ๋ฅผ ๊ณ„์† ๋ฆฌ์…‹ํ•ด์„œ ๋งˆ์ง€๋ง‰ ๊ฐ’๋งŒ ์‚ด์•„๋‚จ๊ฒŒ ํ•œ๋‹ค."


Q3. ์นœ๊ตฌ์—๊ฒŒ ์„ค๋ช…ํ•œ๋‹ค๋ฉด?

์ปค์Šคํ…€ ํ›…์ด ์™œ "์ปดํฌ๋„ŒํŠธ ๋ถ„๋ฆฌ"์™€ ๋‹ค๋ฅธ์ง€, ์˜ˆ์‹œ๋ฅผ ๋“ค์–ด ํ•œ ๋ฌธ๋‹จ์œผ๋กœ ์„ค๋ช…ํ•ด๋ด.

์˜ˆ์‹œ ๋‹ต๋ณ€:

"์ปดํฌ๋„ŒํŠธ ๋ถ„๋ฆฌ๋Š” ํ™”๋ฉด์˜ ์ผ๋ถ€(UI)๋ฅผ ๋”ฐ๋กœ ๋–ผ์–ด๋‚ด๋Š” ๊ฑฐ์•ผ. ์ปค์Šคํ…€ ํ›…์€ ํ™”๋ฉด์—๋Š” ๋ณด์ด์ง€ ์•Š๋Š” '๋™์ž‘ ๋ฐฉ์‹(๋กœ์ง)'์„ ๋”ฐ๋กœ ๋–ผ์–ด๋‚ด๋Š” ๊ฑฐ์•ผ. ์˜ˆ๋ฅผ ๋“ค์–ด ๊ฒ€์ƒ‰์ฐฝ UI๋ฅผ ์žฌ์‚ฌ์šฉํ•˜๊ณ  ์‹ถ์œผ๋ฉด <SearchInput> ์ปดํฌ๋„ŒํŠธ๋กœ ๋ถ„๋ฆฌํ•˜๊ณ , ๊ฒ€์ƒ‰ API ํ˜ธ์ถœ ๋กœ์ง์„ ์žฌ์‚ฌ์šฉํ•˜๊ณ  ์‹ถ์œผ๋ฉด useSearch() ํ›…์œผ๋กœ ๋ถ„๋ฆฌํ•ด. ํ›…์€ JSX๋ฅผ ๋ฐ˜ํ™˜ํ•˜์ง€ ์•Š๊ณ  ๋ฐ์ดํ„ฐ๋‚˜ ํ•จ์ˆ˜๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค๋Š” ๊ฒŒ ํ•ต์‹ฌ ์ฐจ์ด์•ผ."


๐Ÿ”— ๋” ์•Œ์•„๋ณด๊ธฐ