프로젝트를 만족스럽게 진행하고 pr을 올렸는데
" 단일 책임 원칙으로 변경해주세요 라는 코멘트를 받았다

단일책임원칙은 SOLID 에 해당하는 부분으로 오늘 자세히 다뤄볼 것이다!
Solid 원칙: 소프트웨어의 기본 5가지 원칙을 말한다.
프로그래머가 시간이 지나도 유지 보수와 확장이 쉬운 시스템을 만들고자 할 때 이 원칙들을 함께 적용할 수 있다
SOLID 원칙들은 소프트웨어 작업에서 프로그래머가 소스 코드가 읽기 쉽고 확장하기 쉽게 될 때까지 소프트웨어 소스 코드를 리팩터링하여 코드 냄새를 제거하기 위해 적용할 수 있는 지침이다.
아래의 코드를 SOLID 5가지 원칙에 의거해서 리팩토링 해보겠다!
클래스는 단일 책임만 가져야 하며, 즉, 소프트웨어 사양의 한 부분에 대한 변경만 클래스 사양에 영향을 줄 수 있어야 합니다.
이 말을 쉽게하면 컴포넌트가 하나의 기능 또는 하나의 역할만을 책임져야 된다는 말이다.
import { useEffect, useState } from "react";
import axios from "axios";
export default function App() {
const [todos, setTodos] = useState([]);
useEffect(() => {
axios.get("https://example.com/todos").then((res) => {
setTodos(res.data);
});
}, []);
return (
<div className="App">
<h1>todolist</h1>
<ul>
{todos.map((todo) => (
<li key={todo.id}>{todo.text}</li>
))}
</ul>
</div>
);
}
위의 코드는 하나의 컴포넌트가 데이터 가져오기와 렌더링을 담당하고 있다
때문에 단일 책임 원칙을 위반하고 있으며, 코드의 복잡성을 높이고 유지 보수가 어렵다. 위의 코드를 단일 책임 원칙을 적용해보자
import { useEffect, useState } from 'react';
import axios from 'axios';
//렌더링 담당
function TodoRendering({ todos }) {
return (
<div className="App">
<h1>todolist</h1>
<ul>
{todos.map(todo => (
<li key={todo.id}>{todo.text}</li>
))}
</ul>
</div>
);
}
//데이터 가져오기 담당
function TodoFetch() {
const [todos, setTodos] = useState([]);
useEffect(() => {
axios.get('https://example.com/todos').then(res => {
setTodos(res.data);
});
}, []);
return <TodoRendering todos={todos} />;
}
export default function App() {
return <TodoFetch />;
}
➡️ TodoFetch는 데이터 가져오기 역할을, TodoRendering는 렌더링 역할을 함으로써 각각의 컴포넌트의 역할을 분리해줬다👍
export function CouponList() {
const [coupons, setCoupons] = useState([]);
useEffect(() => {
getCoupons().then((res) => setCoupons(res));
}, []);
return (
<div>
{coupons.map((coupon, i) => (
<div
key={i}
title={coupon.title}
onClick={() => handleSelectCoupon(coupon)}
/>
))}
<button title="취소하기" onClick={() => handleSelectCoupon(null)} />
</div>
);
}
useEffect, useState가 있는 부분은 hook으로 뺄 수 있다
그래서 hook으로 분리를 하면 아래와 같은 코드가 된다
즉 서버에서 데이터를 패칭 하는 역할은 useGetCoupons 훅에게 위임하고 List는 데이터를 가지고 화면을 그리는데 집중할 수 있도록 역할 분리를 해준 것
이렇게 했을 책임 분리로 인한 가독성이 올라가고 역할별 테스팅 하기도 용이해진다
function useGetCoupons() {
const [coupons, setCoupons] = useState([]);
useEffect(() => {
getCoupons().then(res => setCoupons(res));
}, []);
return { coupons };
}
export function CouponList() {
const { coupons } = useGetCoupons();
return (
<div>
{coupons.map((coupon, i) => (
<div key={i} title={coupon.title} onClick={() => handleSelectCoupon(coupon)} />
))}
<button title="취소하기" onClick={() => handleSelectCoupon(null)} />
</div>
);
}
확장에 대해 개방적이어야 하지만, 수정에 대해서는 폐쇄적이어야 한다.
이 말을 쉽게하면 기존 코드를 수정하지 않고도 새로운 기능을 추가하거나 확장할 수 있어야 한다는 말이다.
예를 들어 아래의 컴포넌트를 보자 label과 onClick 두 가지 prop을 받은 버튼 컴포넌트다. 버튼 컴포넌트는 초기에는 간단한 동작을 갖고 있을 수 있지만, 시간이 지나면서 다양한 기능을 추가 해야 할 수 있다.
import React from 'react';
interface ButtonProps {
label: string;
onClick: () => void;
}
function Button({ label, onClick }: ButtonProps) {
return (
<button onClick={onClick}>
{label}
</button>
);
}
export default Button;
개방-폐쇄 원리를 따라 버튼 컴포넌트를 수정하지 않고 다양한 기능을 추가하거나 확장할 수 있는 컴포넌트로 개선해보자
import React, { ReactNode } from 'react';
interface ButtonProps {
label: string;
onClick: () => void;
icon?: ReactNode; // 아이콘
disabled?: boolean; // 비활성화
className?: string; // 클래스네임
}
function Button({
label,
onClick,
icon,
disabled = false,
className = '',
}: ButtonProps) {
return (
<button
onClick={onClick}
disabled={disabled}
className={className}
>
{icon && <span className="icon">{icon}</span>}
{label}
</button>
);
}
export default Button;
위의 코드는 다양한 새로운 속성을 prop으로 추가해서 코드를 수정하지 않고도 다양한 기능을 추가할 수 있다.
function QuantityButton({ mode, onClick }: Props) {
return (
<CountButton onClick={onClick}>
{mode === 'plus' ? <AiOutlinePlus /> : <AiOutlineMinus />}
</CountButton>
);
}
위 코드는 아이콘이 Plus, Minus아이콘으로 한정되어 있다
즉 확장에 닫혀있다는 뜻!
위 코드에 확장성을 올리려면 아래와 같이 아이콘을 버튼 안에서 정의하는 것이 아니라 바깥에서 주입받으면 된다
function QuantityButton({ icon, onClick }: Props) {
return <CountButton onClick={onClick}>{icon}</CountButton>;
}
이제 아이콘이 두 개로 한정되어 있는게 아니라, 바깥에서 프롭스로 넘겨받기 때문에 두개로 한정된 게 아니라 여러 가지 아이콘을 적용할 수 있다.
또한 이 컴포넌트 내부에서 아이콘을 바꿀 필요가 없으므로 확장에는 용이하고 변경에는 닫혀있는 원칙이 적용된 것이다
인터페이스 분리 원칙이란 사용하지 않는 인터페이스에 의존하면 안 된다
interface CartProduct {
item_no: number;
item_name: string;
detail_image_url: string;
price: number;
score: number;
availableCoupon?: boolean;
quantity: number;
checked: boolean;
}
interface Props {
product: CartProduct;
onClick: () => void;
}
function CheckBox({ product, onClick }: Props) {
const uniqueID = `checkbox-${product.item_no}`;
return (
<Block>
<input
id={uniqueID}
type={'checkbox'}
defaultChecked={product.checked}
onClick={onClick}
></input>
<label htmlFor={uniqueID}></label>
</Block>
);
}
밑에 체크박스 컴포넌트는 8개의 product 타입을 받아서 그중 product.item_no, product.checked 속성 두 개만 사용하고 있다
즉 사용하지 않는 인터페이스가 너무 많은 것이다
아래처럼 꼭 필요한 인터페이스만 넘겨받으면 된다
interface Props {
item_no: number;
checked: boolean;
onClick: () => void;
}
function CheckBox({ item_no, onClick, checked }: Props) {
const uniqueID = `checkbox-${item_no}`;
return (
<Block>
<input
id={uniqueID}
type={'checkbox'}
defaultChecked={checked}
onClick={onClick}
></input>
<label htmlFor={uniqueID}></label>
</Block>
);
}