Object⏩to⏩Stream 마인드셋 전환
Source: Dev.to
Source:
소개
프로그래밍 수업을 들어본 적이 있다면, 클래스, 상속, 캡슐화 — 즉 객체‑지향 프로그래밍 (OOP) 의 일반적인 개념들을 배웠을 것입니다. 아마도 데이터와 행동을 함께 묶고, 인스턴스 간에 메시지를 주고받으며, 세상을 명확히 모델링하는 시스템을 만드는 것이 의미가 있었을 겁니다.
하지만 사물의 세계에서 워크플로우의 세계로 이동하면 어떻게 될까요? 현실 세계에서는 모든 것이 끊임없이 변합니다, 맞죠?
그때 스트림‑지향 프로그래밍 (SP) 이 등장합니다. “상태를 보유한 객체”를 생각하는 대신, “시간에 따라 흐르는 정보 / 데이터 / 값”을 생각합니다.
그리고 RxJS 를 사용해 본 적이 있다면, 스트림이 비동기 데이터를 매우 자연스럽게 모델링한다는 것을 이미 알고 있을 겁니다. 이제는 스트림을 OOP 프레임워크에 억지로 끼워 넣는 대신, UI 자체를 스트림 그래프로 구축하는 것이 다음 단계입니다.
객체가 편안함을 느끼는 이유
아주 익숙한 OOP 패턴을 살펴봅시다. 간단한 카운터를 상상해 보세요:
class Counter {
private count = 0;
increment() {
this.count++;
this.render();
}
render() {
return `Click me (${this.count})`;
}
}
const counter = new Counter();
document.body.innerHTML = counter.render();
document.body.addEventListener('click', () => counter.increment());
Counter 객체는 상태를 가지고, 메서드를 가지며, 무언가가 변할 때마다 DOM을 업데이트합니다.
마음가짐은 다음과 같습니다:
- 나는 객체를 가지고 있다.
- 그 객체는 상태를 가지고 있다.
- 그 객체는 행동을 가지고 있다.
- 행동이 트리거될 때, 객체는 자신을 변형한다(또는 최악의 경우, 나머지 세계를 변형한다).
스트림은 어떻게 다를까?
이제 SP를 살펴보겠습니다, Rimmel.js를 사용하여:
import { BehaviorSubject, scan } from 'rxjs';
import { rml } from 'rimmel';
const count = new BehaviorSubject(0).pipe(
scan(x => x + 1)
);
const App = () => rml`
<button>
Click me (${count})
</button>
`;
document.body.innerHTML = App();
사고 전환
- 나는 객체가 없다.
- 나는 스트림을 가진다.
- 그 스트림은 시간에 따른 상태를 나타낸다.
- 이벤트도 스트림이며, 같은 흐름으로 병합된다.
- 렌더링은 메서드 호출이 아니라 — 스트림이 어떻게 연결되는지를 설명하는 것이다.
명시적인 render() 호출도 없고, 가변적인 this.count도 없다. 버튼은 count 스트림에 “구독”되어 있으며, count가 변경될 때마다 DOM이 자동으로 업데이트된다.
OOP vs SP, 나란히
| Concept | OOP | SP |
|---|---|---|
| State | 객체 내부의 필드 | 값의 스트림 |
| Behaviour | 변형하는 메서드 | 스트림 변환 |
| Events | 콜백, 리스너 | 이벤트 스트림 |
| Rendering | 명령형 호출 | 반응형 구독 |
| Model | 객체를 엔티티로 | 시간에 따라 흐르는 데이터 |
Source: …
Example 2: 실시간 검색
OOP에서 실시간 검색은 일반적으로 다음을 의미합니다:
- 입력 필드를 잡는다.
keyup리스너를 추가한다.- 보류 중인 타이머를 수동으로 취소한다.
- fetch 요청을 보낸다.
- 결과 목록을 업데이트한다.
Rimmel.js를 사용한 SP에서는 같은 로직을 선언적으로 표현합니다:
// Helper that renders a single list item
const item = (str: string) => rml`- ${str}`;
// Observable that debounces the input, fetches data, and formats the result
const typeahead = new Subject<string>().pipe(
debounceTime(300),
switchMap(q => ajax.getJSON(`/countries?q=${q}`)),
map(list => list.map(item).join(''))
);
// Component that wires everything together
const App = () => rml`
<input
type="text"
placeholder="Search…"
on:input="${typeahead}"
/>
<ul>${typeahead}</ul>
`;
기본 타입어헤드 예제
${typeahead}
StackBlitz에서 실행해 보기. 키 입력은 스트림이다. 디바운싱은 단순히 연산자일 뿐이다. 데이터를 가져오는 것은 변환이며, 결과를 렌더링하는 것은 또 다른 구독이다. 수동 정리도 없고, “변경 가능한 쿼리 필드”도 없다.
예시 3: 폼 검증
OOP 접근법
- 각 입력은 속성에 연결됩니다.
- 각 속성은
isValid와 같은 플래그를 가집니다. - 검증은 모든 변경 시점에 명령형으로 실행됩니다.
- 폼 객체가 결과를 집계합니다.
Rimmel.js를 사용한 SP 접근법
const App = () => rml`
const submit = new Subject();
const valid = submit.pipe(
map(({ email, password }) =>
ALL(
email.includes('@'),
password.length >= 6
)
)
);
const invalid = valid.pipe(
map(v => !v)
);
return rml`
<form on:submit=${submit}>
<input type="email" name="email" placeholder="Email" />
<input type="password" name="password" placeholder="Password" />
<button disabled=${invalid}>Submit</button>
</form>
`;
`;
document.body.innerHTML = App();
검증 로직과 활성화/비활성화 상태는 순수하게 스트림의 조합으로 표현됩니다.
여기서 어디로 갈까
궁금하다면, Rimmel.js는 이 사고방식을 더 탐구할 수 있는 훌륭한 놀이터입니다. 가볍고, RxJS‑친화적이며, 처음부터 스트림을 중심으로 구축되었습니다.
👉 시도해 보세요: Rimmel.js on GitHub
그리고 다음에 render() 메서드를 가진 클래스를 만들려 할 때, 스스로에게 물어보세요: 스트림이라면 어떻게 보일까요?



