Search
🪨

SOILD 원칙과 리액트에서의 예시

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)

“컴포넌트는 교체 가능해야 한다.”
✅ 예시: Input을 TextInput, NumberInput으로 교체 가능해야 함.
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)

“상위 모듈은 하위 모듈에 의존하지 말고, 추상화에 의존해야 한다.”
✅ 예시: 데이터 fetching을 컴포넌트에서 직접 하지 않고 추상화된 훅에 의존
// 추상화된 데이터 훅 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 (의존 역전 원칙)
상위 모듈은 하위 모듈에 의존하지 말고 추상화에 의존 (하위 모듈의 세부 구현 모름)

Reference