React의 Context API는 Prop Drilling을 피하기 위한 도구다.
Context에는 아무거나 넣을 수 있다.
이는 반대로 무엇을 넣는게 모범 사례인지 쉽게 파악할 수 없다는 것이다.
Context API에서 사용하면 좋은 데이터 타입에 대해 이야기 하겠다.
요약하면, 컨텍스트를 인터페이스 처럼 사용하라.
클래스 대신 인터페이스에 의존하는 OO 세계에서와 마찬가지로
런타임에만 어떤 구현체를 사용하는지 알 수 있어야 한다.
예를 들어 화폐 단위를 변경할 때, 통화 기호와 반올림 위치를 바꾸는 로직은 다양하다.
- Patment 컴포넌트는 PaymentStrategy 인터페이스에 의존하여 해당 로직을 사용할 수 있다.
- 리액트 트리 상단에서 PaymentStrategy 구현체를 주입할 수 있다.
인터페이스 도입하기
PaymentStrategy의 인터페이스는 다음과 같다.
interface PaymentStrategy {
getCurrencySign(): string;
roundUp(amount: number): number;
}
각 국가별도 아래와 같이 로직을 구현한다.
class AustralianStrategy implements PaymentStrategy {
getCurrencySign() {
return "$";
}
roundUp(amount: number) {
return Math.floor(amount + 1)
}
}
Payment 컴포넌트는 아래와 같이 런타임에 바뀔 수 있는 부분을 strategy 파라미터로 전달받아 사용할 수 있다.
const Payment = ({amount, strategy}: {amount: number, strategy: PaymentStrategy}) => {
return <button>{strategy.roundUp(amount)}</button>;
}
const strategy = new AustralianStrategy();
const CheckoutPage = () => {
//...
return (
<Payment amount={amount} strategy={strategy} />
)
}
이 방식의 문제점은 Paymemt 컴포넌트를 사용할 때, 전략 객체를 인스턴스화 해야 한다는 것이다.
또한 이 코드는 리액트스럽지 않다.
Context API 사용하기
우리는 많은 컨텍스트를 관심사가 직교하도록 여러개 사용할 수 있다.
(만약 관심사가 섞이는 경우가 있으면 컨텍스트를 합치거나 한다.)
보안 감사, 로깅, 비즈니스 로직 용 등이다.
이를 우리의 전략 패턴을 위해 활용해 보자.
아래와 같이 컨텍스트를 위한 인터페이스를 정의한다.
// PaymentStrategyContext.tsx
export interface PaymentStrategyContextType {
strategy: PaymentStrategy;
}
export default createContext<PaymentStrategyContextType | null>(null);
컨텍스트 API를 아용해 런타임에 전략 구현체 객체를 제공한다.
import PaymentStrategyContext from './PaymentStrategyContext';
const App = () => {
// 팩토리 함수를 이용해 설정에 따라 다른 구현체를 제공할 수 있다.
const strategy = new AustralianStrategy();
return (
<LoggingContext.Provider>
<PaymentStrategyContext.Provider value={{strategy}}>
<Route {...} />
</PaymentStrategyContext.Provider>
</LoggingContext.Provider>
)
}
마지막으로 우리의 Payment 컴포넌트는 useContext 훅을 이용해 구현체를 사용할 수 있다.
const Payment = ({amount}: {amount: number}) => {
const {strategy} = useContext(PaymentStrategyContext);
return <button>{strategy.roundUp(amount)}</button>;
}
해당 방법의 장점은 다음과 같다.
- 유연성
- 구체적인 클래스(구현 세부사항)을 몰라도 된다.
- 종속된 요소에 영향주지 않고 리팩터링하기 쉽다
- 캡슐화
- public API를 통해서만 소통한다.
- 내부 구현은 Context API 혹은 Class 안에 숨긴다.
- 유지보수
- 인터페이스만 주의해서 리팩토링 하면 된다.
- 테스트
- 인터페이스(퍼블릭 API) 관점에서만 테스트를 작성할 수 있다.
- Context API를 이용해 가짜 객체를 주입하여, 해당 객체를 이용한 렌더링만 테스트하면 쉽다.
결론 : 인터페이스를 더 작게
다른 관심사를 다루는 코드에 해당 코드의 최대한 작은 표면적만 노출한다.
런타임에 바뀔수 있는 부분, 부작용이 있는 코드 부분에 구멍을 뚫어 놓아, 테스트 시 해당 구멍을 채우는 식으로 쉽게 테스트한다.
(물론 부작용이 있는 클래스는 테스트 더블을 사용해 테스트했다 하더라도 100% 믿을건 못되지만 말이다.)
데이터에 대한 관심사를 클래스라는 단위로 캡슐화하고, 해당 데이터에 대한 접근은 public API로 제한한다.
'FrontEnd' 카테고리의 다른 글
[Vite, Vitest, Javascript] Vanilla JS로 TDD with Vitest (0) | 2023.03.23 |
---|---|
[번역] React TDD 기초 (0) | 2023.03.21 |
[번역] 서버에서 Redux 사용하기 (0) | 2023.03.15 |
[Redux] 리덕스의 state, action, reducer (0) | 2023.03.14 |
리액트 디자인 패턴 : uncontrolled component pattern (0) | 2023.03.13 |