Object⏩to⏩Stream 마인드셋 전환

발행: (2025년 12월 20일 오전 09:14 GMT+9)
8 min read
원문: Dev.to

Source: Dev.to

Dario Mannu

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, 나란히

ConceptOOPSP
State객체 내부의 필드값의 스트림
Behaviour변형하는 메서드스트림 변환
Events콜백, 리스너이벤트 스트림
Rendering명령형 호출반응형 구독
Model객체를 엔티티로시간에 따라 흐르는 데이터

Source:

Example 2: 실시간 검색

OOP에서 실시간 검색은 일반적으로 다음을 의미합니다:

  1. 입력 필드를 잡는다.
  2. keyup 리스너를 추가한다.
  3. 보류 중인 타이머를 수동으로 취소한다.
  4. fetch 요청을 보낸다.
  5. 결과 목록을 업데이트한다.

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() 메서드를 가진 클래스를 만들려 할 때, 스스로에게 물어보세요: 스트림이라면 어떻게 보일까요?

더 알아보기

스트림‑지향 프로그래밍 — 소개
스트림‑지향 프로그래밍 — 소개

간단한 함수로 웹 컴포넌트 만들기
간단한 함수로 웹 컴포넌트 만들기

콜백에서 콜포워드로: 반응성을 간단히?
콜백에서 콜포워드로: 반응성을 간단히?

Back to Blog

관련 글

더 보기 »

JavaScript에서 일급 함수

소개 개발자들이 JavaScript를 배우면서 “first‑class functions”라는 용어가 토론과 문서에서 자주 등장합니다. JavaScript에서 함수는 …

JavaScript에서 함수 합성

소개: 함수 합성(Functional composition)은 함수 파이프라인(function pipelines)이라고도 하며, 간단한 함수를 연결하여 보다 가독성이 높고 모듈화된 코드를 만들 수 있게 합니다. 정의...