
Research
PyPI Package Disguised as Instagram Growth Tool Harvests User Credentials
A deceptive PyPI package posing as an Instagram growth tool collects user credentials and sends them to third-party bot services.
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.
The npm package x-view-model receives a total of 55 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.
Research
A deceptive PyPI package posing as an Instagram growth tool collects user credentials and sends them to third-party bot services.
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.