리액티브 프로그래밍 (Reactive Programming)

리액티브 프로그래밍 (Reactive Programming)

자바스크립트에서 비동기 데이터 흐름을 다룰 때 리액티브 프로그래밍은 꽤 유용한 도구다. 데이터 스트림과 변경 전파를 중심으로 돌아가는 이 방식은 복잡한 비동기 상황을 깔끔하게 정리해 준다. RxJS 같은 라이브러리와 함께라면 더 강력해진다. 이번에 리액티브 프로그래밍의 기본부터 실전 활용까지 코드와 함께 작성해보려 한다.

리액티브 프로그래밍을 잘 익히면 비동기 로직이 훨씬 직관적이고 관리하기 쉬워진다.

리액티브 프로그래밍 기본

리액티브 프로그래밍은 ObservableObserver라는 두 개념을 중심으로 움직인다. Observable은 데이터 스트림을 만들어내고, Observer는 그 스트림을 구독해서 데이터를 받아 처리한다. 이런 구조 덕분에 비동기 이벤트나 데이터 흐름을 자연스럽게 다룰 수 있다.

RxJS를 활용하면 Observable을 만들고 조작하는 방법을 쉽게 익힐 수 있다. 간단한 예제로 감을 잡아보자.

import { Observable } from 'rxjs';

const observable = Observable.create((observer) => {
    observer.next(1);
    observer.next(2);
    observer.next(3);
    observer.complete();
});

observable.subscribe({
    next: (value) => console.log(value),
    error: (err) => console.error(err),
    complete: () => console.log('끝')
});
// 1
// 2
// 3
// 끝

여기서 Observable.create로 데이터 스트림을 만들고, subscribe로 구독해서 값을 받아봤다. next로 값을 하나씩 내보내고, complete로 스트림을 끝냈다.

1. Observable과 Observer의 흐름

Observable은 데이터 소스 역할을 하고, Observer는 그 데이터를 받아서 처리한다. Observable은 비동기적으로 값을 내보낼 수 있어서, Observer는 그 흐름에 맞춰 반응한다. 이 관계가 리액티브 프로그래밍의 핵심이다.

Observable이 내보낼 수 있는 이벤트는 세 가지다:

  • next: 데이터를 내보낸다.
  • error: 에러를 내보낸다.
  • complete: 스트림이 끝났음을 알린다.

Observer는 이 세 가지 이벤트에 맞춰 동작을 정의한다. 예를 들어, 실시간 알림 시스템을 만든다고 치면, 새 알림이 올 때마다 next로 데이터를 받고, 오류가 생기면 error로 처리하고, 알림이 끝나면 complete로 마무리할 수 있다.

2. RxJS 연산자로 데이터 다루기

RxJS는 Observable을 조작하고 변형할 수 있는 다양한 연산자를 제공한다. 이 연산자들 덕분에 데이터 흐름을 필터링하거나 변환하거나 합치는 작업이 훨씬 쉬워진다.

몇 가지 대표적인 연산자를 코드와 함께 살펴보자.

### map으로 값 변형하기

map은 스트림의 각 값을 변형한다. 숫자를 두 배로 만드는 간단한 예제를 보자.

import { from } from 'rxjs';
import { map } from 'rxjs/operators';

const observable = from([1, 2, 3]);
const mapped = observable.pipe(map((x) => x * 2));

mapped.subscribe((value) => console.log(value));
// 2
// 4
// 6
### filter로 조건 걸기

filter는 조건에 맞는 값만 걸러낸다. 짝수만 출력하는 코드를 보자.

import { from } from 'rxjs';
import { filter } from 'rxjs/operators';

const observable = from([1, 2, 3, 4]);
const filtered = observable.pipe(filter((x) => x % 2 === 0));

filtered.subscribe((value) => console.log(value));
// 2
// 4
### merge로 스트림 합치기

merge는 여러 Observable을 하나로 묶는다. 두 개의 타이머를 합치는 예제를 보자.

import { interval, merge } from 'rxjs';
import { take } from 'rxjs/operators';

const first = interval(1000).pipe(take(3));
const second = interval(500).pipe(take(3));
const merged = merge(first, second);

merged.subscribe((value) => console.log(value));
// 0 (second)
// 0 (first)
// 1 (second)
// 1 (first)
// 2 (second)
// 2 (first)

이 외에도 RxJS는 수많은 연산자를 제공해서 데이터 흐름을 자유롭게 조작할 수 있다.

3. 비동기 작업 다루기

리액티브 프로그래밍은 비동기 작업을 처리할 때 진가를 발휘한다. API 호출 같은 작업을 Observable로 감싸면 코드가 깔끔해진다.

import { fromFetch } from 'rxjs/fetch';
import { switchMap, map } from 'rxjs/operators';

const observable = fromFetch('https://jsonplaceholder.typicode.com/posts/1')
    .pipe(
        switchMap((response) => response.json()),
        map((data) => data.title)
    );

observable.subscribe((title) => console.log(title));
// "sunt aut facere repellat provident occaecati excepturi optio reprehenderit"

fromFetch로 API 호출을 Observable로 만들고, switchMapmap으로 데이터를 가공했다. 이렇게 하면 Promise 체인보다 훨씬 간결해진다.

4. 에러 관리하기

Observable은 에러를 다루는 방법도 제공한다. Observer의 error로 에러를 받고, catchError 연산자로 에러를 잡아서 다른 Observable로 대체할 수 있다.

import { of } from 'rxjs';
import { map, catchError } from 'rxjs/operators';

const observable = of(1, 2, 3).pipe(
    map((x) => {
        if (x === 2) throw new Error('문제 발생');
        return x;
    }),
    catchError((err) => of('문제 해결'))
);

observable.subscribe({
    next: (value) => console.log(value),
    error: (err) => console.error(err),
    complete: () => console.log('끝')
});
// 1
// 문제 해결
// 끝

map에서 에러를 일부러 발생시키고, catchError로 대체 값을 내보냈다. 이런 방식으로 에러 상황도 유연하게 처리할 수 있다.

5. 구독 관리와 리소스 정리

Observable을 구독하면 Subscription 객체가 생긴다. 이걸로 구독을 끊을 수 있어서 리소스 낭비를 막을 수 있다.

import { interval } from 'rxjs';

const observable = interval(1000);
const subscription = observable.subscribe((value) => console.log(value));

setTimeout(() => {
    subscription.unsubscribe();
}, 5000);

interval로 계속 값을 내보내다가 5초 후에 구독을 끊었다. 이렇게 하면 불필요한 연산을 줄일 수 있다.

6. 리액티브 프로그래밍의 좋은 점과 아쉬운 점

리액티브 프로그래밍은 몇 가지 매력적인 장점을 갖고 있다:

  • 비동기 로직의 간결함: 콜백이나 Promise 체인 없이도 깔끔하게 정리된다.
  • 데이터 흐름의 명확함: 데이터가 어떻게 생성되고 변하고 소비되는지 한눈에 보인다.
  • 다양한 연산자: RxJS의 연산자로 복잡한 로직을 쉽게 풀어낼 수 있다.

하지만 단점도 있다:

  • 익숙해지기까지 시간: 개념과 API를 이해하려면 시간이 좀 걸린다.
  • 복잡해지면 머리 아픔: 데이터 흐름이 얽히면 추적하기 어려울 때가 있다.

복잡한 비동기 상황에서는 유용하지만, 단순한 작업에는 굳이 안 써도 괜찮다.

7. 실제로 적용해보기

리액티브 프로그래밍은 몇 가지 상황에서 특히 빛난다:

  • 실시간 데이터: 주식 가격이나 채팅 메시지처럼 계속 업데이트되는 데이터를 다룰 때.
  • 사용자 입력: 검색어 자동완성이나 드래그 앤 드롭 같은 인터랙션을 처리할 때.
  • 비동기 API: 여러 API 호출을 조합하거나 에러를 관리할 때.

사용자 검색어를 최적화해서 API를 호출하는 예제를 보자.

import { fromEvent } from 'rxjs';
import { debounceTime, map, switchMap } from 'rxjs/operators';

const input = document.getElementById('search');
const observable = fromEvent(input, 'input')
    .pipe(
        map((event) => event.target.value),
        debounceTime(300),
        switchMap((query) => fromFetch(`https://api.example.com/search?q=${query}`))
    );

observable.subscribe((response) => {
    console.log(response);
});

입력값을 300ms 동안 기다린 후 API를 호출하고, switchMap으로 이전 요청을 취소하며 최신 요청만 처리한다.

8. 더 많은 연산자 살펴보기

RxJS에는 다양한 연산자가 있어서 상황에 맞게 활용할 수 있다. 몇 가지 추가로 알아보자.

### combineLatest로 여러 스트림 조합하기

combineLatest는 여러 Observable의 최신 값을 조합해서 새 값을 만든다.

import { interval, combineLatest } from 'rxjs';
import { map } from 'rxjs/operators';

const first = interval(1000).pipe(map((x) => `A: ${x}`));
const second = interval(1500).pipe(map((x) => `B: ${x}`));
const combined = combineLatest([first, second]).pipe(
    map(([a, b]) => `${a} - ${b}`)
);

combined.subscribe((value) => console.log(value));
// A: 0 - B: 0
// A: 1 - B: 0
// A: 1 - B: 1
// A: 2 - B: 1
// ...
### scan으로 누적값 계산하기

scan은 스트림의 값을 누적해서 새 값을 만든다. 합계를 계산하는 예제를 보자.

import { from } from 'rxjs';
import { scan } from 'rxjs/operators';

const observable = from([1, 2, 3, 4]).pipe(
    scan((acc, value) => acc + value, 0)
);

observable.subscribe((value) => console.log(value));
// 1
// 3
// 6
// 10

연산자를 잘 활용하면 복잡한 데이터 흐름도 쉽게 다룰 수 있다.

9. 리액티브 프로그래밍과 상태 관리

리액티브 프로그래밍은 상태 관리에도 유용하다. 상태를 Observable로 관리하면 변화에 따라 UI를 자동으로 업데이트할 수 있다.

간단한 카운터를 리액티브하게 구현해보자.

import { BehaviorSubject } from 'rxjs';

const counter = new BehaviorSubject(0);

document.getElementById('increment').addEventListener('click', () => {
    counter.next(counter.value + 1);
});

counter.subscribe((value) => {
    document.getElementById('count').innerText = value;
});

BehaviorSubject로 현재 상태를 유지하고, 버튼 클릭 시 상태를 업데이트한다. 구독자는 상태 변화에 따라 UI를 갱신한다.

마무리

리액티브 프로그래밍은 비동기 데이터 흐름을 다루는 방법이다. Observable과 Observer를 중심으로 복잡한 로직을 간결하게 표현할 수 있고, RxJS의 연산자들로 데이터 스트림을 자유롭게 조작할 수 있다. 실시간 데이터나 사용자 인터랙션 같은 상황에서 특히 유용하다.

+ Recent posts