[React] SOLID 원칙 기반 클린 코딩 1 - SRP

olwooz·2022년 12월 28일
0

React

목록 보기
3/8

SRP - Single Reponsibility Principle

한국어로는 단일 책임 원칙 이라고 부른다.
하나의 클래스는 단 하나의 일만 해야 한다는 원칙이다.
리액트에서는 이 규칙을 컴포넌트, 함수 등에 적용할 수 있다.

개선할 코드이다.

// 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>
  );
}

현재 위 코드에서는 다양한 일이 벌어지고 있다.

  1. fetchproducts 데이터 불러오기
  2. 평점 필터 설정하기
  3. 평점 필터로 필터링된 products 보여주기
    등등...

리액트에서 SRP를 준수해 가독성과 유지보수성이 좋은 코드를 작성하려면 컴포넌트를 역할 단위로 쪼개고 state별로 custom hooks를 정의해 사용하는 것이 좋다.

1. Product Component 분리

첫 번째로 위 코드에서 쪼갤 수 있는 부분은 두 번째 <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>
  );
}

2. 평점 필터 분리

평점 필터를 설정하는 부분에 해당하는 첫 번째 <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>
  );
}

3. state와 연관 로직 묶어서 분리

// 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를 준수하는 깔끔한 코드가 완성됐다.

세 줄 요약

  1. SRP를 준수해 각각의 컴포넌트, 함수 등이 단 한 가지 일만 수행하도록 한다.
  2. 컴포넌트 역할 단위로 쪼갠다.
  3. state별로 custom hooks를 정의해 코드를 쪼갠다.

참고 자료: https://www.youtube.com/watch?v=MSq_DCRxOxw

0개의 댓글