Compound Component: ์ฑ ์ ๋ถ๋ฆฌํ๊ธฐ
SRP: ์ ๋๋์ง?
์ ์ Java์ ๊ฐ์ฒด์งํฅ ์ด์ผ๊ธฐ๋ฅผ ํด๋ณด์.
Clean Architecture์์๋ ๊ฐ์ฒด์งํฅ ์ค๊ณ์ 5๊ฐ์ง ์์น SOLID๋ฅผ ๋ช ์ํ๊ณ ์๋ค.
5๊ฐ ์์น์ ์ด์ฑ์ ๋ฐ์ SOLID์ธ๋ฐ, ๊ทธ ์ค S๋ฅผ ๋ด๋นํ๋ SRP(Single Responsibility Principle, ๋จ์ผ ์ฑ ์ ์์น)์ ๋ํด์ ์์๋ณด์.
โA module should be responsible to one, and only one, actor.โ
์ฌ๊ธฐ์ actor๋ ์ฝ๋ ๋ณ๊ฒฝ์ ์์ฒญํ๋ ๋ถ์์ด๋ค.
์ฑ
์ ์์๋ฅผ ๋น๋ฆฌ๋ฉด, ๊ธ์ฌ ์์คํ
Employee ํด๋์ค์์ CFO, COO, CTO๋ ๊ฐ๊ฐ calculatePay(), reportHours(), save() ๋ฉ์๋์ ๋ช
์ธ๋ฅผ ๊ฐ์ง๋ค.
actor๊ฐ ์ธ ๋ช ์ด ๋๋ ๊ฒ์ด๋ค.
// Clean Architecture ์์๋ฅผ ์ฌ๊ตฌ์ฑํ ์ฝ๋์
๋๋ค.
class Employee {
// CFO: ๊ธ์ฌ ๊ณ์ฐ
double calculatePay() {
double regularHours = getRegularHours();
return regularHours * hourlyRate;
}
// COO: ์
๋ฌด ์๊ฐ
double reportHours() {
double regularHours = getRegularHours(); // CFO๋ ๊ฐ์ ๋ก์ง ๊ณต์
return regularHours;
}
// CTO: DB ์ ์ฅ
void save() {
database.save(this);
}
// calculatePay์ reportHours๊ฐ ๊ณต์
private double getRegularHours() {
return totalHours - overtimeHours;
}
}
์ด๋ CFO์ ๋ช
์ธ ๋ณ๊ฒฝ์ผ๋ก calculatePay()๋ฅผ ์์ ํ๋ฉด reportHours()๊ฐ ๋ด๋ถ์ ์ผ๋ก ๊ฐ์ ๋ก์ง์ ๊ณต์ ํ๊ณ ์๊ธฐ์ COO์ ๊ธฐ๋ฅ reportHours() ๋ํ ์๋์น ์๊ฒ ์ํฅ์ ๋ฐ๊ฒ ๋๋ค.
// ๊ฐ actor๋ณ๋ก ํด๋์ค ๋ถ๋ฆฌ
class PayCalculator {
double calculatePay(Employee e) { ... } // CFO actor
}
class HourReporter {
double reportHours(Employee e) { ... } // COO actor
}
class EmployeeRepository {
void save(Employee e) { ... } // CTO actor
}
// Employee๋ ๋ฐ์ดํฐ๋ง ๋ค๊ณ ์์
class Employee {
double totalHours;
double overtimeHours;
double hourlyRate;
}
Presentational vs Container: ์ด๋ป๊ฒ ๋๋์ง?
์ด๋ฌํ SRP์ ์์น์ ์์ฉํ์ฌ, ํ๋ก ํธ์๋์ ์ฑ ์์ธ ํ๋ฉด ๊ตฌํ๊ณผ ๋ฐ์ดํฐ์ ์ฒ๋ฆฌ๋ฅผ ๋ถ๋ฆฌํ๊ณ ์ Presentational Pattern, Container Pattern์ด ๋์ ๋๋ค.
์ฆ, actor๊ฐ ๋์ด๋ฉด, ์ปดํฌ๋ํธ๋ ๋๋ก ๋๋๋ค.
์๋ก์ ์ฑ ์์ ๋ํ ์ ๋ณด๋ง์ ์์ , ์ฒ๋ฆฌํ๊ณ , ์๋์ ๋ํ ์ ๋ณด๋ ์์ง ๋ชปํ๋ค๋ ์๋ฆฌ๋ค.
Presentational
- UI ๊ตฌํ์ ๋ํ view ์ปดํฌ๋ํธ
- ๋ฐ์ดํฐ๊ฐ ์ด๋ป๊ฒ ์ฒ๋ฆฌ๋๋์ง ๋ชจ๋ฅธ๋ค.
Container
- ๋ฐ์ดํฐ ๊ทธ ์์ฒด์ ๋ฐ์ดํฐ์ ๋ฐ๋ฅธ ๋ก์ง์ ๋ํ model, controller ์ปดํฌ๋ํธ
- UI๊ฐ ์ด๋ป๊ฒ ์๊ฒผ๋์ง ๋ชจ๋ฅธ๋ค.
์๋์ ์์๋ฅผ ๋ณด๋ฉฐ ์ดํดํด๋ณด์.
// AS-IS
function UserCard({ userId }: { userId: string }) {
const [user, setUser] = useState<User | null>(null);
useEffect(() => {
fetch(`/api/users/${userId}`)
.then(res => res.json())
.then(setUser);
}, [userId]);
if (!user) return <div>๋ก๋ฉ ์ค...</div>
return (
<div className="card">
<img src={user.avatar} alt={user.name} />
<h2>{user.name}</h2>
<span>{user.style}</span>
</div>
);
}
์ ์ฝ๋์์ ๋จ์ผ ์ปดํฌ๋ํธ์๋ ์ธ ๋ช ์ actor๊ฐ ์๋ค.
- endpoint๊ฐ ๋ฐ๋๋ฉด fetch ์ ๋ฐ์ดํธ
- ์นด๋ ๋์์ธ ์คํ์ผ
- ๋ก๋ฉ ์ฒ๋ฆฌ ๋ฐฉ์์ด ๋ฐ๋๋ฉด ์ํ ์ ๋ฐ์ดํธ
// TO-BE: Presentational
type UserCardProps = {
avatar: string;
name: string;
role: string;
};
function UserCard({ avatar, name, role }: UserCardProps) {
return (
<div className="card">
<img src={avatar} alt={name} />
<h2>{name}</h2>
<span>{role}</span>
</div>
);
}
// TO-BE: Container
function UserCardContainer({ userId }: { userId: string }) {
const [user, setUser] = useState<User | null>(null);
useEffect(() => {
fetch(`/api/users/${userId}`)
.then(res => res.json())
.then(setUser);
}, [userId]);
if (!user) return <div>๋ก๋ฉ ์ค...</div>
return (
<UserCard
avatar={user.avatar}
name={user.name}
role={user.role}
/>
);
}
Hook: Container์ ๋์ฒด์
๋ค๋ง, ์ง๊ธ๋ ์ด ํจํด์ ์จ์ผ ํ ๊น?
โI no longer suggest splitting your components this way.โ
์ด ๋์์ธ ํจํด์ ๊ณ ์ํ Dan Abramov๊ฐ ์ ๋ฐ์ดํธํ ๋ด์ฉ์ด๋ค.
Container์ ์ญํ ์ ๋์ฒดํ ์ ์๋ Custom Hook์ด ๋ฑ์ฅํ๊ธฐ ๋๋ฌธ์ด๋ค.
Hook์ ๊ธฐ์กด ์ปดํฌ๋ํธ์ ๋ฌ๋ฆฌ ์ด๋ค ์ปดํฌ๋ํธ์๋, ํน์ ๋ค๋ฅธ ๋ก์ง์๋ ์กฐํฉ์ด ์ ์ฐํ๊ฒ ๊ฐ๋ฅํ๊ธฐ์ Container์ ๋นํด ๋์ ์ ์ด์ ์ด ์๋ค.
// AS-IS
function UserCardContainer({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => { fetch(...) }, [userId]);
return <UserCard {...user} />;
}
// TO-BE
function useUser(userId: string) {
const [user, setUser] = useState<User | null>(null);
useEffect(() => {
fetch(`/api/users/${userId}`)
.then(res => res.json())
.then(setUser);
}, [userId]);
return user;
}
// AS-IS
<UserContainer userId={userId}>
{(user) => (
<PostsContainer userId={userId}>
{(posts) => (
<OnlineStatusContainer userId={userId}>
{(isOnline) => (
<Dashboard user={user} posts={posts} isOnline={isOnline} />
)}
</OnlineStatusContainer>
)}
</PostsContainer>
)}
</UserContainer>
// TO-BE
function UserDashboard({ userId }) {
const user = useUser(userId);
const posts = useUserPosts(userId);
const isOnline = useOnlineStatus(userId);
return (
<Dashboard
user={user}
posts={posts}
isOnline={isOnline}
/>
);
}
๊ทธ๋ผ ๋ฌด์๋ฏธํ ๋ ๊ฑฐ์ ์๋๊ฐ?
๋์ ๋ฐฐ๊ฒฝ์ ์ฃผ๋ชฉํ ํ์๊ฐ ์๋ค.
UI ์ปดํฌ๋ํธ๋ ๋ฐ์ดํฐ์ ๊ด๋ จ๋ ์ถ์ฒ๋ฅผ ๋ชฐ๋ผ์ผ ํ๋ค๋ ์์น์ ์ฌ์ ํ ์ ํจํ๊ธฐ ๋๋ฌธ์ด๋ค.
Hook์ด Container์ ์ญํ ์ ๋์ฒดํ์ ๋ฟ, Presentational ์ปดํฌ๋ํธ์ ๊ฐ์น๋ ๋ณํ์ง ์์๋ค.
Compound Component: ๋ณตํฉํ ๊ตฌ์กฐ๋ฅผ ์ฐ์ํ๊ฒ
๋ถ๋ชจ ์ปดํฌ๋ํธ๊ฐ Context๋ก ์ํ๋ฅผ ๋ค๊ณ , ์๋ธ ์ปดํฌ๋ํธ๋ค์ด ๊ทธ๊ฑธ ์ง์ ์๋นํ๋ ํจํด์ด๋ค.
<TodoList>
<TodoList.Header title="๋ด ํ ์ผ" />
<TodoList.AddForm />
<TodoList.Items />
</TodoList>
TodoList.Header๋ TodoList.Items๋ props๋ฅผ ๋ฐ๋ก ๋ฐ์ง ์์๋, ๋ด๋ถ์ ์ผ๋ก Context์์ ์ํ๋ฅผ ๊บผ๋ด ์ด๋ค.
์ ํ์ํ๊ฐ?
// ์ค๊ฐ ์ปดํฌ๋ํธ๊ฐ ์ฐ์ง๋ ์๋ props๋ฅผ ๊ณ์ ๋ด๋ ค์ค์ผ ํจ
<TodoView
todos={todos}
input={input}
onInputChange={setInput}
onAdd={addTodo}
onToggle={toggleTodo}
onDelete={deleteTodo}
/>
Custom Hook์ผ๋ก ๋ก์ง์ ๋ถ๋ฆฌํ์ง๋ง, ๊ตฌ์กฐ ์์ฒด๋ ๊ณ ์ ๋ผ ์๋ค.
๋ฐ๋ผ์ Header๋ฅผ ์๋๋ก ์ฎ๊ธฐ๊ฑฐ๋, AddForm์ ์์ ๋ ค๋ฉด ์ปดํฌ๋ํธ ๋ด๋ถ๋ฅผ ์์ ํด์ผ ํ๋ค.
Compound Component๋ ์ด๊ฑธ ํด๊ฒฐํ๋ค.
// ๊ตฌ์กฐ๋ฅผ ์ฌ์ฉ์ฒ์์ ์์ ๋กญ๊ฒ ์กฐํฉ
<TodoList>
<TodoList.Items />
</TodoList>
<TodoList>
<TodoList.Header title="์ค๋ ํ ์ผ" />
<TodoList.AddForm />
<TodoList.Items />
</TodoList>
๋ด๋ถ ์์ ์์ด ์ฌ์ฉํ๋ ์ชฝ์์ ๊ตฌ์กฐ๋ฅผ ์ ์ดํ ์ ์๋ค.
HTML์
<select>+<option>,<table>+<tr>+<td>๊ฐ ๋ํ์ ์ธ ์์๋ค.
๊ตฌํ ๋ฐฉ๋ฒ
1. Context๋ก ์ํ ๊ณต์
const TodoContext = createContext<TodoContextValue | null>(null);
function useTodoContext() {
const ctx = useContext(TodoContext);
if (!ctx) throw new Error("TodoList ์ปดํฌ๋ํธ ์์์๋ง ์ฌ์ฉํ ์ ์์ต๋๋ค.");
return ctx;
}
2. ๋ถ๋ชจ๊ฐ Context Provider
function TodoList({ initialTodos = [], children }: TodoListProps) {
const [todos, setTodos] = useState<Todo[]>(initialTodos);
return (
<TodoContext.Provider value={{ todos, input, setInput, addTodo, toggleTodo, deleteTodo }}>
<div>{children}</div>
</TodoContext.Provider>
);
}
3. ์๋ธ ์ปดํฌ๋ํธ๊ฐ Context๋ฅผ ์ง์ ์๋น
// props ์์ด Context์์ ์ง์ ๊บผ๋
TodoList.Header = function Header({ title }: { title: string }) {
const { todos } = useTodoContext();
const done = todos.filter((t) => t.completed).length;
return (
<div className="flex items-center justify-between">
<h2>{title}</h2>
<span>{done} / {todos.length} ์๋ฃ</span>
</div>
);
};
TodoList.Items = function Items() {
const { todos, toggleTodo, deleteTodo } = useTodoContext();
if (todos.length === 0) return <TodoList.Empty />;
return (
<ul>
{todos.map((todo) => (
<TodoList.Item key={todo.id} todo={todo} onToggle={toggleTodo} onDelete={deleteTodo} />
))}
</ul>
);
};
props-drilling์ด ์ฌ๋ผ์ง ๊ฒ์ ํ์ธํ ์ ์๋ค.