2. 목록 보기
강의 정리
상품 목록
상품 목록 얻기
상품 목록 보여주기
1번은 useFetchProducts라는 custom hook으로 구현을 하고, 2번은 Products 컴포넌트로 구현을 한다. ProductListPage에선 이 둘을 조합한다.
Products 컴포넌트에서 useFetchProducts hook을 불러도 되지만, page에 들어왔을 때 hook을 호출하는게 더 적합하다고 아샬님은 생각한다. 책임을 분리한다. Products는 데이터를 받아서 뿌려주는 역할만 하도록. 이렇게 해야 테스트를 하기도 더 쉽다. ProductListPage에서는 hook이 called 되었는지, Products에서는 데이터가 화면에 원하는대로 뿌려졌는지만 확인하면 될 것 같다.
// ProductListPage.tsx
import Products from '../components/product-list/Products';
export default function ProductListPage() {
const { products } = useFetchProducts();
}
// Products.tsx
import { Link } from 'react-router-dom';
import styled from 'styled-components';
import { ProductSummary } from '../../types';
const Container = styled.div`
ul {
display: flex;
flex-wrap: wrap;
}
li {
width: 20%;
padding: 1rem;
}
a {
display: block;
text-decoration: none;
}
`;
type ProductProps = {
products: ProductSummary[];
};
export default function Products({ products }: ProductsProps) {
if (products.length === 0) {
return null;
}
return (
<Container>
<ul>
{products.map((product) => (
<li key={product.id}>
<Link to={`/products/${product.id}`}>
<Product product={product} />
</Link>
</li>
))}
</ul>
</Container>
);
}
Products 컴포넌트는 처음부터 폴더에 분리하지 않았다. 우선 작성하다가, 컴포넌트가 많아지면 그때 아샬님은 분리하시는 편이다. 사실 어떻게 해도 상관 없다. 다만 본인이 컨트롤할 수 있어야 한다. Product 컴포넌트를 만들어서 디테일하가 어떻게 뿌려줄지에 대해서는 위임을 한다.
// Product.tsx
import styled from 'styled-components';
import { ProductSummary } from '../../types';
import numberFormat from '../../utils/numberFormat';
const Thumbnail = styled.img.attrs({
alt: 'Thumbnail',
})`
display: block;
width: 100%;
aspect-ratio: 1/1;
`;
type ProductProps = {
product: ProductSummary;
};
export default function Product({ product }: ProductProps) {
return (
<div>
<Thumbnail src={product.thumbnail.url} />
<div>{product.name}</div>
<div>{numberFormat(product.price)}원</div>
</div>
);
}
// useFetchProducts.ts
import { container } from 'tsyringe';
import { useEffectOnce } from 'usehooks-ts';
import { useStore } from 'usestore-ts';
import ProductStore from '../stores/ProductsStore';
import { ProductSummary } from '../types';
export default function useFetchProducts() {
const store = container.resolve(ProductStore);
const [{ products }] = useStore();
useEffectOnce(() => {
store.fetchProducts();
});
return {
products,
};
}
apiBaseUrl은 나중에는 환경 변수로 추출하게 된다. 로컬, 테스트, 라이브서버 어디에서 돌리든 구현 코드를 고칠 필요가 없이 환경 변수값만 바꿔주면 된다. 런타임에 터지는 것을 막기 위해서 만약에 data?.products가 null, undefined이면 빈 배열을 넘겨주도록 했다.
// utils/numberFormat.ts
export default function numberFormat(value: number): string {
return new Intl.NumberFormat().format(value);
}
numberFormat과 같은 util의 경우에 매개변수 네이밍을 신경쓰면 좋다. number:number
이런식으로 쓰면 안되고 위와 같은 경우에는 범용적인 value가 무난하다.
// stores/ProductStore.ts
@singleton()
@Store()
export default class ProductsStore {
products: ProductSummary[] = [];
async fetchProducts() {
this.setProducts([]);
const { data } = await axios.get(`${apiBaseUrl}/products`);
const { products } = data;
this.setProducts(products);
}
@Action()
setProducts(products: ProductSummary[]) {
this.products = products;
}
}
카테고리 목록
헤더에 카테고리 목록을 보여줄 것
// Header.tsx
export default function Header() {
const { categories } = useFetchCategories();
return (
<Container>
<h1>Shop</h1>
<nav>
<ul>
<li>
<Link to="/">Home</Link>
</li>
<li>
<Link to="/products">Products</Link>
{!!categories.length && (
<ul>
{categories.map((category) => (
<li key={category.id}>
<Link to={`/products?categoryId=${category.id}`}>
{category.name}
</Link>
</li>
))}
</ul>
)}
</li>
<li>
<Link to="/cart">Cart</Link>
</li>
</ul>
</nav>
</Container>
);
}
NavLink 보다는 현재 위치를 찾아서 스타일링 해주는 방식이 더 낫다. querystring의 경우에 NavLink로 확인하기 어려운 것 같다.
// stores/CategoriesStore.ts
import axios from 'axios';
import { singleton } from 'tsyringe';
import { Store, Action } from 'usestore-ts';
import { Category } from '../types';
@singleton()
@Store()
export default class CategoriesStore {
categories: Category[] = [];
async fetchCategories() {
this.setCategories([]);
const categories = await apiService.fetchCategories();
this.setCategories(categories);
}
@Action()
setCategories(categories: Category[]) {
this.categories = categories;
}
}
// hooks/useFetchCategories.ts
import { container } from 'tsyringe';
import { useEffectOnce } from 'usehooks-ts';
import { useStore } from 'usestore-ts';
import CategoriesStore from '../stores/CategoriesStore';
export default function useFetchCategories() {
const store = container.resolve(CategoriesStore);
const [{ categories }] = useStore(store);
useEffectOnce(() => {
store.fetchCategories();
});
return { categories };
}
Last updated