객체(클래스)는 단 하나의 책임만을 가져야 한다.
- 책임이란 기능 담당이라고 보면된다. 즉 하나의 객체는 하나의 기능을 담당하여 하나의 책임을 수행하는데 집중하도록 클래스를 따로 여러개 설계하는 원칙이다.
- 한 책임의 변경으로부터 다른 책임의 변경으로의 연쇄작용을 극복할 수 있게 된다.
⇒ SRP의 목적은 프로그램의 유지보수 성을 높이기 위한 설계 기법
확장에 열려있어야 하며, 수정에는 닫혀있어야 한다.
- 기능 추가 요청이 오면 클래스 확장을 통해 손쉽게 구현하면서, 확장에 따른 클래스 수정은 최소화 하도록 프로그램을 작성하는 설계 기법
⇒ 확장에 열려있다는 변경사항이 발생했을 때 유연하게 코드를 추가함으로써 애플리케이션 기능을 확장할 수 있음
⇒ 추상화 사용을 통한 관계 구축을 권장하는 것을 의미한다.
다형성과 확장을 가능케 하는 객체지향의 장점을 극대화하는 기본적 설계원칙이다.
서브 타입은 언제나 기반(부모) 타입으로 교체할 수 있어야 한다.
다형성의 원리를 이용하기 위한 원칙
- 상위 클래스 타입으로 객체를 선언하여 하위 클래스의 인스턴스를 받으면, 업캐스팅된 상태에서 부모의 메서드를 사용해도 동작이 의도대로 흘러가야 하는 것을 의미
- 부모 메서드의 오버라이딩을 따져가며 해야함, 부모 클래스와 동일한 수준의 선행 조건을 기대하고 사용한 프로그램 코드에서 예상치 못한 문제를 일으킬 수 있기 때문
인터페이스를 각각 사용에 맞게 끔 잘게 분리해야한다는 설계 원칙
- 인터페이스의 단일 책임을 강조
- 인터페이스를 사용하는 클라이언트를 기준으로 분리함으로써, 클라이언트의 목적과 용도에 적합한 인터페이스 만을 제공하는 것이 목표
- 주의해야 할점은한번 인터페이스를 분리하여 구성해놓고 나중에 무언가 수정사항이 생겨서 또 인터페이스들을 분리하는 행위
⇒ 인터페이스는 제약 없이 다중 상속이 가능하기 때문에 분리할 수 있으면 분리하여 각 클래스 용도에 맞게 `implements` 하라는 원칙
DIP 원칙은 어떤 Class를 참조해서 사용해야하는 상황이 생긴다면, 그 Class를 직접 참조하는 것이 아니라 그대상의 상위 요소(추상 클래스 or 인터페이스)로 참조하라는 원칙
- 구현 클래스에 의존하지 말고,인터페이스에 의존하라는 뜻
- 의존 관계를 맺을 때 변화하기 쉬운 것 또는 자주 변화하는 것보다는, 변화하기 어려운 것 거의 변화가 없는 것에 의존하라는 것
⇒ 클래스간의 결합도를 낮추는 것
const ActiveUsersList = () => {
// 데이터를 가져오고, 필터링하고, 컴포넌트 자체와 목록의 각 항목을 렌더링하는 등 이미 많은 작업을 수행
const [users, setUsers] = useState([]);
useEffect(() => {
const loadUsers = async () => {
const response = await fetch("/some-api");
const data = await response.json();
setUsers(data);
};
loadUsers();
}, []);
const weekAgo = new Date();
weekAgo.setDate(weekAgo.getDate() - 7);
return (
<ul>
{users
.filter((user) => !user.isBanned && user.lastActivityAt >= weekAgo)
.map((user) => (
<li key={user.id}>
<img src={user.avatarUrl} />
<p>{user.fullName}</p>
<small>{user.role}</small>
</li>
))}
</ul>
);
};
ActiveUsersList 함수는 fetch를 통해 데이터를 가져오고, filter를 통해 데이터를 가공하고, 컴포넌트를 렌더링 하는 등 많은 작업을 수행하고 있습니다.
이 함수는 너무 많은 기능을 수행하고 있어 문제 발생 시 유지보수가 어려워 질 수 있어 SRP 원칙에 따라 리펙토링하여 기능을 분리시켜 보겠습니다.
const useUsers = () => {
const [users, setUsers] = useState([]);
useEffect(() => {
const loadUsers = async () => {
const response = await fetch("/some-api");
const data = await response.json();
setUsers(data);
};
loadUsers();
}, []);
return { users };
};
서로 연관된 useState와 useEffect가 있을 때는 언제든지 커스텀 hook으로 추출할 수 있습니다.
const UserItem = ({ user }) => {
return (
<li>
<img src={user.avatarUrl} />
<p>{user.fullName}</p>
<small>{user.role}</small>
</li>
);
};
객체 배열을 순회하며 매핑하는 경우 배열의 각 항목에 대해 생성하는 JSX의 복잡성에 주의를 기울여야 합니다. 이벤트 핸들러가 연결되지 않은 한 줄짜리 마크업인 경우 인라인으로 유지하는 것이 좋지만 더 복잡한 마크업의 경우 별도의 컴포넌트로 추출하는 것이 좋습니다.
const getOnlyActive = (users) => {
const weekAgo = new Date();
weekAgo.setDate(weekAgo.getDate() - 7);
return users.filter(
(user) => !user.isBanned && user.lastActivityAt >= weekAgo
);
};
API로부터 얻은 전체 사용자 목록에서 비활성 사용자를 필터링하는 로직이 있습니다. 이 로직은 비교적 독립되어 있고 애플리케이션의 다른 부분에서 재사용될 수 있으므로 유틸리티 함수로 쉽게 추출할 수 있습니다.
const useActiveUsers = () => {
const { users } = useUsers();
const activeUsers = useMemo(() => {
return getOnlyActive(users);
}, [users]);
return { activeUsers };
};
const ActiveUsersList = () => {
const { activeUsers } = useActiveUsers();
return (
<ul>
{activeUsers.map((user) => (
<UserItem key={user.id} user={user} />
))}
</ul>
);
};
마지막 개선 사항으로 이 로직을 새로운 커스텀 hook으로 캡슐화할 수 있습니다.
단일 책임 원칙에 따라 우리는 큰 모놀리식 코드 덩어리를 효과적으로 가져와 더 모듈화합니다. 모듈화하면 코드를 파악하기 쉬워지고, 의도치 않은 중복 코드를 작성할 가능성이 줄어듭니다. 또한 작은 모듈은 테스트 및 수정하기 더 쉽기 때문에 결과적으로 코드를 보다 쉽게 유지 관리할 수 있어 좋습니다.
여기에서 본 것은 인위적인 예이며, 여러분의 컴포넌트는 서로 다른 가동부들 사이에 의존성이 훨씬 더 얽혀 있다는 것을 알 수 있습니다. 대부분의 경우 이는 적절하지 못한 추상화, 다재다능한 전역 컴포넌트 생성, 데이터의 잘못된 스코프 설정 등 잘못된 설계를 선택했기 때문입니다. 그리고 이는 광범위한 리팩토링으로 해결할 수 있습니다.
소프트웨어 엔티티는 확장을 위해 열려야 하지만 수정을 위해 닫혀 있어야 한다
React 컴포넌트와 함수는 소프트웨어 엔티티이기 때문에 정의를 바꿀 필요가 없이 원래 형태대로 사용할 수 있습니다.
개방-폐쇄 원칙은 원본 소스 코드를 변경하지 않고 확장할 수 있는 방식으로 구성 요소를 구조화하도록 권고합니다.
예시 코드로 다른 페이지에서 공유된 Header 컴포넌트를 사용하는 애플리케이션에서 작업하고 있으며, 현재 있는 페이지에 따라 Header는 조금 다른 UI를 렌더링해야 합니다.
const Header = () => {
const { pathname } = useRouter();
return (
<header>
<Logo />
<Actions>
{pathname === "/dashboard" && (
<Link to="/events/new">Create event</Link>
)}
{pathname === "/" && <Link to="/dashboard">Go to dashboard</Link>}
</Actions>
</header>
);
};
const HomePage = () => (
<>
<Header />
<OtherHomeStuff />
</>
);
const DashboardPage = () => (
<>
<Header />
<OtherDashboardStuff />
</>
);
여기에서 현재 페이지에 따라 다른 페이지 컴포넌트를 위한 링크를 렌더링합니다. 더 많은 페이지를 추가하기 시작할 때 어떤 일이 일어날지 생각하면 이 구현이 나쁘다는 것을 쉽게 알 수 있습니다. 새 페이지가 생성될 때마다 Header 컴포넌트로 돌아가서 렌더링할 작업 링크를 알 수 있도록 구현을 조정해야 합니다. 이러한 접근 방식은 Header 컴포넌트는 취약해지고 사용되는 컨텍스트와 긴밀하게 결합되어, 개방-폐쇄 원칙에 위배됩니다.
const Header = ({ children }) => (
<header>
<Logo />
<Actions>{children}</Actions>
</header>
);
const HomePage = () => (
<>
<Header>
<Link to="/dashboard">Go to dashboard</Link>
</Header>
<OtherHomeStuff />
</>
);
const DashboardPage = () => (
<>
<Header>
<Link to="/events/new">Create event</Link>
</Header>
<OtherDashboardStuff />
</>
);
이 문제를 해결하기 위해 컴포넌트 합성(component composition)을 사용할 수 있습니다. Header 컴포넌트는 내부에서 무엇을 렌더링할지 신경 쓸 필요가 없으며 대신 children prop을 사용해서 Header를 사용할 컴포넌트에게 이 책임을 위임할 수 있습니다.
합성 (Composition) vs 상속 (Inheritance)
React는 강력한 합성 모델을 가지고 있으며, 상속 대신 합성을 사용하여 컴포넌트 간의 코드를 재사용하는 것이 좋습니다.
고수준 모듈이 저수준 모듈에 의존해서는 안된다.
SOLID 원칙을 지키는 컴포넌트를 작성하는 것은, 각 컴포넌트가 명확한 역할을 수행하도록 함으로써 유지보수성과 코드의 가독성을 향상시키고 이렇게 하면 새로운 기능을 추가하거나 기존 기능을 수정하는 것이 훨씬 용이해집니다.
즉 컴포넌트 중심의 개발은 각 컴포넌트가 독립적이면서도 재사용 가능하게 설계되어야 하며, 이것이 가능하게 된다면 유연하고 가독성 높은 코드를 작성할 수 있습니다.
이러한 점을 고려해봤을 때, SOLID 원칙은 컴포넌트 중심의 개발이 어떻게 이루어져야 하는지를 설명하는 데 있어 상당히 유용한 가이드라인이라고 할 수 있습니다. 이 원칙을 따르면, 개발자는 각 컴포넌트가 어떤 역할을 수행해야 하는지, 어떻게 상호작용해야 하는지에 대한 명확한 이해를 바탕으로 코드를 작성할 수 있게 됩니다.
출처
https://medium.com/dailyjs/applying-solid-principles-in-react-14905d9c5377