한국어로는 단일 책임 원칙
이라고 부른다.
하나의 클래스는 단 하나의 일만 해야 한다는 원칙이다.
리액트에서는 이 규칙을 컴포넌트, 함수 등에 적용할 수 있다.
개선할 코드이다.
// index.tsx
import axios from "axios";
import { useEffect, useMemo, useState } from "react";
import { Rating } from "react-simple-star-rating";
export function ProductPage() {
const [products, setProducts] = useState([]);
const [filterRate, setFilterRate] = useState(1);
const fetchProducts = async () => {
const response = await axios.get(
"https://fakestoreapi.com/products"
);
if (response && response.data) setProducts(response.data);
};
useEffect(() => {
fetchProducts();
}, []);
const handleRating = (rate: number) => {
setFilterRate(rate);
};
const filteredProducts = useMemo(
() =>
products.filter(
(product: any) => product.rating.rate > filterRate
),
[products, filterRate]
);
return (
<div>
<div>
<span>Minimum Rating </span>
<Rating
initialValue={filterRate}
SVGclassName="inline-block"
onClick={handleRating}
/>
</div>
<div>
{filteredProducts.map((product: any) => (
<div>
<a href="#">
<img src={product.image} alt="product image" />
</a>
<div>
<a href="#">
<h5>{product.title}</h5>
</a>
<div>
{Array(parseInt(product.rating.rate))
.fill("")
.map((_, idx) => (
<svg
aria-hidden="true"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<title>First star</title>
<path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z"></path>
</svg>
))}
<span>{parseInt(product.rating.rate)}</span>
</div>
<div>
<span>${product.price}</span>
<a href="#" /* onClick={onAddToCart} */>
Add to cart
</a>
</div>
</div>
</div>
))}
</div>
</div>
);
}
현재 위 코드에서는 다양한 일이 벌어지고 있다.
fetch
로 products
데이터 불러오기리액트에서 SRP를 준수해 가독성과 유지보수성이 좋은 코드를 작성하려면 컴포넌트를 역할 단위로 쪼개고 state
별로 custom hooks를 정의해 사용하는 것이 좋다.
첫 번째로 위 코드에서 쪼갤 수 있는 부분은 두 번째 <div>
안에 있는 {filteredProducts.map...}
부분이다. 보통 map 등의 반복을 통해 생성되는 컴포넌트는 별도의 하위 컴포넌트를 따로 정의해서 코드를 간결하게 만들어 줄 수 있다.
// index.tsx
...
<div>
{filterProducts(products, filterRate).map((product: any) => (
<Product product={product} />
))}
</div>
...
// Product.tsx
interface IProduct {
id: string;
title: string;
price: number;
rating: { rate: number };
image: string;
}
interface IProductProps {
product: IProduct;
}
export function Product(props: IProductProps) {
const { product } = props;
const { id, title, price, rating, image } = product;
return (
<div>
<a href="#">
<img src={image} alt="product image" />
</a>
<div>
<a href="#">
<h5>{title}</h5>
</a>
<div>
{Array(Math.trunc(rating.rate))
.fill("")
.map((_, idx) => (
<svg
aria-hidden="true"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<title>First star</title>
<path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z"></path>
</svg>
))}
<span>{Math.trunc(rating.rate)}</span>
</div>
<div>
<span>${price}</span>
<a href="#" /* onClick={onAddToCart} */>Add to cart</a>
</div>
</div>
</div>
);
}
평점 필터를 설정하는 부분에 해당하는 첫 번째 <div>
또한 "평점 필터 설정"이라는 한 가지의 기능만 수행하기 때문에 분리하면 좋다.
// index.tsx
...
<Filter
filterRate={filterRate as number}
handleRating={handleRating}
/>
...
// Filter.tsx
import { Rating } from "react-simple-star-rating";
export function filterProducts(products: any[], rate: number) {
return products.filter(
(product: any) => product.rating.rate > rate
);
}
interface IFilterProps {
filterRate: number;
handleRating: (rate: number) => void;
}
export function Filter(props: IFilterProps) {
const { filterRate, handleRating } = props;
return (
<div>
<span>Minimum Rating </span>
<Rating
initialValue={filterRate}
SVGclassName="inline-block"
onClick={handleRating}
/>
</div>
);
}
// index.tsx
...
export function ProductPage() {
const [products, setProducts] = useState([]);
const [filterRate, setFilterRate] = useState(1);
const fetchProducts = async () => {
const response = await axios.get("https://fakestoreapi.com/products");
if (response && response.data) setProducts(response.data);
};
useEffect(() => {
fetchProducts();
}, []);
const handleRating = (rate: number) => {
setFilterRate(rate);
};
...
각각의 state
또한 한 가지 작업과 연관이 있기 때문에, 위와 같이 특정 state
와 관련 있는 함수 또는 useEffect
같은 hooks들을 한 데 묶어 아래처럼 custom hook으로 만들어 줄 수 있다.
// index.tsx
...
export function ProductPage() {
const { products } = useProducts();
const { filterRate, handleRating } = useRateFilter();
...
// hooks/useProducts.ts
import axios from "axios";
import { useEffect, useState } from "react";
export const useProducts = () => {
const [products, setProducts] = useState([]);
const fetchProducts = async () => {
const response = await axios.get("https://fakestoreapi.com/products");
if (response && response.data) setProducts(response.data);
};
useEffect(() => {
fetchProducts();
}, []);
return { products };
};
// hooks/useRateFilter.ts
import { useState } from "react";
export function useRateFilter() {
const [filterRate, setFilterRate] = useState(1);
const handleRating = (rate: number) => {
setFilterRate(rate);
};
return { filterRate, handleRating };
}
// index.tsx
import { Product } from "./Product";
import { Filter, filterProducts } from "./Filter";
import { useProducts } from "./hooks/useProducts";
import { useRateFilter } from "./hooks/useRateFilter";
export function ProductPage() {
const { products } = useProducts();
const { filterRate, handleRating } = useRateFilter();
return (
<div>
<Filter
filterRate={filterRate as number}
handleRating={handleRating}
/>
<div>
{filterProducts(products, filterRate).map((product: any) => (
<Product product={product} />
))}
</div>
</div>
);
}
SRP를 준수하는 깔끔한 코드가 완성됐다.
- SRP를 준수해 각각의 컴포넌트, 함수 등이 단 한 가지 일만 수행하도록 한다.
- 컴포넌트 역할 단위로 쪼갠다.
state
별로 custom hooks를 정의해 코드를 쪼갠다.