8.0 KiB
8.0 KiB
AS 프로젝트 교육 자료: 초급 과정 (Beginner Course)
대상: React 및 Next.js 기초를 이해하고 있으며, AS 프로젝트에 신규 참여하여 UI 컴포넌트 및 기본 화면 단위를 구현해야 하는 개발자 목표: FSD 아키텍처 규칙을 준수하며 Tailwind CSS와 CVA를 사용해 UI를 개발하고, React Hook Form과 Zod로 폼 컴포넌트를 설계할 수 있다.
1. Feature-Sliced Design (FSD) 아키텍처 기초
FSD는 기능 기반의 디렉토리 설계 규칙으로, 코드의 유지보수성과 확장성을 극대화하기 위해 탄생한 표준 스펙입니다.
① FSD의 3단계 계층 구조
- Layers (레이어): 가장 큰 단위로, 최상단에서 최하단까지 엄격한 단방향 의존성 규칙을 가집니다.
app: 애플리케이션의 설정, Providers, 글로벌 스타일을 정의합니다. (의존성 가장 높음)pages: 실제 라우팅 단위 화면을 구성합니다.widgets: 독립적인 대형 UI 블록(예: 헤더, 복합 사이드바, 특정 복합 그리드 등)을 정의합니다.features: 사용자의 행동 및 비즈니스 기능(예:auth-login,search-box,add-comment)을 다룹니다.entities: 도메인 단위의 비즈니스 개념(예:user,approval-line,budget)과 그에 대한 단순 조회 UI, API, 상태를 정의합니다.shared: 특정 도메인에 종속되지 않는 범용 재사용 기술 자원(UI Kit, API Client, Util 라이브러리)을 모아둡니다. (의존성 가장 낮음)
- Slices (슬라이스): 도메인에 따른 수직적 분리 단위입니다 (예:
entities/user,features/auth-login). - Segments (세그먼트): 기술적 역할에 따른 물리적 파일 분류입니다.
ui/: React 컴포넌트 파일들model/: 스토어 상태, 데이터 타입, 스키마 등api/: 서버 통신용 Axios 함수 및 쿼리 훅lib/: 내부 Helper 유틸 함수
② FSD 수직적 수평적 import 규칙 (중요!)
- 규칙 1 (수직적 규칙): 레이어는 자신보다 하위 레이어만 import할 수 있습니다. 상위 레이어를 import하는 것은 엄격히 금지됩니다. (예:
features는entities를 import할 수 있으나,entities가features나widgets를 import할 수 없음) - 규칙 2 (수평적 규칙): 동일한 레이어 내부의 다른 슬라이스끼리는 서로 직접 import할 수 없습니다. (예:
features/auth-login에서features/post-create를 import할 수 없음. 이 경우에는 상위 레이어인pages나widgets에서 두 슬라이스를 조합해야 함) - 규칙 3 (Public API): 각 슬라이스 외부에서는 반드시 슬라이스 루트의
index.ts(Public API)를 거쳐서만 모듈을 가져와야 합니다. 슬라이스 내부 폴더 구조에 직접 접근하는 행위(import ... from '@/features/auth/ui/LoginForm')는 린트 에러(Steiger경고)를 발생시킵니다.
2. Tailwind CSS & Class Variance Authority (CVA) 실무
AS 프로젝트는 유틸리티 클래스 중복을 최소화하고, 컴포넌트의 디자인 변형(Variants)을 안전하게 관리하기 위해 CVA를 사용합니다.
CVA 활용 예시 (shared/ui/button.tsx)
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/shared/lib"; // clsx와 tailwind-merge 조합 유틸
export const buttonVariants = cva(
"inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-slate-400 disabled:opacity-50",
{
variants: {
variant: {
default: "bg-slate-900 text-white hover:bg-slate-800",
destructive: "bg-red-500 text-white hover:bg-red-600",
outline: "border border-slate-200 bg-transparent hover:bg-slate-100",
ghost: "hover:bg-slate-100 hover:text-slate-950",
},
size: {
default: "h-10 px-4 py-2",
sm: "h-9 rounded-md px-3",
lg: "h-11 rounded-md px-8",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
);
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {}
export function Button({ className, variant, size, ...props }: ButtonProps) {
return (
<button
className={cn(buttonVariants({ variant, size, className }))}
{...props}
/>
);
}
3. React Hook Form & Zod를 활용한 폼 검증
AS 프로젝트는 성능 최적화를 위해 불필요한 리렌더링을 방지하는 React Hook Form과 타입 안전한 스키마 검증기 Zod를 페어로 사용합니다.
① 스키마 정의 (lib/regist-schema.ts 또는 model/schema.ts)
import { z } from "zod";
export const registrationSchema = z.zodObject({
bizNm: z.string().min(2, "사업명은 필수 항목이며 2글자 이상이어야 합니다."),
bgtBizCd: z.string().min(1, "확정예산사업번호를 입력해주세요."),
bgtAplyYr: z.string().regex(/^\d{4}$/, "예산신청연도는 4자리 숫자여야 합니다."),
email: z.string().email("올바른 이메일 형식이 아닙니다."),
privacyConsent: z.boolean().refine((val) => val === true, {
message: "개인정보 제공에 동의하셔야 합니다.",
}),
});
export type RegistrationFormValues = z.infer<typeof registrationSchema>;
② 컴포넌트 구현 및 바인딩
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { registrationSchema, RegistrationFormValues } from "./schema";
export function RegistrationForm() {
const {
register,
handleSubmit,
formState: { errors, isSubmitting },
} = useForm<RegistrationFormValues>({
resolver: zodResolver(registrationSchema),
defaultValues: {
bizNm: "",
bgtBizCd: "",
bgtAplyYr: new Date().getFullYear().toString(),
email: "",
privacyConsent: false,
},
});
const onSubmit = (data: RegistrationFormValues) => {
console.log("Submit Data:", data);
// API 호출 로직은 중급 과정에서 설명
};
return (
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4 p-4 border rounded-lg">
<div>
<label className="block text-sm font-medium">사업명</label>
<input {...register("bizNm")} className="mt-1 block w-full border rounded p-2" />
{errors.bizNm && <p className="text-red-500 text-xs mt-1">{errors.bizNm.message}</p>}
</div>
<div>
<label className="block text-sm font-medium">예산사업번호</label>
<input {...register("bgtBizCd")} className="mt-1 block w-full border rounded p-2" />
{errors.bgtBizCd && <p className="text-red-500 text-xs mt-1">{errors.bgtBizCd.message}</p>}
</div>
<div>
<label className="flex items-center space-x-2">
<input type="checkbox" {...register("privacyConsent")} />
<span className="text-sm">개인정보 수집 동의</span>
</label>
{errors.privacyConsent && <p className="text-red-500 text-xs mt-1">{errors.privacyConsent.message}</p>}
</div>
<button type="submit" disabled={isSubmitting} className="bg-blue-600 text-white p-2 rounded">
등록하기
</button>
</form>
);
}
4. 초급 실습 과제
과제 내용
src/shared/ui하위에 CVA를 활용하여 커스텀 뱃지(Badge) 컴포넌트를 설계하세요. (예:variant옵션으로'success','warning','error','info'상태를 가짐)- 사용자 정보 입력 폼 스키마를
zod로 설계하고,react-hook-form으로 가상의 회원가입 컴포넌트를 만들어보세요. (이메일 검증, 비밀번호 8자 이상 검증 필수) - 제약 조건: 해당 모듈들을 생성할 때 FSD 레이어 규칙(
shared와features레이어 배치 위치)을 준수하고, Steiger 린팅 규칙에 어긋나지 않도록index.ts를 통해 Public API 형태로 외부에 노출하세요.