๋ฉ”์ธ ์ฝ˜ํ…์ธ ๋กœ ๊ฑด๋„ˆ๋›ฐ๊ธฐ
useListParams๋Š” ๋ชฉ๋ก ํŽ˜์ด์ง€์˜ ํ•„ํ„ฐ๋ง, ์ •๋ ฌ, ํŽ˜์ด์ง€๋„ค์ด์…˜ ์ƒํƒœ๋ฅผ URL ๊ฒ€์ƒ‰ ํŒŒ๋ผ๋ฏธํ„ฐ์™€ ๋™๊ธฐํ™”ํ•˜๋Š” ํ›…์ž…๋‹ˆ๋‹ค. ๋ถ๋งˆํฌ ๊ฐ€๋Šฅํ•œ ์ƒํƒœ ๊ด€๋ฆฌ์™€ ํƒ€์ž… ์•ˆ์ „ํ•œ ํผ ๋ฐ”์ธ๋”ฉ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.

ํ•ต์‹ฌ ๊ธฐ๋Šฅ

URL ๋™๊ธฐํ™”

๊ฒ€์ƒ‰ ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ URL์— ์ €์žฅ๋ถ๋งˆํฌ ๊ฐ€๋Šฅํ•œ ์ƒํƒœ

ํƒ€์ž… ์•ˆ์ „

Zod ์Šคํ‚ค๋งˆ ๊ธฐ๋ฐ˜์ž๋™ ํƒ€์ž… ์ถ”๋ก 

์ž๋™ ํŽ˜์ด์ง€ ๋ฆฌ์…‹

ํ•„ํ„ฐ ๋ณ€๊ฒฝ ์‹œ 1ํŽ˜์ด์ง€๋กœ์‚ฌ์šฉ์ž ๊ฒฝํ—˜ ๊ฐœ์„ 

register ํŒจํ„ด

ํผ ์ปดํฌ๋„ŒํŠธ ๊ฐ„ํŽธ ์—ฐ๊ฒฐ๋ณด์ผ๋Ÿฌํ”Œ๋ ˆ์ดํŠธ ์ œ๊ฑฐ

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

Import ๋ฐ ์ดˆ๊ธฐํ™”

import { useListParams } from "@sonamu-kit/react-components";
import { UserService } from "@/services/services.generated";
import { UserListParams } from "@/services/sonamu.generated";

export function UserListPage() {
  // URL์—์„œ ํŒŒ๋ผ๋ฏธํ„ฐ ํŒŒ์‹ฑ ๋ฐ ์ดˆ๊ธฐํ™”
  const { listParams, setListParams, register } = useListParams(
    UserListParams,
    {
      num: 24,
      page: 1,
      orderBy: "id-desc" as const,
      search: "id" as const,
      keyword: "",
    }
  );

  // API ํ˜ธ์ถœ (listParams๋ฅผ ๊ทธ๋Œ€๋กœ ์ „๋‹ฌ)
  const { data, isLoading } = UserService.useUsers("A", listParams);

  // ...
}
ํŒŒ๋ผ๋ฏธํ„ฐ ์„ค๋ช…:
  • UserListParams: Zod ์Šคํ‚ค๋งˆ (์ž๋™ ์ƒ์„ฑ๋จ)
  • ๊ธฐ๋ณธ๊ฐ’ ๊ฐ์ฒด: URL์— ๊ฐ’์ด ์—†์„ ๋•Œ ์‚ฌ์šฉํ•  ์ดˆ๊ธฐ๊ฐ’
์™œ Zod ์Šคํ‚ค๋งˆ๊ฐ€ ํ•„์š”ํ•œ๊ฐ€์š”?URL ๊ฒ€์ƒ‰ ํŒŒ๋ผ๋ฏธํ„ฐ๋Š” ๋ฌธ์ž์—ด์ด๋ฏ€๋กœ, ์ˆซ์ž๋‚˜ boolean์œผ๋กœ ํŒŒ์‹ฑํ•˜๋ ค๋ฉด ์Šคํ‚ค๋งˆ๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค. UserListParams๋Š” sonamu.generated.ts์— ์ž๋™ ์ƒ์„ฑ๋˜๋ฉฐ, ๋ฐฑ์—”๋“œ์˜ ํƒ€์ž… ์ •์˜์™€ ํ•ญ์ƒ ๋™๊ธฐํ™”๋ฉ๋‹ˆ๋‹ค.

listParams ์‚ฌ์šฉ

ํ˜„์žฌ ํ•„ํ„ฐ๋ง ์ƒํƒœ๋ฅผ ๋‹ด๊ณ  ์žˆ๋Š” ๊ฐ์ฒด์ž…๋‹ˆ๋‹ค.
const { listParams } = useListParams(UserListParams, defaultValue);

// API ํ˜ธ์ถœ ์‹œ ๊ทธ๋Œ€๋กœ ์ „๋‹ฌ
const { data } = UserService.useUsers("A", listParams);

// ๋˜๋Š” ํŠน์ • ๊ฐ’ ํ™•์ธ
console.log(listParams.page);     // 1
console.log(listParams.keyword);  // "admin"
console.log(listParams.orderBy);  // "id-desc"
ํƒ€์ž…:
listParams: Partial<z.infer<UserListParams>> & DefaultValue

setListParams๋กœ ์ƒํƒœ ๋ณ€๊ฒฝ

ํ•„ํ„ฐ ์ƒํƒœ๋ฅผ ๋ณ€๊ฒฝํ•˜๊ณ  URL์„ ์—…๋ฐ์ดํŠธํ•ฉ๋‹ˆ๋‹ค.
const { listParams, setListParams } = useListParams(UserListParams, defaultValue);

// ํŽ˜์ด์ง€ ๋ณ€๊ฒฝ
setListParams({ ...listParams, page: 2 });

// ์ •๋ ฌ ๋ณ€๊ฒฝ
setListParams({ ...listParams, page: 1, orderBy: "created_at-desc" });

// ๊ฒ€์ƒ‰ ํ‚ค์›Œ๋“œ ๋ณ€๊ฒฝ
setListParams({
  ...listParams,
  page: 1,  // ๊ฒ€์ƒ‰ ์‹œ ํ•ญ์ƒ 1ํŽ˜์ด์ง€๋กœ
  keyword: "admin"
});
๋™์ž‘:
  1. listParams์™€ newParams๋ฅผ deep equal ๋น„๊ต
  2. ๋ณ€๊ฒฝ์ด ์žˆ์œผ๋ฉด TanStack Router์˜ navigate๋กœ URL ์—…๋ฐ์ดํŠธ
  3. URL ๋ณ€๊ฒฝ โ†’ useSearch ํ›… ์žฌ์‹คํ–‰ โ†’ listParams ์ž๋™ ๊ฐฑ์‹ 
์ฃผ์˜: ํ•ญ์ƒ spread operator ์‚ฌ์šฉ
// โŒ ์ž˜๋ชป๋œ ์‚ฌ์šฉ (๊ธฐ์กด ๊ฐ’ ์†์‹ค)
setListParams({ page: 2 });

// โœ… ์˜ฌ๋ฐ”๋ฅธ ์‚ฌ์šฉ
setListParams({ ...listParams, page: 2 });
๊ธฐ์กด ํ•„ํ„ฐ ๊ฐ’์„ ์œ ์ง€ํ•˜๋ ค๋ฉด ๋ฐ˜๋“œ์‹œ spread operator๋ฅผ ์‚ฌ์šฉํ•˜์„ธ์š”.

register๋กœ ํผ ๋ฐ”์ธ๋”ฉ

register ํ•จ์ˆ˜๋Š” ํผ ์ปดํฌ๋„ŒํŠธ์— value์™€ onValueChange๋ฅผ ์ž๋™์œผ๋กœ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.
import { Input, Select } from "@sonamu-kit/react-components/components";

const { register } = useListParams(UserListParams, defaultValue);

return (
  <div>
    {/* ๊ฒ€์ƒ‰ ํ‚ค์›Œ๋“œ ์ž…๋ ฅ */}
    <Input {...register("keyword")} placeholder="๊ฒ€์ƒ‰์–ด ์ž…๋ ฅ" />

    {/* ์ •๋ ฌ ์„ ํƒ */}
    <UserOrderBySelect {...register("orderBy")} />

    {/* ํŽ˜์ด์ง€ ์„ ํƒ */}
    <Pagination {...register("page")} total={data?.total ?? 0} />
  </div>
);
register๊ฐ€ ๋ฐ˜ํ™˜ํ•˜๋Š” ๊ฐ’:
{
  value: listParams[name] ?? defaultValue[name] ?? (name === "page" ? 1 : ""),
  onValueChange: (value) => {
    if (name === "page") {
      // ํŽ˜์ด์ง€ ๋ณ€๊ฒฝ์€ ๋‹ค๋ฅธ ํ•„ํ„ฐ ์œ ์ง€
      setListParams({ ...listParams, page: value });
    } else {
      // ๋‹ค๋ฅธ ํ•„ํ„ฐ ๋ณ€๊ฒฝ์€ ํŽ˜์ด์ง€๋ฅผ 1๋กœ ๋ฆฌ์…‹
      setListParams({
        ...listParams,
        page: 1,
        [name]: value === "" ? undefined : value
      });
    }
  }
}
์ž๋™ ํŽ˜์ด์ง€ ๋ฆฌ์…‹์˜ ์ด์œ : ์‚ฌ์šฉ์ž๊ฐ€ 3ํŽ˜์ด์ง€์—์„œ ๊ฒ€์ƒ‰์–ด๋ฅผ ๋ฐ”๊พธ๋ฉด ๊ฒฐ๊ณผ๊ฐ€ ์ ์–ด์„œ 3ํŽ˜์ด์ง€๊ฐ€ ์กด์žฌํ•˜์ง€ ์•Š์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋”ฐ๋ผ์„œ page๋ฅผ ์ œ์™ธํ•œ ๋ชจ๋“  ํ•„ํ„ฐ ๋ณ€๊ฒฝ ์‹œ ์ž๋™์œผ๋กœ 1ํŽ˜์ด์ง€๋กœ ๋Œ์•„๊ฐ‘๋‹ˆ๋‹ค.

์‹ค์ „ ์˜ˆ์ œ

์™„์ „ํ•œ ๋ชฉ๋ก ํŽ˜์ด์ง€

import { useListParams } from "@sonamu-kit/react-components";
import { Input, Pagination } from "@sonamu-kit/react-components/components";
import { UserService } from "@/services/services.generated";
import { UserListParams } from "@/services/sonamu.generated";
import { UserOrderBySelect } from "@/components/user/UserOrderBySelect";
import { UserSearchFieldSelect } from "@/components/user/UserSearchFieldSelect";

export function UserListPage() {
  const { listParams, register } = useListParams(
    UserListParams,
    {
      num: 24,
      page: 1,
      orderBy: "id-desc" as const,
      search: "id" as const,
      keyword: "",
    }
  );

  const { data, isLoading } = UserService.useUsers("A", listParams);

  if (isLoading) return <div>๋กœ๋”ฉ ์ค‘...</div>;

  return (
    <div>
      {/* ํ•„ํ„ฐ ์„น์…˜ */}
      <div className="flex gap-2 mb-4">
        <UserSearchFieldSelect {...register("search")} />
        <Input {...register("keyword")} placeholder="๊ฒ€์ƒ‰์–ด ์ž…๋ ฅ" />
        <UserOrderBySelect {...register("orderBy")} />
      </div>

      {/* ํ…Œ์ด๋ธ” */}
      <table>
        <thead>
          <tr>
            <th>ID</th>
            <th>์ด๋ฆ„</th>
            <th>์ด๋ฉ”์ผ</th>
          </tr>
        </thead>
        <tbody>
          {data?.rows.map((user) => (
            <tr key={user.id}>
              <td>{user.id}</td>
              <td>{user.username}</td>
              <td>{user.email}</td>
            </tr>
          ))}
        </tbody>
      </table>

      {/* ํŽ˜์ด์ง€๋„ค์ด์…˜ */}
      <Pagination {...register("page")} total={data?.total ?? 0} />
    </div>
  );
}

์ปค์Šคํ…€ ํ•„ํ„ฐ ์ถ”๊ฐ€

๋ฐฑ์—”๋“œ์—์„œ ์ปค์Šคํ…€ ํ•„ํ„ฐ๋ฅผ ์ถ”๊ฐ€ํ•œ ๊ฒฝ์šฐ:
// ๋ฐฑ์—”๋“œ: user.types.ts
export const UserListParams = UserBaseListParams.extend({
  role: z.enum(["admin", "normal"]).optional(),
  isVerified: z.boolean().optional(),
});
ํ”„๋ก ํŠธ์—”๋“œ์—์„œ ์‚ฌ์šฉ:
import { UserRoleSelect } from "@/components/user/UserRoleSelect";
import { Switch } from "@sonamu-kit/react-components/components";

const { register } = useListParams(UserListParams, {
  num: 24,
  page: 1,
  role: undefined,
  isVerified: undefined,
});

return (
  <div>
    <UserRoleSelect {...register("role")} clearable />

    <div className="flex items-center gap-2">
      <label>์ธ์ฆ๋œ ์‚ฌ์šฉ์ž๋งŒ</label>
      <Switch {...register("isVerified")} />
    </div>
  </div>
);

URL ๊ณต์œ  ๋ฐ ๋ถ๋งˆํฌ

useListParams์˜ ๊ฐ€์žฅ ํฐ ์žฅ์ ์€ ํ•„ํ„ฐ ์ƒํƒœ๊ฐ€ URL์— ์ €์žฅ๋œ๋‹ค๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค.
# ์‚ฌ์šฉ์ž๊ฐ€ ํ•„ํ„ฐ๋ฅผ ์„ค์ •ํ•œ ํ›„ URL
https://example.com/users?page=2&keyword=admin&orderBy=created_at-desc&role=admin

# ์ด URL์„ ๋ณต์‚ฌํ•ด์„œ ๊ณต์œ ํ•˜๊ฑฐ๋‚˜ ๋ถ๋งˆํฌํ•˜๋ฉด
# ๋‚˜์ค‘์— ์ ‘์†ํ•  ๋•Œ ๋™์ผํ•œ ํ•„ํ„ฐ๊ฐ€ ์ ์šฉ๋จ
๋ถ๋งˆํฌ ๊ฐ€๋Šฅํ•œ ์ƒํƒœ์˜ ์ด์ :
  • ์‚ฌ์šฉ์ž๊ฐ€ ์ž์ฃผ ์‚ฌ์šฉํ•˜๋Š” ํ•„ํ„ฐ ์กฐํ•ฉ์„ ๋ถ๋งˆํฌ
  • URL์„ ๊ณต์œ ํ•˜์—ฌ ๋™๋ฃŒ์—๊ฒŒ ๋™์ผํ•œ ๋ทฐ ์ „๋‹ฌ
  • ๋ธŒ๋ผ์šฐ์ € ๋’ค๋กœ๊ฐ€๊ธฐ/์•ž์œผ๋กœ๊ฐ€๊ธฐ๋กœ ํ•„ํ„ฐ ํžˆ์Šคํ† ๋ฆฌ ํƒ์ƒ‰

์˜ต์…˜

disableSearchParams

URL ๋™๊ธฐํ™”๋ฅผ ๋น„ํ™œ์„ฑํ™”ํ•˜๊ณ  ๋กœ์ปฌ ์ƒํƒœ๋กœ๋งŒ ๊ด€๋ฆฌํ•ฉ๋‹ˆ๋‹ค.
const { listParams, setListParams, register } = useListParams(
  UserListParams,
  defaultValue,
  { disableSearchParams: true }
);
์‚ฌ์šฉ ์ผ€์ด์Šค:
  • ๋ชจ๋‹ฌ ์•ˆ์˜ ๋ชฉ๋ก (URL์„ ๋ณ€๊ฒฝํ•˜๊ณ  ์‹ถ์ง€ ์•Š์„ ๋•Œ)
  • ์ž„๋ฒ ๋””๋“œ ์œ„์ ฏ (๋…๋ฆฝ์ ์ธ ์ƒํƒœ ๊ด€๋ฆฌ๊ฐ€ ํ•„์š”ํ•  ๋•Œ)
  • ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ
disableSearchParams: true๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ๋ถ๋งˆํฌ์™€ URL ๊ณต์œ  ๊ธฐ๋Šฅ์ด ๋™์ž‘ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

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

์ปดํŒŒ์ผ ํƒ€์ž„ ๊ฒ€์ฆ

const { register } = useListParams(UserListParams, defaultValue);

// โœ… OK: UserListParams์— ์ •์˜๋œ ํ•„๋“œ
register("keyword");
register("orderBy");
register("page");

// โŒ ์ปดํŒŒ์ผ ์—๋Ÿฌ: ์กด์žฌํ•˜์ง€ ์•Š๋Š” ํ•„๋“œ
register("invalidField");

์ž๋™ ์™„์„ฑ

IDE์—์„œ register(" ์ž…๋ ฅ ์‹œ ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ๋ชจ๋“  ํ•„๋“œ๊ฐ€ ์ž๋™ ์™„์„ฑ๋ฉ๋‹ˆ๋‹ค.
register("
        โ†“
        keyword
        orderBy
        page
        search
        num
        role          // ์ปค์Šคํ…€ ํ•„๋“œ
        isVerified    // ์ปค์Šคํ…€ ํ•„๋“œ

TanStack Router ํ†ตํ•ฉ

useListParams๋Š” ๋‚ด๋ถ€์ ์œผ๋กœ TanStack Router์˜ ํ›…์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค:
  • useSearch: URL ๊ฒ€์ƒ‰ ํŒŒ๋ผ๋ฏธํ„ฐ ์ฝ๊ธฐ
  • useNavigate: URL ์—…๋ฐ์ดํŠธ
// useListParams ๋‚ด๋ถ€ ๊ตฌ์กฐ (๋‹จ์ˆœํ™”)
export function useListParams<U, T>(zType: U, defaultValue: T) {
  const search = useSearch({ strict: false });
  const navigate = useNavigate();

  const listParams = zType.safeParse(search).success
    ? { ...defaultValue, ...zType.parse(search) }
    : defaultValue;

  const setListParams = (newParams: T) => {
    navigate({ search: newParams });
  };

  // ...
}
strict: false์˜ ์˜๋ฏธuseSearch({ strict: false })๋Š” ํ˜„์žฌ ๋ผ์šฐํŠธ์— ์ •์˜๋˜์ง€ ์•Š์€ ๊ฒ€์ƒ‰ ํŒŒ๋ผ๋ฏธํ„ฐ๋„ ์ฝ์„ ์ˆ˜ ์žˆ๊ฒŒ ํ•ฉ๋‹ˆ๋‹ค. ์ด๋ฅผ ํ†ตํ•ด ๋™์ ์œผ๋กœ ์ถ”๊ฐ€๋˜๋Š” ํ•„ํ„ฐ๋ฅผ ์œ ์—ฐํ•˜๊ฒŒ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

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

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

// โŒ ์ž˜๋ชป๋œ ์ดˆ๊ธฐ๊ฐ’ (num, page ๋ˆ„๋ฝ)
const { listParams } = useListParams(UserListParams, {});

// โœ… ์˜ฌ๋ฐ”๋ฅธ ์ดˆ๊ธฐ๊ฐ’ (ํ•„์ˆ˜ ํ•„๋“œ ํฌํ•จ)
const { listParams } = useListParams(UserListParams, {
  num: 24,
  page: 1,
  orderBy: "id-desc" as const,
  search: "id" as const,
});
ํ•„์ˆ˜ ํ•„๋“œ:
  • num: ํŽ˜์ด์ง€๋‹น ํ•ญ๋ชฉ ์ˆ˜
  • page: ํ˜„์žฌ ํŽ˜์ด์ง€ (1๋ถ€ํ„ฐ ์‹œ์ž‘)

2. Enum ํ•„๋“œ์˜ ํƒ€์ž… ๋‹จ์–ธ

// โŒ ์ปดํŒŒ์ผ ์—๋Ÿฌ: string์„ enum์— ํ• ๋‹น ๋ถˆ๊ฐ€
const defaultValue = {
  orderBy: "id-desc",
};

// โœ… OK: as const๋กœ literal ํƒ€์ž… ๋งŒ๋“ค๊ธฐ
const defaultValue = {
  orderBy: "id-desc" as const,
};

3. register์™€ ์ปค์Šคํ…€ onChange ํ•จ๊ป˜ ์‚ฌ์šฉ

// โŒ register์˜ onValueChange๊ฐ€ ๋ฌด์‹œ๋จ
<Input
  {...register("keyword")}
  onChange={(e) => console.log(e.target.value)}
/>

// โœ… OK: register์˜ onValueChange๋ฅผ ์ง์ ‘ ํ˜ธ์ถœ
<Input
  {...register("keyword")}
  onValueChange={(value) => {
    console.log(value);
    register("keyword").onValueChange(value);
  }}
/>

// ๋˜๋Š” ๋” ๋‚˜์€ ๋ฐฉ๋ฒ•: ๋ณ„๋„ ์ด๋ฒคํŠธ ํ•ธ๋“ค๋Ÿฌ
const handleKeywordChange = (value: string) => {
  console.log(value);
  setListParams({ ...listParams, page: 1, keyword: value });
};

๊ด€๋ จ ๋ฌธ์„œ