Angular 20 Resource API 핵심 정리: httpResource, rxResource 활용법과 실전 면접 대비

Angular 20에서 도입된 Resource API의 핵심 개념을 정리합니다. httpResource와 rxResource를 활용한 시그널 기반 데이터 페칭, Zod 스키마 검증, ResourceStatus 문자열 리터럴, HttpClient 구독 패턴 마이그레이션, 그리고 실전 기술 면접 질문까지 코드 예제와 함께 상세히 다룹니다.

Angular 20 Resource API and httpResource tutorial

Angular 20은 Resource APIhttpResource를 시그널 기반 데이터 페칭의 핵심 도구로 격상시켰습니다. 기존의 HttpClient 구독 패턴에서 발생하던 반복적인 보일러플레이트를 대체하는 이 API는 로딩, 에러, 완료 상태를 리액티브 프리미티브로 자동 추적합니다.

Angular 20의 주요 변경 사항

Resource API는 requestparams로, rxResourceloaderstream으로 이름을 변경했습니다. 상태 값은 숫자형 enum 대신 문자열 리터럴('idle', 'loading', 'resolved', 'error', 'reloading', 'local')을 사용합니다. httpResourceHttpClient 위에 구축되어 인터셉터와 Zod 유효성 검증을 기본으로 지원합니다.

Angular 20이 제공하는 세 가지 Resource 변형

Angular 20은 비동기 데이터를 시그널로 로드하는 세 가지 방법을 제공합니다. 각각 다른 사용 사례를 대상으로 하지만, 모두 동일한 리액티브 모델을 공유합니다. 의존성을 선언하고, 로더를 정의하며, 결과를 시그널을 통해 소비하는 구조입니다.

  • resource() 는 Promise와 함께 동작합니다. fetch()나 Promise 기반 API를 사용할 때 적합합니다.
  • rxResource() 는 Observable과 함께 동작합니다. debounceTime, retry, switchMap 같은 RxJS 연산자가 필요한 경우에 올바른 선택입니다.
  • httpResource() 는 Angular의 HttpClient를 직접 감싸는 래퍼입니다. 인터셉터, 테스트 유틸리티, 스키마 유효성 검증이 별도 설정 없이 동작합니다.

httpResource와 나머지 두 가지의 핵심 차이점은 다음과 같습니다. httpResource는 내부적으로 HttpClient를 사용하므로 기존에 구성된 인터셉터가 그대로 적용됩니다. 원래의 resource() API는 HttpClient를 완전히 우회했으며, 이것은 Angular 19에서 주요 문제점으로 지적되었습니다.

resource()를 활용한 사용자 프로필 구현

resource() 함수는 params 연산과 loader 함수를 인자로 받습니다. params 내부의 시그널이 변경되면 로더가 자동으로 재실행됩니다.

user-profile.component.tstypescript
import { Component, signal, resource } from '@angular/core';

interface User {
  id: number;
  name: string;
  email: string;
}

@Component({
  selector: 'app-user-profile',
  template: `
    @if (userResource.hasValue()) {
      <h2>{{ userResource.value().name }}</h2>
      <p>{{ userResource.value().email }}</p>
    } @else if (userResource.isLoading()) {
      <p>Loading profile...</p>
    } @else if (userResource.error()) {
      <p>Failed to load user</p>
    }
  `,
})
export class UserProfileComponent {
  userId = signal(1);

  // params produces the reactive dependency
  // loader receives it and returns a Promise
  userResource = resource<User, number>({
    params: () => this.userId(),
    loader: async ({ params: id, abortSignal }) => {
      const res = await fetch(`/api/users/${id}`, { signal: abortSignal });
      return res.json();
    },
  });

  loadUser(id: number) {
    this.userId.set(id); // triggers automatic refetch
  }
}

abortSignal 파라미터는 userId가 변경되었을 때 이전 요청이 완료되기 전에 Angular이 진행 중인 요청을 취소할 수 있도록 합니다. 수동 구독 관리 없이도 경쟁 조건을 방지할 수 있습니다.

httpResource를 활용한 리액티브 데이터 페칭

httpResource는 URL 선언과 HTTP 실행을 하나의 호출로 결합하여 보일러플레이트를 제거합니다. HttpResourceRef를 반환하며, 이 객체는 value, isLoading, error, status, headers를 시그널로 노출합니다.

product-list.component.tstypescript
import { Component, signal, computed } from '@angular/core';
import { httpResource } from '@angular/common/http';

interface Product {
  id: number;
  name: string;
  price: number;
  category: string;
}

@Component({
  selector: 'app-product-list',
  template: `
    @if (products.hasValue()) {
      @for (product of products.value(); track product.id) {
        <div class="product-card">
          <h3>{{ product.name }}</h3>
          <span>{{ product.price | currency }}</span>
        </div>
      }
    } @else if (products.isLoading()) {
      <p>Loading products...</p>
    }
  `,
})
export class ProductListComponent {
  category = signal('electronics');

  // httpResource re-fetches whenever category() changes
  products = httpResource<Product[]>(() => ({
    url: '/api/products',
    params: { category: this.category() },
  }));

  filterByCategory(cat: string) {
    this.category.set(cat); // pending request is cancelled, new one starts
  }
}

여기서 몇 가지 세부 사항이 중요합니다. httpResource에 전달되는 함수는 요청 설정 객체를 반환합니다. Angular은 이 함수 내부의 시그널 읽기를 추적하므로, category를 변경하면 새로운 GET 요청이 트리거됩니다. 이미 진행 중인 요청이 있다면 Angular은 새 요청을 시작하기 전에 해당 요청을 취소합니다.

httpResource는 읽기 전용입니다

httpResource는 데이터 페칭(GET 요청)을 위해 설계되었습니다. POST, PUT, DELETE 작업에 사용하는 것은 안전하지 않습니다. 요청 취소로 인해 변경 작업이 중간에 중단될 수 있기 때문입니다. 쓰기 작업에는 HttpClient를 직접 사용하거나 서비스 메서드로 래핑하는 것이 권장됩니다.

Zod와 httpResource를 결합한 스키마 유효성 검증

외부 서비스의 API 응답은 기대하는 형태에서 벗어날 수 있습니다. httpResourceparse 옵션은 Zod와 같은 스키마 유효성 검증 라이브러리를 통합하여, 잘못된 데이터가 조용히 전파되는 대신 런타임에서 불일치를 포착합니다.

order.component.tstypescript
import { Component, signal } from '@angular/core';
import { httpResource } from '@angular/common/http';
import { z } from 'zod';

// Define the expected shape with Zod
const OrderSchema = z.object({
  id: z.number(),
  status: z.enum(['pending', 'shipped', 'delivered', 'cancelled']),
  total: z.number().positive(),
  items: z.array(z.object({
    productId: z.number(),
    quantity: z.number().int().positive(),
    unitPrice: z.number().positive(),
  })),
  createdAt: z.string().datetime(),
});

type Order = z.infer<typeof OrderSchema>;

@Component({
  selector: 'app-order',
  template: `
    @if (order.hasValue()) {
      <h2>Order #{{ order.value().id }}</h2>
      <p>Status: {{ order.value().status }}</p>
      <p>Total: {{ order.value().total | currency }}</p>
    } @else if (order.error()) {
      <p>Invalid order data received</p>
    }
  `,
})
export class OrderComponent {
  orderId = signal(42);

  // parse validates the response before exposing it as a signal
  order = httpResource<Order>(
    () => `/api/orders/${this.orderId()}`,
    { parse: OrderSchema.parse }
  );
}

API가 OrderSchema와 일치하지 않는 데이터를 반환하면, 리소스는 'error' 상태로 전환됩니다. parse 함수의 반환 타입이 value()의 TypeScript 타입도 결정하므로, 스키마 정의가 런타임 유효성 검증기와 타입 생성기의 이중 역할을 수행합니다.

Angular 20에서의 rxResource: stream과 params

Angular 20은 rxResource에서 loaderstream으로, requestparams로 이름을 변경했습니다. 이는 rxResource가 지원하는 스트리밍 시맨틱과 네이밍을 일치시키기 위한 변경입니다. stream 함수는 Observable 컨텍스트를 받아 Observable을 반환해야 합니다.

search.component.tstypescript
import { Component, signal } from '@angular/core';
import { rxResource } from '@angular/core/rxjs-interop';
import { HttpClient } from '@angular/common/http';
import { inject } from '@angular/core';
import { debounceTime, switchMap } from 'rxjs';

interface SearchResult {
  id: number;
  title: string;
  excerpt: string;
}

@Component({
  selector: 'app-search',
  template: `
    <input (input)="query.set($any($event.target).value)" placeholder="Search..." />
    @if (results.isLoading()) {
      <p>Searching...</p>
    }
    @if (results.hasValue()) {
      @for (item of results.value(); track item.id) {
        <div>{{ item.title }}</div>
      }
    }
  `,
})
export class SearchComponent {
  private http = inject(HttpClient);
  query = signal('');

  // params (was "request") provides the reactive input
  // stream (was "loader") returns an Observable
  results = rxResource<SearchResult[], string>({
    params: () => this.query(),
    stream: ({ params: q }) =>
      this.http.get<SearchResult[]>('/api/search', {
        params: { q },
      }),
  });
}

httpResource와 달리 rxResource는 Observable 파이프라인에 대한 완전한 제어권을 부여합니다. debounceTime이나 retry 같은 연산자를 stream 내부에서 체이닝할 수 있습니다. 그러나 가장 일반적인 경우(단일 GET 요청)에는 httpResource가 더 적은 코드로 동일한 결과를 달성합니다.

Angular 면접 준비가 되셨나요?

인터랙티브 시뮬레이터, flashcards, 기술 테스트로 연습하세요.

상태 추적: 문자열 리터럴이 Enum을 대체

Angular 20은 ResourceStatus를 숫자형 enum에서 문자열 유니온 타입으로 변경했습니다. 6개의 상태 값은 리소스 라이프사이클에 대한 세밀한 통찰을 제공합니다.

| 상태 | 의미 | |---|---| | 'idle' | paramsundefined를 반환하여 요청이 발행되지 않은 상태 | | 'loading' | 첫 번째 요청이 진행 중인 상태 | | 'reloading' | 이전 성공 이후 후속 요청이 진행 중인 상태 | | 'resolved' | value()에서 데이터에 접근할 수 있는 상태 | | 'error' | 요청 실패 상태이며 error()에 에러 정보가 포함됨 | | 'local' | .set() 또는 .update()를 통해 로컬로 값이 설정된 상태 |

status-demo.component.tstypescript
import { Component, signal, resource } from '@angular/core';

@Component({
  selector: 'app-status-demo',
  template: `
    <p>Status: {{ data.status() }}</p>
    @switch (data.status()) {
      @case ('loading') { <spinner /> }
      @case ('reloading') { <subtle-spinner /> }
      @case ('resolved') { <data-table [rows]="data.value()" /> }
      @case ('error') { <error-banner [error]="data.error()" /> }
      @case ('idle') { <p>Select a filter to load data</p> }
    }
  `,
})
export class StatusDemoComponent {
  filter = signal<string | undefined>(undefined);

  data = resource({
    params: () => this.filter(),
    loader: async ({ params: f, abortSignal }) => {
      const res = await fetch(`/api/data?filter=${f}`, { signal: abortSignal });
      return res.json();
    },
  });
}

params에서 undefined를 반환하면 상태가 'idle'로 설정되고 로더의 실행이 방지됩니다. 이 패턴은 조건부 데이터 페칭에 효과적입니다. 사용자가 입력을 제공할 때까지 안내 문구를 표시하고, 그 이후에 데이터를 로드하는 방식으로 활용할 수 있습니다.

error 상태에서의 value() 접근

Angular 20부터 'error' 상태의 리소스에서 value()를 호출하면 런타임 예외가 발생합니다. 반드시 hasValue()로 보호하거나 status()를 확인한 후에 value()에 접근해야 합니다. Angular 19에서는 에러 시 value()undefined를 반환했으므로, 이것은 호환성을 깨뜨리는 변경 사항입니다.

Angular 20 httpResource 면접 질문

Resource API는 Angular 기술 면접에서 표준적인 주제로 자리 잡고 있습니다. 다음은 API에 대한 표면적 이해를 넘어 실질적인 이해도를 검증하는 질문들입니다.

Q: httpResource가 resource()로는 해결할 수 없는 어떤 문제를 해결합니까?

resource()fetch()나 Promise 기반 로더를 사용하므로 Angular의 HttpClient를 우회합니다. 이는 인터셉터(인증 토큰, 로깅, 에러 처리용)가 적용되지 않음을 의미합니다. httpResource는 내부적으로 HttpClient를 사용하므로 인터셉터, 테스트 유틸리티(HttpTestingController), withFetch() 같은 기능이 별도의 설정 없이 동작합니다.

Q: rxResource를 httpResource보다 선호해야 하는 경우는 언제입니까?

rxResourcestream 함수를 통해 완전한 Observable 제어권을 제공합니다. 데이터 파이프라인에 RxJS 연산자가 필요한 경우에 선택해야 합니다. 검색 입력의 디바운싱, 지수 백오프를 사용한 실패 요청 재시도, combineLatest를 사용한 다중 스트림 결합 등이 대표적인 사례입니다. 단순한 GET 요청의 경우 httpResource가 더 적은 코드를 요구합니다.

Q: 시그널이 빠르게 변경될 때 Angular은 동시 요청을 어떻게 처리합니까?

세 가지 리소스 변형 모두 params가 새 값을 생성하면 보류 중인 요청을 취소합니다. httpResourceresource()의 경우 AbortSignal이 기저의 fetch를 취소합니다. rxResource의 경우 Angular이 이전 Observable의 구독을 해제합니다. 이는 오래된 응답이 최신 데이터를 덮어쓰는 것을 방지합니다.

Q: 'local' 상태의 목적은 무엇입니까?

리소스에서 .set() 또는 .update()를 호출하면 로더를 트리거하지 않고 로컬에서 값을 변경합니다. 상태가 'local'로 전환되어 현재 값이 서버에서 온 것이 아님을 나타냅니다. 이는 낙관적 UI 업데이트를 지원합니다. 별도의 변경 요청이 실행되는 동안 UI가 즉시 변경 사항을 반영하는 패턴입니다.

Q: Zod 통합이 httpResource에서 어떻게 동작합니까?

parse 옵션은 (data: unknown) => T 시그니처를 가진 함수를 받습니다. HTTP 응답이 도착하면 httpResource는 파싱된 JSON을 value()로 설정하기 전에 parse를 통과시킵니다. parse가 예외를 던지면(예: ZodError) 리소스는 'error' 상태로 전환됩니다. parse의 반환 타입이 value()의 TypeScript 타입도 결정하므로, 스키마 정의가 런타임 유효성 검증기와 타입 생성기의 이중 역할을 수행합니다.

Angular 시그널과 프레임워크와의 통합 방식에 대한 심화 학습은 시그널 모듈에서 computed 시그널, effect, 리액티비티 모델을 다루고 있습니다.

HttpClient 구독에서 httpResource로의 마이그레이션

기존 Angular 애플리케이션은 일반적으로 ngOnInit 내부에서 HttpClient 구독을 사용하거나 Observable과 함께 AsyncPipe를 사용하여 데이터를 페칭합니다. httpResource로의 마이그레이션은 예측 가능한 패턴을 따릅니다.

typescript
// BEFORE: manual subscription in ngOnInit
@Component({ /* ... */ })
export class BeforeComponent implements OnInit, OnDestroy {
  private http = inject(HttpClient);
  private destroy$ = new Subject<void>();
  users: User[] = [];
  loading = false;
  error: string | null = null;

  ngOnInit() {
    this.loading = true;
    this.http.get<User[]>('/api/users')
      .pipe(takeUntil(this.destroy$))
      .subscribe({
        next: (data) => { this.users = data; this.loading = false; },
        error: (err) => { this.error = err.message; this.loading = false; },
      });
  }

  ngOnDestroy() {
    this.destroy$.next();
    this.destroy$.complete();
  }
}

// AFTER: httpResource handles lifecycle automatically
@Component({ /* ... */ })
export class AfterComponent {
  users = httpResource<User[]>(() => '/api/users');
  // No ngOnInit, no Subject, no manual unsubscribe
  // Template uses users.value(), users.isLoading(), users.error()
}

이 마이그레이션은 라이프사이클 관리 보일러플레이트를 제거합니다. 컴포넌트가 파괴될 때 구독 정리가 자동으로 수행됩니다. 로딩과 에러 상태가 리소스에 내장되어 있으므로 별도의 불리언 플래그가 필요하지 않습니다.

이미 독립형 컴포넌트를 사용 중인 애플리케이션에서는 마이그레이션이 간단합니다. HttpClient 주입과 구독 로직을 단일 httpResource 선언으로 교체하면 됩니다.

연습을 시작하세요!

면접 시뮬레이터와 기술 테스트로 지식을 테스트하세요.

결론

  • httpResource는 수동 HttpClient 구독을 로딩, 에러, 취소를 자동으로 처리하는 단일 리액티브 선언으로 대체합니다
  • Promise 기반 API에는 resource()를, RxJS 연산자가 필요한 Observable 파이프라인에는 rxResource()를, 인터셉터를 지원하는 표준 HTTP 호출에는 httpResource()를 사용합니다
  • Angular 20은 rxResource에서 requestparams로, loaderstream으로 이름을 변경했습니다. 기존 코드를 그에 맞게 업데이트해야 합니다
  • 상태 값은 이제 Angular 19의 숫자형 enum을 대체하는 문자열 리터럴('idle', 'loading', 'resolved', 'error', 'reloading', 'local')입니다
  • httpResourceparse 옵션은 TypeScript 타입도 함께 결정하는 런타임 스키마 유효성 검증을 위해 Zod 또는 Valibot을 통합합니다
  • Angular 20에서 에러 상태의 리소스에서 value()를 호출하면 예외가 발생합니다. 반드시 hasValue()로 보호하거나 status()를 먼저 확인해야 합니다
  • 모든 리소스 변형은 의존성이 변경될 때 진행 중인 요청을 자동으로 취소하여 수동 취소 로직 없이 경쟁 조건을 방지합니다

태그

#angular
#angular-20
#resource-api
#httpResource
#signals

공유

관련 기사