FE 도메인 테크스펙 ‐ 공통 - 100-hours-a-week/9-team-Devths-WIKI GitHub Wiki
- 여러 도메인에서 반복되는 구현(요청/캐싱/에러처리/폼/모달)을 공통 규칙으로 통일하여 UX 일관성, 버그 감소, 개발 생산성을 확보함
- 핵심 결과 (Key Result) 1: 도메인마다 다른 방식으로 구현되기 쉬운 로딩/에러/토큰 처리를 한 가지 규칙으로 통일함 (Axios 인스턴스/인터셉터 표준)
- 핵심 결과 (Key Result) 2: OCR/AI 업로드 같은 비동기 흐름에서 대기 모달/실패 토스트/완료 모달 UX를 페이지 상관없이 동일하게 제어함 (Zustand 전역 UI 상태)
- 핵심 결과 (Key Result) 3: 버튼/입력/모달/토스트 등 공통 UI를 재사용해 디자인 일관성 + 개발 속도 확보 (shadcn/ui)
- 핵심 결과 (Key Result) 4: 첨부 파일 업로드 절차가 모든 도메인에서 동일한 파이프라인을 사용함 (Presigned URL 발급 → S3 직접 업로드 → 파일 메타 저장)
- Devths는 캘린더/게시판/채팅/챗봇 등 기능이 많아 인증/에러/로딩/첨부가 여러 화면에 반복됨
- 이것을 화면마다 다르게 구현하면 UX 편차와 버그가 누적됨
- Common 도메인에서 전역 UI(Zustand), 서버 캐시(TanStack Query), 네트워크(Axios), 파일 업로드 훅을 표준화하면 도메인 개발자는 비즈니스 로직에만 집중할 수 있고, UX가 안정됨
- 공통 API 명세(프리사인드/메타 저장/에러 응답)
- Zustand/axios 표준화 근거(전역 UI 상태, 인터셉터 기반 공통 처리)
- 도메인 고유 정책을 Common으로 흡수하지 않음
- 게시판 검색정책, 채팅 읽음처리 정책, 캘린더 세부 모델 규칙 등
- 서버가 파일을 직접 받아 중계하는 업로드 방식은 다루지 않음
- Devths는 FE→S3 직접 업로드 분리를 전제로 함
- 디자인 시스템을 새로 구축하지 않음
- shadcn/ui 기반으로 필요한 공통 UI만 구축
-
프레임워크/라이브러리
- React + Next.js
- App Router 기반으로 라우팅/레이아웃을 표준화할 수 있어 화면 수가 많은 Devths에서 구조를 일관되게 유지
- 페이지 성격에 맞게 SSG/CSR을 선택해 초기 로딩 UX(랜딩)와 인터랙션 중심 화면(채팅/캘린더)을 균형 있게 구성할 수 있음
- React + Next.js
-
상태 관리
- Zustand
- 전역 UI 상태(모달/토스트/로딩/드로어 등)를 store로 단순하게 관리
- 필요한 상태만 선택 구독하여 불필요한 리렌더링 최소화
- Zustand
-
UI 컴포넌트
- shadcn/ui
- Tailwind 기반으로 컴포넌트를 빠르게 구성 + 커스터마이징 가능
- 공통 UI를 표준화(버튼/다이얼로그/토스트/폼)를 팀 규칙으로 강제하기 좋음
- shadcn/ui
-
스타일링
- Tailwind CSS
- 화면이 많아도 클래스 기반으로 빠르게 적용 가능
- 컴포넌트 단위 응집도 높음
- Tailwind CSS
-
폼 관리
- React Hook Form + Zod
- 폼이 많은 서비스이기에 입력 상태/검증/에러 표시 표준화 필요성
- Zod 스키마로 검증 규칙을 한 곳에 모아 타입과 검증의 불일치를 줄일 수 있음
- React Hook Form + Zod
-
데이터 페칭
- TanStack Query
- 서버 데이터가 많은 화면에서 캐시 기반으로 중복 요청을 줄이고, 로딩/에러/성공 상태를 일관되게 관리 가능
- TanStack Query
-
API 통신
- Axios
- baseURL/헤더/인터셉터를 한 곳에서 표준화해 API 호출 방식이 화면마다 달라지는 것을 방지함
- Axios
-
실시간
- STMOP + WebSocket
- Pub/Sub 기반이라 1:1/그룹 채팅, 읽음 처리, 입장/퇴장 이벤트 확장에 유리
- Spring 기반 서버와 궁합이 좋음
- STMOP + WebSocket
-
CSR/SSG 적용 범위
-
/(랜딩) : SSG- 정적 콘텐츠 중심 → 빌드 시 HTML 생성 → 빠른 초기 로딩
-
/auth/*(OAuth 콜백) : CSR- 사용자마다 매번 다른 인가 코드/쿼리를 받아 처리해야 하므로 정적 생성 불가
- 브라우저에서 쿼리 파싱 → 서버 토큰 교환 API 호출 → 로그인 상태 설정 → 리다이렉트 흐름이 자연스러움
-
/signup: CSR- 입력/업로드가 핵심인 폼 화면
-
/calendar,/board,/chat,/profile등 메인 기능 페이지 : CSR- 로그인 이후 사용자 개인화 데이터(일정/글/채팅/알림) 기반 + 필터/무한스크롤/실시간 업데이트 등 클라이언트 상태 변화가 많음
-
- 공개 영역(로그인 전): 랜딩
/, 로그인/login, 회원가입/signup, OAuth 콜백/auth/callback - 보호 영역(로그인 후):
/calendar,/board,/chat,/llm,/notifications,/profile
-
라우팅 그룹
-
app/(public)/: 랜딩/, 로그인/login, 회원가입/signup, OAuth 콜백/auth/callback -
app/(app)/: 보호 페이지/calendar,/board,/chat,/llm,/notifications,/profile
-
-
공통 레이아웃(AppShell) 적용 구간
-
app/(app)/layout.tsx에서 공통 레이아웃 + 전역 Provider + 인증 가드(AuthGate)를 한 번에 적용
-
-
AppShell에서 하는 일
- 공통 UI 레이아웃
-
Header(상단 고정: Devths 로고 + 알림 아이콘) -
BottomNav(하단 고정: 홈/피드/AI/채팅/프로필) -
main컨텐츠 영역(페이지별 children 렌더링)
-
- Provider 장착
-
QueryClientProvider(TanStack Query) -
Toaster(토스트를 쓰는 경우에만)
-
- 전역 UI Host
-
GlobalLoadingHost(전역 로딩 모달) -
GlobalInfoModalHost(전역 안내 모달) -
GlobalConfirmModalHost(전역 확인 모달)
-
-
AuthGate- 보호 페이지 접근 시 로그인 상태 확인
- 미로그인/만료 상태면 로그인 페이지 이동
- 공통 UI 레이아웃
-
주요 기능
- 서비스 소개 슬라이드(온보딩)
- 최대 5개 슬라이드
- 사용자가 스와이프해서 넘길 수 있음
- 자동 재생: 5초마다 다음 슬라이드로 이동(마지막 슬라이드 후 1번으로 순환)
- 현재 위치를 도트 네비게이션으로 표시(도트 클릭 시 해당 슬라이드로 이동)
- 구글 OAuth로그인 시작
-
Sign in with Google버튼 클릭 시 OAuth 플로우 시작 - OAuth 성공 시
/auth/callback로 이동
-
- 로그인 실패 안내
- OAuth 실패/취소/네트워크 오류 시 전역
InfoModal로 안내 - 로그인 처리 시간이 길어질 경우 전역
LoadingModal로 ‘로그인 처리 중’ 표시
- OAuth 실패/취소/네트워크 오류 시 전역
- 서비스 소개 슬라이드(온보딩)
-
사용 컴포넌트
- 페이지 구성 컴포넌트
-
OnboardingCarousel(슬라이드 영역) -
DotPagination(도트 네비게이션) -
GoogleLoginButton(구글 로그인 버튼 UI)
-
- 공통 컴포넌트 (재사용)
-
InfoModal(전역) -
LoadingModal(로그인 요청이 길어질 경우 “로그인 처리 중” 표시)
-
- 페이지 구성 컴포넌트
-
데이터 로딩 시점
- 최초 진입 시 별도 API 호출 없음
- 버튼 클릭 시에만 OAuth 시작
-
/api/auth/google호출은/auth/callback페이지에서 수행(인가 코드가 그때 생김)
-
-
라우팅
-
/login→ (사용자가 Google 로그인 클릭)
→ Google OAuth 페이지
→ (성공)
/auth/callback?code=...→
/api/auth/google로 code 교환→ (회원)
/calendar→ (신규)
/signup
-
-
내부 상태 & 이벤트
-
currentIndex(현재 슬라이드 인덱스) - 자동 재생 타이머(5초): 페이지 이탈/언마운트 시 clear
- 스와이프/도트 클릭 시
currentIndex업데이트 - 로그인 버튼 클릭 시 OAuth 리다이렉트 시작
-
-
에러 처리 / 엣지 케이스
- 이미 로그인 상태로
/login접근- UX를 위해
/calendar로 리다이렉트 처리
- UX를 위해
- 네트워크 오류/서버 오류(5xx)
-
InfoModal: 로그인에 실패했습니다. 잠시 후 다시 시도해 주세요
-
- 이미 로그인 상태로
-
주요 기능
- 헤더/하단탭 유지
- 상단
Header(로고 + 알림 아이콘) 고정 - 하단
BottomNav(홈/피드/AI/채팅/프로필) 고정
- 상단
-
main(children)영역에서 각 도메인 페이지(캘린더/게시판/채팅/AI/알림/프로필) 렌더링 - 전역 UX 일관성 유지
- 도메인과 상관없이 로딩/안내/확인은 전역 모달로 통일
- 긴 작업:
LoadingModal - 결과 안내:
InfoModal - 사용자 선택 필요:
ConfirmModal
- 긴 작업:
- 도메인과 상관없이 로딩/안내/확인은 전역 모달로 통일
- 헤더/하단탭 유지
-
사용 컴포넌트
- 공통 레이아웃 컴포넌트
-
Header,BottomNav
-
- 전역 UI Host (AppShell에서 1회 렌더)
-
GlobalLoadingHost→LoadingModal -
GlobalInfoModalHost→InfoModal -
GlobalConfirmModalHost→ConfirmModal
-
- 공통 레이아웃 컴포넌트
-
데이터 로딩 시점
- AppShell 진입 시
AuthGate에서 내 정보 조회로 로그인 여부 판단- 기준 API:
GET /api/users/me
- 기준 API:
- 성공 시
- 보호 페이지 렌더링 허용
- 실패 시(미로그인/만료)
-
/login으로 리다이렉트 - 이동 전에
InfoModal로 ‘로그인이 필요합니다’ 안내 후 이동
-
- AppShell 진입 시
-
라우팅
- 보호 영역 페이지
-
/calendar,/board,/chat,/llm,/notifications,/profile등
-
- 보호 영역 페이지
-
내부 상태 & 이벤트
- AuthGate 상태
-
isCheckingAuth(인증 확인 중) -
isAuthed(로그인 여부)
-
- AuthGate 상태
-
에러 처리 / 엣지 케이스
- 전역 모달 동시 표시 방지 규칙
-
LoadingModal활성 중에는Info/Confirm을 동시에 띄우지 않고 - 로딩 종료 후 결과를
InfoModal로 안내하는 흐름으로 통일
-
- 네트워크 오류/서버 오류(5xx)
- 일시 오류로 판단:
InfoModal로 안내 + 재시도 유도
- 일시 오류로 판단:
- 전역 모달 동시 표시 방지 규칙
-
공통 정책
- Header / BottomNav 유지
-
main영역에만 에러 안내 UI 표시 - 공통 CTA 버튼 제공: 메인으로 돌아가기
- 클릭 시 캘린더 페이지(메인) 로 이동:
router.replace("/calendar")
- 클릭 시 캘린더 페이지(메인) 로 이동:
-
404:
app/(app)/not-found.tsx- 노출 조건
- 존재하지 않는 경로로 접근했을 때 자동 렌더
- 표시 내용
- 타이틀:
404 Not Found - 안내 문구:
존재하지 않는 페이지입니다. URL을 확인해주세요! - 버튼:
메인으로 돌아가기→/calendar
- 타이틀:
- 엣지 케이스
- 공유 URL/딥링크로 잘못 들어온 경우도 이 화면에서 복구 가능해야 함(버튼으로 메인 복귀)
- 노출 조건
-
500:
app/(app)/error.tsx- 노출 조건
- 런타임 에러(렌더링 오류 등) 발생 시 자동 렌더
- 표시 내용
- 타이틀:
500 Server Error - 안내 문구:
일시적인 오류가 발생했습니다. 잠시 후 다시 시도해주세요! - 버튼:
메인으로 돌아가기→/calendar
- 타이틀:
- 로깅
-
error.tsx에서console.error(error)로 기본 기록
-
- 노출 조건
(1) Header
- 개요 (Overview)
- 역할
- 보호 영역 모든 화면 상단 고정 헤더
- 로고/알림은 공통
- 검색은 페이지별로 옵션 제공(노출/동작이 다름)
- 디자인 시안
- 좌측 ‘Devths’ 텍스트(로고)
- 우측: (조건부)검색 아이콘, 알림(벨) 아이콘
- 읽지 않은 알림 뱃지
- 역할
- props & Interface
-
showSearch?: boolean(기본 false)-
true일 때만 검색 아이콘 노출
-
-
onClickSearch?: () => void- 검색 아이콘 클릭 시 실행할 동작(페이지가 주입)
-
searchHref?: string- 클릭 시 이동할 경로를 문자열로 전달(페이지가 주입)
-
- 내부 상태 & 이벤트
- 상태
-
hasUnread: boolean(읽지 않은 알림 존재 여부)
-
- 이벤트
- 로고 클릭:
/calendar이동 - 검색 아이콘 클릭:
-
onClickSearch가 있으면 실행 - 없고
searchHref가 있으면 해당 경로로 이동
-
- 알림 아이콘 클릭:
/notifications이동
- 로고 클릭:
- 상태
- 사용 예시
- AppShell에서 기본 렌더(검색 없음)
<Header showSearch={false} />
- 게시판 페이지(게시글 검색)
<Header showSearch searchHref="/board/search" />
- 채팅 페이지(채팅 검색)
<Header showSearch onClickSearch={() => router.push("/chat/search")} />
- AppShell에서 기본 렌더(검색 없음)
- Storybook
-
Default: 검색 없음, 뱃지 없음 -
Unread: 검색 없음, 읽지 않은 알림 뱃지 있음 -
SearchOnly: 검색 있음, 뱃지 없음 -
SearchWithUnread: 검색 있음, 읽지 않은 알림 뱃지 있음 -
SearchOnly(Board),SearchOnly(Chat): 검색 클릭 동작이 페이지별로 다른 케이스 검증
-
(2) BottomNav
-
개요
- 역할
- 보호 영역 모든 화면 하단 고정 탭 네비게이션
- 주요 도메인(홈/피드/AI/채팅/프로필)으로 빠른 이동 제공
- 디자인 시안
- 아이콘 + 라벨이 있는 5개 탭
-
홈,피드,AI,채팅,프로필
- 역할
-
props & Interface
- 기본은 props 없이 사용
- 현재 경로(pathname) 기준으로 active 탭을 자동 표시
-
탭 구성/라우팅 매핑
-
홈→/calendar -
피드→/board -
AI→/llm -
채팅→/chat -
프로필→/profile
탭 이동은 router.push()로 통일
-
-
내부 상태 & 이벤트
- 상태
- 별도 local state 없이
usePathname()기반으로 active 판단
- 별도 local state 없이
- 이벤트
- 탭 클릭 → 해당 경로로 이동
- active 판정 규칙
- 정확 매칭 + 하위 경로 포함 처리
-
/board또는/board/*면피드active -
/chat또는/chat/*면채팅active
-
- 정확 매칭 + 하위 경로 포함 처리
- 상태
-
Storybook
- 탭 활성 상태 조합 케이스
HomeActiveBoardActiveLlmActiveChatActiveProfileActive
- 탭 활성 상태 조합 케이스
(3) ConfirmModal (전역)
- 개요
- 역할
- 사용자의 선택(취소/확인)이 필요한 상황에서 사용하는 전역 확인 모달
- 작성 중 이탈, 삭제, 로그아웃, 권한 요청 등
- 디자인 시안
- 제목/설명 + 우측 상단 X
- 하단 버튼 2개:
취소/확인 - 확인 버튼 텍스트는 상황별로 변경(나가기/삭제/로그아웃 등)
- 파괴적 액션은 confirm 버튼을 destructive 스타일로 표시
- 역할
- props
-
title: string- 모달 맨 위에 크게 보이는 제목
- 예:
"작성 중인 내용이 있습니다"
-
description?: string- 제목 아래에 들어가는 설명 문구(부가 안내)
-
?라서 없어도 됨 - 예:
"저장하지 않고 나가시겠습니까?"
-
cancelText?: string- 왼쪽(보통 회색) 취소 버튼에 표시될 글자
- 안 주면 기본으로
"취소"가 뜸 - 예:
"아니요"로 바꾸고 싶으면 여기서 설정
-
confirmText?: string(기본: "확인")- 오른쪽(보통 강조) 확인 버튼에 표시될 글자
- 상황마다
"나가기","삭제","로그아웃"처럼 바꾸려고 쓰는 값
-
confirmVariant?: "default" | "destructive"(기본: default)- 확인 버튼의 성격(스타일)을 정함
-
"default": 일반 확인(예: 저장, 진행) -
"destructive": 위험/삭제/되돌릴 수 없는 행동(예: 삭제, 탈퇴) → 빨간 계열 스타일
-
onConfirm: () => void | Promise<void>- 사용자가 확인 버튼을 눌렀을 때 실행할 함수
- 예
- 글 작성 중 나가기 →
router.back() - 삭제 →
await deletePost()
- 글 작성 중 나가기 →
-
- 내부 상태 & 이벤트
- 상태
-
isOpen: boolean- 모달이 열려있는지/닫혀있는지를 나타내는 값
-
true면 ConfirmModal이 화면에 보이고, -
false면 안 보임 - 전역 store(Zustand)가 이 값을 관리해서, 어느 페이지에서든 opne/close를 동일한 방식으로 제어 가능
-
isSubmitting: boolean- 사용자가 확인 버튼을 눌렀고,
onConfirm작업이 진행 중인지를 나타내는 값 -
true일 때- 확인 버튼을 disabled 처리
- 버튼에 로딩 표시
- 연타로 API가 여러 번 호출되는 것을 방지함
-
onConfirm이 끝나면false로 돌아감- 성공이면 모달 닫기
- 실패면 모달 유지 + 에러 안내
- 사용자가 확인 버튼을 눌렀고,
-
- 이벤트
- 확인 버튼 클릭
-
isSubmitting=true로 설정 (연타 방지) -
await onConfirm()실행 - 성공
-
closeConfirm()(모달 닫기)
-
- 실패
- 모달 유지
-
InfoModal로 에러 안내
- 마지막
- 성공/실패와 무관하게
isSubmitting=false로 복구
- 성공/실패와 무관하게
-
- 취소 버튼 클릭
-
onCancel이 있으면 실행 -
closeConfirm()로 모달 닫기
-
- X 클릭
closeConfirm()
- 확인 버튼 클릭
- 상태
- 사용 예시
- 작성 중 이탈
- 상황: 글 작성/수정 중 뒤로 가기, 탭 이동, 페이지 이탈 시
- 설정
confirmText = "나가기"confirmVariant = "default"
- 동작 예시(
onConfirm)- 작성 페이지 이탈 처리:
router.replace("/board")
- 작성 페이지 이탈 처리:
- 삭제 확인
- 상황: 게시글/댓글/파일 삭제 버튼 클릭 시
- 설정
confirmText = "삭제"confirmVariant = "destructive"
- 동작 예시(
onConfirm)- 삭제 API 호출:
await deletePost(postId) - 성공 후 목록 갱신:
queryClient.invalidateQueries(...)
- 삭제 API 호출:
- 로그아웃
- 상황: 프로필화면에서 로그아웃 버튼 클릭 시
- 설정
confirmText = "로그아웃"confirmVariant = "destructive"
- 동작 예시(
onConfirm)await api.post("/api/auth/logout")-
authStore.logout()(토큰/상태 초기화) router.replace("/login")
- 작성 중 이탈
- 전역 관리
- Zustand store로 단일화
- 페이지/컴포넌트 어디서든 동일한 API로 모달 제어
-
useConfirmStore.openConfirm(payload)- 모달 열기(표시 내용/동작 주입)
-
useConfirmStore.closeConfirm()- 모달 닫기
-
- 페이지/컴포넌트 어디서든 동일한 API로 모달 제어
- 렌더링 위치
-
GlobalConfirmModalHost를 AppShell에서 1회만 렌더 - 각 페이지는 ConfirmModal을 직접 렌더하지 않고, openConfirm만 호출
- 장점: 모달이 중복 렌더되지 않고, UI/동작 규칙이 한 곳에서 통일됨
-
- 책임 분리
- 페이지: 언제 띄울지 + 어떤 문구/액션인지(payload)만 결정
- Host: 어떻게 보여줄지(레이아웃/닫힘 규칙/제스처/스크롤 락)를 담당
- Zustand store로 단일화
(4) LoadingModal (전역)
-
개요
- 역할
- 시간이 걸리는 작업 진행 중 사용자에게 처리 중 상태를 보여주는 UI
- 작업 성격에 따라 Blocking / Non-blocking으로 구분하여 사용
- Blocking(화면 잠금): 사용자가 현재 플로우를 끝내야 하는 짧은 작업(저장/제출/필수 업로드 등)
- Non-blocking(백그라운드 작업): OCR/AI 마스킹처럼 오래 걸리고 사용자가 다른 페이지로 이동할 수 있어야 하는 작업
- 디자인 시안: 딤 + 카드 + 문구 + 스피너
- 역할
- props
-
title: string- 로딩 제목(예: “AI가 개인정보를 마스킹하는 중입니다”)
-
description?: string- 보조 설명(예: “잠시만 기다려 주세요”)
-
showSpinner?: boolean(기본 true)- 스피너 표시 여부
-
blockClose?: boolean(기본 false)- 사용자 화면 이동/닫기 가능 여부
-
true: 모달을 닫을 수 없고(overlay/ESC 포함), 사실상 화면을 잠금(Blocking) -
false: 사용자가 닫거나 이동 가능(Non-blocking 용도)
-
- 내부 상태 & 이벤트
- 상태
-
isOpen: boolean(전역 store에서 제어)
-
- 규칙
-
blockClose=true일 때- X 버튼 숨김/비활성화
- overlay 클릭/ESC로 닫기 불가
- 페이지 이탈도 제한
-
blockClose=false일 때- X/overlay/ESC로 닫기 가능
- 사용자는 다른 페이지로 이동 가능(백그라운드 진행)
-
- 상태
- 전역 관리
- Zustand store로 단일화
-
openLoading(payload): 로딩 열기 -
updateLoading(payload): 문구/옵션 업데이트(진행 단계 문구 변경) -
closeLoading(): 로딩 닫기
-
- Zustand store로 단일화
- 사용 정책
- Blocking Loading(잠금 필요)
blockClose=true- 예: 저장 중, 제출 중, 필수 업로드 중
- Background 작업(이동 가능)
-
blockClose=false+ 완료/실패는InfoModal또는/notifications로 안내 - 예: OCR/AI 마스킹/분석 task 폴링
-
- Blocking Loading(잠금 필요)
(5) InfoModal (전역)
-
개요
- 역할
- 성공/안내/경고/오류 등 사용자에게 결과를 알려주고 사용자가 닫는 전역 모달
- 로딩을 제외한 대부분의 결과 안내는 InfoModal로 통일
- 디자인 시안
- 제목 + 설명 + 우측 상단 X(닫기)
- 확인 버튼 없이 X로 닫는 형태
- 역할
- Props & Interface
-
title: string- 모달에서 가장 크게 보이는 제목 텍스트
- 예: 저장이 완료되었습니다
-
description?: string- 제목 아래에 붙는 추가 설명 문구
- 없어도 됨(
?) - 예: 변경사항이 정상적으로 저장됐어요.
-
variant?: "success" | "info" | "warning" | "error"(기본 "info")- 모달의 상태를 나타내는 값
- 아이콘/색/강조 스타일을 바꾸는 데 사용
-
"success": 성공(예: 저장 완료, 업로드 완료) -
"info": 일반 안내(예: 로그인이 필요합니다) -
"warning": 주의(예: 중요 정보가 포함될 수 있어요) -
"error": 오류(예: 요청에 실패했습니다)
-
onClose?: () => void- 사용자가 X를 눌러서 모달을 닫을 때 추가로 실행할 함수
- 예: 모달 닫은 뒤 특정 페이지로 이동시키고 싶을 때
-
"로그인이 필요합니다"모달 닫으면/login으로 이동 같은 처리
-
-
- 내부 상태 & 이벤트
-
isOpen: boolean(전역 store에서 제어)- InfoModal이 지금 화면에 떠있는지 여부
-
true면 모달이 보이고,false면 안 보임 - 전역 store가 관리하니까 어디서든
openInfo()로 열 수 있음
- X 클릭 →
closeInfo()- 사용자가 우측 상단 X 버튼을 누르면 모달을 닫는 함수 실행
- overlay / ESC 닫기 허용
- overlay: 모달 밖의 어두운 배경
- 배경을 클릭해도 모달이 닫히도록 허용한다는 뜻
- ESC : 키보드 ESC 키
- ESC를 눌러도 모달이 닫히도록 허용한다는 뜻
- overlay: 모달 밖의 어두운 배경
-
- 사용 예시
- 파일 첨부 성공: 첨부가 완료되었습니다
- 저장 완료: 저장이 완료되었습니다
- 권한 안내: 로그인이 필요합니다
- 네트워크/서버 오류: 일시적인 오류가 발생했습니다
- 전역 관리
- Zustand store로 단일화
openInfo({ title, description, ... })closeInfo()
- 렌더링 위치
-
GlobalInfoModalHost를 AppShell에서 1회만 렌더 - 각 페이지는 열기(open)만 호출하고 실제 렌더는 Host가 담당
-
- Zustand store로 단일화
- Storybook
-
Info: 기본 안내 -
Success: 성공 안내 -
Warning: 경고 안내 -
Error: 오류 안내 -
LongText: 긴 설명/줄바꿈 케이스
-
(6) NoticeBanner
-
개요
-
역할
- 채팅/게시글/댓글 작성 등 사용자 입력 화면에서 보안 안내를 항상 고정 노출하는 배너
- 사용자가 작성 중에도 개인정보 공유 주의를 지속적으로 인지하도록 함
-
디자인 시안
- 좌측: 원형 느낌의 주의 아이콘 (!)
- 우측: 텍스트 영역
- 1줄: 제목(보안 안내)
- 2줄: 안내 문구(연락처, 계좌번호, 주민번호 등 개인정보를 공유하지 마세요)
-
역할
-
Props & Interface
-
title: string- 배너 제목(보안 안내)
-
message: string- 안내 문구
-
icon?: ReactNode- 기본은
Info/Alert아이콘을 사용
- 기본은
-
- 내부 상태 & 이벤트
- 닫기 버튼 없음
- overlay/클릭/ESC 등으로 숨김 처리 없음
- 페이지가 렌더링되는 동안 항상 표시
- 사용 예시
- 채팅 작성 상단 고정
title="보안 안내"message="연락처, 계좌번호, 주민번호 등 개인정보를 공유하지 마세요"
- 게시글/댓글 작성 화면 상단 고정(동일 문구 재사용)
- 채팅 작성 상단 고정
- Storybook
-
Default(Security): 기본 보안 안내 배너(항상 노출) -
LongText: 긴 문구 줄바꿈 케이스
-
(7) ChipTabs
-
개요
- 역할
- 카테고리/필터 선택을 위한 칩 형태 탭 UI
- 도메인별 사용 예
- 게시글: 이력서/포트폴리오/면접/코딩테스트
- 프로필: 백엔드/프론트엔드/클라우드/AI
- 캘린더: 서류/코테/1차 면접/2차 면접/개인 일정
- 디자인
- pill 버튼 가로 나열
- 선택된 탭만 검정 배경 + 흰 글자
- 미선택 탭은 연한 배경/테두리
- 역할
- Props & Interface
-
items: { value: string; label: string }[]- 탭 목록(값/표시 라벨)
-
value: string- 현재 선택된 탭의 value (controlled)
-
onChange: (nextValue: string) => void- 탭 클릭 시 부모에게 선택값 변경 요청
-
- 내부 상태
- 내부 상태 없음
- 선택 상태는 항상
value로 판단
- 선택 상태는 항상
- 이벤트
- 탭 클릭 →
onChange(item.value)호출
- 탭 클릭 →
- 내부 상태 없음
- 사용 예시
- 게시글 카테고리 필터
items=[{value:"resume",label:"이력서"}, ...]-
value는 URL 쿼리 또는 로컬 state로 관리 - 변경 시 목록 쿼리 refetch 또는 필터 적용
- 엣지 케이스
- 탭이 많아 한 줄에 다 안들어가면
- 컨테이너에
overflow-x-auto+whitespace-nowrap - 스크롤 시 레이아웃 깨지지 않게
gap유지
- 컨테이너에
-
items가 비어있으면- 렌더하지 않음
-
value가 items에 없는 경우- 첫 번째 탭을 기본값으로 강제
- 탭이 많아 한 줄에 다 안들어가면
- Storybook
-
Default: 4개 탭 기본 -
Active: 특정 탭 active 상태 -
Overflow: 탭이 많은 가로 스크롤 케이스 -
LongLabel: 라벨이 긴 케이스
-
(8) Avatar
- 개요
- 역할
- 사용자 프로필을 원형으로 표시
-
src가 있으면 이미지 표시 - 이미지가 없거나 실패하면 fallback 표시
- 기본: 닉네임(
name)의 앞글자 1개 -
name도 없으면 기본 원형(placeholder)
- 기본: 닉네임(
- 프로필 미등록 상태는 특정 기본 색상 배경 + 이니셜로 표시
- 역할
- Props & Interface
-
src?: string- 프로필 이미지 URL(또는 미리보기 URL)
-
name?: string- fallback 이니셜 생성용 닉네임/이름
-
size?: "xs" | "sm" | "md" | "lg" | "xl"(기본 "md")- 아바타 크기 프리셋
- Avatar의 fallback 배경색은 페이지/상황에 따라 분기하지 않고, 디자인 토큰
avatar-fallback으로 단일화
-
- 내부 상태 & 이벤트
- 내부 상태:
hasError: boolean- 이미지 로드 실패 시
true로 전환 → 이후 fallback로 고정
- 이미지 로드 실패 시
- 이벤트
- 이미지
onError발생 →hasError=true
- 이미지
- 내부 상태:
- Fallback 규칙
-
src가 있고 로드 성공 → 이미지 표시 -
src가 없거나 로드 실패 +name있음 →name의 첫 글자 표시 -
src없고name도 없음 → placeholder 원형 표시
-
- 엣지 케이스
-
name이 빈 문자열/공백이면 이니셜 대신 placeholder 처리 - 이미지가 깨지거나 404면
onError로 fallback 전환 - 원형 크롭 유지:
object-fit: cover적용
-
(9) DevthsButton (Primary Button)
- 개요
- 역할
- 서비스에서 주요 행동을 수행하는 버튼
- 추가/저장/확인/제출/로그인
- 디자인 시안: 검정 배경 + 흰 글자, 둥근 사각형
- 역할
- Props & Interface
- 콘텐츠
-
label?: string- 버튼에 보여줄 문자열 텍스트
- 예:
"저장","확인"
-
- 동작/상태
-
onClick?: () => void- 버튼을 눌렀을 때 실행할 함수(이벤트)
- 예시: 저장 API 호출, 페이지 이동 등
-
disabled?: boolean(기본 false)-
true면 버튼이 비활성화(회색) 되고 클릭이 안 됨 - 예: 입력값이 아직 유효하지 않을 때
-
-
isLoading?: boolean(기본 false)-
true면 지금 처리 중 상태- 버튼에 로딩 표시(스피너)
- 버튼 클릭 불가 처리(연타 방지)
- 예: 저장 버튼 눌렀는데 서버 응답 기다리는 중
-
-
-
type?: "button" | "submit"(폼에서 필요)- HTML 버튼의 타입
-
"button": 그냥 클릭용(기본값으로 많이 둠) -
"submit": 폼 안에서 누르면 폼 제출(onSubmit) 을 트리거함 - 예: 회원가입 폼 “가입하기” 버튼은
type="submit"이 자연스러움
- 콘텐츠
- 상태 규칙
- Enabled(활성)
- 배경: 검정
- 텍스트: 흰색
- Disabled(비활성)
- 배경: 회색
- 클릭 불가 (
disabled=true) - 포커스/호버 효과 최소화
- Loading(로딩)
-
isLoading=true일 때- 버튼 비활성화 처리(연타 방지)
- 스피너(또는 로딩 아이콘) 표시
-
- Enabled(활성)
- 사용 예시
- 저장 버튼:
label="저장"
- 저장 버튼:
- Storybook
-
Default: enabled -
Disabled: disabled=true(회색) -
Loading: isLoading=true -
LongText: 라벨이 긴 케이스 -
Submit: type="submit" 폼 버튼 케이스
-
-
AppShell (app/(app)/layout.tsx)- 보호 영역 공통 레이아웃을 제공하며, 아래 요소들을 항상 고정 렌더링한다.
- 상단:
Header - 중앙:
main(children)→ 각 페이지 컴포넌트가 이 영역에 렌더링됨 - 하단:
BottomNav
- 상단:
- 전역 UI는 AppShell에서 단 한 번만 렌더링한다(중복 렌더 방지).
-
GlobalLoadingHost→LoadingModal -
GlobalInfoModalHost→InfoModal -
GlobalConfirmModalHost→ConfirmModal
-
- 보호 영역 공통 레이아웃을 제공하며, 아래 요소들을 항상 고정 렌더링한다.
-
페이지(Page) 컴포넌트의 역할
- 페이지는 모달 UI를 직접 렌더링하지 않고, 전역 store 호출로만 제어한다.
- 로딩 표시:
useLoadingStore.openLoading()/updateLoading()/closeLoading() - 안내 모달:
useInfoModalStore.openInfo()/closeInfo() - 확인 모달:
useConfirmStore.openConfirm()/closeConfirm()
- 로딩 표시:
- 페이지는 모달 UI를 직접 렌더링하지 않고, 전역 store 호출로만 제어한다.
-
데이터 흐름(요약)
- Page → store에 열기/닫기/문구를 업데이트
- Host → store 상태를 구독하여 실제 모달을 렌더링/숨김 처리
-
Global State (Zustand Stores)
- 목적: 서버 데이터가 아닌 전역 UI 상태와 세션/인증 상태 관리
- 사용 기준
- 여러 페이지에서 공통으로 필요하거나 페이지 이동해도 유지되어야 하는 상태 = Zustand
- 서버에서 내려오는 데이터(목록/상세)는 TanStack Query로 관리
- Store
-
useAuthStore- accessToken, isLoggedIn, logout 등 인증/세션 상태
-
useLoadingStore- 전역 로딩 모달
isOpen,title,description,blockClose
- 전역 로딩 모달
-
useInfoModalStore- 전역 안내 모달
isOpen,title,description,variant
- 전역 안내 모달
-
useConfirmStore- 전역 확인 모달
isOpen,payload(title/description/confirmText/onConfirm...)
- 전역 확인 모달
-
useNavStore- 현재 선택된 하단 탭 상태
-
-
Local State (React
useState,useReducer)-
목적
- 특정 페이지/컴포넌트 내부에서만 쓰이는 일시적인 UI 상태 관리
- 사용 예
- 입력값, 탭 선택값(
ChipTabs), 드롭다운 오픈 여부, 모달 내부 임시 값, 선택 리스트 등
- 입력값, 탭 선택값(
- 사용 기준(룰)
- 그 화면을 벗어나면 의미 없어지는 상태는 Local State
-
목적
-
Server Cache State (TanStack Query)
- 목적
- 서버에서 가져오는 데이터를 캐싱/동기화하고, 로딩/에러/성공 상태를 표준화
- 무한 스크롤/페이지네이션/리패치 정책을 일관되게 적용
- 사용 기준(룰)
- API에서 오는 데이터는 React Query가 단일 소스(Zustand 중복 저장 금지)
- 권장 패턴
-
GET /api/users/me같은 전역 기준 데이터(내 정보)는 AppShell(AuthGate)에서 1회 조회 후 재사용 - 알림/목록/검색은 화면 특성에 따라
staleTime,refetchOnWindowFocus,useInfiniteQuery등을 정책으로 설정 - 변경(작성/삭제/수정) 이후에는
invalidateQueries로 목록/상세를 갱신
-
- 목적
(1) 호출할 백엔드 API 목록
- 인증
-
POST /api/auth/google- OAuth 인가 코드(
code/authCode)를 서버에 전달 → 회원 여부 판단 + 토큰 발급 - 사용 위치:
/auth/callback에서 호출
- OAuth 인가 코드(
-
POST /api/auth/tokens- refreshToken 기반 accessToken 재발급
- 사용 위치: axios 응답 인터셉터의 401 자동 갱신에서만 사용(직접 호출 최소화)
-
POST /api/auth/logout- 로그아웃 처리(토큰 무효화/쿠키 삭제)
- 사용 위치: 프로필/설정 화면 로그아웃 액션
-
- 유저 (Users)
-
GET /api/users/me- 로그인 상태 판별 기준 API
- 사용 위치: AppShell
AuthGate에서 최초 실행
-
PUT /api/users/me- 내 정보 수정(프로필 이미지/닉네임/관심사 등)
- 사용 위치: 프로필 편집 폼 제출
-
DELETE /api/users- 회원 탈퇴
- 사용 위치: 설정/탈퇴 확인(ConfirmModal) 이후 실행
- (프로필/팔로우 관련)
-
GET /api/users/{userId}- 특정 유저 프로필 조회
-
POST /api/users/{userId}/followers- 팔로우
-
DELETE /api/users/{userId}/followers- 언팔로우
-
GET /api/users/me/followers?size=&lastId=- 내 팔로워 목록(무한스크롤)
-
GET /api/users/me/followings?size=&lastId=&nickname=- 내 팔로잉 목록(무한스크롤)
-
GET /api/users/me/posts?size=&lastId=- 내가 쓴 글 목록(무한스크롤)
-
GET /api/users/me/comments?size=&lastId=- 내가 쓴 댓글 목록(무한스크롤)
-
-
- 알림(Notifications)
-
GET /api/notifications?size=&lastId=- 알림 목록 조회(무한스크롤/페이지네이션)
- 사용 위치:
/notifications페이지, Header 뱃지 표시용 최신 상태 확인
- 디바이스 토큰 관리(푸시/디바이스 등록용)
-
POST /api/notifications/tokens/{deviceId}: 등록 -
PATCH /api/notifications/tokens/{deviceId}: 갱신 -
DELETE /api/notifications/tokens/{deviceId}: 삭제 (로그아웃/탈퇴 시)
-
-
- 파일 첨부
-
POST /api/files/presigned- S3 업로드용 Presigned URL 발급
- 사용 위치: 업로드 시작 단계
-
POST /api/files- 업로드 완료 후 서버에 파일 메타 등록(원본명, s3Key 등)
- 사용 위치: S3 업로드 성공 직후
-
DELETE /api/files/{fileId}- 첨부 파일 삭제(서버 메타/권한 처리)
-
- 비동기 작업 상태 조회
-
GET /api/ai/tasks/{taskId}- AI/OCR/마스킹 등 비동기 작업 상태 조회
- 사용 위치: 폴링(예: 1~2초 간격)로 진행/완료/실패 확인
- 완료/실패 시: Loading 종료 + InfoModal/알림으로 결과 안내
-
(2) API 호출 처리
- axios 인스턴스 단일화
-
apiClient = axios.create({ baseURL, withCredentials: true })-
withCredentials: true→ refreshToken이 쿠키라면 필요
-
- 모든 API 호출은
apiClient로만 수행(직접 axios 금지)
-
- Request Interceptor (요청 전)
- Zustand의
authStore.accessToken을 읽어서- 있으면
Authorization: Bearer <accessToken>자동 첨부
- 있으면
-
Content-Type은 기본application/json- 파일은 Presigned 방식이므로 백엔드로 직접 멀티파트 업로드하지 않음(원칙)
- Zustand의
- Response Interceptor (응답 후) : 401 자동 갱신 표준
- 응답이
401이면 아래 순서로 처리-
POST /api/auth/tokens호출로 accessToken 재발급 시도 - 성공하면: 새 accessToken 저장(Zustand) → 원래 요청 1회 재시도
- 실패하면: authStore 초기화 +
InfoModal로 안내 후/login이동
-
- 동시 401 폭주 방지
- refresh 요청은 한 번만 수행
- 나머지 실패 요청은 refresh 완료 후 재시도하도록 큐/락 적용
- 응답이
- 공통 UX 처리(모달 정책)
- LoadingModal 사용 정책을 2가지로 구분
- Blocking(화면 잠금): 사용자가 지금 화면에서 반드시 기다려야 하는 짧은 작업
- 예: 저장/제출/필수 업로드 완료 전 단계
useLoadingStore.openLoading({ title, description, blockClose: true })
- Non-blocking(백그라운드 작업): 시간이 길어 다른 페이지 이동이 가능해야 하는 작업
- 예: OCR/AI 마스킹/분석 task 폴링
-
useLoadingStore.openLoading({ title, description, blockClose: false })또는 LoadingModal 대신 배너/알림으로 대체(정책 선택)
- Blocking(화면 잠금): 사용자가 지금 화면에서 반드시 기다려야 하는 짧은 작업
- 작업 단계 문구 변경
updateLoading({ title?, description?, showSpinner?, blockClose? })
- 완료/실패 시
-
closeLoading()후 결과 안내는InfoModal로 통일useInfoModalStore.openInfo({ title, description })
-
- 사용자 선택이 필요한 경우
useConfirmStore.openConfirm({ title, description, confirmText, onConfirm, ... })
- LoadingModal 사용 정책을 2가지로 구분
- 파일 첨부 표준 플로우 (Presigned)
-
POST /api/files/presigned(fileName, mimeType) → presignedUrl/s3Key 수신 - 브라우저에서 S3로 직접
PUT presignedUrl업로드 - 업로드 성공 후
POST /api/files로 서버에 파일 메타 등록 - 삭제는
DELETE /api/files/{fileId}
-
- 비동기 작업(task) 표준 플로우
- 서버가 taskId를 주는 작업(AI/OCR/마스킹 등)
-
GET /api/ai/tasks/{taskId}를 폴링(예: 1~2초 간격) - 진행 중
- 페이지 이동 필요하면
blockClose=false(Non-blocking) 정책 적용 - 성공/실패:
-
closeLoading()→InfoModal로 결과 안내(또는 다음 화면 이동)
-
- 페이지 이동 필요하면
-
- 서버가 taskId를 주는 작업(AI/OCR/마스킹 등)
- 페이지네이션 규칙
-
size,lastId기반 무한스크롤은 React QueryuseInfiniteQuery로 통일 - 다음 페이지 요청 시
lastId는 마지막 아이템의 id를 사용
-
- Next.js App Router 기반
- 공개/보호 라우팅을 그룹으로 분리
- 보호 영역 접근 제어
-
AuthGate에서 로그인 여부 확인 후 보호 페이지 렌더 - 미로그인 상태면:
- InfoModal로 안내 후
/login이동
- InfoModal로 안내 후
-
-
유효성 검증 로직
-
클라이언트 측
- React Hook Form + Zod로 폼 타입/검증 통일
- 인라인 에러 메시지 + 제출 버튼 disabled 규칙 통일
- 제출 중에는
DevthsButton disabled또는 로딩 처리
-
서버 측(백엔드)에서도 별도 검증 로직 존재
- 백엔드에서 400/422 등으로 내려주는 에러는 공통 에러 파서로 메시지 정리 후
- 필드 에러면 폼 인라인에 연결
- 일반 에러면
InfoModal로 안내
- 백엔드에서 400/422 등으로 내려주는 에러는 공통 에러 파서로 메시지 정리 후
-
클라이언트 측
-
단위(Unit)
- 유틸 함수 중심 테스트
- 에러 메시지 파서, 날짜 포맷, queryKey 생성 규칙 등
- 전역 store(Zustand) 로직 테스트
-
open/close/update같은 상태 전이 테스트 -
ConfirmModal의isSubmitting전환(연타 방지) 같은 로직 포함
-
- 유틸 함수 중심 테스트
-
컴포넌트(Storybook)
- 공통 컴포넌트는 상태별 스토리 필수
-
Header- 검색 없음/있음
- 알림 뱃지 없음/있음
- 검색 있음 + 뱃지 있음 조합
-
BottomNav- 각 탭 active 케이스(경로별)
-
InfoModal- 제목만 / 제목+설명 / 긴 문장 / 줄바꿈 케이스
-
ConfirmModal- confirmText: 나가기/삭제/로그아웃
- variant: default/destructive
- submitting 상태(로딩/disabled) 케이스
-
LoadingModal- blocking(
blockClose=true) 케이스 - non-blocking(
blockClose=false) 케이스 - 문구 변경(
updateLoading) 케이스
- blocking(
-
NoticeBanner- 항상 노출(closable 없음)
- 긴 문구 줄바꿈 케이스
- (선택) variant가 유지된다면 info/warning/danger 케이스
-
ChipTabs- 탭 4개 기본 / 탭 많아서 가로 스크롤 케이스
-
Avatar- 이미지 있음 / src 없음 + 이니셜 / 이미지 로드 실패 폴백 / placeholder
-
DevthsButton- enabled/disabled(회색)
- loading(isLoading) 케이스
- type="submit" 케이스(폼)
-
- 공통 컴포넌트는 상태별 스토리 필수
-
통합(Integration)
- AuthGate 흐름 시나리오 테스트
- 로그인 상태(true/false)에 따라 보호 페이지 접근 제어가 정상 동작하는지 검증한다.
- API 응답
401발생 시, 토큰 재발급(refresh) 성공/실패에 따른 분기 처리가 정상인지 검증한다.
- API 에러 발생 시 전역 UI 호출 검증
- 요청 실패 시 전역 로딩 UI가 해제된 후(
closeLoading) 안내 모달이 노출되는지(openInfo) 호출 순서를 검증한다. -
ConfirmModal의onConfirm실행 실패 시 모달을 유지하고, 적절한 에러 안내(InfoModal 등)가 이루어지는지 검증한다.
- 요청 실패 시 전역 로딩 UI가 해제된 후(
- AuthGate 흐름 시나리오 테스트
-
E2E (Playwright)
- 핵심 사용자 시나리오 위주
- 보호 페이지 진입 → AuthGate 확인 → 성공 시 메인 진입
- 401 발생 → refresh 성공 시 정상 복구 / 실패 시 로그인 이동
- 작성 중 나가기 → ConfirmModal 노출 → 나가기/취소 분기
- AI 마스킹/분석 → (정책에 따라)
- blocking이면 LoadingModal 유지 → 완료 후 InfoModal
- non-blocking이면 다른 페이지 이동 가능 + 완료 후 InfoModal/알림 안내
- 핵심 사용자 시나리오 위주
- 에러 로깅
- 전역 에러 처리 지점에서 에러 수집
- Next
app/(app)/error.tsx에서 런타임 에러console.error - axios 인터셉터에서 API 에러를 공통 포맷으로 정리하여 로깅
- Next
- 로그에 포함할 정보(개인정보 제외)
-
endpoint,status,errorCode,requestId,userId
-
- 전역 에러 처리 지점에서 에러 수집
- 성능/UX 모니터링
- 페이지 진입 시 초기 로딩 시간(내 정보 조회 + 핵심 데이터)
- TanStack Query 캐시 hit 비율(불필요 refetch 확인)
- 로딩 모달 노출 시간 분포(너무 길면 UX 개선 필요)
- 에러 처리 정책
- HTTP 에러 공통
-
401: refresh 시도 → 성공 시 원 요청 재시도 → 실패 시 로그아웃 + 로그인 유도(InfoModal 또는 /login) -
403: 권한 없음 안내(InfoModal) -
404: 리소스 없음 안내(InfoModal 또는 페이지 내 empty state) -
5xx: 일시적 서버 오류 안내(InfoModal) + 재시도 버튼 제공
-
- 전역 UI 우선 순위
- LoadingModal이 열려 있으면 다른 모달(Info/Confirm)은 원칙적으로 동시에 띄우지 않음
- 로딩 종료 후 결과를 InfoModal로 이어서 안내하는 패턴을 기본으로 채택
- HTTP 에러 공통
-
AppShell
- 보호 영역에서 공통으로 유지되는 레이아웃( Header + BottomNav + main )과 전역 Provider/Host를 포함한 뼈대
-
AuthGate
- 보호 페이지 진입 시 로그인 여부를 확인하고, 미로그인일 경우 접근을 막는 컴포넌트/로직
-
Global Host
- 전역 UI를 딱 한 번만 렌더링하는 컴포넌트
- 예:
GlobalLoadingHost,GlobalInfoModalHost,GlobalConfirmModalHost
-
InfoModal
- 성공/안내/경고 등을 사용자에게 보여주고 X로 닫는 전역 안내 모달(버튼 없음이 기본)
-
ConfirmModal
- 사용자의 선택(취소/확인)이 필요한 전역 확인 모달(확인 버튼 텍스트는 상황별 변경)
-
LoadingModal
- 긴 작업 진행 중 표시하는 전역 로딩 모달(기본적으로 닫기 불가)
-
NoticeBanner
- 화면 내에 삽입하거나 고정 노출하는 안내 배너(보안 안내 등)
-
Controlled Component
- 컴포넌트 내부가 아닌 부모가 값을 관리하는 방식(예: ChipTabs의 value/onChange)
-
Server Cache State
- 서버 데이터를 캐싱/동기화하는 상태(TanStack Query가 담당)
-
Global State
- 앱 전체에서 공유되는 UI/세션 상태(Zustand가 담당)