|
| 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