Files
frontend/docs/as/02.curriculum-beginner.md
T

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단계 계층 구조

  1. 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 라이브러리)을 모아둡니다. (의존성 가장 낮음)
  2. Slices (슬라이스): 도메인에 따른 수직적 분리 단위입니다 (예: entities/user, features/auth-login).
  3. 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 형태로 외부에 노출하세요.