Skip to content

Commit b67e5fe

Browse files
committed
docs: 리액트에서도 의존성 주입으로 결합도를 낮춰보자! - Renderer Props 패턴 (feat.DIP)
1 parent 9bde3c2 commit b67e5fe

File tree

1 file changed

+303
-0
lines changed

1 file changed

+303
-0
lines changed
Lines changed: 303 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,303 @@
1+
---
2+
title: "리액트에서도 의존성 주입으로 결합도를 낮춰보자! - Renderer Props 패턴 (feat.DIP)"
3+
createdAt: 2025-11-01
4+
category: React
5+
description: 의존성 주입이 백엔드에서만 사용되는 패턴이라고? 리액트에서도 의존성 주입을 활용해 컴포넌트 간 결합도를 낮추고 재사용성을 높여보자! Renderer Props 패턴을 의존성 주입과 OCP, DIP 원칙과 연결지어 알아봅니다
6+
comment: true
7+
head:
8+
- - meta
9+
- name: keywords
10+
content: React, Renderer Props, 의존성 주입, DIP, OCP, SOLID 원칙, 리액트 디자인 패턴, 컴포넌트 재사용성
11+
---
12+
13+
의존성 주입 (Dependency Injection, DI) 는 흔히 NestJS, Spring 같은 백엔드나 서버 프레임워크에서 많이 사용됩니다.
14+
15+
> "객체 간 결합도를 낮추기 위해, 의존성을 외부에서 주입한다"
16+
17+
그런데, 이 개념은 단순히 서버에서 객체를 생성하는 방식이 아니라, <br/>
18+
`한 컴포넌트가 다른 컴포넌트의 구체적인 구현에 의존하지 않도록 분리하는 원리` 입니다.
19+
20+
React 에서도 이 원칙을 그대로 적용해서 컴포넌트 간 결합도를 낮추고 재사용성을 높일 수 있습니다<br/>
21+
그렇다면 의존성, 의존성주입이 뭐고, 리액트에서 어떤식으로 활용할 수 있는지 살펴봅시당
22+
23+
## 🫨 의존성 (Dependency) 이 뭔데?
24+
25+
먼저 의존성이 뭔지 한번 짚고 넘어가겠습니다.
26+
27+
> A 가 B 를 사용한다면, A 는 B 에 의존하고 있다.
28+
29+
음... 좀 이해가 안가니 예시를 들어보겠습니다.
30+
31+
바리스타가 커피를 만드는 상황을 생각해봅시다. <br/>
32+
위의 문장에 대입해보면 `바리스타(A)``커피 원두(B)` 를 사용해서 커피를 만듭니다. <br/>
33+
즉, "`바리스타(A)``커피 원두(B)` 에 의존하고 있다" 고 할 수 있습니다.
34+
35+
### ☕️ 케냐산 원툴 바리스타
36+
37+
```ts
38+
class Barista {
39+
coffeeBean: KenyaBean = new KenyaBean();
40+
41+
makeCoffee() {
42+
this.coffeeBean.grind(); // 커피 원두를 분쇄합니다
43+
this.coffeeBean.brew(); // 커피 원액을 추출합니다
44+
}
45+
}
46+
```
47+
48+
바리스타는 커피를 만들기 전에, 직접 케냐산 커피원두를 직접 구매해서 사용하고 있습니다. <br/>
49+
50+
언뜻 보면 별 문제가 없어보이지만, 이 바리스타는 케냐산 커피원두만 사용해서 커피를 만들 수 있습니다. <br/>
51+
만약 바리스타가 에티오피아산 커피원두로 바꾸고 싶다면, 코드를 수정해야 합니다. <br/>
52+
즉, `Barista` 클래스가 `KenyaBean` 클래스의 **구체적인 구현에 의존**하고 있고, **강하게 결합**되어 있습니다. <br/>
53+
54+
:::info 정리하면,
55+
56+
- 이 바리스타는 "케냐 원두" 로만 커피를 만들 수 있는 (케냐산 원툴?) 바리스타 입니다.
57+
- 다른 원두로 바꾸려면, 바리스타 자체를 다시 교육 (코드 수정) 해야 합니다
58+
- 또 연습을 할 때도 꼭 비싼 케냐 원두를 사용해야 합니다.
59+
:::
60+
61+
<br/>
62+
63+
### 🧑‍🍳 의존성 주입으로 만능 바리스타를 만들어보자!
64+
65+
의존성 주입을 활용하면 바리스타가 특정 커피 원두에 의존하지 않도록 만들 수 있습니다! <br/>
66+
67+
> A 가 직접 의존 대상을 만들지 말고, 외부에서 주입(inject) 받아라 <br/>
68+
> 즉, 필요한 것을 스스로 생성하지 말고 외부에서 받아서 쓰자!
69+
70+
아까의 상황에 대입해서 의존성을 주입해보겠습니다. <br/>
71+
`바리스타(A)``커피 원두(B)` 를 직접 만들지 않고, 외부에서 주입받도록 바꿔보겠습니다.
72+
73+
```ts
74+
class Barista {
75+
constructor(private readonly coffeeBean: CoffeeBean) {}
76+
77+
makeCoffee() {
78+
this.coffeeBean.grind(); // 커피 원두를 분쇄합니다
79+
this.coffeeBean.brew(); // 커피 원액을 추출합니다
80+
}
81+
}
82+
```
83+
84+
이제 `Barista` 클래스는 `CoffeeBean` 인터페이스에만 의존하게 되었습니다. <br/>
85+
바리스타는 "원두가 있다" 라는 사실만 알고, 어떤 종류의 원두인지는 신경쓰지 않습니다! <br/>
86+
즉, `Barista` 클래스는 어떤 종류의 커피 원두가 주입되든 상관없이 커피를 만들 수 있습니다. <br/>
87+
88+
```ts
89+
interface CoffeeBean {
90+
grind(): void;
91+
brew(): void;
92+
}
93+
94+
class KenyaBean implements CoffeeBean {
95+
/* ... */
96+
}
97+
98+
class EthiopiaBean implements CoffeeBean {
99+
/* ... */
100+
}
101+
102+
const kenyaBean = new KenyaBean();
103+
const ethiopiaBean = new EthiopiaBean();
104+
105+
const kenyaBarista = new Barista(kenyaBean); // 케냐 원두로 커피 제작
106+
kenyaBarista.makeCoffee();
107+
108+
const ethiopiaBarista = new Barista(ethiopiaBean); // 에티오피아 원두로 커피 제작
109+
ethiopiaBarista.makeCoffee();
110+
```
111+
112+
:::info 정리하면,
113+
이제 이 바리스타는
114+
115+
- 어떤 원두가 들어오든 상관없이 커피를 만들 수 있고, (이제 원툴 아님)
116+
- 새로운 원두가 추가되어도 바리스타를 재교육 할 필요가 없으며, (코드 수정 X)
117+
- 연습할 때도 저렴한 원두로 연습할 수 있습니다. (비싼 케냐 원두 안써도 됨)
118+
:::
119+
120+
### ✍️ 의존성 주입 (DI) 와 의존성 역전 (DIP)
121+
122+
의존성 주입 (Dependency Injection, DI) 은 의존성 역전 원칙 (Dependency Inversion Principle, DIP) 을 구현하는 한 가지 방법입니다. <br/>
123+
124+
즉, 의존성 역전 원칙 (DIP) = 원칙 이고, <br/>
125+
의존성 주입 (DI) = 구현 방법 입니다.
126+
127+
| 개념 || 관계 |
128+
| ------------------------------------ | ----------------------------------------------------- | ------------------------------------ |
129+
| DIP (Dependency Inversion Principle) | 의존 관계의 방향을 "구체 → 추상"으로 뒤집는 설계 원칙 | "어떻게 의존해야 하는가"에 대한 철학 |
130+
| DI (Dependency Injection) | 의존 대상을 외부에서 주입(inject) 하는 구현 방법 | DIP를 실행에 옮기는 수단 |
131+
132+
의존성 주입은 크게 3가지 방법을 통해 이루어질 수 있습니다.
133+
134+
| 방식 | 예시 | 설명 |
135+
| --------------- | --------------------------------------------- | ----------------------------------- |
136+
| 생성자 주입 | `constructor(private coffeeBean: CoffeeBean)` | 가장 일반적, 불변성 보장 |
137+
| Setter 주입 | `setBean(coffeeBean: CoffeeBean)` | 런타임 교체 가능 |
138+
| 인터페이스 주입 | `implements ICoffeeBeanAware` | 특정 프레임워크(Spring 등)에서 사용 |
139+
140+
:::details 🙋‍♂️ 잘 이해가 안돼요
141+
142+
1. 생성자 주입
143+
144+
- 바리스타가 커피를 만들 때, 처음부터 원두를 받아서 그 원두로만 커피를 만듭니다.
145+
- 원두는 한 번 정해지면 바뀌지 않으므로 가장 안정적이고 불변성이 보장됩니다.
146+
147+
2. Setter 주입
148+
149+
- 바리스타가 일하다가 "오늘은 에티오피아 원두로 바꿔볼까?" 하고 중간에 원두를 교체할 수 있는 방식입니다.
150+
- 유연하지만, 잘못하면 맛이 들쭉날쭉할 수 있습니다.
151+
152+
3. 인터페이스 주입
153+
154+
- 바리스타가 "나는 어떤 원두가 들어오든 만들 수 있으니까, 커피 원두 공급 시스템이 알아서 나에게 원두를 넣어줘" 라고 말하는 구조입니다.
155+
- 즉, 바리스타가 ICoffeeBeanAware 라는 계약(인터페이스)을 구현하면, 외부의 커피 공급 기계(컨테이너) 가 자동으로 적절한 원두를 넣어주는 방식입니다.
156+
:::
157+
158+
## ⚛️ 리액트 관점으로 보면...
159+
160+
리액트를 개발하면서도 알게 모르게 의존성 주입을 활용하고 있을 때가 많습니다. <br/>
161+
위의 의존성 주입 방법 3가지를 리액트에서 찾아보겠습니다.
162+
163+
| 개념 | React에서는?? | 설명 |
164+
| --------------- | ------------------------------------------------------- | ------------------------------------------------ |
165+
| 생성자 주입 | `function Component({ service }: { service: Service })` | props를 통해 주입받는 패턴 (가장 일반적) |
166+
| Setter 주입 | `useEffect(() => setFetcher(newFetcher), [deps])` | 훅이나 상태를 통해 런타임에 교체 가능 |
167+
| 인터페이스 주입 | `Context.Provider` / `useContext()` | Context 시스템이 내부적으로 "주입자 역할"을 수행 |
168+
169+
즉, OOP 에서의 생성자, Setter, 인터페이스 주입은 <br/>
170+
리액트에서는 각각 props, 상태/훅, Context 로 대응된다고 볼 수 있습니다.
171+
172+
## 💉 Renderer Props 패턴?
173+
174+
리액트에서 의존성 주입을 활용하는 대표적인 디자인 패턴 중 하나가 [Renderer Props 패턴](https://patterns-dev-kr.github.io/design-patterns/render-props-pattern/)입니다. <br/>
175+
Renderer Props 패턴은 컴포넌트가 렌더링할 UI를 외부에서 주입받는 방식입니다. <br/>
176+
177+
이름에서도 유추할 수 있듯이, `render` 라는 prop을 통해 렌더링할 내용을 주입받습니다.
178+
179+
### 🫘 Renderer Props 의존성 주입으로 만능 바리스타 만들기
180+
181+
그럼 다시 바리스타 예시로 돌아가서, Renderer Props 패턴을 활용해봅시다. <br/>
182+
비슷하게 추상화된 "커피원두" 인터페이스를 먼저 정의해 보겠습니다.
183+
184+
```ts
185+
interface CoffeeBean {
186+
origin: string;
187+
grind(): Promise<void>;
188+
brew(): Promise<void>;
189+
}
190+
```
191+
192+
그리고 해당 추상화된 커피 원두를 사용하는 `Barista` 컴포넌트를 만들어보겠습니다. <br/>
193+
(추상화된 커피원두 인터페이스에 의존하기 때문에 DIP 원칙을 지키고 있습니다)
194+
195+
```tsx
196+
type 커피상태 = "대기" | "분쇄중" | "추출중" | "완료";
197+
198+
interface BaristaProps {
199+
bean: CoffeeBean;
200+
render: (bean: CoffeeBean, status: 커피상태) => React.ReactNode;
201+
}
202+
203+
function Barista({ bean, render }: BaristaProps) {
204+
const [status, setStatus] = useState<커피상태>("대기");
205+
206+
useEffect(() => {
207+
async function makeCoffee() {
208+
setStatus("분쇄중");
209+
await bean.grind();
210+
setStatus("추출중");
211+
await bean.brew();
212+
setStatus("완료");
213+
}
214+
makeCoffee();
215+
}, [bean]);
216+
217+
return (
218+
<div>
219+
<p>☕️ 바리스타가 {bean.origin} 원두로 커피를 만듭니다.</p>
220+
<p>{render(bean, status)}</p>
221+
</div>
222+
);
223+
}
224+
```
225+
226+
이제 `Barista` 컴포넌트는 `bean` prop을 통해 어떤 커피 원두가 들어오든 상관없이 커피를 만들 수 있습니다. <br/>
227+
또한, `render` prop을 통해 커피 상태에 따른 **UI**를 외부에서 주입받을 수 있습니다.
228+
229+
```tsx
230+
const kenyaBean = {
231+
origin: "케냐산",
232+
async grind() {
233+
console.log("케냐 원두 분쇄 완료");
234+
},
235+
async brew() {
236+
console.log("케냐 커피 추출 완료");
237+
},
238+
};
239+
240+
function Cafe() {
241+
return (
242+
<Barista
243+
bean={kenyaBean}
244+
render={(bean, status) => (
245+
<span>
246+
손님 지금 {bean.origin} 원두로 만든 커피가 {status} 상태입니다!
247+
</span>
248+
)}
249+
/>
250+
);
251+
}
252+
```
253+
254+
:::details 🙋‍♂️ 케냐산 원두 (kenyaBean) 가 CoffeeBean 인터페이스를 구현하지 않았는데도 되는 이유가 뭔가요? - Duck Typing
255+
256+
TypeScript 에서는 덕 타이핑(Duck Typing) 이라는 개념이 있어서, 객체가 특정 인터페이스의 모든 속성과 메서드를 가지고 있으면 해당 인터페이스를 구현한 것으로 간주합니다. <br/>
257+
258+
```ts
259+
interface Duck {
260+
quack(): void;
261+
}
262+
263+
const duck: Duck = {
264+
quack() {
265+
console.log("꽥꽥");
266+
},
267+
};
268+
269+
const person: Duck = {
270+
quack() {
271+
console.log("꽥! ☠️ (대충 사람 죽을때 나는 소리)");
272+
},
273+
};
274+
```
275+
276+
쉽게 말해, 사람이지만 꽥 소리를 냈으니까 TypeScript 입장에서도 얘도 오리로 인정해주는거죠!
277+
:::
278+
279+
이제 바리스타가 어떤 원두가 들어오든 상관없이 커피를 만들 수 있고, <br/>
280+
커피 상태에 따라 어떤 말을 할지 (UI) 도 외부에서 주입받을 수 있게 되었습니다! <br/>
281+
282+
```tsx
283+
function Cafe() {
284+
return (
285+
<Barista
286+
bean={kenyaBean}
287+
render={(bean, status) => (
288+
<span>
289+
매니저님 지금 {bean.origin} 원두로 만든 커피가 {status} 상태입니다!
290+
</span>
291+
)}
292+
/>
293+
);
294+
}
295+
```
296+
297+
## ⚙️ 결론!
298+
299+
1. "의존성 주입" 이란, 컴포넌트(혹은 함수)가 필요한 자원을 스스로 만들지 않고, 외부에서 전달받는 설계 방식이다.
300+
2. 의존성 주입은 DIP (의존성 역전 원칙) 을 구현하는 한 가지 방법이다.
301+
3. 3가지 의존성 주입 방식 (생성자 주입, Setter 주입, 인터페이스 주입) 이 있으며, 리액트에서는 각각 props, 상태/훅, Context 로 대응된다.
302+
4. Renderer Props 패턴은 리액트에서 의존성 주입을 활용하는 대표적인 디자인 패턴으로 UI도 어떻게 렌더링할지 외부에서 주입받을 수 있다.
303+
5. 사람이 죽을때 꽥! 소리를 내도 TypeScript 입장에서는 오리로 인정해준다 (덕 타이핑)

0 commit comments

Comments
 (0)