Dependency Inversion Principle(DIP)
DIP
자체는 SOLID
법칙인 OCP , LSP
를 잘 지킨다면 자동으로 지켜지는 원칙이다.
의존성 역전 법칙인 DIP
는 다음과 같다.
네 ?
DIP
를 이해하기 위해선 우선 의미 용어들을 정확하게 짚고 넘어가자
export default function Button({ backgroundColor }) {
return <button style={{ backgroundColor: backgroundColor }}></button>;
}
export default function BlackButton() {
return <Button backgroundColor='black' />;
}
클래스 기반의 객체지향 언어가 아닌 리액트를 기준으로 설명하려다 보니 억지스러운 면도 있긴 하지만 내용은 일맥 상통한다.
기본적으로 컴포넌트는 계층적 관계를 갖는데 위 예시에서 Button
컴포넌트는 Button
컴포넌트에 의존성을 갖는 BlackButton
의 내용이 수정 되더라도 영향을 받지 않는다.
의존성의 흐름이 Button -> BlackButton
으로 향하기 때문이다.
이처럼 추상화된 컴포넌트는 구체화된 컴포넌트의 수정에는 영향을 받지 않으나
반대로 구체화된 컴포넌트는 추상화된 컴포넌트의 수정에 영향을 받는다.
영향의 흐름은 의존성 흐름과 동일하게 흐르기 때문이다.
구체화된 컴포넌트는 수정이 잦게 일어나며 반대로 추상화 된 컴포넌트는 수정이 자주 일어나지 않는다.
다음과 같은 예시를 들어 살펴보자 (클래스 기반의 객체 지향에서의 DIP
를 통해 살펴보자 )
class Sqaure {
constructor() {
this.width = 5;
this.heigth = 5;
}
}
class GeometricCalculator {
constructor() {
this.shape = new Sqaure();
}
getArea() {
return this.width * 2;
}
}
에를 들어 다음처럼 GeometricCalculator
클래스는 내부에서 Square
구현체를 this.shape
에 담아
넓이를 계산하는 getArea
메소드를 구현해두었다.
이 때 GeometricCalculator
에서 Sqaure
가 아닌 Rectangular
를 사용하고 싶다면
GeometricCalculator
내부에서 this.shape
를 new Rectangular()
로 변경해줘야 할 것이다.
이는 OCP
원칙을 거스른다.
GeometricCalculator
는 여러 기하학적 도형의 너비를 구해야 하는데 현재는 new Sqaure
구현체의 너비를 구하기만 하기 때문에 문제가 생긴 것이다.
class Sqaure {
constructor() {
this.width = 5;
this.heigth = 5;
}
}
class Rectangular {
constructor() {
this.width = 5;
this.heigth = 10;
}
}
class GeometricCalculator {
constructor(shape) {
this.shape = shape;
}
getArea() {
return this.shape.width * this.shape.heigth;
}
}
const calculator = new GeometricCalculator(new Rectanguler());
console.log(calculator.getArea()); // 50
다음처럼 할 경우엔 GeometricCalculator
는 Sqaure , Rectangular
의 너비는 잘 계산하지만
다른 도형들이 들어올 경우 (원과 같은) 에는 올바른 답을 구하지 못할 것이다.
이는 GeometricCalculator
의 의존성이 Rectangular , sqaure
와 같은 사각형이라는 구현체 (추상화된 객체보다 비교적 구체화 된) 에 의존하고 있기 때문이다.
GeometricCalculator
가 모든 도형들에 대해 너비를 잘 구할 수 있게 하고 싶다고 해보자
class Shape {
getArea() {
throw new Error('getArea 메소드는 재정의 되어야 합니다.');
}
}
다음처럼 모든 도형들의 추상화 된 클래스인 Shape
를 생성해주자
class Sqaure extends Shape {
constructor() {
super();
this.width = 5;
this.height = 5;
}
getArea() {
return this.width * 2;
}
}
class Rectangular extends Shape {
constructor() {
super();
this.width = 5;
this.height = 10;
}
getArea() {
return this.width * this.height;
}
}
class Circle extends Shape {
constructor() {
super();
this.radius = 10;
}
getArea() {
return Math.PI * this.radius ** 2;
}
}
이후 Shape
의 구현체인 Square , Rectangular , Circle
들을 만들어주자
width , height , radisu
는constructor
에서 받아 줄 수있지만 귀찮으니 그냥 넣어줬다.
class GeometricCalculator {
constructor(shape) {
if (!(shape instanceof Shape))
throw Error('shape 는 항상 Shape 의 자식 객체여야 합니다');
this.shape = shape;
}
getArea() {
return this.shape.getArea();
}
}
이후 GeometricCalculator
는 shape instaceof Shape
결과에만 의존 할 뿐 Shape
의 구현체라면 getArea
는 항상 잘 작동한다.
이렇게 의존성의 관계를 추상화 된 객체인 Shape
에게 갖게 함으로서
클래스를 확장하여 생성하는 것이 가능하다.
이후 추상화 팩토리에 대한 이야기가 나오는데 그 내용은 나중에 다루기로 하자
React
에서 DIP
원칙을 통한 책임 분리DIP
는 기존적인 의존성의 관계를 변경해줌으로서 컴포넌트간의 관계를 독립적인 구조로 만들어주는 것에 중점을 둔다.
이전 SRP
에서 보았듯 컴포넌트의 책임을 분리하는 것 또한 의존성 역전과 관계가 있다.
import { useState, useEffect } from 'react';
export default function UserList() {
const [userList, setUserList] = useState([]);
useEffect(() => {
const fetching = async () => {
// 이 안에 어떤 복잡한 로직이 존재한다고 해보자
const res = await fetch('/user');
const json = await res.json();
setUserList(json);
};
fetching();
}, []);
return (
<ul>
{userList.map(({ id, userName }) => (
<li key={id}>{userName}</li>
))}
</ul>
);
}
이렇게 있을 때 UserList
컴포넌트는 useEffect
내부에 존재하는 콜백 함수의 구현 로직에 따라 렌더링 로직이 결정되는 ,
useEffect
문에 의존하고 있다.
그 뿐만 아니라 useEffect
내부에서 /user
라고 적혀있는 엔드포인트에도 의존하고 있다.
UserList
컴포넌트 내부의 useEffect
는 그저 렌더링에 필요한 데이터를 패칭해오는 모듈일 뿐이다.
DIP
에서는 모듈간 의존성을 명확히 함으로서 컴포넌트의 관심사를 명확하게 한다.
import { useState, useEffect } from 'react';
const useUserList = (endPoint) => {
const [userList, setUserList] = useState([]);
useEffect(() => {
const fetching = async () => {
const res = await fetch(endPoint);
const json = await res.json();
setUserList(json);
};
fetching();
}, [endPoint]);
return userList;
};
다음처럼 복잡한 useEffect
내부의 글을 useUserList
라는 커스텀훅으로 캡슐화 해줌으로서 추상화 된 객체를 이용했듯
추상화 된 커스텀 훅을 이용해주도록 하자
export default function UserList({ endPoint }) {
const userList = useUserList(endPoint);
return (
<ul>
{userList.map(({ id, userName }) => (
<li key={id}>{userName}</li>
))}
</ul>
);
}
UserList
컴포넌트에서는 어떤 값에 의존하고 있을까 ?
props
로 넘어오는 endPoint props
에 의존하고 있다.
이전과 달라진 점
이전에는useEffect
자체에 의존하고 있었다면 현재는props
로 넘어오는endPoint props
에 의존하고 있다.이전에는
useEffect
에서endPoint
가 지정되어 있어 항상/user
에서 얻어지는 정보만 사용 가능했던 점을 기억하자.
props drilling
을 해제하여 의존성 역전export function App({ content }) {
return <FristComponent content={content}></FristComponent>;
}
export function FristComponent({ content }) {
return (
<div>
저는 첫번째 컴포넌트예요
<SecondComponent content={content}></SecondComponent>
</div>
);
}
export function SecondComponent({ content }) {
return <div>{content}</div>;
}
다음과 같은 코드가 있다고 생각해보자
이 때 의존성 관게를 살펴보면
App , FirstComponent , SecondComponent
모두 content props
에 의존성을 가지고 있다.
SecondComponent -> FirstComponent -> App -> Content
방향으로 말이다.
이 처럼 본인의 컴포넌트에서 사용하지 않는 props
를 하위 컴포넌트에게 건내주기 위해 props
로 받는 경우로 props drilling
이라고 한다.
FirstComponent , App
컴포넌트는 content props
에 의존성을 가지고 있을 필요가 없다.
이 때 content
를 Context
나 전역 상태관리 라이브러리을 이용하여 SecondeComponent
에게 전달해줘보자
import { useContext } from 'react';
export function App() {
return <FristComponent></FristComponent>;
}
export function FristComponent() {
return (
<div>
저는 첫번째 컴포넌트예요
<SecondComponent></SecondComponent>
</div>
);
}
export function SecondComponent() {
const content = useContext('ContentContext');
return <div>{content}</div>;
}
이렇게 되면 SecondeComponent
의 의존성은 useContext
의 반환값을 향해지고 App , FirstComponent
의 의존성은 해제 된다.
이처럼 리액트에서는 의존성의을 다른 요소로 체계화 함으로서 불필요한 의존성 관계를 해제하여
최적화 하는 것을 중요하게 여긴다.