widgets 레이어는 독립적인 UI 블록(복합 컴포넌트)을 포함합니다.
widgets 레이어는 여러 features나 entities를 조합한 복합적인 UI 블록을 포함합니다. 페이지의 큰 섹션을 구성하거나, 여러 기능을 통합한 독립적인 UI 컴포넌트입니다.
- ✅ 복합 컴포넌트: 여러 features/entities를 조합
- ✅ 독립적: 자체적으로 완성된 UI 블록
- ✅ 재사용 가능: 여러 페이지에서 사용 가능
- ✅ 비즈니스 로직 최소화: 주로 UI 조합에 집중
widgets/
├── header/ # 헤더 위젯
│ ├── ui/
│ │ ├── Header.tsx
│ │ └── HeaderNavigation.tsx
│ └── model/ # 위젯 관련 상태 (선택적)
│ └── useHeader.ts
├── product-list/ # 상품 목록 위젯
│ ├── ui/
│ │ ├── ProductList.tsx
│ │ └── ProductListHeader.tsx
│ └── model/
│ └── useProductList.ts
├── user-profile-card/ # 사용자 프로필 카드 위젯
│ └── ui/
│ └── UserProfileCard.tsx
└── README.md # 이 문서
위젯의 UI 컴포넌트를 포함합니다. 여러 features나 entities를 조합합니다.
예시: widgets/product-list/ui/ProductList.tsx
import { View } from "react-native";
import { SearchBar } from "@/features/search-products/ui/SearchBar";
import { ProductCard } from "@/features/view-product/ui/ProductCard";
import { Product } from "@/entities/product/model/types";
interface ProductListProps {
products: Product[];
}
export function ProductList({ products }: ProductListProps) {
return (
<View>
<SearchBar />
<View>
{products.map((product) => (
<ProductCard key={product.id} product={product} />
))}
</View>
</View>
);
}위젯 관련 상태 관리나 커스텀 훅을 포함합니다. 위젯 내부에서만 사용되는 상태입니다.
예시: widgets/product-list/model/useProductList.ts
import { useState } from "react";
import { Product } from "@/entities/product/model/types";
export function useProductList(initialProducts: Product[]) {
const [products, setProducts] = useState<Product[]>(initialProducts);
const [filter, setFilter] = useState<string>("");
const filteredProducts = products.filter((product) =>
product.name.toLowerCase().includes(filter.toLowerCase())
);
return {
products: filteredProducts,
filter,
setFilter,
};
}새로운 위젯을 추가할 때는 다음 구조를 따릅니다.
예시: widgets/user-dashboard/ 생성
- UI 컴포넌트 (
ui/UserDashboard.tsx)
import { View, Text } from "react-native";
import { UserProfileCard } from "@/widgets/user-profile-card/ui/UserProfileCard";
import { EditProfileButton } from "@/features/edit-user-profile/ui/EditProfileButton";
import { User } from "@/entities/user/model/types";
interface UserDashboardProps {
user: User;
}
export function UserDashboard({ user }: UserDashboardProps) {
return (
<View>
<UserProfileCard user={user} />
<EditProfileButton userId={user.id} />
</View>
);
}- 상태 관리 (
model/useUserDashboard.ts) - 선택적
import { useState } from "react";
import { User } from "@/entities/user/model/types";
export function useUserDashboard(initialUser: User) {
const [user, setUser] = useState<User>(initialUser);
return {
user,
setUser,
};
}위젯은 여러 features를 조합하여 더 큰 UI 블록을 만듭니다.
예시: widgets/product-search-section/ui/ProductSearchSection.tsx
import { View } from "react-native";
import { SearchBar } from "@/features/search-products/ui/SearchBar";
import { SearchResults } from "@/features/search-products/ui/SearchResults";
import { FilterButton } from "@/features/filter-products/ui/FilterButton";
import { useSearchProducts } from "@/features/search-products/model/useSearchProducts";
export function ProductSearchSection() {
const { query, results, search } = useSearchProducts();
return (
<View>
<View className="flex-row gap-2">
<SearchBar onSearch={search} />
<FilterButton />
</View>
<SearchResults results={results} />
</View>
);
}-
복합 컴포넌트 중심
// ✅ 좋은 예: 여러 features를 조합한 위젯 export function ProductList() { return ( <View> <SearchBar /> <ProductCard /> <FilterButton /> </View> ); }
-
독립적인 UI 블록
// ✅ 좋은 예: 자체적으로 완성된 위젯 export function Header() { return ( <View> <Logo /> <Navigation /> <UserMenu /> </View> ); }
-
Features와 Entities 활용
// ✅ 좋은 예: 하위 레이어 활용 import { SearchBar } from "@/features/search-products/ui/SearchBar"; import { User } from "@/entities/user/model/types";
-
재사용 가능한 구조
// ✅ 좋은 예: 여러 페이지에서 사용 가능 import { Header } from "@/widgets/header/ui/Header";
-
단순 컴포넌트 포함 금지
// ❌ 나쁜 예: 단순한 버튼 컴포넌트 // widgets/button/ui/Button.tsx export function Button() { ... } // shared/ui에 있어야 함 // ✅ 좋은 예: 복합 위젯 export function ProductList() { // 여러 features를 조합 }
-
비즈니스 로직 포함 금지
// ❌ 나쁜 예: 복잡한 비즈니스 로직 export function ProductList() { const handleComplexBusinessLogic = () => { // 복잡한 로직은 features에 있어야 함 }; } // ✅ 좋은 예: UI 조합에 집중 export function ProductList() { return ( <View> <SearchBar /> <ProductCard /> </View> ); }
-
페이지 레이아웃 포함 금지
// ❌ 나쁜 예: 전체 페이지 구조 export function ProductPage() { return ( <ScrollView> <Header /> <ProductList /> <Footer /> </ScrollView> ); } // ✅ 좋은 예: 페이지의 일부 섹션 export function ProductList() { // 페이지의 일부만 담당 }
-
상태 관리 최소화
// ⚠️ 주의: 위젯 내부 상태는 최소화 // 복잡한 상태는 features나 pages에서 관리 // ✅ 좋은 예: 위젯 내부 UI 상태만 const [isExpanded, setIsExpanded] = useState(false); // ❌ 나쁜 예: 복잡한 비즈니스 상태 const [products, setProducts] = useState([]); const [filters, setFilters] = useState({}); // 이런 상태는 features나 pages에서 관리
-
다른 widgets 직접 import 제한
// ⚠️ 주의: widgets 간 직접 import는 피하는 것이 좋음 // 대신 pages에서 조합 // ❌ 나쁜 예: widget 간 직접 import import { Header } from "@/widgets/header/ui/Header"; // ✅ 좋은 예: pages에서 조합 // pages/product-page/ui/ProductPage.tsx import { Header } from "@/widgets/header/ui/Header"; import { ProductList } from "@/widgets/product-list/ui/ProductList";
-
상위 레이어 import 금지
// ❌ 나쁜 예: 상위 레이어 import import { SomePage } from "@/pages/some-page"; // ✅ widgets는 features, entities, shared만 import 가능
widgets/product-list/ui/ProductList.tsx
import { View, ScrollView, Text } from "react-native";
import { SearchBar } from "@/features/search-products/ui/SearchBar";
import { ProductCard } from "@/features/view-product/ui/ProductCard";
import { AddToCartButton } from "@/features/add-to-cart/ui/AddToCartButton";
import { Product } from "@/entities/product/model/types";
import { useProductList } from "../model/useProductList";
interface ProductListProps {
products: Product[];
}
export function ProductList({ products: initialProducts }: ProductListProps) {
const { products, filter, setFilter } = useProductList(initialProducts);
return (
<ScrollView className="flex-1 bg-canvas">
<View className="p-screen-m">
<SearchBar
value={filter}
onChangeText={setFilter}
placeholder="상품 검색..."
/>
{products.length === 0 ? (
<View className="items-center justify-center py-8">
<Text className="text-content-secondary">
검색 결과가 없습니다
</Text>
</View>
) : (
<View className="mt-4 space-y-gutter">
{products.map((product) => (
<View
key={product.id}
className="bg-neutral rounded-lg p-4"
>
<ProductCard product={product} />
<View className="mt-3">
<AddToCartButton productId={product.id} />
</View>
</View>
))}
</View>
)}
</View>
</ScrollView>
);
}widgets/product-list/model/useProductList.ts
import { useState, useMemo } from "react";
import { Product } from "@/entities/product/model/types";
export function useProductList(initialProducts: Product[]) {
const [products] = useState<Product[]>(initialProducts);
const [filter, setFilter] = useState<string>("");
const filteredProducts = useMemo(() => {
if (!filter) return products;
return products.filter((product) =>
product.name.toLowerCase().includes(filter.toLowerCase())
);
}, [products, filter]);
return {
products: filteredProducts,
filter,
setFilter,
};
}widgets/header/ui/Header.tsx
import { View, Text, TouchableOpacity } from "react-native";
import { UserAvatar } from "@/entities/user/ui/UserAvatar";
import { User } from "@/entities/user/model/types";
interface HeaderProps {
user?: User;
onProfilePress?: () => void;
}
export function Header({ user, onProfilePress }: HeaderProps) {
return (
<View className="bg-neutral px-screen-m py-4 flex-row items-center justify-between">
<Text className="text-xl font-bold text-content-primary">
ASSU
</Text>
{user && (
<TouchableOpacity onPress={onProfilePress}>
<UserAvatar user={user} size={32} />
</TouchableOpacity>
)}
</View>
);
}- FSD 아키텍처: Feature-Sliced Design 공식 문서
- Widgets 가이드: FSD Widgets 레이어 가이드