스콘 북카페(Web)–스콘 앱(App) 연동 기반의 디지털 교재 구매 서비스를 운영하기 위한 어드민 및 파트너 사이트 개발에 참여했습니다.
프로젝트 기간
사용 기술
TypeScript
Next.js
Zustand
React Query
AWS Amplify
Ant Design
shadcn/ui
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
주문 내역 컴포넌트 기준으로 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.tsconst 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.tsexport 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를 사용해 유연하게 처리할 수 있도록 분리했습니다.