HooneyLog
© 2026 Seunghoon Shin. All rights reserved.
모든 게시글
Frontend
2026. 3. 29.•
1

[RxJS] 비동기 데이터의 '수도관' 설계하기: 기초부터 실무 패턴까지

Seunghoon Shin
작성자 Seunghoon Shin풀스택 개발자

[RxJS] 비동기 데이터의 '수도관' 설계하기: 기초부터 실무 패턴까지

1. 문제의 배경

현대적인 웹 개발에서 비동기 처리는 피할 수 없는 숙명입니다. API 호출, 사용자 클릭 이벤트, 웹소켓 메시지 등 수많은 데이터가 서로 다른 시점에 발생합니다.

단순한 비동기 작업은 Promise나 async/await로 충분합니다. 하지만 다음과 같은 상황이 닥치면 코드가 기하급수적으로 복잡해집니다.

  • "사용자가 검색어를 입력할 때마다 API를 쏘되, 너무 잦으면 끊어줘 (Debounce)."
  • "여러 API 요청 중 가장 먼저 도착한 녀석만 사용해줘 (Race Condition)."
  • "데이터가 오고 있는 중간에 가공하고, 에러가 나면 재시도해줘 (Retry)."

이런 복잡한 '시간의 흐름'이 섞인 데이터를 제어하기 위해 RxJS가 등장했습니다.

2. 해결 방안: 리액티브 프로그래밍 (Reactive Programming)

RxJS는 데이터를 하나의 '흐름(Stream)'으로 봅니다. 데이터가 발생할 때마다 우리가 미리 설계해둔 '파이프라인'을 따라 흐르며 자동으로 가공되고 전달되는 방식입니다. 이를 통해 복잡한 상태 관리와 비동기 로직을 선언적으로(무엇을 할지 기술하는 방식) 작성할 수 있습니다.

3. 핵심 개념: 정수기 비유로 이해하기

RxJS의 아키텍처는 정수기 시스템과 매우 흡사합니다.

  1. Observable (수도꼭지): 데이터가 뿜어져 나오는 원천입니다. 물(데이터)이 언제 나올지, 얼마나 나올지는 수도꼭지만 압니다.
  2. Operator (필터): 물이 흘러가는 파이프 중간에 설치하는 장치입니다. 흙탕물을 거르거나(filter), 물의 성분을 비타민 물로 바꾸는(map) 역할을 합니다.
  3. Observer/Subscription (컵): 마지막에 물을 받아서 마시는 사람입니다. 필터를 다 거친 최종 결과물을 소비합니다.

4. 실무 구현 및 패턴

4.1. NestJS 인터셉터에서의 활용

NestJS 인터셉터는 RxJS의 힘을 빌려 응답 데이터를 우아하게 가공합니다.

import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common'; import { Observable } from 'rxjs'; import { map, tap } from 'rxjs/operators'; @Injectable() export class ResponseInterceptor implements NestInterceptor { intercept(context: ExecutionContext, next: CallHandler): Observable<any> { // next.handle()은 데이터가 흐르기 시작하는 '수도꼭지'입니다. return next.handle().pipe( // tap: 데이터 흐름을 방해하지 않고 옆에서 지켜보기 (로깅) tap(() => console.log('데이터가 지나갑니다...')), // map: 데이터를 예쁜 봉투(공통 포맷)로 감싸기 map(data => ({ success: true, data, timestamp: new Date().toISOString() })) ); } }

4.2. 프론트엔드 검색창 최적화 (Debounce)

사용자의 입력을 실시간으로 처리할 때 서버 부하를 줄이는 전형적인 패턴입니다.

import { fromEvent } from 'rxjs'; import { debounceTime, map, distinctUntilChanged } from 'rxjs/operators'; const searchInput = document.getElementById('search'); // 입력 이벤트를 '스트림'으로 만듭니다. fromEvent(searchInput, 'input').pipe( map(event => event.target.value), debounceTime(300), // 0.3초 동안 입력이 멈춰야 다음으로 넘김 distinctUntilChanged(), // 이전 값과 같으면 무시 ).subscribe(value => { console.log('최종 검색어:', value); // 여기서 API 호출 수행 });

4.3. 에러 발생 시 재시도 (Retry)

네트워크 불안정으로 실패했을 때 자동으로 다시 시도하는 로직도 단 한 줄로 가능합니다.

http$.pipe( retry(3), // 실패하면 최대 3번까지 자동으로 다시 쏴라! catchError(err => of('에러 발생 시 보여줄 기본 데이터')) ).subscribe(data => console.log(data));

5. 결과 및 Trade-off

도입 이점:

  • 코드의 간결성: 수많은 if문과 setTimeout, Flag 변수가 사라지고 선언적인 파이프라인만 남습니다.
  • 강력한 비동기 제어: 시간과 관련된 복잡한 로직을 아주 정교하게 다룰 수 있습니다.
  • 일관성: 클릭, 타이머, HTTP 요청 등 모든 이벤트를 동일한 인터페이스(Observable)로 다룹니다.

Trade-off (한계점):

  • 학습 곡선 (Learning Curve): RxJS 특유의 사고방식(생각의 흐름을 스트림으로 바꾸는 것)에 익숙해지는 데 시간이 걸립니다.
  • 메모리 관리: 구독(subscribe)을 시작했다면, 필요 없을 때 반드시 해제(unsubscribe)해야 합니다. (메모리 누수 주의!)

6. 마치며

RxJS는 단순히 라이브러리를 넘어 '데이터를 대하는 새로운 관점'을 제시합니다. 모든 것을 흐르는 스트림으로 바라보고, 이를 필터(Operator)로 조립해 나가는 경험은 개발자에게 큰 즐거움을 줍니다.

처음에는 수많은 Operator들 때문에 혼란스러울 수 있지만, map, filter, tap 같은 기본 필터부터 하나씩 익혀보세요. 어느새 여러분의 비동기 코드는 정수기 물처럼 깨끗해져 있을 것입니다.

참고 자료:

  • RxJS 공식 문서
  • Learn RxJS - 실무 패턴 가이드
← 이전 글[NestJS] 미들웨어부터 필터까지: 요청 라이프사이클 심층 분석