๐Ÿš€ Big News: Socket Acquires Coana to Bring Reachability Analysis to Every Appsec Team.Learn more โ†’
Socket
DemoInstallSign in
Socket

x-view-model

Package Overview
Dependencies
Maintainers
1
Versions
125
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

x-view-model

A lightweight, type-safe MVVM state management solution for React applications. Features reactive updates, computed properties, and deep path selection with minimal bundle size.

3.1.0
Source
npm
Version published
Maintainers
1
Created
Source

x-view-model

npm version License: ISC TypeScript Bundle Size PRs Welcome

React ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ์œ„ํ•œ ๊ฐ€๋ฒผ์šฐ๋ฉด์„œ ํƒ€์ž… ์•ˆ์ „ํ•œ MVVM ์ƒํƒœ ๊ด€๋ฆฌ ์†”๋ฃจ์…˜

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;
  },
});

// ๋ทฐ (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-modelReduxMobXZustand
๋ฒˆ๋“ค ํฌ๊ธฐ~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 }
);

ํ›…

useViewModel

๋ทฐ ๋ชจ๋ธ ์ƒํƒœ์™€ ๋ฉ”์„œ๋“œ์— ์ ‘๊ทผํ•˜๊ธฐ ์œ„ํ•œ ๊ธฐ๋ณธ ํ›…:

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๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋ฉด ํ’€์–ด์„œ ๋ฐ˜ํ™˜
 *   - ๋ฉ”์„œ๋“œ๊ฐ€ ์ง์ ‘ ๊ฐ’์„ ๋ฐ˜ํ™˜ํ•˜๋ฉด ํ•ด๋‹น ๊ฐ’์„ ๋ฐ˜ํ™˜
 */

useMemoizedViewModel

๋ทฐ ๋ชจ๋ธ์—์„œ ํŠน์ • ๊ฒฝ๋กœ๋ฅผ ์„ ํƒํ•˜๊ธฐ ์œ„ํ•œ ์ตœ์ ํ™”๋œ ํ›…:

const [state, send] = useMemoizedViewModel(userVM, [
  "name",
  "profile.avatar",
  "settings.theme",
] as const);

/* useMemoizedViewModel์€ ์ง€์ •๋œ ์ƒํƒœ ๊ฒฝ๋กœ๋งŒ ๊ตฌ๋…ํ•˜๊ณ  ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.
 * ์ด ์˜ˆ์ œ์—์„œ state ๊ฐ์ฒด๋Š” ๋‹ค์Œ๋งŒ ํฌํ•จํ•ฉ๋‹ˆ๋‹ค:
 * - state.name
 * - state.profile.avatar
 * - state.settings.theme
 * ๋‹ค๋ฅธ ์†์„ฑ์€ state ๊ฐ์ฒด์— ํฌํ•จ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.
 */

useComputedViewModel

๋ทฐ ๋ชจ๋ธ ์ƒํƒœ์—์„œ ๊ณ„์‚ฐ๋œ ๊ฐ’์„ ์ƒ์„ฑ:

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
    }
  }
);

์ด ๊ธฐ๋Šฅ์€ ๋‹ค์Œ๊ณผ ๊ฐ™์€ ์ƒํ™ฉ์—์„œ ํŠนํžˆ ์œ ์šฉํ•ฉ๋‹ˆ๋‹ค:

  • ๋””๋ฒ„๊น… ๋ฐ ๋ฌธ์ œ ํ•ด๊ฒฐ
  • ์‚ฌ์šฉ์ž ํ–‰๋™ ๋ถ„์„
  • ์—๋Ÿฌ ์ถ”์  ๋ฐ ๋ชจ๋‹ˆํ„ฐ๋ง
  • ์ƒํƒœ ๋ณ€ํ™” ๊ฐ์‚ฌ(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();
};

// 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

๋ฏธ๋“ค์›จ์–ด ์‚ฌ์šฉ ์‚ฌ๋ก€

  • ๋กœ๊น… ๋ฐ ๋””๋ฒ„๊น…

    • ์ƒํƒœ ๋ณ€๊ฒฝ ์ถ”์ 
    • ์„ฑ๋Šฅ ๋ชจ๋‹ˆํ„ฐ๋ง
    • ๋””๋ฒ„๊ทธ ์ •๋ณด ์ˆ˜์ง‘
  • ๊ฒ€์ฆ ๋ฐ ๋ณด์•ˆ

    • ์ƒํƒœ ๋ณ€๊ฒฝ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ
    • ์ ‘๊ทผ ์ œ์–ด
    • ๋ฐ์ดํ„ฐ ๋ฌด๊ฒฐ์„ฑ ๊ฒ€์ฆ
  • ์ƒํƒœ ๋™๊ธฐํ™”

    • ๋‹ค๋ฅธ ์‹œ์Šคํ…œ๊ณผ์˜ ์ƒํƒœ ๋™๊ธฐํ™”
    • ๋ฐฑ์—”๋“œ 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์„ ์‚ฌ์šฉํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ๋ณด์—ฌ์ค๋‹ˆ๋‹ค:

// 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

์ง€์›

์ž์ฃผ ๋ฌป๋Š” ์งˆ๋ฌธ

์ผ๋ฐ˜์ ์ธ ์งˆ๋ฌธ

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์ด ๋งŒ๋“  โค๏ธ

Keywords

react

FAQs

Package last updated on 24 Apr 2025

Did you know?

Socket

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.

Install

Related posts