[FE] 공통 도메인 테크 스펙 - 100-hours-a-week/KTB4-5th-wiki GitHub Wiki

배경 (Background)

다먹자 v3는 인증·온보딩, 재고 등록·관리, 유통기한 알림, 냉장고 공유, AI 요리 추천·재료 차감으로 구성됩니다. 도메인은 서로 다르지만 AppShell, Header, BottomTabBar, Modal, BottomSheet, Toast, 비동기 상태 UI처럼 동일한 구조와 동작이 여러 화면에서 반복됩니다.

각 도메인이 이를 따로 구현하면 같은 UI의 동작·접근성·상태 처리 방식이 달라지고 수정 범위가 중복됩니다. 반대로 외형이 비슷하다는 이유만으로 모두 공통화하면 도메인별 예외 props와 조건문이 늘어나 공통 계층이 비즈니스 규칙에 의존하게 됩니다.

따라서 이 문서는 Figma 화면설계서 v3와 화면 흐름도에서 실제 반복이 확인된 UI를 기준으로 다음 내용을 확정합니다.

공통 영역은 별도의 비즈니스 도메인이 아니라 여러 도메인이 재사용하는 UI 표현 계층과 애플리케이션 기반입니다.

목표가 아닌 것 (Non-goals) (Optional)

  • 인증·회원 등 기존 비즈니스 도메인을 공통 도메인으로 합치거나 전체 도메인 경계를 다시 정의하지 않습니다.
  • 라우터 트리, 백엔드 API·DTO·DB 구조를 이 문서에서 확정하지 않습니다.
  • 미래에 재사용될 가능성만으로 컴포넌트를 미리 공통화하지 않습니다.
  • 재고 카드·알림 카드·추천 카드처럼 외형만 비슷하고 데이터 계약·클릭 결과가 다른 UI를 하나의 거대 컴포넌트로 통합하지 않습니다.
  • 도메인 Query Key·API 호출·mutation·폼 검증 규칙을 공통 UI 또는 Zustand로 이동하지 않습니다.
  • 모든 UI를 Client Component로 만들거나 하나의 SSR·CSR 방식으로 고정하지 않습니다.
  • 최신 화면에서 충돌하거나 근거가 부족한 BottomTabBar 구성, 현재 냉장고 아이디 지속 방식, ExpiryDatePicker 공통화 여부를 임의로 확정하지 않습니다.
  • 승인된 기술 스택 밖의 라이브러리나 새로운 UI 프레임워크를 도입하지 않습니다.

설계 및 기술 자료 (Architecture and Technical Documentation)

🔥 1. 확인한 설계 범위

  • 탭바는 홈·냉장고·등록·추천·마이페이지에 반복되고, 바텀시트·모달이 열리면 조작할 수 없습니다.
  • 헤더 알림 종은 홈·냉장고 관리·냉장고 빈 상태·추천 목록·마이페이지·만료 재고 목록에 반복된다. 등록 화면에는 없습니다.
  • 모달은 문구·버튼 순서·바깥 탭 차단 규칙을 공유합니다.
  • 값 선택 바텀시트는 드래그 핸들을 사용하고, 바깥 탭·아래 스와이프를 취소와 같이 처리합니다.
  • 토스트는 화면 하단에서 나타나 3초 뒤 사라지고 동시에 한 개만 표시한다. 같은 토스트가 반복되면 새로 쌓지 않고 타이머를 3초로 되돌립니다.
  • OCR 오류와 추천 API 오류가 공용 에러 UI와 공통 재시도 규칙을 사용한다. 재시도 중 버튼을 “로딩 중”으로 바꿔 중복 요청을 막습니다.
  • 공용 영역은 빈 상태의 구조를 정의하지만, 빈 상태 문구는 화면마다 다르므로 각 화면 명세에 둔다고 명시합니다.

🔥 2. 공통화 판정 기준

참고: 공통 컴포넌트를 건강하게 기르기 위한 고민

단순한 사용 횟수보다 하단의 기준에 따라 다먹자 화면 정의서에서 컴포넌트를 구체화할 예정입니다.

  1. 동일한 구조와 동작이 실제로 반복되는가

    단순히 UI가 비슷해 보이는 것이 아니라, 여러 화면에서 같은 컴포넌트 조합과 상태 처리 방식이 반복되는지를 확인합니다. 미래에 재사용될 것이라는 예상만으로 미리 공통화하기보다는, 실제 반복 사례가 확인된 경우에 분리합니다.

  2. 조합의 책임과 경계가 명확한가

    공통 컴포넌트가 담당할 역할과 사용하는 화면이 담당할 역할을 구분할 수 있어야 합니다. 공통 컴포넌트는 공통된 구조와 동작을 책임지고, 각 화면은 데이터와 화면별 비즈니스 로직을 전달하는 방식입니다. 즉, “이 컴포넌트가 무엇을 결정하고 무엇은 외부에 맡기는가”를 설명할 수 있어야 합니다.

  3. 화면별 예외가 계속 추가되지 않는가

    공통 컴포넌트를 사용하기 위해 화면마다 조건문이나 예외용 props를 계속 추가해야 한다면 잘못된 공통화일 가능성이 큽니다. 이런 경우에는 억지로 하나의 컴포넌트로 묶기보다, 공통되는 작은 UI 요소만 분리하고 화면별 조합은 각 도메인에서 관리하는 편이 적절합니다.

    유한한 경우를 다루는 variant 또는 상태에 따라 액션이 달라지는 slot으로 표현할 수 있습니다.

    [slot 사용 제한]

    • slot은 예외를 숨기는 만능 children 통로가 아니라, 사용처가 소유하는 변형 영역을 이름으로 드러내는 계약입니다.

    • 공통 컴포넌트는 slot 안의 도메인 데이터·mutation·라우팅을 알지 않습니다.

      query mutation
      하는 일 읽기 쓰기
      서버 상태 안 바뀜 바뀜
      여러 번 호출하면 같은 결과 문제 생길 수 있음 (중복 등록)
      언제 실행 화면 진입 시 자동 사용자가 버튼 눌렀을 때

      TanStack Query에서도 그 이름 그대로 API를 나눕니다.

      • useQuery는 자동 실행, 캐싱, 재요청을 설정해줘야합니다.
      • useMutation은 명시적 실행, 캐싱 처리 없습니다. 직접 호출해야 나간다. → 단, 무효화 처리해줘야 합니다.
      • slot 추가 요청이 반복되면 공통 템플릿의 경계가 아직 검증되지 않았다는 신호로 보고 분리 또는 도메인 내부 복귀를 검토한다.

[판정표]

판정 결정 구현 원칙
1·2·3 모두 충족 공통 조합 또는 템플릿으로 분리 고정 규칙을 공통 계약으로 만들고 사용처와 소유자를 문서화한다.
1·2 충족, 3 불확실 고정된 기반만 공통화하고 변형부는 slot으로 연다 동작·접근성처럼 검증된 불변부만 공통에 둔다. slot의 위치와 소유자가 명확하지 않거나 slot이 계속 늘어나면 화면 내부에 유지한다.
1만 충족 화면 또는 도메인 내부에 유지 두 화면에 보인다는 이유만으로 추출하지 않는다. 실제 중복과 변경 이유가 확인된 뒤 이동한다.
미래 사용만 예상 추출하지 않음 나중에 공통으로 옮길 수 있도록 의존만 정리한다.
외형만 유사하고 데이터·행동이 다름 조합은 각 도메인에 유지 필요하다면 CardSurface·Button 같은 표현 primitive만 공유한다.

[다먹자 프로젝트에 대입]

대상 variant slot
토스트 성공 / 실패 —
StatusView 로딩 / 에러 / 빈값 액션 슬롯
버튼 취소(테두리) / 실행(검정 채움) 라벨
모달 프리셋으로 처리 없어도 됨
모달 · 중복 재료 합산 — 본문 슬롯 필요 (합산 목록 테이블)
모달 · 회원 탈퇴 2단계 — 시퀀스라 둘 다 아님

🔥 3. 공통 UI·애플리케이션 기반 경계

영역 공통 계층이 소유 각 도메인이 소유 판정
App Shell·Navigation 공통 레이아웃, 탭바 외형·선택 표시·접근성, 공통 헤더 슬롯, overlay 중 내비게이션 차단 어떤 행동이 어느 화면으로 가는지, 진입 가능 조건, 등록 100종 한도처럼 업무 조건 공통으로 분리
Modal·Bottom Sheet focus trap, backdrop, 버튼 배치, dismiss 정책, drag handle, scroll lock 제목·설명·버튼 문구, 실행 mutation, 성공·실패 처리 공통으로 분리
Toast 위치, 3초 timer, 1개 제한, 중복 갱신, 성공·실패 variant, Sonner 어댑터 언제 띄우는지와 메시지 내용 공통으로 분리
Async View State LoadingState·EmptyState·ErrorState의 공통 배치, 접근성, 재시도 중 중복 요청 차단 Query, 빈 상태 판정, 화면별 카피·아이콘·복구 action 공통 프레임 + 도메인 slot
현재 냉장고 문맥 하위 도메인이 fridgeId·개인/공유 상태·역할을 읽는 공통 인터페이스 냉장고 전환·복귀·공유 정책과 API는 냉장고·공유 도메인이 소유 공통 컨텍스트로 노출, 정책은 이동하지 않음
Notification Bell AppHeader의 action slot과 badge primitive 안 읽은 수 Query, 읽음 mutation, NOTI-001 이동은 알림 도메인 UI와 비즈니스 분리
Expiry Date Picker 달력과 값 선택 바텀시트의 UI 계약 허용 날짜, 기본값, 저장 mutation, 서버 기준 날짜 정책 화면 내부 유지 — STOCK-002의 두 번째 사용과 동일 계약 확인 후 재판정
인증·세션 연동 세션 읽기 인터페이스, 보호 route의 접근 제어, 인증 만료 시 공통 전환 로그인·회원가입·로그아웃, 토큰 정책·API·사용자 모델은 인증 도메인이 소유 공통 애플리케이션 관심사 — 인증 도메인 자체와 분리
전역 Fallback Next.js route 단위 loading·error·not-found 경계, 공통 오류 프레임과 접근 가능한 재시도 동작 오류 코드 해석, 화면 문구, 복구 action과 데이터 재조회는 각 도메인이 소유 공통 기반 + 도메인 slot

[UI 예시]

공통 경계를 식별하기 위한 대표 화면 예시입니다. 화면 전체를 공통 컴포넌트로 만든다는 뜻이 아닙니다.

App Shell(Layout)·Navigation / 현재 냉장고 상태 / Notification Bell

  • 화면 근거: MAIN-001 · 홈
  • 공통 경계: 상단 헤더와 본문·하단 탭의 고정 배치, 헤더 action slot, 활성 탭 표현과 접근성.
  • 도메인 경계: “민주네 냉장고” 값은 회원 도메인에서 읽고, 알림 badge Query·클릭 이동은 알림 도메인이 맡는다. 따라서 AppShell·Context 자체는 비시각적 기반이고 이 화면은 그 소비 결과다.

Modal

  • 화면 근거: 모달 · 중복 재료 합산 확인
  • 공통 경계: focus trap, backdrop, scroll lock, 닫기 정책, 취소·실행 버튼 배치.
  • 도메인 경계: 합산 목록 계산·문구·합치기 mutation은 재고 등록이 소유한다. 즉 AppDialog는 공통이고 “중복 재료 합산 확인 모달”은 도메인 Business Component다.

Toast

  • 화면 근거: 블록 · 토스트 그룹 · 공용 영역
  • 공통 경계: 위치, 3초 timer, 동시에 1개, 중복 갱신, 성공·실패 variant.
  • 도메인 경계: 노출 시점과 실제 메시지는 호출 도메인이 정한다.

Async View State / 전역 Fallback

  • 화면 근거: 로딩·에러·빈값 상태 공용 UI
  • 공통 경계: 상태 프레임의 배치·접근성·재시도 중 중복 요청 차단. route의 loading·error·not-found 경계는 이 표현을 사용할 수 있는 상위 애플리케이션 기반이다.
  • 도메인 경계: Query 상태 판정, 문구·아이콘·복구 action은 각 화면이 이름 있는 slot으로 주입한다.

Bottom Sheet

  • 화면 근거: REG-004-2 · 부분 성공 결과 시트
  • 공통 경계: drag handle, backdrop, scroll lock, 바깥 탭·스와이프 dismiss 정책, action area 배치.
  • 도메인 경계: 성공·실패 건수, 실패 이유, 재시도 대상과 버튼 action은 재고 등록이 소유한다.

Expiry Date Picker

  • 화면 근거: REG-005 · 유통기한 선택
  • 현재 판정: 날짜 선택 UI의 구조는 식별되지만, STOCK-002의 두 번째 사용처가 같은 허용 날짜·기본값·저장 계약인지 확인되기 전에는 재고 등록 화면 내부에 둔다.

인증·세션 연동

  • 화면 근거: 모달 · 제안 · 세션 만료
  • 공통 경계: 세션 읽기 인터페이스, 보호 route 접근 제어, 만료 시 공통 전환. 이미지는 그 비시각적 기반이 사용자에게 드러나는 대표 결과다.
  • 인증 도메인 경계: 토큰 정책·API·사용자 모델과 로그인 mutation은 인증 도메인이 소유한다.

🔥 4. 컴포넌트별 책임과 인터페이스

하단의 흐름에 따라 공통 컴포넌트를 검증하겠습니다.

  1. 화면 UI : 어떤 화면의 어느 UI 영역을 하나의 컴포넌트로 볼 것인가?

    ⇒ 분석할 컴포넌트의 UI 경계 확정

  2. 중복 근거 : 어느 도메인에서 반복되는가? 반복된다고 판단했을 때, 실제 책임과 계약도 같은가?

    ⇒ 공통화 검토 대상 확정

  3. 책임 경계 : 반복되는 부분 중 컴포넌트가 직접 책임질 동작은 무엇이며, 부모·도메인에 남길 정책은 무엇인가?

    ⇒ 공통 책임과 도메인 책임의 분리 확정

  4. 인터페이스 : 책임을 수행하기 위해 외부에서 받아야 할 데이터·이벤트·슬롯은 무엇인가? 허용하지 않을 도메인 예외 props는 무엇인가?

    ⇒ 컴포넌트의 공개 계약과 확장 방식 확정

  5. 상태 소유 : TanStack Query·Zustand·RHF/Zod·URL·로컬 중 누가 소유하고, 누가 변경하는가?

    ⇒ 상태별 단일 원천과 변경 주체 확정

  6. 렌더 경계 : 어디까지 Server/Client 경계로 볼거고 어디서부터 Client Component로 전환되는가? 브라우저 API·구독·cleanup 의존성은 어디 컴포넌트가 담당하는가?

    ⇒ Server/Client 실행 경계와 effect 생명 주기 확정

  7. 배치 판정 : 공통 컴포넌트, 공통 기반+도메인 조합, 도메인 전용 중 어떤 형태로 구현할 것인가? 공통 영역이 특정 도메인에 의존하지 않는가?

    ⇒ 폴더 위치 확정

[공통 컴포넌트별 7단계 판정]

v3 화면설계서에서 확인된 사용처를 기준으로 작성했습니다. 정확한 탭 구성과 아직 두 번째 동일 계약이 확인되지 않은 항목은 미확정으로 표시하겠습니다.

AuthSessionBoundary

  1. 화면 UI: 보호 화면 접근, 세션 만료 시 AUTH-001 전환, 전역 인증 fallback으로 사용자에게 드러난다.
  2. 중복 근거: 로그인 이후의 모든 보호 route에서 같은 접근 제어가 반복된다.
  3. 책임 경계: 세션 읽기와 보호 route 접근 제어만 담당한다. 로그인·회원가입·로그아웃·사용자 모델은 인증 도메인이 담당한다.
  4. 인터페이스: sessionStatus, children, fallback 수준의 계약만 노출한다. 토큰 저장 방식과 인증 API DTO는 공통 계층에 노출하지 않는다.
  5. 상태 소유: 세션 원본은 인증 서버·인증 도메인이 소유한다. UI 상태를 Zustand에 복제하지 않는다.
  6. 렌더 경계: 서버에서 세션을 확인하는 경계를 우선 검토하고, 브라우저에서 필요한 만료 감지·전환만 Client로 제한한다.
  7. 배치 판정: 공통 애플리케이션 관심사. 인증 도메인 자체가 아니라 보호 route 경계로 분리한다.

CurrentFridgeContext

  1. 화면 UI: 직접 보이는 컴포넌트가 아니라 현재 선택된 냉장고 이름·종류·역할이 각 화면에 나타나게 하는 공통 문맥이다.
  2. 중복 근거: 재고·알림·공유·추천의 모든 조회와 변경 범위가 현재 fridgeId에 따라 달라진다.
  3. 책임 경계: 하위 도메인에 현재 냉장고 snapshot을 읽는 계약만 제공한다. 전환·복귀·공유 정책과 API는 냉장고·공유 도메인이 담당한다.
  4. 인터페이스: fridgeId, kind, role, switching을 노출한다. Query Key Factory와 mutation 구현은 노출하지 않는다.
  5. 상태 소유: 실제 냉장고 데이터는 TanStack Query가 소유하며 모든 관련 Query Key에 fridgeId를 포함한다. 지속 위치는 미확정이다.
  6. 렌더 경계: 인증 계약과 fridgeId 지속 방식이 정해지기 전까지 Server prefetch·Client Provider 범위는 미확정이다.
  7. 배치 판정: 공통 애플리케이션 기반. UI 컴포넌트가 아니라 app provider·공개 read interface로 관리한다.

AppShell

  1. 화면 UI: 상단 헤더·본문·하단 내비게이션이 배치되는 앱의 공통 화면 틀이다.
  2. 중복 근거: MAIN-001, STOCK-001, NOTI-001, MY-001 등 주요 화면에서 같은 배치가 반복된다.
  3. 책임 경계: 공통 레이아웃과 overlay가 열린 동안의 내비게이션 차단만 담당한다. 화면 데이터와 진입 조건은 각 도메인이 담당한다.
  4. 인터페이스: header, children, navigation, navigationVisible을 받는다. 재고·알림 DTO는 받지 않는다.
  5. 상태 소유: 활성 경로는 route에서 파생한다. 서버 데이터를 AppShell이나 Zustand에 복제하지 않는다.
  6. 렌더 경계: Server Layout을 우선하고, 내비게이션·overlay 연동 부분만 Client Component로 분리한다.
  7. 배치 판정: 공통 컴포넌트. shared layout에 두고 각 도메인이 콘텐츠를 주입한다.

BottomTabBar

  1. 화면 UI: 주요 화면 하단의 탭과 중앙 등록 FAB 영역이다.
  2. 중복 근거: 홈·냉장고·알림·마이페이지 등 전역 이동이 가능한 화면에서 같은 위치와 선택 표현이 반복된다.
  3. 책임 경계: 탭 외형·활성 표시·접근성·이동 요청을 담당한다. 100종 한도와 화면 진입 가능 여부는 도메인이 판단한다.
  4. 인터페이스: items, activeKey, disabledKeys, centerAction, onNavigate를 받는다. isStockLimitReached 같은 정책 prop은 금지한다.
  5. 상태 소유: activeKey는 route에서 파생하고, 비활성 값은 도메인이 계산해 전달한다.
  6. 렌더 경계: 경로 판정과 클릭 동작 때문에 최소 범위를 Client Component로 둔다.
  7. 배치 판정: 공통 컴포넌트.

AppHeader

  1. 화면 UI: 화면 상단의 제목, 뒤로가기, 닫기, 알림 등의 action 영역이다.
  2. 중복 근거: 홈·재고·추천·마이페이지와 등록·상세 화면에서 헤더 구조가 반복된다.
  3. 책임 경계: 제목과 leading·action slot의 배치·접근성을 담당한다. 뒤로가기 정책과 알림 조회는 담당하지 않는다.
  4. 인터페이스: title, leading, actions를 받는다. unreadCount 같은 도메인 데이터는 직접 받지 않는다.
  5. 상태 소유: 자체 상태는 없다. action의 상태와 이벤트는 사용하는 도메인이 소유한다.
  6. 렌더 경계: 기본 헤더는 Server Component가 가능하다. Client action은 slot 단위로 주입한다.
  7. 배치 판정: 공통 기반 + 도메인 조합. AppHeader는 shared, NotificationBell 등은 각 도메인에 둔다.

AppDialog

  1. 화면 UI: 화면 중앙의 제목·설명·취소·실행 버튼과 backdrop으로 구성된 확인창이다.
  2. 중복 근거: REG-007 작성 중 이탈, 중복 재료 합산, STOCK-002 삭제 확인 등에서 같은 확인 구조가 반복된다.
  3. 책임 경계: focus trap, backdrop, 버튼 순서, dismiss 정책을 담당한다. 데이터 폐기·합산·삭제 mutation은 도메인이 담당한다.
  4. 인터페이스: open, title, description, cancelLabel, actionLabel, tone, dismissPolicy, onCancel, onAction, onOpenChange를 받는다.
  5. 상태 소유: open은 가까운 Business Component의 로컬 상태가 우선이다. mutation과 pending 상태는 TanStack Query가 소유한다.
  6. 렌더 경계: Client Component다. 열릴 때 focus·scroll을 제어하고 닫힐 때 listener·scroll lock·focus를 복구한다.
  7. 배치 판정: 공통 기반 + 도메인 조합. AppDialog 껍데기만 shared에 두고 업무별 Dialog는 도메인에 둔다.

AppBottomSheet

  1. 화면 UI: 화면 하단에서 올라오는 drag handle·본문·action 영역과 backdrop이다.
  2. 중복 근거: REG-004-2 부분 성공, REC-002-1 재료 차감, 만료 일괄 정리, 값 선택 화면에서 반복된다.
  3. 책임 경계: 위치·drag handle·focus·scroll·dismiss 동작을 담당한다. 성공·실패 계산, 차감 대상, 선택 가능 값은 도메인이 담당한다.
  4. 인터페이스: open, children, actionArea, dismissPolicy, onDismiss, onOpenChange를 받는다. 등록·추천 전용 prop은 금지한다.
  5. 상태 소유: open은 부모 로컬 상태가 우선이고 여러 형제가 제어할 때만 Zustand를 검토한다. Query·폼 값은 도메인에 유지한다.
  6. 렌더 경계: Client Component다. 열림 시 focus·scroll lock을 적용하고 닫힘 시 cleanup한다.
  7. 배치 판정: 공통 기반 + 도메인 조합. AppBottomSheet는 shared, 부분 성공·차감 시트는 각 도메인에 둔다.

AsyncViewState 계열

  1. 화면 UI: LoadingState, EmptyState, ErrorState, RetryButton, PaginationState로 구성되는 비동기 화면 상태다.
  2. 중복 근거: MAIN-001 빈 재고, OCR 결과 없음, REC-001-2/-3/-4, route 전역 fallback에서 반복된다.
  3. 책임 경계: 공통 정렬·접근성·재시도 중 중복 클릭 방지를 담당한다. Query 실행, 상태 판정, 문구·아이콘·복구 행동은 도메인이 담당한다.
  4. 인터페이스: status, title, description, illustration, action, retryPending을 받는다. TanStack Query 객체 전체는 받지 않는다.
  5. 상태 소유: loading·error·data는 TanStack Query가 소유한다. AsyncViewState는 화면 표시용 값만 받는다.
  6. 렌더 경계: 표시만 하면 Server·Client 모두 가능하다. retry callback이 있는 최소 영역만 Client 경계에 둔다.
  7. 배치 판정: 공통 프레임 + 도메인 slot. 화면별 카피·아이콘·복구 action은 도메인에서 주입한다.

AppToast

  1. 화면 UI: 성공·실패·한도 안내가 화면 하단에 잠시 표시되는 피드백이다.
  2. 중복 근거: 재고 100종 한도, 등록·수정·삭제 결과 등 여러 도메인에서 동일 표시 규칙을 사용한다.
  3. 책임 경계: 위치, 3초 timer, 동시에 1개 표시, 중복 메시지 timer 갱신, 성공·실패 variant를 담당한다. 표시 시점과 문구는 도메인이 정한다.
  4. 인터페이스: message, variant, dedupeKey를 받는다. 실행 callback과 서버 DTO는 받지 않는다.
  5. 상태 소유: Sonner가 queue와 timer를 소유한다. 별도 Zustand toast store를 만들지 않는다.
  6. 렌더 경계: Client Component이며 앱 root에 Toaster를 한 번 배치한다.
  7. 배치 판정: 공통 컴포넌트·어댑터. 중요한 선택과 되돌릴 수 없는 작업에는 Toast 대신 Dialog 또는 BottomSheet를 사용한다.

NotificationBell

  1. 화면 UI: AppHeader 우측의 알림 아이콘과 안 읽은 수 badge다.
  2. 중복 근거: 홈·냉장고 관리·추천 목록·마이페이지·만료 재고 목록 등 여러 화면에 반복된다.
  3. 책임 경계: 아이콘·badge 조합과 알림 화면 이동을 담당한다. 안 읽은 수 조회·읽음 처리는 알림 도메인이 담당한다.
  4. 인터페이스: unreadCount, onNavigate 또는 알림 링크를 받는다. AppHeader가 알림 Query를 직접 호출하지 않는다.
  5. 상태 소유: 안 읽은 수는 알림 도메인의 TanStack Query cache가 단일 원천이다.
  6. 렌더 경계: 서버 prefetch 값을 받을 수 있고, 실시간 갱신·클릭 영역만 Client Component로 둔다.
  7. 배치 판정: 알림 도메인 컴포넌트. shared의 AppHeader·IconButton·Badge를 조합하며 공통 UI 자체로 올리지 않는다.

Button · IconButton

  1. 화면 UI: 로그인, 등록, 저장, 취소, 뒤로가기, 삭제 등에 쓰이는 버튼 표현이다.
  2. 중복 근거: 모든 도메인에서 채움·테두리·비활성·로딩 표현이 반복된다.
  3. 책임 경계: 시각적 위계, disabled·loading 표현, 접근 가능한 버튼 semantics를 담당한다. 클릭 결과는 담당하지 않는다.
  4. 인터페이스: variant, size, type, disabled, loading, children, onClick을 받는다. 도메인 이름이 들어간 variant는 금지한다.
  5. 상태 소유: loading·disabled의 원인은 부모 폼이나 mutation이 소유하고 버튼은 값만 받는다.
  6. 렌더 경계: 정적 버튼 마크업은 Server에서 가능하고, 이벤트 핸들러를 전달하는 사용처는 Client 경계 안에 둔다.
  7. 배치 판정: 공통 primitive. Button과 IconButton을 분리하되 동일 토큰과 상태 규칙을 사용한다.

FormField

  1. 화면 UI: label, 입력 요소, helper text, validation error가 묶인 입력 영역이다.
  2. 중복 근거: AUTH-003 닉네임, REG-004 재료 입력, SHARE-003 초대 코드에서 같은 구조가 반복된다.
  3. 책임 경계: label·hint·error 연결과 접근성 ID를 담당한다. 닉네임·수량·초대 코드 검증 규칙은 담당하지 않는다.
  4. 인터페이스: id, label, hint, error, required, children을 받는다. 도메인 검증용 boolean prop은 금지한다.
  5. 상태 소유: 입력값·필드 오류는 각 도메인의 React Hook Form과 Zod가 소유한다.
  6. 렌더 경계: FormField 자체는 표현 컴포넌트이고, 실제 폼 controller와 제출 영역은 Client Component다.
  7. 배치 판정: 공통 primitive. 검증 schema와 서버 오류 매핑은 도메인 폼에 둔다.

Badge · Chip · CardSurface

  1. 화면 UI: D-day·알림 수 badge, 필터 chip, 재고·알림·추천 card의 공통 표면이다.
  2. 중복 근거: STOCK-001, NOTI-001, MY-004, REC-001 등에서 표현 요소가 반복된다.
  3. 책임 경계: 색상·간격·선택·강조 표현만 담당한다. D-day 계산, 필터 적용, 카드 클릭 결과는 도메인이 담당한다.
  4. 인터페이스: Badge는 label, tone; Chip은 selected, disabled, onSelect; CardSurface는 children과 의미 있는 HTML 속성만 받는다.
  5. 상태 소유: D-day·알림 수는 Query, 재고 필터는 URL, 선택의 일시 상태는 가까운 부모가 소유한다.
  6. 렌더 경계: 표시 전용은 Server에서 가능하고, 선택 가능한 Chip만 Client 경계에서 사용한다.
  7. 배치 판정: 공통 primitive. 재고 카드·알림 카드·추천 카드는 각 도메인에서 이 primitive를 조합한다.

ExpiryDatePicker

  1. 화면 UI: 유통기한 값을 표시하고 달력 또는 값 선택 BottomSheet를 여는 입력 영역이다.
  2. 중복 근거: REG-005 사용은 확인됐고 STOCK-002 수정 화면과 동일 계약인지는 미확정이다.
  3. 책임 경계: 달력 표시와 날짜 선택 이벤트만 담당한다. 허용 날짜·기본값·D-day·저장 mutation은 도메인이 담당한다.
  4. 인터페이스: 후보 계약은 value, onChange, minDate, maxDate, disabledDates, open, onOpenChange다.
  5. 상태 소유: 입력값과 검증은 React Hook Form·Zod, 서버 기준 날짜와 저장 결과는 도메인 Query·mutation이 소유한다.
  6. 렌더 경계: React DayPicker 상호작용 때문에 Client Component다. 날짜 계산은 date-fns와 Asia/Seoul 기준을 사용한다.
  7. 배치 판정: 현재는 도메인 내부 유지. STOCK-002와 동일 계약이 확인되면 공통 기반으로 재판정한다.

🔥 5. 공통 컴포넌트 도메인

각 도메인이 공통 계층에서 공식적으로 가져다 쓸 수 있도록 공개한 컴포넌트·타입·Hook 목록을 뜻합니다.

분류 의미 다먹자에서의 사용 예
layout 여러 화면의 공통 골격 상단 헤더, 본문 영역, 하단 탭
overlay 현재 화면 위에 띄우는 UI 모달, 바텀시트
feedback 작업 결과를 짧게 알리는 UI로 추가 판단이 필요없는 피드백에 활용 토스트
async-view API 요청 상태를 표현하는 UI 로딩, 데이터 없음, 조회 실패, 재시도
primitives 비즈니스 의미가 없는 작은 표현 요소 → 재사용 가능한 표현 계층 버튼, 배지, 칩, 카드 배경 etc
context 여러 도메인이 읽어야 하는 공통 실행 문맥 현재 선택된 냉장고 정보

용어 정의 (Glossary) (Optional)

  • 컴포넌트 설계 용어

    용어 정의 다먹자 적용
    공통 UI·shared 특정 비즈니스 도메인의 규칙 없이 여러 화면이 재사용하는 UI 표현 계층입니다. AppHeader, Button, AppDialog의 기반 구조가 해당합니다. 공통 비즈니스 도메인을 뜻하지 않습니다.
    도메인 컴포넌트·Business Component 특정 사용자 흐름과 업무 규칙을 알고 실행하는 컴포넌트입니다. 부분 성공 결과 시트, 재료 차감 시트, NotificationBell처럼 Query·mutation·라우팅을 연결합니다.
    primitive 재사용하는 가장 기초적인 UI 요소입니다. Button, IconButton, Badge, Chip, CardSurface가 해당합니다.
    조합·composition 작은 공통 요소를 묶어 화면 목적에 맞는 컴포넌트를 만드는 방식입니다. AppBottomSheet 안에 등록 결과 목록을 넣어 RegistrationResultSheet를 구성합니다.
    template 반복되는 컴포넌트 조합과 변경하지 않기로 한 결정을 하나의 구조로 고정한 것입니다. 화면별 예외가 충분히 검증된 경우에만 사용하며 미래 사용 예상만으로 만들지 않습니다.
    props 부모가 컴포넌트에 전달하는 데이터와 설정입니다. Button의 variant·disabled, AppDialog의 title·open 등이 해당합니다.
    public interface·공개 계약 사용처가 컴포넌트에 전달하거나 받을 수 있도록 외부에 공개한 props·event·slot의 집합입니다. 공통 컴포넌트가 책임질 수 있는 값만 공개하고 도메인 DTO는 포함하지 않습니다.
    slot 사용처가 소유하는 변형 영역을 이름으로 드러내어 주입받는 자리입니다. AppHeader의 actions, AppBottomSheet의 actionArea가 해당하며 예외를 숨기는 만능 children으로 사용하지 않습니다.
    variant 컴포넌트가 공식적으로 지원하는 제한된 표현 또는 상태 종류입니다. Toast의 success·error, Button의 primary·secondary처럼 유한한 값으로 정의합니다.
    어댑터·adapter 서로 다른 UI나 외부 라이브러리를 애플리케이션이 하나의 공통 인터페이스로 사용할 수 있게 변환하는 코드입니다. Sonner의 API를 AppToast 계약으로 감싸 도메인이 Sonner에 직접 의존하지 않게 합니다.
  • 상태·데이터 용어

    용어 정의 다먹자 적용
    서버 상태 서버가 원본을 소유하며 조회·변경·동기화가 필요한 데이터입니다. 재고, 알림, 추천, 참여자와 안 읽은 알림 수를 TanStack Query로 관리합니다.
    클라이언트 UI 상태 서버 데이터와 무관하게 현재 브라우저의 표현과 상호작용만 제어하는 상태입니다. 모달·바텀시트 open, 일시적인 탭 상태가 해당합니다.
    query 서버 데이터를 읽는 요청입니다. 다먹자에서는 주로 GET 요청이며 TanStack Query가 캐시·재요청을 관리합니다. 현재 냉장고의 재고 목록이나 알림 목록 조회가 해당합니다.
    mutation 서버 데이터를 변경하는 요청입니다. 다먹자에서는 주로 POST·PUT·PATCH·DELETE 요청이며 사용자가 행동했을 때 명시적으로 실행합니다. 재고 등록·수정·삭제와 레시피 재료 차감이 해당하며 성공 후 관련 Query를 갱신합니다.
    Query Key TanStack Query가 서버 데이터를 캐시에서 구분하기 위해 사용하는 식별자입니다. 재고·알림·공유·추천 Query Key에 현재 fridgeId를 포함합니다.
    cache·캐시 조회한 서버 데이터를 다시 사용할 수 있도록 클라이언트에 보관한 사본입니다. 같은 냉장고의 재고를 여러 화면이 조회할 때 동일 Query Cache를 사용합니다.
    invalidate·무효화 mutation 이후 기존 캐시가 오래되었음을 표시하고 다시 조회하게 만드는 처리입니다. 재고 변경 후 홈 용량·재고 목록·상세·마이페이지 요약 Query를 무효화합니다.
    단일 원천·source of truth 동일 상태의 기준이 되는 원본을 한곳만 두는 원칙입니다. 서버 데이터를 Zustand에 복제하지 않고 TanStack Query Cache를 원본으로 사용합니다.
    Context·Provider 여러 하위 컴포넌트가 공통 값을 읽을 수 있도록 상위에서 제공하는 React 구조입니다. CurrentFridgeContext는 fridgeId·냉장고 종류·역할을 읽는 인터페이스만 제공합니다.
    URL 상태 주소에 저장하여 새로고침과 뒤로가기 후에도 복원해야 하는 화면 상태입니다. STOCK-001의 검색·필터·정렬을 URL 검색 파라미터로 관리합니다.
  • 렌더링·생명주기 용어

    용어 정의 다먹자 적용
    Server Component 서버에서 실행되며 브라우저 JavaScript를 줄일 수 있는 React 컴포넌트입니다. 상호작용이 없는 AppShell 레이아웃과 초기 데이터 표시 영역에 우선 사용합니다.
    Client Component 이벤트·로컬 상태·브라우저 API가 필요하여 브라우저에서도 실행되는 React 컴포넌트입니다. AppDialog, AppBottomSheet, DatePicker와 폼 controller에 사용합니다.
    SSR 요청 시 서버에서 HTML을 만들어 사용자에게 전달하는 렌더링 방식입니다. 초기 접근에서 필요한 개인화 데이터를 서버에서 준비할 때 검토합니다.
    CSR 브라우저에서 JavaScript가 데이터를 조회하고 화면을 갱신하는 렌더링 방식입니다. 사용자 상호작용 이후 자주 갱신되는 재고·알림·추천 영역에 선택적으로 사용합니다.
    prefetch 화면이 데이터를 사용하기 전에 서버 또는 클라이언트에서 미리 조회하는 처리입니다. 초기 화면의 독립 Query를 병렬로 준비하여 요청 워터폴을 줄입니다.
    hydration 서버가 만든 HTML에 클라이언트 JavaScript가 연결되어 상호작용할 수 있게 되는 과정입니다. Client Component 범위를 최소화하여 hydration 대상과 번들 크기를 줄입니다.
    effect·생명주기 컴포넌트가 화면에 나타나거나 상태가 바뀔 때 외부 환경과 동기화하는 처리와 그 유지 기간입니다. 키보드 이벤트, body scroll lock, 푸시 구독처럼 외부 시스템과 연결할 때만 사용합니다.
    cleanup 컴포넌트가 닫히거나 사라질 때 등록한 이벤트·구독·화면 제어를 원래 상태로 되돌리는 처리입니다. BottomSheet가 닫힐 때 listener와 scroll lock을 해제하고 이전 focus를 복구합니다.
    overlay·backdrop 현재 화면 위에 Modal·BottomSheet를 표시하고 뒤쪽 조작을 막는 레이어와 배경입니다. overlay가 열린 동안 하단 내비게이션을 조작할 수 없게 처리합니다.
    focus trap Modal·BottomSheet가 열린 동안 키보드 focus가 overlay 내부에서만 이동하도록 제한하는 접근성 처리입니다. AppDialog와 AppBottomSheet의 공통 책임으로 둡니다.
    dismiss policy 바깥 클릭·뒤로가기·ESC·스와이프 등으로 overlay를 닫을 수 있는지 정하는 규칙입니다. 되돌릴 수 없는 차감과 값 선택 시트처럼 사용 목적에 맞는 제한된 정책을 전달합니다.
    fallback 정상 콘텐츠를 표시할 수 없을 때 대신 보여주는 로딩·오류·빈 상태 UI입니다. Next.js route fallback과 AsyncViewState가 공통 프레임을 제공하고 문구·복구 행동은 도메인이 정합니다.

🧩 결론: 이 문서는 공통 비즈니스 도메인이 아니라 여러 화면이 공유하는 UI 조합과 애플리케이션 기반을 정의한다. 화면 조합은 반복 횟수만으로 분리하지 않고, 반복 근거·고정할 공통 결정·검증된 변형 경계를 모두 확인한다. 인증은 독립 도메인으로 두고 접근 제어·전역 Fallback 같은 횡단 처리는 공통 기반에서 다룬다.