
Product
Socket Now Supports pylock.toml Files
Socket now supports pylock.toml, enabling secure, reproducible Python builds with advanced scanning and full alignment with PEP 751's new standard.
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์ ์ํ ๋ณ๊ฒฝ์ ๊ฐ๋ก์ฑ๊ณ ์ฒ๋ฆฌํ ์ ์๋ ๋ฏธ๋ค์จ์ด ์์คํ ์ ์ ๊ณตํฉ๋๋ค. ๋ฏธ๋ค์จ์ด๋ ์ํ ๋ณ๊ฒฝ ์ ํ์ ํน์ ์์ ์ ์ํํ๊ฑฐ๋, ๋ณ๊ฒฝ์ ๊ฒ์ฆํ๊ฑฐ๋, ๋ก๊น ๋ฑ์ ํ ์ ์์ต๋๋ค.
๋ฏธ๋ค์จ์ด๋ ๋ค์๊ณผ ๊ฐ์ ํํ๋ก ์ ์๋ฉ๋๋ค:
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.
The npm package x-view-model receives a total of 18 weekly downloads. As such, x-view-model popularity was classified as not popular.
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.
Product
Socket now supports pylock.toml, enabling secure, reproducible Python builds with advanced scanning and full alignment with PEP 751's new standard.
Security News
Research
Socket uncovered two npm packages that register hidden HTTP endpoints to delete all files on command.
Research
Security News
Malicious Ruby gems typosquat Fastlane plugins to steal Telegram bot tokens, messages, and files, exploiting demand after Vietnamโs Telegram ban.