x-view-model
x-view-model์ ์ ํํ๋ ์ด์
x-view-model์ React ์ ํ๋ฆฌ์ผ์ด์
์ ๊ฐ๋จํ๋ฉด์๋ ๊ฐ๋ ฅํ ์ํ ๊ด๋ฆฌ ์๋ฃจ์
์ ์ ๊ณตํ๊ธฐ ์ํด ์ค๊ณ๋์์ต๋๋ค. MVVM ํจํด์ ๋ชจ๋ฒ ์ฌ๋ก๋ฅผ ํ๋์ ์ธ React ๊ธฐ๋ฅ๊ณผ ๊ฒฐํฉํฉ๋๋ค:
- ๐ ๋์ ์ฑ๋ฅ: ์ต์ํ์ ๋ฆฌ๋ ๋๋ง๊ณผ ํจ์จ์ ์ธ ์
๋ฐ์ดํธ์ ์ต์ ํ
- ๐ช ํ์
์์ ์ฑ: ํฌ๊ด์ ์ธ ํ์
์ถ๋ก ์ ํตํ ์๋ฒฝํ TypeScript ์ง์
- ๐ฏ MVVM ํจํด: ๋ทฐ์ ๋น์ฆ๋์ค ๋ก์ง ๊ฐ์ ๋ช
ํํ ๊ด์ฌ์ฌ ๋ถ๋ฆฌ
- ๐ ๋ฐ์ํ: ์ํ ๋ณ๊ฒฝ ์ ์๋ ์
๋ฐ์ดํธ
- ๐จ ๊ณ์ฐ๋ ์์ฑ: ์๋ ์
๋ฐ์ดํธ์ ํจ๊ป ์ํ์์ ๊ฐ์ ํ์
- ๐ ๊น์ ๊ฒฝ๋ก ์ ํ: ์ค์ฒฉ๋ ์ํ ๋ณ๊ฒฝ์ ํจ์จ์ ์ผ๋ก ๊ตฌ๋
- ๐ฆ ๊ฒฝ๋: ์ต์ ๋ฒ๋ค ํฌ๊ธฐ (~13.5KB ์์ถ, ~5KB gzipped)
- ๐ ๊ฐ๋ฐ์ ๊ฒฝํ: ํฌ๊ด์ ์ธ ๋๊ตฌ์ ์ง๊ด์ ์ธ API
- ๐ ์ค๋งํธ ๋ฉ๋ชจ๋ฆฌ ๊ด๋ฆฌ: ์ฐธ์กฐ ์นด์ดํ
์ ํตํ ์๋ ์ฒ๋ฆฌ
๋ค๋ฅธ ์๋ฃจ์
๋์ x-view-model์ ์ ํํ๋ ์ด์
๐ ์ฐ์ํ TypeScript ์ง์
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;
},
});
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) {
}
๐ ์ฑ๋ฅ ๋น๊ต
๋ฒ๋ค ํฌ๊ธฐ | ~13.5KB | ~7KB | ~16KB | ~1KB |
TypeScript ์ง์ | โญโญโญโญโญ | โญโญโญ | โญโญโญโญ | โญโญโญโญ |
ํ์ต ๊ณก์ | โญโญโญโญ | โญโญ | โญโญโญ | โญโญโญโญ |
์ฑ๋ฅ | โญโญโญโญโญ | โญโญโญ | โญโญโญโญ | โญโญโญโญ |
์ฝ๋ ๋ณต์ก์ฑ | โญโญโญโญโญ | โญโญ | โญโญโญ | โญโญโญโญ |
๋น๋๊ธฐ ์ง์ | โญโญโญโญโญ | โญโญโญ | โญโญโญโญ | โญโญโญโญ |
์ค์น
npm install x-view-model
yarn add x-view-model
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() {
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 }) {
return {
id: data.userId,
name: "John Doe",
email: "john@example.com",
};
},
},
{ name: "user-view-model", deep: true }
);
ํ
useViewModel
๋ทฐ ๋ชจ๋ธ ์ํ์ ๋ฉ์๋์ ์ ๊ทผํ๊ธฐ ์ํ ๊ธฐ๋ณธ ํ
:
const [state, send] = useViewModel(userVM, ["name", "email"]);
send("updateProfile", { name: "John Doe" });
await send("updateProfile", { name: "John Doe" }, true);
const userData = await send("fetchUserData", { userId: "123" }, true);
useMemoizedViewModel
๋ทฐ ๋ชจ๋ธ์์ ํน์ ๊ฒฝ๋ก๋ฅผ ์ ํํ๊ธฐ ์ํ ์ต์ ํ๋ ํ
:
const [state, send] = useMemoizedViewModel(userVM, [
"name",
"profile.avatar",
"settings.theme",
] as const);
useComputedViewModel
๋ทฐ ๋ชจ๋ธ ์ํ์์ ๊ณ์ฐ๋ ๊ฐ์ ์์ฑ:
const [state, send] = useComputedViewModel(
userVM,
(state) => ({
fullName: `${state.firstName} ${state.lastName}`,
}),
["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) => {
console.log('Method called via send:', {
name: history.name,
payload: history.payload,
result: history.result,
error: history.error,
timestamp: history.timestamp
});
},
maxSize: 100
}
}
);
const [state, send] = useViewModel(counterVM, ["count"]);
await send("increment", undefined, true);
await send("decrement", undefined, true);
ํ์คํ ๋ฆฌ ๊ธฐ๋ฅ์ ์ฃผ์ ํน์ง
-
send ๋ฉ์๋ ํธ์ถ ์ถ์
send
๋ฉ์๋๋ฅผ ํตํ ๋ชจ๋ ๋ฉ์๋ ํธ์ถ ๊ธฐ๋ก
- ํธ์ถ๋ ๋ฉ์๋ ์ด๋ฆ, ์ ๋ฌ๋ ์ธ์, ๊ฒฐ๊ณผ๊ฐ, ์๊ฐ ๊ธฐ๋ก
- ์๋ฌ ๋ฐ์ ์ ์๋ฌ ์ ๋ณด๋ ๊ธฐ๋ก
-
์ปค์คํ
ํธ๋ค๋ฌ
- ํ์คํ ๋ฆฌ ์ด๋ฒคํธ๋ฅผ ์์ ๋กญ๊ฒ ์ฒ๋ฆฌํ ์ ์๋ ํธ๋ค๋ฌ ์ ๊ณต
- ์ธ๋ถ ๋ก๊น
์์คํ
์ฐ๋ ๊ฐ๋ฅ
- ์ํ ๋ณํ ์ถ์ ๊ฐ๋ฅ
-
ํ์คํ ๋ฆฌ ํฌ๊ธฐ ๊ด๋ฆฌ
maxSize
์ต์
์ผ๋ก ํ์คํ ๋ฆฌ ์ ์ฅ ๊ฐ์ ์ ํ
- ๋ฉ๋ชจ๋ฆฌ ์ฌ์ฉ๋ ์ต์ ํ
-
ํ์คํ ๋ฆฌ ์กฐํ ๋ฐ ๊ด๋ฆฌ
const history = counterVM.context.getSendHistory();
counterVM.context.clearSendHistory();
ํ์ฉ ์์
const vm = registViewModel<UserContext>(
{
},
{
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>(
{
},
{
name: "payment",
deep: true,
history: {
handler: async (history, state) => {
if (history.error) {
await sendToErrorTracking({
method: history.name,
error: history.error,
state: state
});
}
},
maxSize: 50
}
}
);
์ด ๊ธฐ๋ฅ์ ๋ค์๊ณผ ๊ฐ์ ์ํฉ์์ ํนํ ์ ์ฉํฉ๋๋ค:
- ๋๋ฒ๊น
๋ฐ ๋ฌธ์ ํด๊ฒฐ
- ์ฌ์ฉ์ ํ๋ ๋ถ์
- ์๋ฌ ์ถ์ ๋ฐ ๋ชจ๋ํฐ๋ง
- ์ํ ๋ณํ ๊ฐ์ฌ(audit) ๋ก๊น
- ์คํ ์ทจ์/๋ค์ ์คํ ๊ธฐ๋ฅ ๊ตฌํ
๋ฏธ๋ค์จ์ด
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();
};
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');
};
๋ฏธ๋ค์จ์ด ์ฌ์ฉ ์ฌ๋ก
-
๋ก๊น
๋ฐ ๋๋ฒ๊น
- ์ํ ๋ณ๊ฒฝ ์ถ์
- ์ฑ๋ฅ ๋ชจ๋ํฐ๋ง
- ๋๋ฒ๊ทธ ์ ๋ณด ์์ง
-
๊ฒ์ฆ ๋ฐ ๋ณด์
- ์ํ ๋ณ๊ฒฝ ์ ํจ์ฑ ๊ฒ์ฌ
- ์ ๊ทผ ์ ์ด
- ๋ฐ์ดํฐ ๋ฌด๊ฒฐ์ฑ ๊ฒ์ฆ
-
์ํ ๋๊ธฐํ
- ๋ค๋ฅธ ์์คํ
๊ณผ์ ์ํ ๋๊ธฐํ
- ๋ฐฑ์๋ API ํธ์ถ
- ๋ก์ปฌ ์คํ ๋ฆฌ์ง ์ ์ฅ
-
์ฑ๋ฅ ์ต์ ํ
- ๋ณ๊ฒฝ ๋ฐฐ์น ์ฒ๋ฆฌ
- ๋ถํ์ํ ์
๋ฐ์ดํธ ํํฐ๋ง
- ์บ์ฑ ๋ฐ ๋ฉ๋ชจ์ด์ ์ด์
๊ณ ๊ธ ์ฌ์ฉ๋ฒ
๊ฐ๋จํ ํผ ์์
๊ธฐ๋ณธ์ ์ธ ์ํ ๊ด๋ฆฌ๋ฅผ ๋ณด์ฌ์ฃผ๋ ๊ฐ๋จํ ํผ ์์ ์
๋๋ค:
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์ ์ฌ์ฉํ๋ ๋ฐฉ๋ฒ์ ๋ณด์ฌ์ค๋๋ค:
export interface CanvasState {
width: number;
height: number;
color: string;
lineWidth: number;
isDrawing: boolean;
points: Array<{ x: number; y: number }>;
}
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;
}
}
}
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
);
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
์ง์
์์ฃผ ๋ฌป๋ ์ง๋ฌธ
์ผ๋ฐ์ ์ธ ์ง๋ฌธ
Q: ๋ทฐ ๋ชจ๋ธ๊ณผ ์ผ๋ฐ์ ์ธ React ์ํ ๊ด๋ฆฌ์ ์ฐจ์ด์ ์ ๋ฌด์์ธ๊ฐ์?
A: ๋ทฐ ๋ชจ๋ธ์ MVVM ํจํด์ ์ฌ์ฉํ์ฌ ์ํ์ ๋น์ฆ๋์ค ๋ก์ง์ ๊ด๋ฆฌํ๋ ๊ตฌ์กฐํ๋ ๋ฐฉ๋ฒ์ ์ ๊ณตํฉ๋๋ค. ์ผ๋ฐ์ ์ธ React ์ํ์ ๋ฌ๋ฆฌ:
- ๋น์ฆ๋์ค ๋ก์ง์ UI ์ปดํฌ๋ํธ์ ๋ถ๋ฆฌ
- ํ์
์์ ํ ์ํ ๊ด๋ฆฌ ์ ๊ณต
- ๊ฒฝ๋ก ๊ธฐ๋ฐ ๊ตฌ๋
์ ํตํ ํจ์จ์ ์ธ ์
๋ฐ์ดํธ ์ง์
- ๊ณ์ฐ๋ ์์ฑ๊ณผ ๋น๋๊ธฐ ์์
์ง์
Q: MVVM ํจํด์ ์ฌ์ฉํ๋ ์ด์ ๋ ๋ฌด์์ธ๊ฐ์?
A: MVVM ํจํด์ ๋ค์๊ณผ ๊ฐ์ ์ด์ ์ ์ ๊ณตํฉ๋๋ค:
- ๋ทฐ์ ๋น์ฆ๋์ค ๋ก์ง ๊ฐ์ ๋ช
ํํ ๊ด์ฌ์ฌ ๋ถ๋ฆฌ
- ๋น์ฆ๋์ค ๋ก์ง์ ๋ ๋์ ํ
์คํธ ๊ฐ๋ฅ์ฑ
- ๋ ์ ์ง๋ณด์ ๊ฐ๋ฅํ๊ณ ํ์ฅ ๊ฐ๋ฅํ ์ฝ๋ ๊ตฌ์กฐ
- ๋ณต์กํ ์ ํ๋ฆฌ์ผ์ด์
์์ ๋ ์ฌ์ด ์ํ ๊ด๋ฆฌ
Q: Redux๋ MobX ๋์ x-view-model์ ์ ํํ๋ ์ด์ ๋ ๋ฌด์์ธ๊ฐ์?
A: x-view-model์ ๋ค์์ ์ ๊ณตํฉ๋๋ค:
- ๋ ๊ฐ๋จํ API์ ๋ ์ ์ ๋ณด์ผ๋ฌํ๋ ์ดํธ
- ๊ธฐ๋ณธ์ ์ผ๋ก ๋ ๋์ TypeScript ์ง์
- ๋ ์์ ๋ฒ๋ค ํฌ๊ธฐ
- ๋ ์ง๊ด์ ์ธ ์ํ ๊ด๋ฆฌ
- ์ต์ ํ๋ ์
๋ฐ์ดํธ๋ฅผ ํตํ ๋ ๋์ ์ฑ๋ฅ
์ฑ๋ฅ
Q: ํ๋ก๋์
์์ ์ฑ๋ฅ์ ์ด๋ ํ๊ฐ์?
A: x-view-model์ ํ๋ก๋์
์ฌ์ฉ์ ์ต์ ํ๋์ด ์์ต๋๋ค:
- ์ต์ํ์ ๋ฆฌ๋ ๋๋ง์ผ๋ก ํจ์จ์ ์ธ ์
๋ฐ์ดํธ
- ์์ ๋ฒ๋ค ํฌ๊ธฐ (~13.5KB ์์ถ)
- ๋ ๋์ ์ฑ๋ฅ์ ์ํ ์ ๋ก ์์กด์ฑ
- ์์ ์ ํ๋ฆฌ์ผ์ด์
๊ณผ ํฐ ์ ํ๋ฆฌ์ผ์ด์
๋ชจ๋์ ์ต์ ํ
Q: ๋๊ท๋ชจ ์ ํ๋ฆฌ์ผ์ด์
์์ ์ ์๋ํ๋์?
A: ๋ค, x-view-model์ ํ์ฅ์ ์ํด ์ค๊ณ๋์์ต๋๋ค:
- ํจ์จ์ ์ธ ์
๋ฐ์ดํธ๋ฅผ ์ํ ๊ฒฝ๋ก ๊ธฐ๋ฐ ์ํ ์ ํ
- ํ์ ์ํ๋ฅผ ์ํ ๊ณ์ฐ๋ ์์ฑ
- ๋ ๋์ ์ฝ๋ ๊ตฌ์ฑ์ ์ํ ๋ชจ๋์ ์ํคํ
์ฒ
- ๋ ๋์ ์ ์ง๋ณด์์ฑ์ ์ํ ํ์
์์ ํ ์ํ ๊ด๋ฆฌ
Q: ๋ฉ๋ชจ๋ฆฌ ์ฌ์ฉ๋์ ์ด๋ ํ๊ฐ์?
A: ๋ฉ๋ชจ๋ฆฌ ์ฌ์ฉ๋์ ๋ค์์ ํตํด ์ต์ ํ๋ฉ๋๋ค:
- ํจ์จ์ ์ธ ์ํ ์
๋ฐ์ดํธ
- ์ค๋งํธ ๊ฐ๋น์ง ์ปฌ๋ ์
- ์ํ ๊ด๋ฆฌ์์ ์ต์ํ์ ์ค๋ฒํค๋
- ๋ถํ์ํ ๋ฆฌ๋ ๋๋ง ์์
TypeScript
Q: TypeScript ์์ด ์ฌ์ฉํ ์ ์๋์?
A: ๋ค, x-view-model์ ์ผ๋ฐ JavaScript์์๋ ์๋ํ์ง๋ง ๋ค์์ ๋์น๊ฒ ๋ฉ๋๋ค:
- ํ์
์์ ์ฑ
- ๋ ๋์ IDE ์ง์
- ๋ ์ฌ์ด ๋ฆฌํฉํ ๋ง
- ํ์
์ ํตํ ๋ ๋์ ๋ฌธ์ํ
Q: ๋ณต์กํ ํ์
์ ์๋ ์ด๋ป๊ฒ ์ฒ๋ฆฌํ๋์?
A: ๋ณต์กํ ํ์
์ ๊ฒฝ์ฐ:
- ๊ฐ๋
์ฑ์ ์ํ ํ์
๋ณ์นญ ์ฌ์ฉ
- TypeScript์ ์ ํธ๋ฆฌํฐ ํ์
ํ์ฉ
- ๋ณต์กํ ํ์
์ ๋ ์์ ์ธํฐํ์ด์ค๋ก ๋ถํด
- ์ฌ์ฌ์ฉ ๊ฐ๋ฅํ ์ปดํฌ๋ํธ์ ์ ๋ค๋ฆญ ์ฌ์ฉ
Q: ์ ๋ค๋ฆญ ํ์
์ฌ์ฉ์ ๋ํ ํ์ด ์๋์?
A: ์ ๋ค๋ฆญ์ ์ฌ์ฉํ ๋:
- ๋ช
ํํ ํ์
์ ์ฝ ์กฐ๊ฑด ์ ์
- ๊ฐ๋ฅํ ๋ ํ์
์ถ๋ก ์ฌ์ฉ
- ์ ๋ค๋ฆญ ํ์
๋งค๊ฐ๋ณ์ ๋ฌธ์ํ
- ๋ค๋ฅธ ํ์
๋งค๊ฐ๋ณ์๋ก ํ
์คํธ
์ํ ๊ด๋ฆฌ
Q: ์ ์ญ ์ํ์ ์ง์ญ ์ํ๋ฅผ ์ด๋ป๊ฒ ๊ตฌ๋ถํ๋์?
A: ๋ชจ๋ฒ ์ฌ๋ก:
- ๊ณต์ ๋ฐ์ดํฐ์๋ ์ ์ญ ์ํ ์ฌ์ฉ
- ์ปดํฌ๋ํธ๋ณ ๋ฐ์ดํฐ์๋ ์ง์ญ ์ํ ์ฌ์ฉ
- ๋ค๋ฅธ ๊ด์ฌ์ฌ์ ๋ํด ์ฌ๋ฌ ๋ทฐ ๋ชจ๋ธ ์ฌ์ฉ ๊ณ ๋ ค
- ํจ์จ์ ์ธ ์
๋ฐ์ดํธ๋ฅผ ์ํ ๊ฒฝ๋ก ๊ธฐ๋ฐ ์ ํ ์ฌ์ฉ
Q: ๋น๋ฒํ ์ํ ์
๋ฐ์ดํธ๋ฅผ ์ด๋ป๊ฒ ์ต์ ํํ๋์?
A: ์ต์ ํ ์ ๋ต:
- ๊ฒฝ๋ก ๊ธฐ๋ฐ ์ ํ ์ฌ์ฉ
- ๋น ๋ฅธ ์
๋ฐ์ดํธ์ ๋๋ฐ์ด์ฑ ๊ตฌํ
- ํ์ ์ํ์ ๊ณ์ฐ๋ ์์ฑ ์ฌ์ฉ
- ์
๋ฐ์ดํธ ๋ฐฐ์น ๊ณ ๋ ค
Q: ์ค์ฒฉ๋ ์ํ๋ฅผ ์ด๋ป๊ฒ ํจ์จ์ ์ผ๋ก ๊ด๋ฆฌํ๋์?
A: ์ค์ฒฉ๋ ์ํ์ ๊ฒฝ์ฐ:
- ๊ฒฝ๋ก ๊ธฐ๋ฐ ์ ํ ์ฌ์ฉ
- ์ ์ ํ ํ์
์ ์ ๊ตฌํ
- ํ์ ๊ฐ์ ๊ณ์ฐ๋ ์์ฑ ์ฌ์ฉ
- ๊น๊ฒ ์ค์ฒฉ๋ ์ํ๋ฅผ ํํํํ๋ ๊ฒ ๊ณ ๋ ค
๋น๋๊ธฐ ์์
Q: ๋น๋๊ธฐ ์์
์ ์ฒ๋ฆฌํ๋ ๊ฐ์ฅ ์ข์ ๋ฐฉ๋ฒ์ ๋ฌด์์ธ๊ฐ์?
A: ๊ถ์ฅ ์ ๊ทผ ๋ฐฉ์:
- async ํ๋๊ทธ์ ํจ๊ป
send
ํจ์ ์ฌ์ฉ
- ์ ์ ํ ์ค๋ฅ ์ฒ๋ฆฌ ๊ตฌํ
- ๋ ๋์ UX๋ฅผ ์ํ ๋ก๋ฉ ์ํ ์ฌ์ฉ
- ๋ ๊น๋ํ ์ฝ๋๋ฅผ ์ํด async/await ์ฌ์ฉ ๊ณ ๋ ค
Q: ์ค๋ฅ๋ ์ด๋ป๊ฒ ์ฒ๋ฆฌํ๋์?
A: ์ค๋ฅ ์ฒ๋ฆฌ ๋ชจ๋ฒ ์ฌ๋ก:
- try/catch ๋ธ๋ก ์ฌ์ฉ
- ์ ์ ํ ์ค๋ฅ ๊ฒฝ๊ณ ๊ตฌํ
- ์๋ฏธ ์๋ ์ค๋ฅ ๋ฉ์์ง ์ ๊ณต
- ๋ทฐ ๋ชจ๋ธ์์ ์ค๋ฅ ์ํ ์ฌ์ฉ ๊ณ ๋ ค
Q: ๋ก๋ฉ ์ํ๋ ์ด๋ป๊ฒ ๊ด๋ฆฌํ๋์?
A: ๋ก๋ฉ ์ํ ๊ด๋ฆฌ:
- ์ํ์์ ๋ถ๋ฆฌ์ธ ํ๋๊ทธ ์ฌ์ฉ
- ๋ก๋ฉ ํ์๊ธฐ ๊ตฌํ
- ๋ก๋ฉ ํ ์ฌ์ฉ ๊ณ ๋ ค
- UI ์ปดํฌ๋ํธ์์ ๋ก๋ฉ ์ํ ์ฒ๋ฆฌ
ํ
์คํธ
Q: ๋ทฐ ๋ชจ๋ธ์ ์ด๋ป๊ฒ ํ
์คํธํ๋์?
A: ํ
์คํธ ์ ๋ต:
- ๋น์ฆ๋์ค ๋ก์ง ๋จ์ ํ
์คํธ
- ์์กด์ฑ ๋ชจํน
- ์ํ ์
๋ฐ์ดํธ ๊ฒ์ฆ
- ๊ณ์ฐ๋ ์์ฑ ํ์ธ
- ๋น๋๊ธฐ ์์
ํ
์คํธ
Q: ํ
์คํธ ํ๊ฒฝ์ ์ด๋ป๊ฒ ์ค์ ํ๋์?
A: ํ
์คํธ ์ค์ :
- Jest ๋๋ ์ ํธํ๋ ํ
์คํธ ํ๋ ์์ํฌ ์ฌ์ฉ
- React ์์กด์ฑ ๋ชจํน
- ์ ์ ํ TypeScript ๊ตฌ์ฑ ์ค์
- ํ
์คํธ ์ ํธ๋ฆฌํฐ ๊ตฌํ
Q: ๋ชจํน์ ์ด๋ป๊ฒ ์ฒ๋ฆฌํ๋์?
A: ๋ชจํน ์ ๊ทผ ๋ฐฉ์:
- ์ธ๋ถ ์์กด์ฑ ๋ชจํน
- ์์กด์ฑ ์ฃผ์
์ฌ์ฉ
- ์ ์ ํ ํ
์คํธ ํฝ์ค์ฒ ๊ตฌํ
- ํ
์คํธ ํฉํ ๋ฆฌ ์ฌ์ฉ ๊ณ ๋ ค
๋ง์ด๊ทธ๋ ์ด์
Q: Redux์์ ์ด๋ป๊ฒ ๋ง์ด๊ทธ๋ ์ด์
ํ๋์?
A: ๋ง์ด๊ทธ๋ ์ด์
๋จ๊ณ:
- Redux ์คํ ์ด ์ฌ๋ผ์ด์ค ์๋ณ
- ํด๋น ๋ทฐ ๋ชจ๋ธ ์์ฑ
- Redux ์ฌ์ฉ์ ์ ์ง์ ์ผ๋ก ๊ต์ฒด
- ์ปดํฌ๋ํธ๋ฅผ ๋ทฐ ๋ชจ๋ธ ์ฌ์ฉ์ผ๋ก ์
๋ฐ์ดํธ
- Redux ์์กด์ฑ ์ ๊ฑฐ
Q: ๊ธฐ์กด ์ฝ๋๋ฅผ ์ด๋ป๊ฒ ๋ฆฌํฉํ ๋งํ๋์?
A: ๋ฆฌํฉํ ๋ง ์ ๊ทผ ๋ฐฉ์:
- ์๊ณ ๊ฒฉ๋ฆฌ๋ ์ปดํฌ๋ํธ๋ก ์์
- ๋น์ฆ๋์ค ๋ก์ง์ ๋ํ ๋ทฐ ๋ชจ๋ธ ์์ฑ
- ์ปดํฌ๋ํธ๋ฅผ ๋ทฐ ๋ชจ๋ธ ์ฌ์ฉ์ผ๋ก ์
๋ฐ์ดํธ
- ๊ฐ ๋ณ๊ฒฝ ํ ์ฒ ์ ํ ํ
์คํธ
- ๋ฆฌํฉํ ๋ง์ ์ ์ง์ ์ผ๋ก ํ์ฅ
Q: ์ ์ง์ ๋ง์ด๊ทธ๋ ์ด์
์ด ๊ฐ๋ฅํ๊ฐ์?
A: ๋ค, x-view-model์ ๋ค์์ ์ง์ํฉ๋๋ค:
- ์ ์ง์ ์ฑํ
- ๋ค๋ฅธ ์ํ ๊ด๋ฆฌ์์ ๊ณต์กด
- ๋จ๊ณ๋ณ ๋ง์ด๊ทธ๋ ์ด์
- ์ ํ ๊ธฐ๊ฐ ๋์ ๋ณ๋ ฌ ์ฌ์ฉ
์ปค๋ฎค๋ํฐ
Q: ์ด๋์ ๋์์ ๋ฐ์ ์ ์๋์?
A: ์ง์ ์ฑ๋:
- GitHub ์ด์
- GitHub ํ ๋ก
- ๋ฌธ์
- ์ปค๋ฎค๋ํฐ ํฌ๋ผ
Q: ์ด๋ป๊ฒ ๊ธฐ์ฌํ ์ ์๋์?
A: ๊ธฐ์ฌ ์ต์
:
- ๋ฒ๊ทธ ๋ณด๊ณ
- ๊ธฐ๋ฅ ์ ์
- ๋ฌธ์ ๊ฐ์
- Pull Request ์ ์ถ
- ์์ ๊ณต์
Q: ๋ฒ๊ทธ๋ฅผ ๋ฐ๊ฒฌํ๋ฉด ์ด๋ป๊ฒ ํด์ผ ํ๋์?
A: ๋ฒ๊ทธ ๋ณด๊ณ ๋จ๊ณ:
- ๊ธฐ์กด ์ด์ ํ์ธ
- ์ต์ ์ฌํ ์์ฑ
- ์์ธํ ์ ๋ณด ์ ๊ณต
- ๋ฒ๊ทธ ๋ณด๊ณ ์ ์ ์ถ
seokhwan.kim์ด ๋ง๋ โค๏ธ