본문으로 바로가기

(React) clean code - SOLID

category software engineering/frontend 2022. 11. 29. 12:11
728x90

SOLID
S: 
Single Responsibility

  • 한 컴포넌트 안에 useState가 두 개 이상 있다면 리팩토링을 고려하자
export function Bad() {
    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();
      }, []);
      
      ...
}
# hooks/useProducts.tsx
export const useProducts = () => {
  const [products, setProducts] = useState<any[]>([]);

  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.tsx
export function useRateFilter() {
  const [filterRate, setFilterRate] = useState(1);

  const handleRating = (rate: number) => {
    setFilterRate(rate);
  };

  return { filterRate, handleRating };
}

 

  • 한 컴포넌트 내 두개 이상의 map 함수를 랜더링 하고 있다면 리팩토링을 고려하자.
export function Bad() {
	return (
		<div className="h-full flex flex-wrap justify-center">
        {filteredProducts.map((product: any) => (
        ...
		div/>
        ...
		<div className="flex items-center mt-2.5 mb-5 flex-1">
                {Array(parseInt(product.rating.rate))
                  .fill("")
                  .map((_, idx) => (
                    <svg
                    ...
         </div>
         ...
    )
}
# product.tsx
export function Product(props: IProductProps) {...}

# filter.tsx
export function Filter(props: IFilterProps) {...}

# good.tsx
export function Good() {
    <div className="flex flex-col h-full">
      <Filter
        filterRate={filterRate as number}
        handleRating={handleRating}
      />
      <div className="h-full flex flex-wrap justify-center">
        {filterProducts(products, filterRate).map((product: any) => (
          <Product product={product} />
        ))}
      </div>
    </div>
}

 

O: Open-Closed

  • Software Entities(Classes, Modules, Functions)은 확장에 열려있고 수정에 닫혀있게 구성해야 한다. 다시 말해, 더 많은 기능을 추가하기 위해 소스 코드를 수정해선 안된다.
interface IButtonProps
  extends React.ButtonHTMLAttributes<HTMLButtonElement> {
  text: string;
  role?: "back" | "forward" | "main" | "not-found";
  icon?: React.ReactNode;
}

export function Button(props: IButtonProps) {
  const { text, role, icon } = props;
  return (
    ...
      <div className="m-2">
      	/* BAD */
        {/* {role === "forward" && <HiOutlineArrowNarrowRight />}
        {role === "back" && <HiOutlineArrowNarrowLeft />} */}
      	/* GOOD */
        {icon}
      </div>
     ...
  );
}

export function OCP() {
  return (
    <div className="flex space-x-10">
      <Button
        text="Go Home"
        // role="forward"
        icon={<HiOutlineArrowNarrowRight />}
      />
      <Button
        text="Go Back"
        // role="back"
        icon={<HiOutlineArrowNarrowLeft />}
      />
    </div>
  );
}
# BAD
const TextInput = ({
  type,
  name,
  value,
  textareaRows,
  isShortInput,
  isLoading,
  className,
  containerClassName,
  formGroupClassName,
  placeholder,
  id,
  onChange,
  onInput,
  isTouched,
  onClearClick,
  addonLeft,
  addonRight,
  helpText,
  labelStyle,
  onInvalid,
  onBlur,
  maxLength,
  hasClearButton,
  label,
  addonLeftClassName,
  addonRightClassName,
  inputRef
}) => {
  return <div><input />...</div>;
};

# GOOD
<TextInputContainer ...props>
    <TextInputLabel ...props />
    <TextInputLeftAddon ...props />
    <TextInputControl ...props />
    <TextInputRightAddon ...props />
</TextInputContainer>


L
: Liskov Subsititution

  • 하위 유형 개체는 상위 유형 개체를 대체할 수 있어야 한다.
# LSP.tsx
export function LSP() {
  const [value, setValue] = useState("");

  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    setValue(e.target.value);
  };

  return (
    <SearchInput value={value} onChange={handleChange} isLarge />
  );
}

# SearchInput.tsx
interface ISearchInputProps
  extends React.InputHTMLAttributes<HTMLInputElement> {
  isLarge?: boolean;
}

export function SearchInput(props: ISearchInputProps) {
  const { value, onChange, isLarge, ...restProps } = props;

  return (
    <div className="flex w-10/12">
      	...
        <input
          type="search"
          required
          value={value}
          onChange={onChange}
          {...restProps}
        />
      </div>
    </div>
  );
}

 

I: Interface Segregation

  • 클라이언트가 사용하지 않는 인터페이스에 의존하지 않게 분리해야 한다.
# BAD
<Thumbnail product={product} />

# GOOD
<Thumbnail imageUrl={product.image} />

 

D: Dependency Inversion

  • 낮은 결합도와 테스트 용이성을 위해 추상화에 의존해야지, 구체화에 의존하면 안된다

예를 들어, Create Form vs Edit Form 두 가지 영역에서 Form을 공유하고 있다고 하자.

하지만 두 개는 다른 로직으로 Submit이 처리되야 한다. 그래서 Form을 분리하고 Submission Logic만 분리하는 방법이다.

import { Form } from "./form";

export function ConnectedForm() {
  const handleSubmit = async (email: string, password: string) => {
    await axios.post("https://localhost:3000/login", {
      email,
      password,
    });
  };
  return <Form onSubmit={handleSubmit} />;
}

 

참조
https://blog.bitsrc.io/open-closed-principle-for-react-js-applications-583ac7f887a7

 

SOLID: Open Closed Principle for React Apps

To be a successful front-end developer, you must keep improving your code writing skills. No matter if you are a junior, mid, or senior…

blog.bitsrc.io

https://www.youtube.com/watch?v=MSq_DCRxOxw&t=226s&ab_channel=CoderOne 

 

'software engineering > frontend' 카테고리의 다른 글

웹 접근성(a11y)과 storybook addon  (0) 2023.01.06
Goodbye, useEffect - David Khourshid  (0) 2022.12.05
SSR 그리고 사용자 경험  (0) 2022.11.25
Migrate to Next.js 13 on Turborepo  (0) 2022.10.28
FE CONF 2021 Track A  (0) 2022.10.09