반응형 아키텍처와 어니언 아키텍처
데이터 변경에 따른 UI 업데이트, 복잡한 이벤트 흐름, 도메인 로직과 데이터 접근의 뒤섞임 등은 유지보수를 어렵게 만듭니다.
이번 글에서는 이러한 복잡도를 줄이는 두가지 아키텍처, 반응형 아키텍처와 어니언 아키텍처에 대해서 살펴봅니다.
반응형 아키텍처
예제: n×m 문제

- 장바구니 데이터를 예로 들때, 데이터가 바뀔 때마다 UI를 업데이트해야 한다면, 장바구니를 수정하는 곳마다 DOM을 갱신하는 함수를 일일이 추가해야 합니다.
- 이때 수정할 곳이 m개, DOM 업데이트가 n개라면 총 n×m번의 코드 수정이 발생합니다.
개선하기: 원인과 효과의 분리

- 반응형 아키텍처로 개선하게 되면, 원인과 효과가 결합한 것을 분리합니다.
- 이렇게 개선하면 장바구니를 변경하는 방법이 m개 추가되고, DOM 업데이트가 n개 추가되어도 mxn개의 코드 수정이 아닌 m+n개의 코드 수정이 이루어지게 됩니다.
- 따라서 장바구니를 변경하는 곳은 바꾸는 일에만 신경을 쓰면되고 DOM을 업데이트 하는 곳에서는 DOM 업데이트만 신경쓰면 됩니다.
반응형 아키텍처란?
- 애플리케이션을 구조하는 방법입니다.
- 이벤트에 대한 반응으로 일어날 일을 지정하는 것입니다.
- 기존 순차적으로 X를 하고 Y를 하는 대신, X가 일어나면 Y를 합니다.
셀을 이용하여 반응형 아키텍처 구현하기
tsx
var shopping_cart = {};
function add_item_to_cart(name, price) {
var item = make_cart_item(name, price);
shopping_cart = add_item(shopping_cart, item);
var total = calc_total(shopping_cart);
set_cart_total_dom(total);
update_shipping_icons(shopping_cart);
update_tax_dom(total);
}- 기존 코드는 장바구니를 변경시키는 곳마다 장바구니 데이터에 영향을 받는 함수를 추가해줘야합니다.
tsx
function ValueCell(initialValue) {
var currentValue = initialValue;
var watchers = [];
return {
val: function () {
return currentValue;
},
update: function (f) {
var oldValue = currentValue;
var newValue = f(oldValue);
if (oldValue !== newValue) {
currentValue = newValue;
forEach(watchers, function (watcher) {
watcher(newValue);
});
}
},
addWatcher: function (f) {
watchers.push(f);
},
};
}ValueCell은 상태를 담고, 바뀔 때마다 감시자(watcher)를 실행합니다.- 데이터를 읽고(
val()), 업데이트하고(update()), 변경 시 실행할 동작을 등록(addWatcher())할 수 있습니다.
FormulaCell 은 파생된 값을 계산합니다
tsx
function FormulaCell(upstreamCell, f) {
var myCell = ValueCell(f(upstreamCell.val()));
upstreamCell.addWatcher(function (newUpstreamValue) {
myCell.update(function (currentValue) {
return f(newUpstreamValue);
});
});
return {
val: myCell.val,
addWatcher: myCell.addWatcher,
};
}- FormulaCell로 이미 있는 셀에서 파생한 셀을 만들 수 있습니다.
- 직접 변경할수는 없지만 감시하던 상위 셀이 변경되면 FormulaCell이 바뀝니다.
tsx
var shopping_cart = ValueCell({});
var cart_total = FormulaCell(shopping_cart, calc_total);
function add_item_to_cart(name, price) {
var item = make_cart_item(name, price);
shopping_cart.update(function (cart) {
return add_item(cart, item);
});
}
shopping_cart.addWatcher(update_shipping_icons);
cart_total.addWatcher(set_cart_total_dom);
cart_total.addWatcher(update_tax_dom);- FormulaCell로 개선하면 장바구니를 바꾸는 핸들러와 DOM 업데이트 함수를 분리할 수 있습니다.
공통 함수를 정의하여 쓰는 방법에 대한 고민
tsx
var shopping_cart = {};
function add_item_to_cart(name, price) {
var item = make_cart_item(name, price);
shopping_cart = add_item(shopping_cart, item);
var total = calc_total(shopping_cart);
on_change_cart(shopping_cart, total);
}
function on_change_cart(shopping_cart, total) {
set_cart_total_dom(total);
update_shipping_icons(shopping_cart);
update_tax_dom(total);
}- 코드 변경을 최소화하는 방법중 하나로, 장바구니 변경에 대한것을 on_change_cart에 선언하고 사용하는 것에 대해 고민해보았습니다. 하지만 이 방식은 여전히 "호출을 잊는" 위험이 있고, 관심사가 분리되지 않습니다.
- 반응형 아키텍처를 적용하는 것이 함수 호출이 아니라 "데이터 변화" 자체에 반응하도록 설계해 더 안정적입니다.
어니언 아키텍처

- 전통적인 아키텍처에서는 계층이 순서대로 쌓여있습니다. 함수형 프로그래밍에서 데이터베이스에서 데이터를 가져오는 작업은 ‘액션’이므로 가장 하위계층에 데이터 베이스가 존재하게되면 모든 것을 ‘액션’으로 만들어버립니다.
- 어니언 아키텍처의 핵심은 데이터베이스와 도메인의 분리입니다. 도메인 계층은 데이터베이스와 분리되어 있기에 계산으로 만들 수 있고 인터렉션 계층은 가장 상위에 위치하므로 바꾸기 쉽습니다.
아키텍처 설계시 고려할 점
- 도메인 계층을 계산으로 만들어 인터렉션 계층과 분리하면서 읽기 좋은 코드를 만들려고 노력해야 합니다.
- 비즈니스 이유로 기능을 빨리 출시해야 하는 경우도 있습니다. 이런 경우에는 나중에 아키텍처에 맞춰 코드를 정리할 준비를 하는 것이 좋습니다.
- 변경 가능한 데이터 구조는 불변 데이터 구조보다 빠릅니다. 이때 성능개선과 도메인을 계산으로 만드는 것은 따로 생각하는 것이 좋습니다. 최적화는 인터렉션 계층에서 하고 도메인 계층은 재사용 가능한 계산으로 만드는 것이 좋습니다.
정리하기
- 반응형 아키텍처는 "데이터 변화에 반응하는 구조" 를 통해 관심사를 분리하고, 어니언 아키텍처는 "도메인 중심 설계" 를 통해 복잡한 의존성을 줄입니다.
