Angular에서 RxJS와 어댑터로 비동기 요청 상태 관리하기
2024-10-12 08:12:09비동기 요청 상태 관리의 필요성
Angular 애플리케이션을 개발할 때, 서버로부터 데이터를 요청하고 그 결과를 관리하는 것은 매우 일반적인 작업입니다. 서버 요청을 할 때 발생하는 여러 상태, 즉 요청이 진행 중인 경우, 데이터가 성공적으로 반환된 경우, 오류가 발생한 경우를 명확하게 관리하는 것이 중요합니다. 이는 사용자 경험을 개선하고, 더 나은 디버깅을 가능하게 합니다. 그러나 이러한 상태를 수동으로 관리하다 보면 코드가 복잡해지고 반복적인 작업이 늘어나게 됩니다.
이 블로그 글에서는 RxJS와 어댑터 패턴을 활용하여 Angular에서 비동기 요청 상태를 깔끔하고 일관되게 관리하는 방법을 살펴보겠습니다.
문제: 수많은 상태 관리
서버 요청을 처리할 때 겪는 여러 상태는 다음과 같습니다:
- 로딩(Loading): 요청이 진행 중인 상태.
- 성공(Success): 요청이 성공적으로 완료된 상태.
- 오류(Error): 네트워크 문제나 서버 오류와 같은 문제가 발생한 상태.
보통은 이 상태들을 컴포넌트에서 수동으로 관리하게 되며, 이는 코드의 중복 및 유지보수에 어려움을 초래할 수 있습니다. 그렇다면 어떻게 하면 이 상태 관리 로직을 재사용 가능한 어댑터 형태로 추상화할 수 있을까요?
해결책: 어댑터 패턴 사용하기
어댑터는 이러한 상태를 관리하는 재사용 가능한 유틸리티로, 코드 중복 없이 상태 전환을 관리할 수 있게 도와줍니다. 여기서는 로딩, 성공, 오류의 세 가지 핵심 상태를 처리할 수 있는 커맨드 어댑터를 만들어 보겠습니다.
커맨드 어댑터 인터페이스 정의하기
먼저, 커맨드 어댑터의 인터페이스를 정의해보겠습니다. 커맨드 상태를 보유하는 CommandState 인터페이스와 상태의 유형을 정의하는 CommandStatus 타입을 생성합니다.
// @file: interfaces.ts
export type CommandStatus = 'pending' | 'loading' | 'success' | 'error';
export interface CommandState<T, E = unknown> {
status: CommandStatus;
error: E | null;
data: T | null;
}
커맨드 어댑터 생성하기
이제 createCommandAdapter 함수를 살펴보겠습니다. 이 함수는 상태 전환을 관리하기 위한 특정 세터 및 게터를 포함하는 객체를 반환합니다. 세터 함수를 통해 상태를 설정할 수 있으며, 게터를 통해 상태를 선택할 수 있습니다.
다음은 커맨드 세터를 만드는 방법입니다.
// @file: command-adapter.ts
import { CommandState, CommandStatus } from './interfaces';
export function commandSetters<T, E = unknown>() {
return {
setPending: (state: CommandState<T, E>): CommandState<T, E> => ({
...state,
status: 'pending',
error: null,
}),
setLoading: (state: CommandState<T, E>): CommandState<T, E> => ({
...state,
status: 'loading',
error: null,
}),
setSuccess: (state: CommandState<T, E>, data: T): CommandState<T, E> => ({
...state,
status: 'success',
error: null,
data,
}),
setError: (state: CommandState<T, E>, error: E): CommandState<T, E> => ({
...state,
status: 'error',
error,
}),
reset: (state: CommandState<T, E>): CommandState<T, E> => ({
...state,
status: 'pending',
error: null,
data: null,
}),
};
}
이제 게터도 정의해 보겠습니다. commandGetters 함수를 통해 상태 선택기를 생성합니다.
// @file: command-adapter.ts
export function commandSelectors<T, E = unknown>() {
return {
selectData: (state: CommandState<T, E>): T | null => state.data,
selectError: (state: CommandState<T, E>): E | null => state.error,
selectStatus: (state: CommandState<T, E>): CommandStatus => state.status,
isPending: (state: CommandState<T, E>): boolean =>
state.status === 'pending',
isLoading: (state: CommandState<T, E>): boolean =>
state.status === 'loading',
isSuccess: (state: CommandState<T, E>): boolean =>
state.status === 'success',
hasError: (state: CommandState<T, E>): boolean => state.status === 'error',
isDone: (state: CommandState<T, E>): boolean =>
state.status === 'success' || state.status === 'error',
};
}
이제 세터와 게터를 하나의 createCommandAdapter 함수로 결합하여 모든 필요한 기능을 갖춘 객체를 반환합니다.
// @file: command-adapter.ts
export function createCommandAdapter<T, E = unknown>() {
return {
getInitialState: (
data: T | null = null,
initialState: Partial<CommandState<T, E>> = {}
): CommandState<T, E> => ({
status: 'pending',
error: null,
...initialState,
data,
}),
...commandSetters<T, E>(),
...commandSelectors<T, E>(),
};
}
RxJS와 통합하기
이제 상태를 관리하는 어댑터를 생성했으므로, RxJS 스트림과 통합하여 비동기 요청을 처리할 수 있습니다. 이를 위해 withCommandState라는 고차 함수를 만들어서 Observable을 감싸고 상태 전환을 처리합니다.
// @file: with-command-state.ts
import { Observable, catchError, map, of, startWith } from 'rxjs';
import { createCommandAdapter } from './command-adapter';
export function withCommandState<T, E = unknown>() {
const adapter = createCommandAdapter<T, E>();
const initialState = adapter.getInitialState();
return (o: Observable<T>) =>
o.pipe(
map((data: T) => adapter.setSuccess(initialState, data)),
catchError((error: E) => of(adapter.setError(initialState, error))),
startWith(adapter.setLoading(initialState))
).pipe(
map((state) => ({
...state,
isLoading: adapter.isLoading(state),
isSuccess: adapter.isSuccess(state),
hasError: adapter.hasError(state),
isDone: adapter.isDone(state),
}))
);
}
컴포넌트에서 어댑터 사용하기
이제 커맨드 어댑터를 Angular 컴포넌트에 통합하여 쉽게 사용할 수 있습니다. 아래 코드는 RxJS를 이용하여 서버 요청을 시뮬레이션하고 withCommandState를 적용하여 상태를 관리하는 기본적인 예입니다.
// @file: server-request.component.ts
import { HttpClient } from '@angular/common/http';
import { Component, inject } from '@angular/core';
import { AsyncPipe, JsonPipe } from '@angular/common';
import { withCommandState } from './with-command-state';
@Component({
selector: 'app-server-request',
standalone: true,
imports: [AsyncPipe, JsonPipe],
template: `
<h1>서버 요청 예제</h1>
<ng-container *ngIf="serverRequest$ | async as state">
<ng-container *ngIf="state.isLoading">🕦 로딩 중...</ng-container>
<ng-container *ngIf="state.isSuccess">✅ 데이터: {{ state.data | json }}</ng-container>
<ng-container *ngIf="state.hasError">❌ 데이터 로딩 실패: {{ state.error | json }}</ng-container>
<ng-container *ngIf="state.isDone && !state.hasError && !state.data">🤷 데이터 없음</ng-container>
</ng-container>
`,
})
export class ServerRequestComponent {
private readonly http = inject(HttpClient);
readonly serverRequest$ = this.http
.get('https://api.example.com/data')
.pipe(withCommandState()); // 👈 상태 관리기 적용
}
결론
Angular와 RxJS를 사용할 때 비동기 요청의 상태를 관리하는 것은 애플리케이션의 진정한 성능을 이끌어낼 수 있는 중요한 과정입니다. 어댑터 패턴을 활용하면 이러한 상태 관리 로직을 깨끗하고 명확하게 유지할 수 있습니다. 이는 개발자뿐만 아니라 사용자에게도 더 나은 경험을 제공하게 됩니다.
이제 여러분도 Angular 및 RxJS를 통해 비동기 요청 상태를 관리하는 방법을 배우셨습니다. 다음 프로젝트에서 이 패턴을 활용해 보세요!
참고자료
블로그 글에서는 Angular에서 효율적으로 비동기 요청 상태를 관리하는 방법을 다루었습니다. 추가적인 정보가 필요하다면 위의 링크를 참고하세요.