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"
});
๋์:
listParams์ newParams๋ฅผ deep equal ๋น๊ต
- ๋ณ๊ฒฝ์ด ์์ผ๋ฉด TanStack Router์
navigate๋ก URL ์
๋ฐ์ดํธ
- 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 });
};
๊ด๋ จ ๋ฌธ์