Vue·React에서 Strategy와 Observer가 구현되는 방식
Vue Composition API와 React Hook의 전략 캡슐화, Store 기반 상태 관리의 Observer 통지 구조를 가격 엔진·실시간 대시보드 코드로 비교한다.
2026-08-13 · 최초 발행 2025-10-14
Vue의 Composition API와 React의 Hook은 프레임워크 문서 어디에도 "디자인 패턴"이라는 말을 쓰지 않는다. 그런데 실제 코드를 뜯어보면 전략 교체는 Strategy 패턴의 모양을, 상태 구독은 Observer 패턴의 모양을 그대로 따르고 있다. 이 글은 두 프레임워크에서 이 패턴들이 어떻게 구현되는지 코드로 정리한다.
Composition API·Store가 구현하는 패턴
Composition API는 Vue 3의 함수형 조합 메커니즘으로, 상태·로직을 composable 함수로 캡슐화하고 재사용하는 체계다. Strategy 패턴은 동적으로 교체 가능한 알고리즘군(전략)을 동일 인터페이스로 캡슐화해 런타임 교체를 가능케 하는 객체지향 패턴이다. Composition API 자체는 패턴이 아니지만, 전략을 composable이나 주입 가능한 함수 객체로 캡슐화하면 Strategy 패턴의 의도를 자연스럽게 구현하게 된다.
Store는 상태의 단일 진실 공급원(Single Source of Truth)으로, 변경은 액션·리듀서 등 통제된 경로로만 수행된다. Observer 패턴은 관찰 대상(Subject)의 상태 변경을 구독자(Observer)에게 알려 느슨한 결합의 반응형 업데이트를 구현하는 패턴이다. Vue의 반응성 시스템, Redux·Pinia, RxJS 기반 스토어는 모두 내부적으로 Observer 메커니즘을 활용하며, 변경 통지와 구독 해지 전략이 핵심이다.
전략 캡슐화와 옵저버 통지가 실제로 작동하는 방식
전략의 공통 인터페이스를 정의하고 구현체를 분리해 런타임에 교체할 수 있게 구조화한다. setStrategy, execute 형태의 API가 권장된다. 선택 기준은 별도로 분리한다 — 사용자 등급, 피처 플래그, 환경설정 같은 외부 조건으로 전략을 선택하되 사이드이펙트는 최소화한다.
반응형 상태와 옵저버 통지에서는 Store 변경 시 최소 영향 범위만 업데이트하는 것이 목표다. Vue는 의존성 추적으로, React는 구독·메모이제이션으로 재렌더를 제한한다. 구독 관리 생명주기는 mount→subscribe, unmount→unsubscribe로 표준화해야 누수와 중복 구독을 막을 수 있다.
일관성과 트랜잭션적 업데이트를 위해서는 변경을 배치 처리한다. React 18은 자동 배칭을, Vue는 다음 틱(nextTick)을 고려하며, 동일 틱 내 연속 업데이트의 원자성을 확보해야 한다. 실패 시에는 보상 트랜잭션이나 fallback 전략을 적용한다.
테스트 가능성과 교차 프레임워크 재사용을 위해서는 전략과 스토어를 프레임워크 비의존 모듈로 분리해 단위 테스트를 쉽게 만든다. UI는 얇게 두고 도메인 로직은 순수 함수로 구성하며, DI·팩토리로 교체 가능성을 확보한다.
입력부터 렌더링까지
메커니즘을 지표로 비교하면
| 항목 | 성능 | 확장성 | 일관성 | 안정성 | 운영 편의 |
|---|---|---|---|---|---|
| Strategy 적용(Composition/Hook) | 메모이제이션 시 재연산 최소화 | 전략 추가·교체 OCP 준수 | 인터페이스 불변으로 동작 일관성 | Fallback으로 장애 격리 | 기능 토글·A/B 테스트에 적합 |
| Observer 기반 상태 관리 | 부분 업데이트로 렌더 최소화 | 구독자 수 증가에도 O(N) 선형 관리 | 단일 진실 공급원으로 데이터 정합성 | 구독 해지·에러 전파로 누수 방지 | DevTools/타임트래블 디버깅 지원 |
Vue와 React 코드로 본 적용
전제조건은 Vue 3.4+ 또는 React 18+, Node 18+이며 TypeScript는 선택 사항이다. Observer 데모에는 RxJS 7.x를 쓴다.
Vue 3 Composition API로 Strategy 패턴 적용
src/composables/usePricingStrategy.ts
import { ref } from "vue";
export type User = { tier: "vip" | "normal" };
export type PricingStrategy = { calc: (price: number, user: User) => number };
export function createPricingStrategy(initial: PricingStrategy) {
const strategy = ref<PricingStrategy>(initial);
const setStrategy = (s: PricingStrategy) => (strategy.value = s);
const priceWith = (price: number, user: User) =>
strategy.value.calc(price, user);
return { strategy, setStrategy, priceWith };
}
// 전략 구현체
export const normalStrategy: PricingStrategy = {
calc: (p) => p,
};
export const vipStrategy: PricingStrategy = {
calc: (p) => Math.round(p * 0.9),
};
src/components/PriceView.vue
<script setup lang="ts">
import { computed, watchEffect } from "vue";
import {
createPricingStrategy,
normalStrategy,
vipStrategy,
type User,
} from "@/composables/usePricingStrategy";
const props = defineProps<{ basePrice: number; user: User }>();
const { setStrategy, priceWith } = createPricingStrategy(normalStrategy);
watchEffect(() => {
setStrategy(props.user.tier === "vip" ? vipStrategy : normalStrategy);
});
const finalPrice = computed(() => priceWith(props.basePrice, props.user));
</script>
<template>
<div>최종 가격: {{ finalPrice }}</div>
</template>
전략 교체는 setStrategy 한 곳으로 집중 관리하고, 계산 로직 테스트는 전략 객체 단위로 진행한다.
React Hook으로 Strategy 패턴 적용
src/hooks/usePricingStrategy.ts
import { useCallback, useState } from "react";
export type User = { tier: "vip" | "normal" };
export type PricingStrategy = { calc: (price: number, user: User) => number };
export function usePricingStrategy(initial: PricingStrategy) {
const [strategy, setStrategy] = useState(initial);
const priceWith = useCallback(
(price: number, user: User) => strategy.calc(price, user),
[strategy],
);
return { strategy, setStrategy, priceWith };
}
export const normalStrategy: PricingStrategy = { calc: (p) => p };
export const vipStrategy: PricingStrategy = {
calc: (p) => Math.round(p * 0.9),
};
src/components/PriceView.tsx
import { useEffect } from "react";
import {
usePricingStrategy,
normalStrategy,
vipStrategy,
User,
} from "../hooks/usePricingStrategy";
export function PriceView({
basePrice,
user,
}: {
basePrice: number;
user: User;
}) {
const { setStrategy, priceWith } = usePricingStrategy(normalStrategy);
useEffect(() => {
setStrategy(user.tier === "vip" ? vipStrategy : normalStrategy);
}, [user.tier]);
return <div>최종 가격: {priceWith(basePrice, user)}</div>;
}
전략 변경은 useEffect로 외부 조건과 동기화하고, priceWith는 useCallback 의존성으로 불필요한 재생성을 막는다.
Observer 패턴으로 Store 구성(RxJS)
src/store/appStore.ts
import { BehaviorSubject } from "rxjs";
export type AppState = { count: number; loading: boolean };
const initial: AppState = { count: 0, loading: false };
class AppStore {
private subject = new BehaviorSubject<AppState>(initial);
readonly state$ = this.subject.asObservable();
getState() {
return this.subject.getValue();
}
setState(patch: Partial<AppState>) {
const next = { ...this.getState(), ...patch };
this.subject.next(next);
}
}
export const appStore = new AppStore();
Vue 연동 (src/components/Counter.vue)
<script setup lang="ts">
import { onMounted, onUnmounted, reactive } from "vue";
import { Subscription } from "rxjs";
import { appStore } from "@/store/appStore";
const state = reactive(appStore.getState());
let sub: Subscription | undefined;
onMounted(() => {
sub = appStore.state$.subscribe((s) => Object.assign(state, s));
});
onUnmounted(() => sub?.unsubscribe());
function inc() {
appStore.setState({ count: state.count + 1 });
}
</script>
<template>
<button @click="inc">Count: {{ state.count }}</button>
</template>
React 연동 (src/components/Counter.tsx)
import { useEffect, useState } from "react";
import { appStore } from "../store/appStore";
export function Counter() {
const [state, setState] = useState(appStore.getState());
useEffect(() => {
const sub = appStore.state$.subscribe(setState);
return () => sub.unsubscribe();
}, []);
return (
<button onClick={() => appStore.setState({ count: state.count + 1 })}>
Count: {state.count}
</button>
);
}
Observer의 핵심은 subscribe·unsubscribe 수명주기를 지키는 것이다. 부분 상태만 구독하려면 React의 useSyncExternalStore나 Vue의 computed selector로 고급화할 수 있다.
가격 엔진부터 대시보드까지
가격·프로모션 엔진에서는 사용자 등급·채널·캠페인에 따라 전략을 교체하고 AB 테스트·피처 플래그와 연계한다. 폼 검증 파이프라인에서는 필드 타입·국가 규정에 따라 검증 전략을 분기하고 실패 시 fallback 검증을 적용한다. 콘텐츠 추천·정렬에서는 실시간 지표에 따라 랭킹 전략을 교체하고 캐시·메모이제이션으로 비용을 절감한다. 실시간 대시보드에서는 WebSocket→Store→Observer로 구독자를 동기화하고, 느린 위젯은 지연 구독으로 격리한다.
실무 적용의 이득과 한계
전략 추가 시 기존 코드 수정을 최소화(OCP)하면 결합도가 줄어 회귀 결함률도 낮아진다. 부분 구독과 메모이제이션으로 불필요한 렌더를 절감하면 대규모 화면 기준으로 커밋이 20~50% 줄어들 수 있는데, 프로젝트별로 편차가 크다. 순수 전략·스토어 단위 테스트로 커버리지를 높이면 UI 테스트 의존도는 낮아진다. 운영 측면에서는 피처 토글·롤백 속도가 개선되고, 장애 영향 범위가 전략·구독 단위로 한정된다.
전략 캡슐화·구독 관리·배치 업데이트를 설계 기준으로 삼고, 프레임워크 비의존 도메인 모듈화와 useSyncExternalStore 같은 선택적 고급 API를 함께 쓰면 성능과 안정성을 균형 있게 확보할 수 있다.