HOWTO · Angular
Angular Observable VS. Promise
이 기사에서는 Angular에서 Observable과 Promise가 무엇인지 설명합니다. 또한 Observable과 Promise의 근본적인 차이점을 설명합니다.
이 페이지의 내용
하나의 결과를 기다리는 작업에는 Promise를, 시간이 지나면서 값이 이어지는 작업에는 Observable을 선택하세요. Angular를 사용한다고 두 추상화가 서로 바꿔 쓸 수 있는 것은 아닙니다. 코드에 필요한 동작이 무엇인지에 따라 적합한 선택이 달라집니다.
Angular에서 Promise란 무엇입니까?
Promise는 나중에 결정될 하나의 결과를 나타내는 JavaScript 객체입니다. 결과는 값으로 이행되거나 이유와 함께 거부됩니다. Promise는 대기 중 상태에서 이행 또는 거부 상태로 바뀌며, then, catch, finally 메서드로 작업을 연결하고 결과를 처리할 수 있습니다. Promise는 한 번만 결정되므로, 이행된 뒤에 다른 값을 다시 내보내지 않습니다.
Promise의 실행 함수는 Promise를 생성할 때 시작됩니다. Promise API에는 취소나 재시도 기능이 내장되어 있지 않지만, 애플리케이션 코드에서 취소를 고려한 작업이나 재시도 로직을 Promise 주위에 구현할 수 있습니다. 작업 흐름이 길어지면 긴 체인을 따라가기 어려워질 수도 있습니다.
Angular에서 Observable이란 무엇입니까?
Observable은 값이 0개 이상 방출된 뒤 완료되거나 오류가 발생할 수 있는 스트림을 나타냅니다. 완료나 오류 알림 없이 무기한 열린 상태로 남을 수도 있습니다. Angular의 HttpClient는 RxJS Observable을 반환합니다. 보통 Observable은 구독할 때 작업을 시작하지만, 생성 방식에 따라 달라질 수 있습니다. RxJS 연산자는 스트림을 변환하고 결합할 수 있으며, 구독을 해제하면 값 전달을 중단할 수 있습니다. 생산자가 취소를 지원하는 경우에는 진행 중인 작업도 취소될 수 있습니다.
Observable을 사용한다고 취소나 재시도가 자동으로 제공되거나 항상 성능이 향상되는 것은 아닙니다. 이러한 동작은 생산자와 연산자에 따라 달라집니다. RxJS는 유용한 도구를 제공하지만, 스트림에 익숙하지 않은 개발자에게는 학습이 필요합니다.
Angular에서 Promise와 Observable의 주요 차이점
| 비교 항목 | Promise | Observable |
|---|---|---|
| 값 | 이행 값 하나 또는 거부 이유 하나 | next 값이 0개 이상일 수 있습니다. 스트림은 완료되거나 오류가 발생할 수 있고, 어느 알림도 없이 열린 상태로 남을 수도 있습니다. |
| 시작 시점 | 생성할 때 실행 함수가 시작됩니다. | 대개 구독할 때 시작되지만, 생성 방식에 따라 달라집니다. |
| 오류 | catch 또는 거부 처리기로 처리합니다. |
구독 안에서 처리하거나 catchError 같은 연산자를 사용합니다. |
| 취소 | 취소 메서드가 내장되어 있지 않습니다. | 구독을 해제하면 값 관찰이 중단되며, 지원되는 작업은 취소할 수 있습니다. |
| 적합한 경우 | 간단한 계산처럼 결과 하나가 필요한 작업 | 이벤트, 변하는 값 또는 RxJS 조합이 유용한 작업 |
| 재시도 | 재시도가 내장되어 있지 않습니다. 값을 만드는 함수를 직접 다시 호출하세요. | retry 같은 연산자는 실패 후 소스를 다시 구독합니다. |
| 공유 | 모든 처리기가 확정된 하나의 같은 결과를 봅니다. | 콜드 Observable은 구독자마다 생산자를 다시 실행합니다. 한 번의 실행으로 여러 구독자를 처리해야 한다면 핫 소스를 명시적으로 공유하세요. |
Angular에서 HTTP 요청을 보낼 때 RxJS 조합이나 취소 흐름이 필요하다면 HttpClient의 Observable을 사용하세요. 응답 본문에 배열이 들어 있을 수 있지만, 배열에 항목이 여러 개라는 이유만으로 Observable이 필요한 것은 아닙니다. 요청 자체는 응답 값 하나를 반환합니다. 같은 응답은 Promise로도 나타낼 수 있습니다. 작업의 특성과 주변 API에 맞는 추상화를 선택하세요.
취소, 지연 실행, 재시도를 자세히 살펴보기
취소는 가장 뚜렷한 실무 차이를 보여 줍니다. 입력하면서 검색하는 필드에서는 키를 누를 때마다 이전 검색어에 대한 요청을 버려야 합니다. HttpClient Observable에서는 switchMap으로 새 요청으로 전환하면 이전 구독이 해제되고, HttpClient는 진행 중인 그 요청을 취소합니다. Promise에는 이에 대응하는 전환이 없습니다. 일단 시작된 하위 작업은 호출한 API가 fetch용 AbortController처럼 자체 취소 수단을 제공하지 않는 한 계속 실행되며, 그 수단은 Promise 자체와는 별개입니다.
실행 시점도 다릅니다. Promise는 처리기가 등록되는지와 관계없이 생성될 때 작업을 시작하므로, Promise를 만드는 것 자체가 이미 부수 효과입니다. 반면 콜드 Observable은 지연 실행됩니다. 만드는 것만으로는 작업이 설명될 뿐이며, subscribe를 호출할 때마다 생산자가 다시 실행되므로 구독자 두 명이 두 번의 별도 실행을 일으킬 수 있습니다. 한 번의 공유 실행을 기대하는 코드는 공유 연산자를 쓰거나 Promise를 저장하는 등 그 뜻을 명시해야 합니다.
재시도와 조합도 같은 패턴을 따릅니다. 거부된 Promise는 끝난 것이며, 복구하려면 값을 만드는 함수를 직접 만든 루프에서 다시 호출해야 합니다. Observable은 대신 retry 연산자로 소스를 다시 구독할 수 있고, Promise 주변에서는 타이머와 플래그를 직접 작성해야 할 타이밍 패턴, 예를 들어 키 입력 사이의 멈춤을 기다리는 debounceTime이나 변하지 않은 입력을 무시하는 distinctUntilChanged는 RxJS에서 연산자 하나로 해결됩니다.
실무에서의 선택
결과가 정말로 하나뿐이고 버려도 되는 경우, 즉 한 번뿐인 계산, 이미 Promise를 반환하는 타사 SDK, async와 await로 읽는 편이 자연스럽고 취소할 필요가 전혀 없는 짧고 선형적인 흐름에는 Promise를 선택하세요. 소스가 본질적으로 스트림인 경우, 즉 조합하거나 취소하려는 HttpClient 응답, 라우트 매개변수, 폼 값 변경, DOM 이벤트, WebSocket에는 Observable을 선택하고, 늦게 구독하는 구독자, 반복되는 값, 해제가 중요할 때도 마찬가지입니다. 판단이 서지 않으면 경계마다 변환하지 말고, 값을 만드는 API가 이미 반환하는 타입을 그대로 유지하세요.
Promise와 Observable 사이 변환
두 모델은 서로 운용할 수 있지만 변환이 공짜는 아닙니다. RxJS의 from은 Promise를 확정된 값을 한 번 방출하는 Observable로 감싸고, firstValueFrom과 lastValueFrom은 Observable을 async와 await 코드를 위한 Promise로 되돌립니다. HttpClient Observable을 이런 식으로 변환하면 응답 하나는 유지되지만 구독 해제가 제공하던 취소를 포기하게 되고, 그 지점에서 추가 연산자 조합도 끝납니다. 재사용 가능한 스트림 로직 안이 아니라, 서비스의 가장자리처럼 명확한 경계에서 변환하세요.
흔한 실수
- 하나의 HTTP 응답 안에 있는 배열을 여러 번의 방출로 취급하는 것. 요청은 여전히 한 번만 방출합니다. 정말로 반복되는 소스만 반복해서 방출합니다.
- 요청을
Promise로 변환한 뒤 페이지를 벗어나면 취소될 것으로 기대하는 것. 해제할 구독이 없으면 생산자에게 멈추라는 신호를 보낼 방법이 없습니다. async파이프나takeUntilDestroyed같은 해제 전략 없이 수명이 긴 컴포넌트에서 구독해, 뷰가 사라진 뒤에도 오래된 구독이 계속 값을 받는 것.- 이미 확정된
Promise가 나중에 더 새로운 값을 받아들일 것으로 기대하는 것.Promise는 결과 하나를 기억합니다. 현재 상태를 다시 읽으려면 새 호출이나 스트림이 필요합니다.
Angular에서 Promise와 Observable 사용 예제
이 독립형 JavaScript 예제는 간단한 교육용 Observable을 사용해 구독과 여러 값의 방출을 보여 줍니다. Angular에서 사용하는 RxJS 구현은 아닙니다. 구독하는 동안 Observable은 queued와 saved를 내보내고, 이후 마이크로태스크에서 Promise 콜백이 실행됩니다. 코드 블록은 바이트 단위로 유지되므로 이 표시 문자열은 영어로 남습니다.
이 독립형 예제를 실행해 보세요:
class Observable {
constructor(producer) {
this.producer = producer;
}
subscribe(next) {
const teardown = this.producer(next);
return { unsubscribe: teardown };
}
}
const promise = Promise.resolve("saved");
const updates = new Observable((next) => {
next("queued");
next("saved");
return () => {};
});
console.log("before subscription");
updates.subscribe((value) => console.log(`Observable: ${value}`));
promise.then((value) => console.log(`Promise: ${value}`));
console.log("after scheduling");
before subscription
Observable: queued
Observable: saved
after scheduling
Promise: saved
교육용 Observable은 구독할 때 생산자를 호출하고 두 값을 내보냅니다. 반면 Promise는 한 번만 결정되며, 그 콜백은 나중 마이크로태스크에서 실행됩니다. 애플리케이션 코드에서는 RxJS Observable과 구독 API를 사용하세요. 결과 하나가 필요하면 Promise를 선택하고, 스트림 조합이나 반복 방출 또는 구독 관리가 작업에 적합하면 Observable을 선택하세요.
예제: 즉시 실행되는 Promise, 지연 실행되고 취소 가능한 Observable
이 두 번째 독립형 예제는 같은 교육용 Observable을 사용해 표의 시점 및 취소 행을 보여 줍니다. Promise 실행 함수는 처리기가 등록되기 전에 실행되고, Observable 생산자는 subscribe를 기다리며 구독자가 두 번째 틱 뒤에 구독을 해제하면 해제 작업을 실행합니다. Promise 콜백은 다시 마이크로태스크로, 첫 번째 타이머 틱보다 먼저 실행됩니다. 코드 블록은 그대로 유지되므로 표시 문자열은 영어로 남습니다.
class Observable {
constructor(producer) {
this.producer = producer;
}
subscribe(next) {
const teardown = this.producer(next);
return { unsubscribe: teardown };
}
}
const promise = new Promise((resolve) => {
console.log("Promise executor: runs at construction");
resolve("done");
});
const ticks = new Observable((next) => {
console.log("Observable producer: runs at subscription");
let count = 0;
const id = setInterval(() => next(++count), 10);
return () => {
clearInterval(id);
console.log("teardown: interval cleared");
};
});
console.log("before subscription");
const sub = ticks.subscribe((value) => {
console.log(`tick ${value}`);
if (value === 2) sub.unsubscribe();
});
promise.then((value) => console.log(`Promise: ${value}`));
Promise executor: runs at construction
before subscription
Observable producer: runs at subscription
Promise: done
tick 1
tick 2
teardown: interval cleared
실행 결과는 세 가지 동작을 순서대로 확인해 줍니다. 실행 함수는 생성될 때 동작했고, 생산자는 구독 전까지 대기했으며, 구독 해제가 인터벌을 지우는 해제 작업을 실행해 더 이상 틱이 도착하지 않습니다.