
Security News
Vite Releases Technical Preview of Rolldown-Vite, a Rust-Based Bundler
Vite releases Rolldown-Vite, a Rust-based bundler preview offering faster builds and lower memory usage as a drop-in replacement for Vite.
x-view-model
Advanced tools
A lightweight, type-safe MVVM state management solution for React applications. Features reactive updates, computed properties, and deep path selection with minimal bundle size.
x-view-model์ React ์ ํ๋ฆฌ์ผ์ด์ ์ ๊ฐ๋จํ๋ฉด์๋ ๊ฐ๋ ฅํ ์ํ ๊ด๋ฆฌ ์๋ฃจ์ ์ ์ ๊ณตํ๊ธฐ ์ํด ์ค๊ณ๋์์ต๋๋ค. MVVM ํจํด์ ๋ชจ๋ฒ ์ฌ๋ก๋ฅผ ํ๋์ ์ธ React ๊ธฐ๋ฅ๊ณผ ๊ฒฐํฉํฉ๋๋ค:
TypeScript ์ง์์ ์ฌํ ๊ณ ๋ ค๋ก ์ถ๊ฐํ๋ ๋ค๋ฅธ ์ํ ๊ด๋ฆฌ ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ ๋ฌ๋ฆฌ, x-view-model์ ์ฒ์๋ถํฐ TypeScript๋ก ๊ตฌ์ถ๋์์ต๋๋ค:
// ์ํ์ ๋ฉ์๋์ ๋ํ ์์ ํ ํ์
์ถ๋ก
const [state, send] = useViewModel(userVM, ["name", "email"]);
// ํ์
์์ ํ ๊ฒฝ๋ก ์ ํ
const [state] = useMemoizedViewModel(userVM, [
"profile.avatar",
"settings.theme",
] as const);
// ํ์
์์ ํ ๊ณ์ฐ๋ ๊ฐ
const [state] = useComputedViewModel(
userVM,
(state) => ({
fullName: `${state.firstName} ${state.lastName}`,
}),
["firstName", "lastName"]
);
x-view-model์ ์ต๊ณ ์ ์ฑ๋ฅ์ ์ํด ์ค๊ณ๋์์ต๋๋ค:
MVVM ํจํด์ ๊ด์ฌ์ฌ์ ๋ช ํํ ๋ถ๋ฆฌ๋ฅผ ์ ๊ณตํฉ๋๋ค:
// ๋ทฐ ๋ชจ๋ธ (๋น์ฆ๋์ค ๋ก์ง)
const userVM = registViewModel<UserContext>({
name: "",
email: "",
updateProfile(data) {
if (data.name) this.state.name = data.name;
if (data.email) this.state.email = data.email;
},
});
// ๋ทฐ (UI)
function UserProfile() {
const [state, send] = useViewModel(userVM, ["name", "email"]);
return (
<div>
<p>Name: {state.name}</p>
<p>Email: {state.email}</p>
<button onClick={() => send("updateProfile", { name: "John" })}>
Update
</button>
</div>
);
}
๋น๋๊ธฐ ์์ ์ ์ฝ๊ฒ ์ฒ๋ฆฌํ ์ ์์ต๋๋ค:
const [state, send] = useViewModel(userVM, ["loading", "data"]);
// ์ด๋ฒคํธ ๊ธฐ๋ฐ ํธ์ถ
send("fetchData");
// ๋ฐํ ๊ฐ์ด ์๋ ๋น๋๊ธฐ ํธ์ถ
const result = await send("fetchData", {}, true);
// ํ์
์์ ํ ์ค๋ฅ ์ฒ๋ฆฌ
try {
const data = await send("fetchData", {}, true);
} catch (error) {
// ์ค๋ฅ ์ฒ๋ฆฌ
}
๊ธฐ๋ฅ | x-view-model | Redux | MobX | Zustand |
---|---|---|---|---|
๋ฒ๋ค ํฌ๊ธฐ | ~13.5KB | ~7KB | ~16KB | ~1KB |
TypeScript ์ง์ | โญโญโญโญโญ | โญโญโญ | โญโญโญโญ | โญโญโญโญ |
ํ์ต ๊ณก์ | โญโญโญโญ | โญโญ | โญโญโญ | โญโญโญโญ |
์ฑ๋ฅ | โญโญโญโญโญ | โญโญโญ | โญโญโญโญ | โญโญโญโญ |
์ฝ๋ ๋ณต์ก์ฑ | โญโญโญโญโญ | โญโญ | โญโญโญ | โญโญโญโญ |
๋น๋๊ธฐ ์ง์ | โญโญโญโญโญ | โญโญโญ | โญโญโญโญ | โญโญโญโญ |
npm install x-view-model
# ๋๋ yarn ์ฌ์ฉ
yarn add x-view-model
# ๋๋ pnpm ์ฌ์ฉ
pnpm add x-view-model
๊ฐ๋จํ ์นด์ดํฐ ์์ ๋ก ์์ํด๋ณด์ธ์:
import { registViewModel, useViewModel } from "x-view-model";
// ๋ทฐ ๋ชจ๋ธ ์ธํฐํ์ด์ค ์ ์
interface CounterViewModel {
count: number;
increment(): void;
decrement(): void;
}
// ๋ทฐ ๋ชจ๋ธ ์์ฑ
const counterVM = registViewModel<CounterViewModel>(
{
count: 0,
increment() {
this.state.count += 1;
},
decrement() {
this.state.count -= 1;
},
},
{ name: "counter-view-model", deep: true }
);
// ์ปดํฌ๋ํธ์์ ์ฌ์ฉ
function Counter() {
// ๋ ๋ฒ์งธ ๋งค๊ฐ๋ณ์ ["count"]๋ ๊ตฌ๋
ํ ์ํ ์์ฑ์ ์ง์ ํฉ๋๋ค
// ์ด๋ ์ด๋ฌํ ํน์ ์์ฑ์ด ๋ณ๊ฒฝ๋ ๋๋ง ์
๋ฐ์ดํธํ์ฌ ๋ฆฌ๋ ๋๋ง์ ์ต์ ํํฉ๋๋ค
const { state, increment, decrement } = useViewModel(counterVM, ["count"]);
return (
<div>
<p>Count: {state.count}</p>
<button onClick={increment}>+</button>
<button onClick={decrement}>-</button>
</div>
);
}
๋ทฐ ๋ชจ๋ธ์ ์ ํ๋ฆฌ์ผ์ด์ ์ ๋น์ฆ๋์ค ๋ก์ง๊ณผ ์ํ๋ฅผ ์บก์ํํฉ๋๋ค. UI์ ๋น์ฆ๋์ค ๋ก์ง ์ฌ์ด์ ๊น๋ํ ๋ถ๋ฆฌ๋ฅผ ์ ๊ณตํฉ๋๋ค:
type UserState = {
name: string;
email: string;
firstName: string;
lastName: string;
profile: {
avatar: string;
};
settings: {
theme: "dark" | "light";
};
};
type UserAction = {
updateProfile(payload: { name?: string; email?: string }): void;
fetchUserData(payload: { userId: string }): Promise<{
id: string;
name: string;
email: string;
}>;
};
export type UserContext = UserState & UserAction;
const userVM = registViewModel<UserContext>(
{
name: "",
email: "",
firstName: "",
lastName: "",
profile: {
avatar: "",
},
settings: {
theme: "dark",
},
updateProfile(data: { name?: string; email?: string }) {
if (data.name) this.state.name = data.name;
if (data.email) this.state.email = data.email;
},
async fetchUserData(data: { userId: string }) {
// API ํธ์ถ ์๋ฎฌ๋ ์ด์
return {
id: data.userId,
name: "John Doe",
email: "john@example.com",
};
},
},
{ name: "user-view-model", deep: true }
);
๋ทฐ ๋ชจ๋ธ ์ํ์ ๋ฉ์๋์ ์ ๊ทผํ๊ธฐ ์ํ ๊ธฐ๋ณธ ํ :
const [state, send] = useViewModel(userVM, ["name", "email"]);
// send ํจ์ ์ฌ์ฉ ์์:
// 1. ํ๋กํ ์
๋ฐ์ดํธ (void ๋ฐํ)
send("updateProfile", { name: "John Doe" }); // ์ด๋ฒคํธ ๊ธฐ๋ฐ ํธ์ถ
await send("updateProfile", { name: "John Doe" }, true); // ๋น๋๊ธฐ ํธ์ถ
// 2. ๋ฐ์ดํฐ ๊ฐ์ ธ์ค๊ธฐ (๋ฐํ ๊ฐ ์์)
const userData = await send("fetchUserData", { userId: "123" }, true); // ์ฌ์ฉ์ ๋ฐ์ดํฐ ๋ฐํ
// userData๋ ๋ค์๊ณผ ๊ฐ์ต๋๋ค: { id: "123", name: "John Doe", email: "john@example.com" }
/* send ํจ์์ ๋์์ async ๋งค๊ฐ๋ณ์์ ๋ฐ๋ผ ๋ค๋ฆ
๋๋ค:
* - async๊ฐ false์ธ ๊ฒฝ์ฐ (๊ธฐ๋ณธ๊ฐ): ๋ฉ์๋๋ฅผ ์ด๋ฒคํธ๋ก ํธ์ถํ๊ณ void๋ฅผ ๋ฐํ
* - async๊ฐ true์ธ ๊ฒฝ์ฐ: ๋ฉ์๋๋ฅผ ํธ์ถํ๊ณ ๊ฒฐ๊ณผ๋ฅผ ๋ฐํ
* - ๋ฉ์๋๊ฐ Promise๋ฅผ ๋ฐํํ๋ฉด ํ์ด์ ๋ฐํ
* - ๋ฉ์๋๊ฐ ์ง์ ๊ฐ์ ๋ฐํํ๋ฉด ํด๋น ๊ฐ์ ๋ฐํ
*/
๋ทฐ ๋ชจ๋ธ์์ ํน์ ๊ฒฝ๋ก๋ฅผ ์ ํํ๊ธฐ ์ํ ์ต์ ํ๋ ํ :
const [state, send] = useMemoizedViewModel(userVM, [
"name",
"profile.avatar",
"settings.theme",
] as const);
/* useMemoizedViewModel์ ์ง์ ๋ ์ํ ๊ฒฝ๋ก๋ง ๊ตฌ๋
ํ๊ณ ๋ฐํํฉ๋๋ค.
* ์ด ์์ ์์ state ๊ฐ์ฒด๋ ๋ค์๋ง ํฌํจํฉ๋๋ค:
* - state.name
* - state.profile.avatar
* - state.settings.theme
* ๋ค๋ฅธ ์์ฑ์ state ๊ฐ์ฒด์ ํฌํจ๋์ง ์์ต๋๋ค.
*/
๋ทฐ ๋ชจ๋ธ ์ํ์์ ๊ณ์ฐ๋ ๊ฐ์ ์์ฑ:
const [state, send] = useComputedViewModel(
userVM,
(state) => ({
fullName: `${state.firstName} ${state.lastName}`,
}),
["firstName", "lastName"]
);
/* useComputedViewModel์ ์์กด์ฑ์ด ๋ณ๊ฒฝ๋ ๋๋ง ๊ณ์ฐ๋ ๊ฐ์ ๋ฐํํฉ๋๋ค.
* ์ด ์์ ์์ firstName์ด๋ lastName์ด ๋ณ๊ฒฝ๋ ๋ state ๊ฐ์ฒด๋ ๋ค์๋ง ํฌํจํฉ๋๋ค:
* - state.fullName
* ๊ณ์ฐ๋ ๊ฐ fullName์ firstName์ด๋ lastName์ด ๋ณ๊ฒฝ๋ ๋๋ง๋ค ์๋์ผ๋ก ์
๋ฐ์ดํธ๋ฉ๋๋ค.
*/
x-view-model์ send
๋ฉ์๋๋ฅผ ํตํ ๋ชจ๋ ๋ฉ์๋ ํธ์ถ์ ๋ํ ํ์คํ ๋ฆฌ๋ฅผ ๊ธฐ๋กํ๊ณ ๊ด๋ฆฌํ๋ ๊ธฐ๋ฅ์ ์ ๊ณตํฉ๋๋ค. ์ด๋ฅผ ํตํด ๋ชจ๋ ์ํ ๋ณ๊ฒฝ ๋ฉ์๋์ ํธ์ถ์ ์ถ์ ํ๊ณ ๋ชจ๋ํฐ๋งํ ์ ์์ต๋๋ค:
// ํ์คํ ๋ฆฌ ํธ๋ค๋ฌ ์ ์
const counterVM = registViewModel<CounterContext>(
{
count: 0,
increment() {
this.count += 1;
return this.count;
},
decrement() {
this.count -= 1;
return this.count;
}
},
{
name: "counter",
deep: true,
history: {
handler: (history, state) => {
// send ๋ฉ์๋๋ฅผ ํตํ ํธ์ถ ๊ธฐ๋ก ์ฒ๋ฆฌ
console.log('Method called via send:', {
name: history.name, // ํธ์ถ๋ ๋ฉ์๋ ์ด๋ฆ
payload: history.payload, // send ๋ฉ์๋์ ์ ๋ฌ๋ ์ธ์
result: history.result, // ๋ฉ์๋์ ๋ฐํ๊ฐ
error: history.error, // ์๋ฌ ๋ฐ์ ์
timestamp: history.timestamp // ํธ์ถ ์๊ฐ
});
},
maxSize: 100 // ์ต๋ ํ์คํ ๋ฆฌ ์ ์ฅ ๊ฐ์ (์ ํ์ )
}
}
);
// ์ฌ์ฉ ์์
const [state, send] = useViewModel(counterVM, ["count"]);
// ์ด send ํธ์ถ๋ค์ด ํ์คํ ๋ฆฌ์ ๊ธฐ๋ก๋ฉ๋๋ค
await send("increment", undefined, true); // history์ ๊ธฐ๋ก
await send("decrement", undefined, true); // history์ ๊ธฐ๋ก
send ๋ฉ์๋ ํธ์ถ ์ถ์
send
๋ฉ์๋๋ฅผ ํตํ ๋ชจ๋ ๋ฉ์๋ ํธ์ถ ๊ธฐ๋ก์ปค์คํ ํธ๋ค๋ฌ
ํ์คํ ๋ฆฌ ํฌ๊ธฐ ๊ด๋ฆฌ
maxSize
์ต์
์ผ๋ก ํ์คํ ๋ฆฌ ์ ์ฅ ๊ฐ์ ์ ํํ์คํ ๋ฆฌ ์กฐํ ๋ฐ ๊ด๋ฆฌ
// ํ์คํ ๋ฆฌ ์กฐํ
const history = counterVM.context.getSendHistory();
// ํ์คํ ๋ฆฌ ์ด๊ธฐํ
counterVM.context.clearSendHistory();
// ๋๋ฒ๊น
์ ์ํ ํ์คํ ๋ฆฌ ๋ก๊น
const vm = registViewModel<UserContext>(
{
// ... state and methods
},
{
name: "user",
deep: true,
history: {
handler: (history, state) => {
if (process.env.NODE_ENV === 'development') {
console.log(`[${new Date(history.timestamp).toISOString()}]`, {
method: history.name,
payload: history.payload,
result: history.result
});
}
}
}
}
);
// ์ธ๋ถ ๋ชจ๋ํฐ๋ง ์๋น์ค ์ฐ๋
const vm = registViewModel<PaymentContext>(
{
// ... state and methods
},
{
name: "payment",
deep: true,
history: {
handler: async (history, state) => {
if (history.error) {
await sendToErrorTracking({
method: history.name,
error: history.error,
state: state
});
}
},
maxSize: 50
}
}
);
์ด ๊ธฐ๋ฅ์ ๋ค์๊ณผ ๊ฐ์ ์ํฉ์์ ํนํ ์ ์ฉํฉ๋๋ค:
x-view-model์ ์ํ ๋ณ๊ฒฝ์ ๊ฐ๋ก์ฑ๊ณ ์ฒ๋ฆฌํ ์ ์๋ ๋ฏธ๋ค์จ์ด ์์คํ ์ ์ ๊ณตํฉ๋๋ค. ๋ฏธ๋ค์จ์ด๋ ์ํ ๋ณ๊ฒฝ ์ ํ์ ํน์ ์์ ์ ์ํํ๊ฑฐ๋, ๋ณ๊ฒฝ์ ๊ฒ์ฆํ๊ฑฐ๋, ๋ก๊น ๋ฑ์ ํ ์ ์์ต๋๋ค.
๋ฏธ๋ค์จ์ด๋ ๋ค์๊ณผ ๊ฐ์ ํํ๋ก ์ ์๋ฉ๋๋ค:
type Middleware<T> = (changes: Change[], next: () => void) => void;
changes
: ์ํ ๋ณ๊ฒฝ ์ ๋ณด๋ฅผ ๋ด์ ๋ฐฐ์ดnext
: ๋ค์ ๋ฏธ๋ค์จ์ด๋ฅผ ํธ์ถํ๋ ํจ์import { registViewModel } from 'x-view-model';
import { Change } from 'x-view-model/core/observer';
// ๋ก๊น
๋ฏธ๋ค์จ์ด
const loggingMiddleware = (changes: Change[], next: () => void) => {
console.log('State changes:', changes);
next();
};
// ๊ฒ์ฆ ๋ฏธ๋ค์จ์ด
const validationMiddleware = (changes: Change[], next: () => void) => {
const invalidChanges = changes.filter(change => {
// ํน์ ์กฐ๊ฑด์ ๋ง์ง ์๋ ๋ณ๊ฒฝ์ ํํฐ๋ง
return !isValid(change);
});
if (invalidChanges.length > 0) {
throw new Error('Invalid state changes detected');
}
next();
};
// ViewModel ์ ์
const counterVM = registViewModel({
count: 0,
increment() {
this.count += 1;
}
}, {
name: 'counter',
deep: true,
middlewares: [loggingMiddleware, validationMiddleware]
});
๋ฏธ๋ค์จ์ด๋ ๋ฑ๋ก๋ ์์๋๋ก ์คํ๋ฉ๋๋ค. ๊ฐ ๋ฏธ๋ค์จ์ด๋ next()
๋ฅผ ํธ์ถํ์ฌ ๋ค์ ๋ฏธ๋ค์จ์ด๋ก ์ ์ด๋ฅผ ๋๊ธธ ์ ์์ต๋๋ค.
const middleware1 = (changes: Change[], next: () => void) => {
console.log('Middleware 1: before');
next();
console.log('Middleware 1: after');
};
const middleware2 = (changes: Change[], next: () => void) => {
console.log('Middleware 2: before');
next();
console.log('Middleware 2: after');
};
// ์คํ ์์:
// 1. Middleware 1: before
// 2. Middleware 2: before
// 3. Middleware 2: after
// 4. Middleware 1: after
๋ก๊น ๋ฐ ๋๋ฒ๊น
๊ฒ์ฆ ๋ฐ ๋ณด์
์ํ ๋๊ธฐํ
์ฑ๋ฅ ์ต์ ํ
๊ธฐ๋ณธ์ ์ธ ์ํ ๊ด๋ฆฌ๋ฅผ ๋ณด์ฌ์ฃผ๋ ๊ฐ๋จํ ํผ ์์ ์ ๋๋ค:
type FormState = {
username: string;
email: string;
isValid: boolean;
};
type FormAction = {
updateField(payload: { field: keyof FormState; value: string }): void;
validateForm(): boolean;
};
type FormContext = FormState & FormAction;
const formVM = registViewModel<FormContext>({
username: "",
email: "",
isValid: false,
updateField({ field, value }) {
this.state[field] = value;
this.state.isValid = this.validateForm();
},
validateForm() {
return this.state.username.length > 0 && this.state.email.includes("@");
},
});
function FormComponent() {
const [state, send] = useViewModel(formVM, ["username", "email", "isValid"]);
return (
<form>
<input
value={state.username}
onChange={(e) =>
send("updateField", { field: "username", value: e.target.value })
}
placeholder="Username"
/>
<input
value={state.email}
onChange={(e) =>
send("updateField", { field: "email", value: e.target.value })
}
placeholder="Email"
/>
<button disabled={!state.isValid}>Submit</button>
</form>
);
}
์ด ์์ ๋ Canvas์ ๊ฐ์ ๋ณต์กํ DOM ์กฐ์์ ์ฒ๋ฆฌํ๊ธฐ ์ํด ์ปจํธ๋กค๋ฌ ํจํด๊ณผ ํจ๊ป x-view-model์ ์ฌ์ฉํ๋ ๋ฐฉ๋ฒ์ ๋ณด์ฌ์ค๋๋ค:
// types/canvas.ts
export interface CanvasState {
width: number;
height: number;
color: string;
lineWidth: number;
isDrawing: boolean;
points: Array<{ x: number; y: number }>;
}
// controllers/CanvasController.ts
export class CanvasController {
private ctx: CanvasRenderingContext2D | null = null;
constructor(private state: CanvasState) {}
setCanvas(canvas: HTMLCanvasElement) {
this.ctx = canvas.getContext("2d");
if (this.ctx) {
this.ctx.lineWidth = this.state.lineWidth;
this.ctx.strokeStyle = this.state.color;
this.ctx.lineCap = "round";
}
}
startDrawing(x: number, y: number) {
if (!this.ctx) return;
this.state.isDrawing = true;
this.state.points = [{ x, y }];
this.ctx.beginPath();
this.ctx.moveTo(x, y);
}
draw(x: number, y: number) {
if (!this.ctx || !this.state.isDrawing) return;
this.state.points.push({ x, y });
this.ctx.lineTo(x, y);
this.ctx.stroke();
}
stopDrawing() {
if (!this.ctx) return;
this.state.isDrawing = false;
this.ctx.closePath();
}
clearCanvas() {
if (!this.ctx) return;
this.ctx.clearRect(0, 0, this.state.width, this.state.height);
this.state.points = [];
}
setColor(color: string) {
this.state.color = color;
if (this.ctx) {
this.ctx.strokeStyle = color;
}
}
setLineWidth(width: number) {
this.state.lineWidth = width;
if (this.ctx) {
this.ctx.lineWidth = width;
}
}
}
// viewModels/canvasViewModel.ts
import { registViewModel } from "x-view-model";
import { CanvasController } from "../controllers/CanvasController";
import { CanvasState } from "../types/canvas";
const initialState: CanvasState = {
width: 800,
height: 600,
color: "#000000",
lineWidth: 2,
isDrawing: false,
points: [],
};
const controller = new CanvasController(initialState);
export const canvasViewModel = registViewModel<CanvasState, CanvasController>(
initialState,
{
name: "canvas-view-model",
deep: true,
},
controller
);
// components/CanvasComponent.tsx
import React, { useRef, useEffect } from "react";
import { useViewModel } from "x-view-model";
import { canvasViewModel } from "../viewModels/canvasViewModel";
const CanvasComponent: React.FC = () => {
const canvasRef = useRef<HTMLCanvasElement>(null);
const [state, send, controller] = useViewModel(canvasViewModel, [
"color",
"lineWidth",
"width",
"height",
]);
useEffect(() => {
if (canvasRef.current) {
controller.setCanvas(canvasRef.current);
}
}, [controller]);
const handleMouseDown = (e: React.MouseEvent<HTMLCanvasElement>) => {
const rect = canvasRef.current?.getBoundingClientRect();
if (!rect) return;
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
controller.startDrawing(x, y);
};
return (
<div>
<div className="controls">
<input
type="color"
value={state.color}
onChange={(e) => controller.setColor(e.target.value)}
/>
<input
type="range"
min="1"
max="20"
value={state.lineWidth}
onChange={(e) => controller.setLineWidth(Number(e.target.value))}
/>
<button onClick={() => controller.clearCanvas()}>Clear</button>
</div>
<canvas
ref={canvasRef}
width={state.width}
height={state.height}
onMouseDown={handleMouseDown}
onMouseMove={(e) => {
const rect = canvasRef.current?.getBoundingClientRect();
if (!rect) return;
controller.draw(e.clientX - rect.left, e.clientY - rect.top);
}}
onMouseUp={() => controller.stopDrawing()}
onMouseLeave={() => controller.stopDrawing()}
style={{ border: "1px solid #000" }}
/>
</div>
);
};
x-view-model์ ์ฑ๋ฅ์ ์ต์ ํ๋์ด ์์ต๋๋ค:
๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ ์ฐ์ํ TypeScript ์ง์์ ์ ๊ณตํฉ๋๋ค:
๊ธฐ์ฌ๋ฅผ ํ์ํฉ๋๋ค! Pull Request๋ฅผ ์์ ๋กญ๊ฒ ์ ์ถํด์ฃผ์ธ์.
ISC ยฉ seokhwan.kim
A: ๋ทฐ ๋ชจ๋ธ์ MVVM ํจํด์ ์ฌ์ฉํ์ฌ ์ํ์ ๋น์ฆ๋์ค ๋ก์ง์ ๊ด๋ฆฌํ๋ ๊ตฌ์กฐํ๋ ๋ฐฉ๋ฒ์ ์ ๊ณตํฉ๋๋ค. ์ผ๋ฐ์ ์ธ React ์ํ์ ๋ฌ๋ฆฌ:
A: MVVM ํจํด์ ๋ค์๊ณผ ๊ฐ์ ์ด์ ์ ์ ๊ณตํฉ๋๋ค:
A: x-view-model์ ๋ค์์ ์ ๊ณตํฉ๋๋ค:
A: x-view-model์ ํ๋ก๋์ ์ฌ์ฉ์ ์ต์ ํ๋์ด ์์ต๋๋ค:
A: ๋ค, x-view-model์ ํ์ฅ์ ์ํด ์ค๊ณ๋์์ต๋๋ค:
A: ๋ฉ๋ชจ๋ฆฌ ์ฌ์ฉ๋์ ๋ค์์ ํตํด ์ต์ ํ๋ฉ๋๋ค:
A: ๋ค, x-view-model์ ์ผ๋ฐ JavaScript์์๋ ์๋ํ์ง๋ง ๋ค์์ ๋์น๊ฒ ๋ฉ๋๋ค:
A: ๋ณต์กํ ํ์ ์ ๊ฒฝ์ฐ:
A: ์ ๋ค๋ฆญ์ ์ฌ์ฉํ ๋:
A: ๋ชจ๋ฒ ์ฌ๋ก:
A: ์ต์ ํ ์ ๋ต:
A: ์ค์ฒฉ๋ ์ํ์ ๊ฒฝ์ฐ:
A: ๊ถ์ฅ ์ ๊ทผ ๋ฐฉ์:
send
ํจ์ ์ฌ์ฉA: ์ค๋ฅ ์ฒ๋ฆฌ ๋ชจ๋ฒ ์ฌ๋ก:
A: ๋ก๋ฉ ์ํ ๊ด๋ฆฌ:
A: ํ ์คํธ ์ ๋ต:
A: ํ ์คํธ ์ค์ :
A: ๋ชจํน ์ ๊ทผ ๋ฐฉ์:
A: ๋ง์ด๊ทธ๋ ์ด์ ๋จ๊ณ:
A: ๋ฆฌํฉํ ๋ง ์ ๊ทผ ๋ฐฉ์:
A: ๋ค, x-view-model์ ๋ค์์ ์ง์ํฉ๋๋ค:
A: ์ง์ ์ฑ๋:
A: ๊ธฐ์ฌ ์ต์ :
A: ๋ฒ๊ทธ ๋ณด๊ณ ๋จ๊ณ:
seokhwan.kim์ด ๋ง๋ โค๏ธ
FAQs
A lightweight, type-safe MVVM state management solution for React applications. Features reactive updates, computed properties, and deep path selection with minimal bundle size.
We found that x-view-model demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago.ย It has 1 open source maintainer collaborating on the project.
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Security News
Vite releases Rolldown-Vite, a Rust-based bundler preview offering faster builds and lower memory usage as a drop-in replacement for Vite.
Research
Security News
A malicious npm typosquat uses remote commands to silently delete entire project directories after a single mistyped install.
Research
Security News
Malicious PyPI package semantic-types steals Solana private keys via transitive dependency installs using monkey patching and blockchain exfiltration.