본문 바로가기

FrontEnd

[React] Context API를 활용한 전략 패턴

반응형

원문 : https://itnext.io/the-interface-mindset-how-to-build-flexible-maintainable-react-components-with-context-api-8b332d76f6b9

 

The Interface Mindset: How to Build Flexible, Maintainable React Components with Context API

Thinking in interfaces can enhance the flexibility and maintainability of the code, despite appearing more complex at first glance.

itnext.io

React의 Context API는 Prop Drilling을 피하기 위한 도구다.

Context에는 아무거나 넣을 수 있다.

이는 반대로 무엇을 넣는게 모범 사례인지 쉽게 파악할 수 없다는 것이다.

 

Context API에서 사용하면 좋은 데이터 타입에 대해 이야기 하겠다.

요약하면, 컨텍스트를 인터페이스 처럼 사용하라.

클래스 대신 인터페이스에 의존하는 OO 세계에서와 마찬가지로

런타임에만 어떤 구현체를 사용하는지 알 수 있어야 한다.

 

예를 들어 화폐 단위를 변경할 때, 통화 기호와 반올림 위치를 바꾸는 로직은 다양하다.

다른 통화와 수치

  • Patment 컴포넌트는 PaymentStrategy 인터페이스에 의존하여 해당 로직을 사용할 수 있다.
  • 리액트 트리 상단에서 PaymentStrategy 구현체를 주입할 수 있다.

The interface + multiple implementation approach

인터페이스 도입하기

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

해당 방법의 장점은 다음과 같다.

  1. 유연성
    • 구체적인 클래스(구현 세부사항)을 몰라도 된다.
    • 종속된 요소에 영향주지 않고 리팩터링하기 쉽다
  2. 캡슐화
    • public API를 통해서만 소통한다.
    • 내부 구현은 Context API 혹은 Class 안에 숨긴다.
  3. 유지보수
    • 인터페이스만 주의해서 리팩토링 하면 된다.
  4. 테스트
    • 인터페이스(퍼블릭 API) 관점에서만 테스트를 작성할 수 있다.
    • Context API를 이용해 가짜 객체를 주입하여, 해당 객체를 이용한 렌더링만 테스트하면 쉽다.

결론 : 인터페이스를 더 작게

빙산의 일각

다른 관심사를 다루는 코드에 해당 코드의 최대한 작은 표면적만 노출한다.

런타임에 바뀔수 있는 부분, 부작용이 있는 코드 부분에 구멍을 뚫어 놓아, 테스트 시 해당 구멍을 채우는 식으로 쉽게 테스트한다.

(물론 부작용이 있는 클래스는 테스트 더블을 사용해 테스트했다 하더라도 100% 믿을건 못되지만 말이다.)

데이터에 대한 관심사를 클래스라는 단위로 캡슐화하고, 해당 데이터에 대한 접근은 public API로 제한한다.

반응형