실전 예제들
1. 회원가입 폼 (조건부 분기)
미성년자면 보호자 동의 받고, 성인이면 바로 환경설정으로 가는 예제입니다.
tsx
import { useMozard, Entry, MozardStepProps } from 'react-form-mozard';
import { useState } from 'react';
import { useForm } from 'react-hook-form';
// 타입 정의
type Profile = { name: string; age: number; email: string };
type ParentConsent = { parentEmail: string; agreed: boolean };
type Preferences = { newsletter: boolean; theme: 'light' | 'dark' };
type Schema = {
profile: Profile;
parentConsent: ParentConsent;
preferences: Preferences;
};
type Result = {
profile: Profile;
parentConsent?: ParentConsent;
preferences: Preferences;
};
const ProfileForm = (props: MozardStepProps<Profile>) => {
const { register, handleSubmit } = useForm<Profile>();
return (
<form onSubmit={handleSubmit(props.onSubmit)}>
<h2>기본 정보</h2>
<input {...register("name", { required: true })} placeholder="이름" />
<input {...register("age", { required: true, valueAsNumber: true })} placeholder="나이" />
<input {...register("email", { required: true })} placeholder="이메일" />
<button type="submit">다음</button>
</form>
);
};
const ParentConsentForm = (props: MozardStepProps<ParentConsent>) => {
const { register, handleSubmit } = useForm<ParentConsent>();
return (
<form onSubmit={handleSubmit(props.onSubmit)}>
<h2>보호자 동의 (18세 미만)</h2>
<input {...register("parentEmail", { required: true })} placeholder="보호자 이메일" />
<label>
<input {...register("agreed", { required: true })} type="checkbox" />
보호자가 가입에 동의했습니다
</label>
<button type="submit">다음</button>
</form>
);
};
const PreferencesForm = (props: MozardStepProps<Preferences> & { isMinor: boolean }) => {
const { register, handleSubmit } = useForm<Preferences>();
return (
<form onSubmit={handleSubmit(props.onSubmit)}>
<h2>환경설정</h2>
{!props.isMinor && (
<label>
<input {...register("newsletter")} type="checkbox" />
뉴스레터 구독
</label>
)}
<select {...register("theme")}>
<option value="light">라이트 테마</option>
<option value="dark">다크 테마</option>
</select>
<button type="submit">완료</button>
</form>
);
};
export function RegistrationApp() {
const [values, setValue] = useState<Entry<Schema>[]>([]);
const { elements, done, value, get } = useMozard<Schema, Result>({
values,
onNext: setValue,
*do(step) {
const profile = yield* step("profile", ProfileForm, {});
let parentConsent: ParentConsent | undefined;
if (profile.age < 18) {
parentConsent = yield* step("parentConsent", ParentConsentForm, {});
}
const preferences = yield* step("preferences", PreferencesForm, {
isMinor: profile.age < 18
});
return { profile, parentConsent, preferences };
}
}, []);
const profile = get("profile");
const currentStep = values.length + 1;
const totalSteps = profile?.age < 18 ? 3 : 2;
return (
<div>
<div>진행률: {currentStep}/{totalSteps}</div>
{done ? (
<div>
<h1>가입 완료!</h1>
<pre>{JSON.stringify(value, null, 2)}</pre>
<button onClick={() => setValue([])}>다시 시작</button>
</div>
) : (
<div>
{values.length > 0 && (
<button onClick={() => setValue(values.slice(0, -1))}>
이전
</button>
)}
{elements.at(-1)}
</div>
)}
</div>
);
}
2. 동적 아이템 수집
동적 폼 루프를 구현하는 방법을 보여주는 예제입니다:
tsx
type User = { name: string; role: 'user' | 'admin' };
type Item = { title: string; description: string };
type ContinueChoice = { continue: boolean };
const ItemForm = (props: MozardStepProps<Item> & { index: number }) => {
const { register, handleSubmit } = useForm<Item>();
return (
<form onSubmit={handleSubmit(props.onSubmit)}>
<h2>아이템 #{props.index + 1}</h2>
<input {...register("title", { required: true })} placeholder="제목" />
<textarea {...register("description")} placeholder="설명" />
<button type="submit">추가</button>
</form>
);
};
const ContinueForm = (props: MozardStepProps<ContinueChoice> & { itemCount: number }) => {
const { register, handleSubmit } = useForm<ContinueChoice>();
return (
<form onSubmit={handleSubmit(props.onSubmit)}>
<h2>현재 {props.itemCount}개 아이템이 추가되었습니다</h2>
<label>
<input {...register("continue")} type="checkbox" />
더 추가하시겠습니까?
</label>
<button type="submit">계속</button>
</form>
);
};
export function DynamicItemApp() {
const [values, setValue] = useState<Entry<any>[]>([]);
const { elements, done, value } = useMozard({
values,
onNext: setValue,
*do(step) {
const user = yield* step("user", UserForm, {});
const items: Item[] = [];
let shouldContinue = true;
const firstItem = yield* step("item-0", ItemForm, { index: 0 });
items.push(firstItem);
while (shouldContinue) {
const { continue: wantMore } = yield* step(
`continue-${items.length}`,
ContinueForm,
{ itemCount: items.length }
);
if (wantMore) {
const nextItem = yield* step(
`item-${items.length}`,
ItemForm,
{ index: items.length }
);
items.push(nextItem);
} else {
shouldContinue = false;
}
}
return { user, items };
}
}, []);
return (
<div>
{done ? (
<div>
<h1>완료!</h1>
<h2>{value.user.name}님의 아이템들:</h2>
<ul>
{value.items.map((item, i) => (
<li key={i}>{item.title}: {item.description}</li>
))}
</ul>
</div>
) : (
<div>{elements.at(-1)}</div>
)}
</div>
);
}
3. 복잡한 조건부 플로우
사용자 타입에 따른 다중 분기 경로를 보여주는 예제입니다:
tsx
type UserType = { type: 'admin' | 'user'; email: string };
type AdminSettings = { permissions: string[]; department: string };
type UserProfile = { bio: string; interests: string[] };
type Verification = { code: string };
export function ConditionalFlowApp() {
const [values, setValue] = useState<Entry<any>[]>([]);
const { elements, done, value } = useMozard({
values,
onNext: setValue,
*do(step) {
const userType = yield* step("userType", UserTypeForm, {});
if (userType.type === "admin") {
const verification = yield* step("verification", VerificationForm, {});
const adminSettings = yield* step("adminSettings", AdminSettingsForm, {});
return {
type: "admin" as const,
email: userType.email,
verification,
settings: adminSettings
};
} else {
const profile = yield* step("userProfile", UserProfileForm, {});
if (profile.interests.length >= 3) {
const survey = yield* step("survey", SurveyForm, { interests: profile.interests });
return {
type: "user" as const,
email: userType.email,
profile,
survey
};
}
return {
type: "user" as const,
email: userType.email,
profile
};
}
}
}, []);
return (
<div>
{done ? (
<div>
<h1>설정 완료</h1>
<p>사용자 타입: {value.type}</p>
<pre>{JSON.stringify(value, null, 2)}</pre>
</div>
) : (
<div>{elements.at(-1)}</div>
)}
</div>
);
}
공통 패턴과 주의사항
1. 단계 키 명명 규칙
typescript
// ✅ 좋은 예
yield* step("profile", ProfileForm, {});
yield* step("address", AddressForm, {});
yield* step(`item-${index}`, ItemForm, {}); // 반복문에서
// ❌ 피해야 할 예
yield* step("step1", ProfileForm, {}); // 의미 없는 이름
yield* step("item", ItemForm, {}); // 반복문에서 중복 가능
2. 조건부 분기에서 타입 처리
typescript
*do(step) {
const user = yield* step("user", UserForm, {});
if (user.type === "admin") {
const settings = yield* step("adminSettings", AdminSettingsForm, {});
return { user, adminSettings: settings }; // 명시적 키 이름
}
const profile = yield* step("userProfile", UserProfileForm, {});
return { user, userProfile: profile };
}
이러한 예제들은 Mozard의 모나딕 합성이 어떻게 복잡한 폼 플로우를 간결하고 타입 안전하게 표현할 수 있는지 보여줍니다. Generator의 yield*
구문을 통해 각 단계를 자연스럽게 연결하고, JavaScript의 제어 구조(if
, while
, for
)를 그대로 사용할 수 있습니다.