스콘 리뉴얼 백오피스 개발

스콘 북카페(Web)–스콘 앱(App) 연동 기반의 디지털 교재 구매 서비스를 운영하기 위한 어드민 및 파트너 사이트 개발에 참여했습니다.

프로젝트 기간
사용 기술
TypeScript
TypeScript
Next.js
Next.js
Zustand
React Query
React Query
AWS Amplify
Ant Design
Ant Design
shadcn/ui
shadcn/ui
Tailwind CSS
Tailwind CSS
상세 내용

프로젝트 개요

스콘 북카페(Web)와 스콘 앱(App)이 연동된 디지털 교재 구매 서비스를 운영하기 위한 어드민 및 파트너 사이트 개발 프로젝트입니다. Next.js 기반으로 구축했으며, 현대적인 상태 관리와 폼 검증 시스템을 도입했습니다.

어드민은 빠른 개발을 위해 Ant Design을 사용했고, 파트너 사이트는 브랜드 톤에 맞추기 위해 shadcn/ui를 커스터마이징해 적용했습니다.


프로젝트 규모 및 기여도

총 43개 화면(상품/사용자/주문/정산/인증/대시보드) 을 개발 및 개선하였습니다.

주요 역할 및 기여

서버 상태 관리 현대화

  • Zustand → React Query 마이그레이션
    • 클라이언트 상태에 섞여있던 API 호출/캐싱 로직을 서버 상태 관리로 분리
    • React Query 기반으로 데이터 패칭 패턴 표준화
    • 캐싱, 리페칭, 낙관적 업데이트 등 서버 상태 관리 최적화

Server Actions 기반 등록 플로우 구축

  • 서버 중심 데이터 처리
    • Server Actions로 등록 플로우를 서버 측으로 재구성
    • Zod 스키마를 단일 소스(Single Source of Truth)로 활용
    • 검증, 에러 처리, 타입을 통합 관리하여 유지보수성 향상

재사용 가능한 폼 시스템 구축

  • react-hook-form 기반 Form 컴포넌트
    • 재사용 가능한 Form 컴포넌트 설계 및 작성
    • Controlled/Uncontrolled 컴포넌트 통합 관리
    • 검증 로직과 UI의 명확한 분리

리치 텍스트 편집 기능 구현

  • Tiptap 에디터 통합
    • 어드민 내 콘텐츠 등록 및 편집을 위한 리치 텍스트 에디터 구현
    • 이미지 업로드, 링크 삽입 기능 등 다양한 기능 지원

배포 자동화

  • AWS Amplify 활용

기술적 도전 및 성과

1. API 상태 관리 개선 (Zustand → React Query)

문제 인식

기존에 Zustand로 API 응답을 store에 담아 쓰는 방식으로 개발되어있었습니다.
화면이 늘어나면서, API 붙일 때마다 같은 패턴의 코드(useEffect + fetch + loading/error 처리) 를 계속 복붙하게 됐고,
어디에서 언제 호출되는지도 한눈에 파악하기가 어려워졌습니다.

// 기존 Zustand 기반
function OrderList() {
  // store안에서 fetch 처리 및 상태(loading/error)등을 처리 코드를 반복적으로 작성
  const { orders, fetchOrders, loading } = useOrderStore()
 
  useEffect(() => {
    fetchOrders()
  }, [fetchOrders])
 
  if (loading) return <Spinner />
  return <OrderTable data={orders} />
}

해결 방법

API 응답은 “서버 상태”로 보고, React Query로 책임을 분리했습니다. 또한 호출 로직을 화면에 흩뿌리지 않도록, 도메인 단위로 query/mutation을 모아두는 구조로 정리했습니다.

  • 서버 상태: React Query로 관리 (data / isLoading / error / 캐시)
  • 클라이언트 상태: 필요한 것만 Zustand(또는 로컬 state)에 남김
  • API 로직은 FSD 구조로 도메인별 분리하였습니다. e.g. /features/[domain]/api/queries.ts, mutations.ts
// 개선 코드
 
// /features/order/api/queries.ts
const useGetOrderList = useQuery({
    queryKey: ['orders'],
    queryFn: api.getOrders,
    staleTime: 5 * 60 * 1000
})
 
// /features/order/ui/OrderList.tsx
function OrderList() {
  const { data: orders, isLoading } = useGetOrderList()
 
  if (isLoading) return <Spinner />
  return <OrderTable data={orders} />
}

결과 및 임팩트

  • 주문 내역 컴포넌트 기준으로 36줄 → 11줄로 줄어서, 화면 코드가 “렌더링”에 더 집중할 수 있게 됐습니다.
  • 캐싱/중복 요청 처리/백그라운드 갱신 같은 부분을 직접 구현하지 않아도 돼서, API 연결할 때마다 만들던 공통 코드가 사라졌습니다.

2. Server Actions + Zod 기반 Type-Safe 데이터 처리

문제 인식

폼이 많아지면서 “검증을 어디서 어떻게 관리할지”가 점점 부담이 됐습니다. 클라이언트에서는 에러 문구를 보여주기 위해 한 번 검증하고, Server Actions에서도 API 요청 전에 같은 검증을 다시 수행하는 구조라 규칙을 수정할 때마다 두 군데를 같이 건드려야 했고, 에러 처리도 화면마다 달라졌습니다.

if (!password) return '비밀번호를 입력해주세요'
if (password.length < 8) return '8자 이상 입력해주세요'
if (password !== repassword) return '비밀번호가 일치하지 않습니다'
// 서버에서도 비슷한 검증을 또 구현...

해결 방법

검증 규칙을 “한 곳”에서 관리하고, 그 결과를 클라이언트/서버가 같이 쓰도록 구조를 바꿨습니다.

  • Zod 스키마를 기준으로 타입 + 클라이언트 검증 + 서버 검증을 같이 사용
  • 필드 간 조건(비밀번호/확인)은 refine()로 한 번에 표현
  • Server Actions에서 발생한 Zod 에러/API 에러를 폼에서 바로 쓸 수 있는 형태로 변환해서 내려주도록 정리
// /features/provider/lib/schema.ts
const account = z
  .object({
    uid: z.string().min(1, '아이디를 입력해주세요.'),
    password: z.string().min(12, '12자 이상 입력해주세요.'),
    repassword: z.string().min(1, '비밀번호 확인을 입력해주세요.')
  })
  .refine(data => data.password === data.repassword, {
    path: ['repassword'],
    message: '비밀번호가 일치하지 않습니다.'
  }) satisfies z.ZodType<TAccount>
 
// /features/provider/lib/actions.ts
export const createAccount = async (_, payload) => {
  try {
    const validated = accountSchema.parse(payload)
    const result = await fetcher('/providers', {
      method: 'POST',
      body: JSON.stringify(validated)
    })
    return { success: true, result }
  } catch (err) {
    // Zod 에러 → 폼에서 바로 쓸 수 있게 변환
    if (err instanceof z.ZodError)
      return { success: false, fields: zodErr2FieldErrors(err) }
 
    // API 에러도 특정 필드로 매핑
    if (err.errorCode === 'UID_DUPLICATE')
      return { success: false, errors: { uid: '중복된 아이디입니다.' } }
  }
}

결과 및 임팩트

  • 검증 규칙을 바꿀 때 클라이언트/서버를 따로 고칠 필요가 없어져 수정 누락이 줄었습니다.
  • “비밀번호 확인”처럼 필드 간 의존성이 있는 검증도 스키마에서 한 번에 정의할 수 있어 화면 코드가 단순해졌습니다.
  • 서버에서 내려오는 에러를 폼에서 그대로 쓸 수 있게 맞춰두어, 에러 표시 방식이 화면마다 달라지는 문제가 줄었습니다.
  • satisfies z.ZodType<T>로 스키마와 타입이 어긋나면 컴파일 단계에서 잡히도록 해, 변경 시 안정성이 좋아졌습니다.

3. 재사용 가능한 폼 시스템 구축 (Ant Design Form → react-hook-form)

문제 인식

어드민/파트너 페이지에서 폼이 계속 늘어나면서 Ant Design Form.Item 기반 구현에 한계를 느꼈습니다.
특히 시작일/종료일처럼 필드 간 관계 검증이나 비밀번호 정책 오류를 확인 필드에도 함께 노출하는 UX를 맞추려다 보니 로직이 복잡해지고 재사용이 어려웠습니다.

// 기존: Ant Design Form (장황하고 복잡한 에러 처리)
const [form] = Form.useForm()
 
<Form form={form}>
  <Form.Item
    name="email"
    rules={[{ required: true, message: '이메일을 입력하세요' }]}
  >
    <Input placeholder="test@example.com" />
  </Form.Item>
</Form>

해결 방법

  • react-hook-form 기반으로 공통 Form 래퍼 컴포넌트를 설계하고, Ant Design Input/DatePicker 등을 Controller로 감싼 Form.Input, Form.DatePicker 형태로 표준화했습니다.
  • 기본 검증은 Zod + zodResolver로 통일하되, 위처럼 스키마로 표현이 애매한 케이스(필드 간 의존/복수 위치 에러 노출/특정 필드에만 에러 표시)는 react-hook-form의 setError / clearErrors를 사용해 유연하게 처리할 수 있도록 분리했습니다.
// 개선: react-hook-form + Ant Design 통합
const FormInput = ({ name, control, label, error, ...props }) => (
  <Field label={label} error={error}>
    <Controller
      name={name}
      control={control}
      render={({ field, fieldState }) => (
        <Input
          {...field}
          {...props}
          status={fieldState.error ? 'error' : undefined}
        />
      )}
    />
  </Field>
)
 
// 사용 예시
const { control, formState: { errors } } = useForm({
  resolver: zodResolver(adminSchema.create),
  mode: 'onChange'
})
 
<Form.Input
  control={control}
  name="email"
  label="계정명"
  error={errors.email?.message}
  placeholder="test@flexcil.com"
/>

결과 및 임팩트

  • 화면에서는 Form.Item 반복 없이 선언적으로 폼을 구성할 수 있게 되었고, 폼 구현 방식이 도메인 전반에서 통일됐습니다.
  • “기간/비밀번호 확인” 같은 필드 간 의존성이 있는 에러 케이스도 한 패턴으로 처리할 수 있어 유지보수가 쉬워졌습니다.

기술 스택

  • Frontend: Next.js, TypeScript, Tailwind CSS, Ant Design, shadcn/ui
  • State Management: Zustand, React Query (TanStack Query)
  • Form & Validation: react-hook-form, Zod
  • Editor: Tiptap
  • Deployment: AWS Amplify