๋ฉ”์ธ ์ฝ˜ํ…์ธ ๋กœ ๊ฑด๋„ˆ๋›ฐ๊ธฐ
์™ธ๋ž˜ํ‚ค ๊ด€๊ณ„๋ฅผ ์ฒ˜๋ฆฌํ•˜๊ธฐ ์œ„ํ•œ ID Async Select ์ปดํฌ๋„ŒํŠธ ์‚ฌ์šฉ๋ฒ•์„ ์•Œ์•„๋ด…๋‹ˆ๋‹ค.

ID Async Select ๊ฐœ์š”

๋น„๋™๊ธฐ ๋กœ๋”ฉ

API ํ˜ธ์ถœ์ž๋™ ์™„์„ฑ

ํƒ€์ž… ์•ˆ์ „

Entity ๊ธฐ๋ฐ˜ID ํƒ€์ž… ๋ณด์žฅ

๊ฒ€์ƒ‰ ์ง€์›

์‹ค์‹œ๊ฐ„ ํ•„ํ„ฐ๋ง๋””๋ฐ”์šด์Šค

๋‹ค์ค‘ ์„ ํƒ

๋‹จ์ผ/๋‹ค์ค‘ ๋ชจ๋“œ๋ฐฐ์—ด ๋ฐ˜ํ™˜

ID Async Select๋ž€?

๋ฌธ์ œ: ์™ธ๋ž˜ํ‚ค ์„ ํƒ์˜ ๋ณต์žก์„ฑ

๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์—์„œ ์™ธ๋ž˜ํ‚ค ๊ด€๊ณ„๋ฅผ ์ฒ˜๋ฆฌํ•  ๋•Œ, ํ”„๋ก ํŠธ์—”๋“œ์—์„œ๋Š” ๊ด€๋ จ ์—”ํ‹ฐํ‹ฐ๋ฅผ ์„ ํƒํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ์ „ํ†ต์ ์ธ ๋ฐฉ์‹์˜ ๋ฌธ์ œ์ :
// โŒ ๋ชจ๋“  ์‚ฌ์šฉ์ž๋ฅผ ํ•œ ๋ฒˆ์— ๋กœ๋“œ (๋น„ํšจ์œจ์ )
const [users, setUsers] = useState([]);

useEffect(() => {
  userService.list({ pageSize: 1000 }).then(({ users }) => {
    setUsers(users); // 1000๋ช… ์ „๋ถ€ ๋กœ๋“œ!
  });
}, []);

return (
  <select>
    {users.map((user) => (
      <option key={user.id} value={user.id}>
        {user.username}
      </option>
    ))}
  </select>
);
๋ฌธ์ œ์ :
  1. ์„ฑ๋Šฅ: ๋ฐ์ดํ„ฐ๊ฐ€ ๋งŽ์œผ๋ฉด ๋กœ๋”ฉ ์‹œ๊ฐ„ ์ฆ๊ฐ€
  2. ๋ฉ”๋ชจ๋ฆฌ: ๋ถˆํ•„์š”ํ•œ ๋ฐ์ดํ„ฐ๋ฅผ ๋ชจ๋‘ ๋ฉ”๋ชจ๋ฆฌ์— ์ ์žฌ
  3. UX: ์‚ฌ์šฉ์ž๊ฐ€ ์›ํ•˜๋Š” ํ•ญ๋ชฉ์„ ์ฐพ๊ธฐ ์–ด๋ ค์›€
  4. ํ™•์žฅ์„ฑ: ๋ฐ์ดํ„ฐ๊ฐ€ ๊ณ„์† ๋Š˜์–ด๋‚˜๋ฉด ๊ฐ๋‹น ๋ถˆ๊ฐ€

ํ•ด๊ฒฐ: ID Async Select

Sonamu์˜ ID Async Select๋Š” ํ•„์š”ํ•  ๋•Œ๋งŒ ๋น„๋™๊ธฐ๋กœ ๋กœ๋“œํ•˜๊ณ  ๊ฒ€์ƒ‰ ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.
// โœ… ๊ฒ€์ƒ‰์–ด ๊ธฐ๋ฐ˜์œผ๋กœ ํ•„์š”ํ•œ ๋ฐ์ดํ„ฐ๋งŒ ๋กœ๋“œ
<IdAsyncSelect
  entity="User"
  value={selectedUserId}
  onChange={(userId) => setSelectedUserId(userId)}
  searchField="username"
/>
์žฅ์ :
  1. โœจ ์„ฑ๋Šฅ: ์ฒ˜์Œ์—๋Š” ์ตœ์†Œํ•œ๋งŒ ๋กœ๋“œ, ๊ฒ€์ƒ‰ ์‹œ ํ•„์š”ํ•œ ๊ฒƒ๋งŒ
  2. โœจ ์‚ฌ์šฉ์ž ๊ฒฝํ—˜: ์ž๋™์™„์„ฑ์œผ๋กœ ๋น ๋ฅธ ์„ ํƒ
  3. โœจ ํƒ€์ž… ์•ˆ์ „: Entity ํƒ€์ž…์ด ์ž๋™์œผ๋กœ ์ ์šฉ
  4. โœจ ํ™•์žฅ์„ฑ: ๋ฐ์ดํ„ฐ๊ฐ€ ์•„๋ฌด๋ฆฌ ๋งŽ์•„๋„ ๋ฌธ์ œ์—†์Œ

๊ธฐ๋ณธ ์‚ฌ์šฉ๋ฒ•

๋‹จ์ผ ์„ ํƒ

ํ•˜๋‚˜์˜ ์—”ํ‹ฐํ‹ฐ๋ฅผ ์„ ํƒํ•ฉ๋‹ˆ๋‹ค.
import { IdAsyncSelect } from "@/components/common/IdAsyncSelect";
import { useState } from "react";

function PostForm() {
  const [authorId, setAuthorId] = useState<number | null>(null);
  
  return (
    <div>
      <label>Author</label>
      <IdAsyncSelect
        entity="User"
        value={authorId}
        onChange={setAuthorId}
        searchField="username"
        placeholder="Select author..."
      />
    </div>
  );
}
๋™์ž‘ ๋ฐฉ์‹:
  1. ์ปดํฌ๋„ŒํŠธ ๋งˆ์šดํŠธ ์‹œ ์ดˆ๊ธฐ ์˜ต์…˜ ๋กœ๋“œ (๋ณดํ†ต ์ตœ๊ทผ 10๊ฐœ)
  2. ์‚ฌ์šฉ์ž๊ฐ€ ํƒ€์ดํ•‘ํ•˜๋ฉด ๊ฒ€์ƒ‰ API ํ˜ธ์ถœ
  3. ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๋ฅผ ๋“œ๋กญ๋‹ค์šด์— ํ‘œ์‹œ
  4. ์„ ํƒ ์‹œ ID๋งŒ ์ €์žฅ (๋ฉ”๋ชจ๋ฆฌ ํšจ์œจ์ )

๋‹ค์ค‘ ์„ ํƒ

์—ฌ๋Ÿฌ ์—”ํ‹ฐํ‹ฐ๋ฅผ ์„ ํƒํ•ฉ๋‹ˆ๋‹ค.
function PostForm() {
  const [tagIds, setTagIds] = useState<number[]>([]);
  
  return (
    <div>
      <label>Tags</label>
      <IdAsyncSelect
        entity="Tag"
        value={tagIds}
        onChange={setTagIds}
        searchField="name"
        multiple
        placeholder="Select tags..."
      />
    </div>
  );
}
๋‹ค์ค‘ ์„ ํƒ ํŠน์ง•:
  • ์„ ํƒ๋œ ํ•ญ๋ชฉ์„ ํƒœ๊ทธ ํ˜•ํƒœ๋กœ ํ‘œ์‹œ
  • X ๋ฒ„ํŠผ์œผ๋กœ ๊ฐœ๋ณ„ ์ œ๊ฑฐ
  • ๋ฐฐ์—ด ํ˜•ํƒœ๋กœ ID ๋ฐ˜ํ™˜ (number[])

์ดˆ๊ธฐ๊ฐ’ ์„ค์ •

๊ธฐ์กด ๋ฐ์ดํ„ฐ ์ˆ˜์ • ์‹œ ์ดˆ๊ธฐ๊ฐ’์„ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค.
function EditPostForm({ post }: { post: Post }) {
  const [authorId, setAuthorId] = useState(post.author_id);
  
  return (
    <IdAsyncSelect
      entity="User"
      value={authorId}
      onChange={setAuthorId}
      searchField="username"
    />
  );
}
์ดˆ๊ธฐ๊ฐ’ ์ฒ˜๋ฆฌ:
  • value์— ID๋ฅผ ์ „๋‹ฌํ•˜๋ฉด ์ž๋™์œผ๋กœ ํ•ด๋‹น ์—”ํ‹ฐํ‹ฐ ๋กœ๋“œ
  • ๋ ˆ์ด๋ธ” ํ‘œ์‹œ๋ฅผ ์œ„ํ•ด ๋ฐฑ๊ทธ๋ผ์šด๋“œ์—์„œ ๋‹จ๊ฑด ์กฐํšŒ
  • ๋กœ๋”ฉ ์ค‘์—๋Š” ID๋งŒ ํ‘œ์‹œ

๊ณ ๊ธ‰ ์‚ฌ์šฉ๋ฒ•

์ปค์Šคํ…€ ๊ฒ€์ƒ‰ ๋กœ์ง

๊ธฐ๋ณธ ๊ฒ€์ƒ‰ ๋Œ€์‹  ์ปค์Šคํ…€ ๋กœ์ง์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
<IdAsyncSelect
  entity="User"
  value={userId}
  onChange={setUserId}
  // ์ปค์Šคํ…€ ๊ฒ€์ƒ‰ ํ•จ์ˆ˜
  onSearch={async (query) => {
    const { users } = await userService.search({
      query,
      role: "admin", // ๊ด€๋ฆฌ์ž๋งŒ ๊ฒ€์ƒ‰
      isActive: true, // ํ™œ์„ฑ ์‚ฌ์šฉ์ž๋งŒ
    });
    return users;
  }}
  // ๋ ˆ์ด๋ธ” ํฌ๋งท
  getLabel={(user) => `${user.username} (${user.email})`}
/>
์ปค์Šคํ…€ ๊ฒ€์ƒ‰์˜ ์‚ฌ์šฉ ์‚ฌ๋ก€:
  • ํŠน์ • ์กฐ๊ฑด ํ•„ํ„ฐ๋ง (์—ญํ• , ์ƒํƒœ ๋“ฑ)
  • ๋ณต์žกํ•œ ๊ฒ€์ƒ‰ ๋กœ์ง
  • ๋‹ค๋ฅธ API ์—”๋“œํฌ์ธํŠธ ์‚ฌ์šฉ

๋ ˆ์ด๋ธ” ์ปค์Šคํ„ฐ๋งˆ์ด์ง•

ํ‘œ์‹œ๋˜๋Š” ๋ ˆ์ด๋ธ”์„ ์›ํ•˜๋Š” ํ˜•์‹์œผ๋กœ ๋ณ€๊ฒฝํ•ฉ๋‹ˆ๋‹ค.
<IdAsyncSelect
  entity="User"
  value={userId}
  onChange={setUserId}
  searchField="username"
  // ๋ ˆ์ด๋ธ” ํฌ๋งท: "์ด๋ฆ„ (์ด๋ฉ”์ผ)"
  getLabel={(user) => `${user.username} (${user.email})`}
  
  // ๋˜๋Š” ์ปดํฌ๋„ŒํŠธ๋กœ ๋ Œ๋”๋ง
  renderOption={(user) => (
    <div className="user-option">
      <img src={user.avatar} alt="" />
      <div>
        <div>{user.username}</div>
        <div className="email">{user.email}</div>
      </div>
    </div>
  )}
/>

์กฐ๊ฑด๋ถ€ ์˜ต์…˜ ํ•„ํ„ฐ๋ง

ํŠน์ • ์กฐ๊ฑด์— ๋งž๋Š” ํ•ญ๋ชฉ๋งŒ ์„ ํƒ ๊ฐ€๋Šฅํ•˜๊ฒŒ ํ•ฉ๋‹ˆ๋‹ค.
<IdAsyncSelect
  entity="User"
  value={userId}
  onChange={setUserId}
  searchField="username"
  // ์กฐ๊ฑด: ์—ญํ• ์ด 'editor' ๋˜๋Š” 'admin'์ธ ์‚ฌ์šฉ์ž๋งŒ
  filterOption={(user) => 
    user.role === "editor" || user.role === "admin"
  }
  // ๋น„ํ™œ์„ฑํ™” ์กฐ๊ฑด
  isOptionDisabled={(user) => !user.isActive}
/>

๋””๋ฐ”์šด์Šค ์„ค์ •

๊ฒ€์ƒ‰ API ํ˜ธ์ถœ ๋นˆ๋„๋ฅผ ์กฐ์ ˆํ•ฉ๋‹ˆ๋‹ค.
<IdAsyncSelect
  entity="User"
  value={userId}
  onChange={setUserId}
  searchField="username"
  // 500ms ๋””๋ฐ”์šด์Šค (๊ธฐ๋ณธ๊ฐ’: 300ms)
  debounceMs={500}
  // ์ตœ์†Œ ์ž…๋ ฅ ๊ธ€์ž ์ˆ˜ (๊ธฐ๋ณธ๊ฐ’: 1)
  minSearchLength={2}
/>
๋””๋ฐ”์šด์Šค ์„ค๋ช…:
  • ์‚ฌ์šฉ์ž๊ฐ€ ํƒ€์ดํ•‘์„ ๋ฉˆ์ถ˜ ํ›„ ์ง€์ •๋œ ์‹œ๊ฐ„์ด ์ง€๋‚˜๋ฉด ๊ฒ€์ƒ‰ ์‹คํ–‰
  • API ํ˜ธ์ถœ ํšŸ์ˆ˜๋ฅผ ์ค„์—ฌ ์„œ๋ฒ„ ๋ถ€ํ•˜ ๊ฐ์†Œ
  • ์‚ฌ์šฉ์ž ๊ฒฝํ—˜๋„ ํ–ฅ์ƒ (๋ถˆํ•„์š”ํ•œ ๋กœ๋”ฉ ๊ฐ์†Œ)

ํƒ€์ž… ์•ˆ์ „์„ฑ

์ž๋™ ํƒ€์ž… ์ถ”๋ก 

Entity ์ด๋ฆ„์„ ์ง€์ •ํ•˜๋ฉด ํƒ€์ž…์ด ์ž๋™์œผ๋กœ ์ถ”๋ก ๋ฉ๋‹ˆ๋‹ค.
// entity="User"๋ฅผ ์ง€์ •ํ•˜๋ฉด
<IdAsyncSelect
  entity="User"
  value={userId}  // number | null
  onChange={(id) => {
    // id์˜ ํƒ€์ž…์ด number | null๋กœ ์ž๋™ ์ถ”๋ก 
  }}
  onSearch={async (query) => {
    // ๋ฐ˜ํ™˜ ํƒ€์ž…์ด User[]์—ฌ์•ผ ํ•จ
    return users;
  }}
  getLabel={(user) => {
    // user์˜ ํƒ€์ž…์ด User๋กœ ์ž๋™ ์ถ”๋ก 
    return user.username;
  }}
/>
ํƒ€์ž… ์ฒด์ธ:
Entity Definition 
  โ†’ Type Generation 
  โ†’ IdAsyncSelect Props 
  โ†’ Type-safe Callbacks

์ œ๋„ค๋ฆญ ์‚ฌ์šฉ

๋ช…์‹œ์ ์œผ๋กœ ํƒ€์ž…์„ ์ง€์ •ํ•  ์ˆ˜๋„ ์žˆ์Šต๋‹ˆ๋‹ค.
import type { User } from "@/types/user.types";

<IdAsyncSelect<User>
  entity="User"
  value={userId}
  onChange={setUserId}
  searchField="username"
/>

์‹ค์ „ ์˜ˆ์ œ

๊ฒŒ์‹œ๊ธ€ ์ž‘์„ฑ ํผ

import { useState } from "react";
import { IdAsyncSelect } from "@/components/common/IdAsyncSelect";
import { postService } from "@/services/post.service";

function CreatePostForm() {
  const [title, setTitle] = useState("");
  const [content, setContent] = useState("");
  const [authorId, setAuthorId] = useState<number | null>(null);
  const [categoryId, setCategoryId] = useState<number | null>(null);
  const [tagIds, setTagIds] = useState<number[]>([]);
  
  async function handleSubmit() {
    if (!authorId || !categoryId) {
      alert("Please select author and category");
      return;
    }
    
    await postService.create({
      title,
      content,
      author_id: authorId,
      category_id: categoryId,
      tag_ids: tagIds,
    });
  }
  
  return (
    <form onSubmit={handleSubmit}>
      <div>
        <label>Title</label>
        <input
          value={title}
          onChange={(e) => setTitle(e.target.value)}
        />
      </div>
      
      <div>
        <label>Content</label>
        <textarea
          value={content}
          onChange={(e) => setContent(e.target.value)}
        />
      </div>
      
      {/* ์ž‘์„ฑ์ž ์„ ํƒ (๋‹จ์ผ) */}
      <div>
        <label>Author *</label>
        <IdAsyncSelect
          entity="User"
          value={authorId}
          onChange={setAuthorId}
          searchField="username"
          placeholder="Select author..."
        />
      </div>
      
      {/* ์นดํ…Œ๊ณ ๋ฆฌ ์„ ํƒ (๋‹จ์ผ) */}
      <div>
        <label>Category *</label>
        <IdAsyncSelect
          entity="Category"
          value={categoryId}
          onChange={setCategoryId}
          searchField="name"
          placeholder="Select category..."
        />
      </div>
      
      {/* ํƒœ๊ทธ ์„ ํƒ (๋‹ค์ค‘) */}
      <div>
        <label>Tags</label>
        <IdAsyncSelect
          entity="Tag"
          value={tagIds}
          onChange={setTagIds}
          searchField="name"
          multiple
          placeholder="Select tags..."
        />
      </div>
      
      <button type="submit">Create Post</button>
    </form>
  );
}

๊ฒŒ์‹œ๊ธ€ ์ˆ˜์ • ํผ

function EditPostForm({ postId }: { postId: number }) {
  const { post, isLoading } = usePost(postId);
  const [authorId, setAuthorId] = useState<number | null>(null);
  
  // post ๋กœ๋“œ ์‹œ ์ดˆ๊ธฐ๊ฐ’ ์„ค์ •
  useEffect(() => {
    if (post) {
      setAuthorId(post.author_id);
    }
  }, [post]);
  
  if (isLoading) return <div>Loading...</div>;
  
  return (
    <form>
      {/* ์ดˆ๊ธฐ๊ฐ’์ด ์žˆ๋Š” ๊ฒฝ์šฐ */}
      <IdAsyncSelect
        entity="User"
        value={authorId}
        onChange={setAuthorId}
        searchField="username"
      />
    </form>
  );
}

์กฐ๊ฑด๋ถ€ ํ•„ํ„ฐ๋ง

function AssignTaskForm() {
  const [assigneeId, setAssigneeId] = useState<number | null>(null);
  
  return (
    <div>
      <label>Assign To</label>
      <IdAsyncSelect
        entity="User"
        value={assigneeId}
        onChange={setAssigneeId}
        searchField="username"
        // ํ™œ์„ฑ ์‚ฌ์šฉ์ž๋งŒ ์„ ํƒ ๊ฐ€๋Šฅ
        filterOption={(user) => user.isActive}
        // ์—ญํ• ์ด 'developer'์ธ ์‚ฌ์šฉ์ž๋งŒ
        onSearch={async (query) => {
          const { users } = await userService.search({
            query,
            role: "developer",
            isActive: true,
          });
          return users;
        }}
        getLabel={(user) => `${user.username} (${user.role})`}
      />
    </div>
  );
}

๊ณ„์ธต์  ์„ ํƒ

๋ถ€๋ชจ๋ฅผ ์„ ํƒํ•œ ํ›„ ์ž์‹์„ ์„ ํƒํ•˜๋Š” ํŒจํ„ด์ž…๋‹ˆ๋‹ค.
function ProductForm() {
  const [categoryId, setCategoryId] = useState<number | null>(null);
  const [subcategoryId, setSubcategoryId] = useState<number | null>(null);
  
  return (
    <div>
      {/* 1๋‹จ๊ณ„: ์นดํ…Œ๊ณ ๋ฆฌ ์„ ํƒ */}
      <div>
        <label>Category</label>
        <IdAsyncSelect
          entity="Category"
          value={categoryId}
          onChange={(id) => {
            setCategoryId(id);
            setSubcategoryId(null); // ๋ถ€๋ชจ ๋ณ€๊ฒฝ ์‹œ ์ž์‹ ์ดˆ๊ธฐํ™”
          }}
          searchField="name"
        />
      </div>
      
      {/* 2๋‹จ๊ณ„: ํ•˜์œ„ ์นดํ…Œ๊ณ ๋ฆฌ ์„ ํƒ (์นดํ…Œ๊ณ ๋ฆฌ ์„ ํƒ ํ›„์—๋งŒ) */}
      {categoryId && (
        <div>
          <label>Subcategory</label>
          <IdAsyncSelect
            entity="Subcategory"
            value={subcategoryId}
            onChange={setSubcategoryId}
            searchField="name"
            // ๋ถ€๋ชจ ์นดํ…Œ๊ณ ๋ฆฌ์— ์†ํ•œ ๊ฒƒ๋งŒ
            onSearch={async (query) => {
              const { subcategories } = await subcategoryService.search({
                query,
                category_id: categoryId,
              });
              return subcategories;
            }}
          />
        </div>
      )}
    </div>
  );
}

๋‚ด๋ถ€ ๊ตฌํ˜„

Service ํ†ตํ•ฉ

๋‚ด๋ถ€์ ์œผ๋กœ ์ƒ์„ฑ๋œ Service๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.
// components/common/IdAsyncSelect.tsx (๋‚ด๋ถ€ ๊ตฌํ˜„)
import { useQuery } from "@tanstack/react-query";

interface IdAsyncSelectProps<T> {
  entity: string;
  value: number | number[] | null;
  onChange: (value: number | number[] | null) => void;
  searchField: keyof T;
  multiple?: boolean;
}

export function IdAsyncSelect<T extends { id: number }>({
  entity,
  value,
  onChange,
  searchField,
  multiple = false,
}: IdAsyncSelectProps<T>) {
  const [inputValue, setInputValue] = useState("");
  const [debouncedInput] = useDebounce(inputValue, 300);
  
  // Service ๋™์  import
  const service = getServiceByEntity(entity); // userService, postService ๋“ฑ
  
  // ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ ๋กœ๋“œ
  const { data: options } = useQuery({
    queryKey: [entity, "search", debouncedInput],
    queryFn: () => service.search({
      query: debouncedInput,
      searchField,
    }),
    enabled: !!debouncedInput,
  });
  
  // ์ดˆ๊ธฐ๊ฐ’ ๋กœ๋“œ (value๊ฐ€ ์žˆ์ง€๋งŒ options์— ์—†๋Š” ๊ฒฝ์šฐ)
  const { data: initialValues } = useQuery({
    queryKey: [entity, value as number],
    queryFn: () => service.get(value as number),
    enabled: !!value && !multiple,
  });
  
  // ์„ ํƒ๋œ ํ•ญ๋ชฉ ํ‘œ์‹œ
  const selectedOptions = multiple
    ? options?.filter((opt: any) => (value as number[]).includes(opt.id))
    : initialValues || options?.find((opt: any) => opt.id === value);
  
  return (
    <Select
      options={options}
      value={selectedOptions}
      onChange={(selected) => {
        if (multiple) {
          onChange(selected.map((s: any) => s.id));
        } else {
          onChange(selected?.id || null);
        }
      }}
      onInputChange={setInputValue}
      isMulti={multiple}
    />
  );
}

์บ์‹ฑ ์ „๋žต

TanStack Query๋ฅผ ํ™œ์šฉํ•˜์—ฌ ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๋ฅผ ์บ์‹ฑํ•ฉ๋‹ˆ๋‹ค.
// ๊ฐ™์€ ๊ฒ€์ƒ‰์–ด๋Š” ์บ์‹œ์—์„œ ์ฆ‰์‹œ ๋ฐ˜ํ™˜
const { data } = useQuery({
  queryKey: [entity, "search", debouncedInput],
  queryFn: () => service.search({ query: debouncedInput }),
  staleTime: 5000, // 5์ดˆ๊ฐ„ fresh ์ƒํƒœ ์œ ์ง€
  refetchOnWindowFocus: false, // ํฌ์ปค์Šค ์‹œ ์žฌ๊ฒ€์ฆ ์•ˆํ•จ
});

์„ฑ๋Šฅ ์ตœ์ ํ™”

1. ๋””๋ฐ”์šด์Šค๋กœ API ํ˜ธ์ถœ ์ตœ์†Œํ™”

const [inputValue, setInputValue] = useState("");
const [debouncedInput] = useDebounce(inputValue, 300);

// debouncedInput์ด ๋ณ€๊ฒฝ๋  ๋•Œ๋งŒ API ํ˜ธ์ถœ
useQuery({
  queryKey: ["search", debouncedInput],
  queryFn: fetcher,
  enabled: !!debouncedInput,
});

2. ๊ฐ€์ƒ ์Šคํฌ๋กค๋ง

์˜ต์…˜์ด ๋งŽ์„ ๋•Œ ๊ฐ€์ƒ ์Šคํฌ๋กค๋ง์œผ๋กœ ์„ฑ๋Šฅ ํ–ฅ์ƒ:
<IdAsyncSelect
  entity="User"
  value={userId}
  onChange={setUserId}
  searchField="username"
  virtualScroll // 1000๊ฐœ ์ด์ƒ์ผ ๋•Œ ์ž๋™ ํ™œ์„ฑํ™”
  virtualiRowHeight={40}
/>

3. ์ดˆ๊ธฐ ๋กœ๋”ฉ ์ตœ์†Œํ™”

<IdAsyncSelect
  entity="User"
  value={userId}
  onChange={setUserId}
  searchField="username"
  // ์ฒ˜์Œ์—๋Š” ๋กœ๋“œํ•˜์ง€ ์•Š์Œ
  loadOnMount={false}
  // ์ตœ์†Œ ๊ฒ€์ƒ‰ ๊ธธ์ด
  minSearchLength={2}
/>

์ฃผ์˜์‚ฌํ•ญ

ID Async Select ์‚ฌ์šฉ ์‹œ ์ฃผ์˜์‚ฌํ•ญ:
  1. searchField๋Š” Entity์˜ ์‹ค์ œ ํ•„๋“œ๋ช…๊ณผ ์ผ์น˜ํ•ด์•ผ ํ•จ
  2. API์— ๊ฒ€์ƒ‰ ์—”๋“œํฌ์ธํŠธ ํ•„์ˆ˜ (search ๋ฉ”์„œ๋“œ)
  3. ๋‹ค์ค‘ ์„ ํƒ ์‹œ ๋ฐฐ์—ด ํƒ€์ž… ํ™•์ธ
  4. ๋””๋ฐ”์šด์Šค ์‹œ๊ฐ„์€ API ์‘๋‹ต ์†๋„์— ๋งž์ถฐ ์กฐ์ •
  5. ๋งŽ์€ ๋ฐ์ดํ„ฐ๋Š” ํŽ˜์ด์ง€๋„ค์ด์…˜ ๊ณ ๋ ค
  6. ์ดˆ๊ธฐ๊ฐ’ ๋กœ๋”ฉ ์‹คํŒจ ์‹œ ์—๋Ÿฌ ์ฒ˜๋ฆฌ ํ•„์ˆ˜

๋‹ค์Œ ๋‹จ๊ณ„