리액티브 프로그래밍 (Reactive Programming)
자바스크립트에서 비동기 데이터 흐름을 다룰 때 리액티브 프로그래밍은 꽤 유용한 도구다. 데이터 스트림과 변경 전파를 중심으로 돌아가는 이 방식은 복잡한 비동기 상황을 깔끔하게 정리해 준다. RxJS
같은 라이브러리와 함께라면 더 강력해진다. 이번에 리액티브 프로그래밍의 기본부터 실전 활용까지 코드와 함께 작성해보려 한다.
리액티브 프로그래밍을 잘 익히면 비동기 로직이 훨씬 직관적이고 관리하기 쉬워진다.
리액티브 프로그래밍 기본
리액티브 프로그래밍은 Observable과 Observer라는 두 개념을 중심으로 움직인다. 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로 만들고, switchMap
과 map
으로 데이터를 가공했다. 이렇게 하면 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
는 여러 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
의 연산자들로 데이터 스트림을 자유롭게 조작할 수 있다. 실시간 데이터나 사용자 인터랙션 같은 상황에서 특히 유용하다.
'코딩 공부 > 자바스크립트' 카테고리의 다른 글
100. 자바스크립트 디자인 패턴 (Design Patterns in JavaScript) (1) | 2025.04.03 |
---|---|
98. 자바스크립트 웹어셈블리 입문 (WebAssembly Introduction) (0) | 2025.04.02 |
97. 자바스크립트 함수형 프로그래밍 기초 (Functional Programming Basics) (0) | 2025.04.02 |
96. 자바스크립트 문서화 (Documentation Practices) (0) | 2025.04.01 |
95. 자바스크립트 코드 리뷰 (Code Review Practices) (4) | 2025.04.01 |