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