SOLID의 주목적
•
변경에 유연하다.
•
이해하기 쉽다.
•
여러 소프트웨어에 사용 가능한 컴포넌트의 기반이 된다.
다섯가지 원칙
1. 단일 책임 원칙 (SRP)
“하나의 컴포넌트는 하나의 이유(책임)로만 변경되어야 한다.”
•
◦
UserProfile은 UI만 담당,
◦
UserProfileContainer는 데이터 로딩을 담당 → 책임이 분리됨.
// ✅ UI와 상태 관리가 분리됨
function UserProfile({ user }: { user: User }) {
return (
<div>
<img src={user.avatarUrl} alt={user.name} />
<h2>{user.name}</h2>
</div>
);
}
function UserProfileContainer() {
const { data: user } = useQuery(["user"], fetchUser);
if (!user) return <p>Loading...</p>;
return <UserProfile user={user} />;
TypeScript
복사
•
// ❌ 데이터 패칭 + 로딩 처리 + UI까지 한 곳에 몰려있음
function UserProfile() {
const { data: user } = useQuery(["user"], fetchUser);
if (!user) return <p>Loading...</p>;
return (
<div>
<img src={user.avatarUrl} alt={user.name} />
<h2>{user.name}</h2>
</div>
);
}
TypeScript
복사
2. 개방-폐쇄 원칙 (OCP, Open/Closed Principle)
“컴포넌트는 확장에는 열려 있어야 하고, 변경에는 닫혀 있어야 한다.”
•
/*
UserList.jsx
*/
import React, { useEffect, useState } from 'react';
import { fetchUsers } from './UserService';
import UserCard from './UserCard';
const UserList = () => {
const [users, setUsers] = userState([]);
useEffect(() => {
fetchUsers().then(setUsers);
}, []);
return (
<div>
{users.map(user =>
<UserCard key={user.id} user={user} />
))}
</div>
);
};
export default UserList;
JavaScript
복사
•
사용자 필터링 기능을 추가하면서 기존 코드를 변경하지 않고 확장을 하려고 한다.
/*
UserFilter.jsx
*/
import React, { useState } from 'react';
const UserFilter = ({ onFilter }) => {
const [filter, setFilter] = useState('');
const handleChange = (e) => {
setFilter(e.target.value);
onFilter(e.target.value);
};
return (
<input
type="text"
value={filter}
onChange={handleChange}
placeholder="Filter Users..."
/>
);
};
export default UserFilter;
JavaScript
복사
•
다시, UserList.jsx에서 필터 기능을 통합한다.
/*
UserList.jsx
*/
import React, { useEffect, useState } from 'react';
import { fetchUsers } from './UserService';
import UserCard from './UserCard';
import UserFilter from './UserFilter';
const UserList = () => {
const [users, setUsers] = userState([]);
const [filteredUsers, setFilteredUsers] = useState([]);
useEffect(() => {
fetchUsers().then({
setUsers(users);
setFilteredUsers(users);
});
}, []);
const handleFilter = (filter) => {
setFilteredUsers(users.filter(user => user.name.includes(filter)));
};
return (
<div>
<UserFilter onFilter={handleFilter} />
{filteredUsers.map(user =>
<UserCard key={user.id} user={user} />
))}
</div>
);
};
export default UserList;
JavaScript
복사
•
통합을 위해 불가피하게 필요한 코드들을 제외하면, 기존 ‘UserList.jsx’의 코드를 ‘변경’하지 않고도 기능을 ‘확장’하였다고 할 수 있다.
3. 리스코프 치환 원칙 (LSP, Liskov Substitution Principle)
“컴포넌트는 교체 가능해야 한다.”
•
function TextInput(props: React.InputHTMLAttributes<HTMLInputElement>) {
return <input type="text" {...props} />;
}
function NumberInput(props: React.InputHTMLAttributes<HTMLInputElement>) {
return <input type="number" {...props} />;
}
// 부모 컴포넌트에서는 둘 다 같은 방식으로 사용 가능
function Form() {
return (
<form>
<TextInput placeholder="Name" />
<NumberInput placeholder="Age" />
</form>
);
}
TypeScript
복사
4. 인터페이스 분리 원칙 (ISP, Interface Segregation Principle)
“클라이언트가 자신이 사용하지 않는 메소드에 의존하지 않도록 한다”
•
동물의 소리를 위한 인터페이스를 각각 정의한다.
// IBark.js
export default class IBark {
bark() {
throw new Error("Method not implemented.");
}
}
// ITweet.js
export default class ITweet {
tweet() {
throw new Error("Method not implemented.");
}
}
JavaScript
복사
•
개와 새를 각각 구현한다.
// Dog.js
import IBark from './IBark';
class Dog extends IBark {
bark() {
console.log("Woof! Woof!");
}
}
export default Dog;
// Bird.js
import ITweet from './ITweet';
class Bird extends ITweet {
tweet() {
console.log("Tweet! Tweet!");
}
}
export default Bird;
JavaScript
복사
•
각 동물의 소리를 사용하는 클라이언트 코드를 작성한다.
// AnimalSounds.js
import Dog from './Dog';
import Bird from './Bird';
class AnimalSounds {
constructor() {
this.dog = new Dog();
this.bird = new Bird();
}
makeDogSound() {
this.dog.bark();
}
makeBirdSound() {
this.bird.tweet();
}
}
export default AnimalSounds;
JavaScript
복사
•
최종적으로 AnimalSounds를 사용하여 동물의 소리를 출력한다.
// main.js
import AnimalSounds from './AnimalSounds';
const animalSounds = new AnimalSounds();
animalSounds.makeDogSound(); // Woof! Woof!
animalSounds.makeBirdSound(); // Tweet! Tweet!
JavaScript
복사
인터페이스 분리: IBark, ITweet 인터페이스로 분리하여, 각 동물 클래스가 자신이 필요한 인터페이스만 구현하도록 하였다.
구현 클래스: Dog 클래스는 IBark 인터페이스만 구현, Bird 클래스는 ITweet 인터페이스만 구현한다.
클라이언트 코드: AnimalSounds 클래스는 필요한 소리의 인터페이스만 사용하여 동물의 소리를 출력한다.
•
이처럼 인터페이스를 분리함으로써 클라이언트는 자신이 사용하지 않는 메소드에 의존하지 않게 되었다.
•
이는 코드의 유연성과 유지보수성을 높여주는 코드라고 할 수 있다.
5. 의존 역전 원칙 (DIP, Dependency Inversion Principle)
“상위 모듈은 하위 모듈에 의존하지 말고, 추상화에 의존해야 한다.”
•
// 추상화된 데이터 훅
function useUser() {
return useQuery(["user"], fetchUser);
}
// UI 컴포넌트는 fetchUser의 내부 구현을 모름
function UserProfile() {
const { data: user } = useUser();
if (!user) return <p>Loading...</p>;
return <div>{user.name}</div>;
}
JavaScript
복사
요약
•
목적: 변경에 유연 / 이해하기 쉬움 / 여러 소프트웨어에서 사용하는 기반
•
5가지 원칙
SRP (단일 책임 원칙) | 하나의 컴포넌트는 하나의 이유로만 변경 (UI, 비지니스 로직 컴포넌트 분리) |
OCP (개방-폐쇄 원칙) | 확장에는 열림, 변경에는 닫힘 (기존 코드 변경 X → 확장 가능) |
LSP (리스코프 치환 원칙) | 교체 가능하고 일관된 인터페이스 유지 (컴포넌트는 교체 가능해야함) |
ISP (인터페이스 분리 원칙) | 클라이언트가 자신이 사용하지 않는 메서드에 의존 X |
DIP (의존 역전 원칙) | 상위 모듈은 하위 모듈에 의존하지 말고 추상화에 의존 (하위 모듈의 세부 구현 모름) |