Angular 20 Resource API와 httpResource 완벽 가이드: 시그널 기반 데이터 페칭과 기술 면접 질문
Angular 20의 Resource API를 체계적으로 분석합니다. resource(), rxResource(), httpResource()를 활용한 시그널 기반 리액티브 데이터 페칭, Zod 런타임 유효성 검증, ResourceStatus 상태 관리, 그리고 2026년 기술 면접 핵심 질문을 다룹니다.

Angular 20에서 Resource API가 안정화 단계에 진입했습니다. Angular 19에서 개발자 프리뷰로 도입된 이 API 계열 — resource(), rxResource(), httpResource() — 은 버전 20에서 정식 안정 API가 되었으며, 시그널 시스템과 긴밀하게 통합된 선언적 데이터 페칭 모델을 제공합니다. 기존에 ngOnInit에서 HttpClient를 구독하고, Subject로 해제를 관리하며, 로딩과 에러 상태를 수동으로 추적하던 패턴은 이제 단일 선언으로 대체될 수 있습니다. Resource API는 비동기 데이터의 로딩, 추적, 무효화를 프레임워크 레벨에서 처리하여, 컴포넌트 코드의 복잡성을 획기적으로 줄여줍니다.
이 글에서는 Resource API의 세 가지 변형을 상세히 분석하고, 프로덕션 수준의 코드 예제를 통해 구현 패턴을 설명합니다. Zod를 활용한 런타임 유효성 검증, ResourceStatus를 통한 세밀한 상태 관리, 기존 구독 패턴에서의 마이그레이션 전략, 그리고 2026년 Angular 기술 면접에서 빈출되는 질문과 답변을 포괄적으로 다룹니다.
Angular 20에서 Resource API는 개발자 프리뷰를 벗어나 안정 API로 전환되었습니다. 주요 변경 사항은 다음과 같습니다. request 매개변수가 params로 이름이 변경되었고, rxResource()의 loader는 stream으로 변경되었습니다. 또한 ResourceStatus 타입이 숫자형 enum 대신 문자열 리터럴('loading', 'resolved', 'error' 등)을 사용합니다. httpResource는 parse 옵션을 통한 네이티브 유효성 검증을 지원합니다. Angular 19에서 마이그레이션하는 경우 구문 변경이 필요하지만, 핵심 동작 원리는 동일합니다.
resource(): Promise 기반 기본 프리미티브
resource()는 Resource API의 기본 프리미티브로, Promise 기반의 비동기 데이터 로딩을 시그널 시스템과 통합합니다. params 함수가 리액티브 의존성을 정의하고, loader 함수가 해당 매개변수를 받아 Promise를 반환합니다. params가 반환하는 값이 변경될 때마다 프레임워크가 이전 요청을 AbortSignal로 자동 취소하고 새로운 요청을 시작합니다.
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
}
}이 패턴의 핵심은 리액티브 의존성과 데이터 로딩 로직의 분리에 있습니다. params 함수는 프레임워크의 리액티브 컨텍스트 내에서 실행되므로, 내부에서 읽히는 모든 시그널이 자동으로 추적됩니다. userId가 변경되면 Angular는 진행 중인 요청을 abortSignal을 통해 취소하고 새로운 fetch를 시작합니다. 컴포넌트에 별도의 클린업 코드를 작성할 필요가 없습니다.
resource()는 HTTP가 아닌 비동기 소스(예: IndexedDB, WebSocket, Web Worker)와의 통합에도 적합합니다. loader가 Promise를 반환하기만 하면 어떤 데이터 소스든 사용할 수 있습니다. 시그널 시스템의 기초에 대한 자세한 내용은 Angular Signals 가이드에서 확인할 수 있습니다.
httpResource(): 보일러플레이트 없는 데이터 페칭
httpResource()는 HttpClient 위에 구축된 고수준 추상화로, 별도의 loader 함수 없이 URL과 파라미터만으로 리액티브 데이터 페칭을 구현합니다. Angular의 인터셉터, 헤더 설정, 직렬화 기능이 자동으로 적용되므로 대부분의 표준 데이터 페칭 시나리오에서 권장되는 선택입니다.
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
}
}사용자가 filterByCategory()를 호출하면 category 시그널이 업데이트됩니다. httpResource에 전달된 설정 함수가 this.category()를 읽고 있으므로, 프레임워크는 변경을 감지하여 진행 중인 HTTP 요청을 자동으로 취소하고 새로운 파라미터로 요청을 시작합니다. 이 동작은 RxJS의 switchMap과 동일한 의미를 가지지만, 연산자 체인의 구문적 복잡성이 없습니다.
httpResource는 읽기 전용 작업(GET)만 지원한다는 점에 유의해야 합니다. POST, PUT, DELETE와 같은 쓰기 작업에는 HttpClient를 직접 사용하거나 서버 액션을 활용해야 합니다. .set() 또는 .update() 호출은 시그널의 로컬 값만 변경할 뿐, 서버로 요청을 보내지 않습니다.
Zod와 parse 옵션을 활용한 런타임 유효성 검증
프론트엔드 데이터 페칭에서 반복적으로 발생하는 문제 중 하나는 서버 응답의 실제 구조가 기대와 다른 경우입니다. Angular 20은 httpResource의 parse 옵션을 통해 이 문제를 해결합니다. Zod와 통합하면 모든 응답이 템플릿에 노출되기 전에 자동으로 유효성 검증을 거칩니다. 검증에 실패하면 리소스가 error 상태로 전환되어 잘못된 데이터의 렌더링을 방지합니다.
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가 별도 팀에 의해 개발되고 응답 구조가 사전 조율 없이 변경될 수 있는 엔터프라이즈 환경에서 특히 유용합니다. Zod 검증이 실패하면 error()에 ZodError 객체가 포함되어 어떤 필드가 기대와 다른지 정확히 파악할 수 있습니다.
rxResource(): RxJS와의 네이티브 통합
디바운스가 적용된 검색 필드처럼 RxJS 연산자가 필요한 시나리오에서는 rxResource()가 적합합니다. stream 함수(Angular 19에서 loader로 불렸던 것)가 리액티브 매개변수를 받아 Observable을 반환하며, 프레임워크가 구독과 해제를 내부적으로 관리합니다.
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 },
}),
});
}rxResource와 resource의 근본적인 차이는 데이터 로딩 함수의 반환 타입에 있습니다. Promise 대신 Observable을 반환합니다. 프레임워크가 내부적으로 구독을 관리하고, 파라미터가 변경되면 기존 구독을 자동으로 해제한 후 새로운 구독을 생성합니다. 이는 switchMap과 동일한 동작이지만, 프레임워크 레벨에서 처리됩니다. 이미 RxJS와 HttpClient를 광범위하게 사용하는 코드베이스에서는 rxResource가 가장 자연스러운 마이그레이션 경로가 됩니다.
Angular 면접 준비가 되셨나요?
인터랙티브 시뮬레이터, flashcards, 기술 테스트로 연습하세요.
ResourceStatus: 로딩 상태의 세밀한 관리
모든 리소스 인스턴스는 현재 로딩 상태를 문자열 리터럴로 반환하는 status() 시그널을 제공합니다. 이전 버전의 숫자형 enum 대신 문자열 리터럴을 사용하여, 코드의 가독성과 템플릿의 @switch 구문과의 호환성이 향상되었습니다.
| Status | 의미 |
|---|---|
| 'idle' | params가 undefined를 반환 — 요청 미발생 |
| 'loading' | 최초 요청 진행 중 |
| 'reloading' | 이전 성공 후 재요청 진행 중 |
| 'resolved' | 데이터가 value()에서 사용 가능 |
| 'error' | 요청 실패 — error()에 에러 포함 |
| 'local' | .set() 또는 .update()로 로컬 설정된 값 |
'loading'과 'reloading'의 구분은 차별화된 UX 패턴을 가능하게 합니다. 최초 로딩에는 전체 화면 스피너를, 재로딩에는 기존 데이터를 유지하면서 상단에 미세한 프로그레스 바를 표시하는 방식입니다.
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();
},
});
}'idle' 상태는 특별한 주의가 필요합니다. params 함수가 undefined를 반환하면 리소스는 어떤 요청도 시작하지 않습니다. 이 패턴은 조건부 로딩을 구현할 때 유용합니다. 예를 들어, 사용자가 필터를 선택하거나 특정 액션을 수행해야만 데이터를 가져오는 시나리오에서 활용할 수 있습니다. 'idle' 상태는 'loading'과 명확하게 구분됩니다. 전자는 요청이 아직 시작되지 않은 상태이고, 후자는 요청이 진행 중인 상태입니다.
value() 메서드는 리소스가 'error' 상태에 있을 때 호출하면 예외를 발생시킵니다. 안전하게 값에 접근하려면 템플릿에서 hasValue()를 조건부 가드로 사용하거나, value.asReadonly()를 사용하여 데이터가 없을 때 undefined를 반환받아야 합니다. 이러한 설계는 에러 상태의 명시적 처리를 강제하여, 유효하지 않은 데이터의 우발적 렌더링을 방지합니다.
ngOnInit + subscribe 패턴에서의 마이그레이션
Resource API는 기존의 ngOnInit + HttpClient.subscribe() + takeUntil 패턴을 대폭 단순화합니다. 아래 비교를 통해 보일러플레이트 코드의 감소와 라이프사이클 관리의 자동화를 확인할 수 있습니다.
// 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()
}"Before" 컴포넌트는 요청을 시작하기 위해 OnInit을 구현하고, 취소를 위한 Subject를 생성하며, 데이터 / 로딩 / 에러에 대한 개별 상태 변수를 관리하고, 메모리 누수를 방지하기 위해 OnDestroy를 구현해야 합니다. "After" 컴포넌트는 이 모든 로직을 단 한 줄의 선언으로 응축합니다. 프레임워크가 요청 시작, 상태 추적, 취소, 컴포넌트 파괴 시 클린업을 내부적으로 처리합니다.
이 마이그레이션 패턴은 ngOnInit + subscribe + takeUntil 패턴을 따르는 수백 개의 컴포넌트가 존재하는 대규모 코드베이스에서 특히 높은 효과를 발휘합니다. 마이그레이션은 점진적으로 수행할 수 있으며, 각 컴포넌트를 독립적으로 변환해도 나머지 애플리케이션에 영향을 주지 않습니다.
Angular 20 Resource API 기술 면접 질문
다음은 2026년 Angular 기술 면접에서 자주 출제되는 Resource API 관련 질문입니다. 각 답변은 면접에서 실무 수준의 이해를 입증하는 데 필요한 깊이를 제공합니다.
Q1: resource(), rxResource(), httpResource()의 차이점은 무엇입니까?
resource()는 loader 함수가 Promise를 반환하는 기본 프리미티브입니다. rxResource()는 RxJS 전용 변형으로 stream 함수가 Observable을 반환합니다. httpResource()는 HttpClient를 내부적으로 사용하는 고수준 추상화로, URL과 파라미터 설정만 필요합니다. 세 가지 변형 모두 params 함수를 통해 리액티브 의존성을 자동 추적하고, 파라미터 변경 시 진행 중인 요청을 취소하며, value(), status(), error(), isLoading() 등의 시그널을 통해 상태를 노출합니다. 선택 기준은 다음과 같습니다: 표준 HTTP GET에는 httpResource, HTTP 외 데이터 소스에는 resource, RxJS 연산자가 필요한 경우에는 rxResource를 사용합니다.
Q2: Resource API에서 자동 요청 취소는 어떻게 동작합니까?
params()가 반환하는 값이 변경되면 프레임워크가 진행 중인 요청을 자동으로 취소합니다. resource()의 경우, loader 함수에 전달된 AbortSignal이 기저의 fetch()에 중단을 통지합니다. rxResource()의 경우, 현재 Observable의 구독이 해제된 후 새로운 구독이 생성됩니다. 이는 switchMap과 동일한 동작입니다. httpResource()의 경우, HttpClient가 취소를 내부적으로 처리합니다. 이 메커니즘은 느린 응답이 더 최근의 응답을 덮어쓰는 경쟁 조건(stale response override)을 방지합니다.
Q3: params() 함수가 undefined를 반환하면 어떤 동작이 발생합니까?
params()가 undefined를 반환하면 리소스는 'idle' 상태로 진입하고 어떠한 HTTP 요청도 시작하지 않습니다. 이는 조건부 로딩 패턴입니다. 예를 들어, 상세 보기 컴포넌트에서 아무 항목도 선택되지 않은 상태에서 undefined를 반환하고, 사용자가 항목을 선택했을 때만 로딩을 시작할 수 있습니다. 'idle' 상태는 'loading'과 구별됩니다. 전자는 요청이 대기 중이 아니며 예정되지도 않은 상태이고, 후자는 요청이 활발히 진행 중인 상태입니다.
Q4: httpResource에서 Zod 유효성 검증은 어떻게 통합됩니까?
httpResource의 parse 옵션에 유효성 검증 함수를 전달하면, HTTP 응답이 시그널의 value()에 노출되기 전에 해당 함수가 실행됩니다. OrderSchema.parse를 parse 값으로 전달하면 모든 응답이 정의된 Zod 스키마에 대해 검증됩니다. 검증 실패 시 리소스는 'error' 상태로 전환되고, error()에 위반 세부 사항이 포함된 ZodError 객체가 담깁니다. 이 패턴은 프론트엔드와 백엔드 사이에 런타임 계약을 수립하여, API 응답 구조의 예기치 않은 변경으로 인한 런타임 오류를 즉시 감지할 수 있게 합니다.
Q5: ResourceStatus에서 'loading'과 'reloading'의 차이는 무엇입니까?
'loading'은 최초 요청이 진행 중이며 리소스에 이전 데이터가 없는 상태를 나타냅니다. 'reloading'은 이전에 성공적으로 데이터를 가져온 후 새로운 요청이 시작된 상태를 나타내며, 이전 값은 value()를 통해 여전히 접근 가능합니다. 이 구분을 통해 차별화된 UX를 구현할 수 있습니다. 최초 로딩에는 스켈레톤 로더 또는 전체 화면 스피너를, 재로딩에는 기존 데이터를 유지하면서 미세한 프로그레스 인디케이터를 표시하여, 빈 화면이 깜빡이는 현상(flash of empty content)을 방지할 수 있습니다.
Q6: Resource API를 사용하면 기존 ngOnInit + subscribe 패턴 대비 어떤 이점이 있습니까?
기존 패턴은 OnInit을 구현하여 요청을 시작하고, Subject와 takeUntil을 사용하여 메모리 누수를 방지하며, 데이터 / 로딩 / 에러에 대한 개별 상태 변수를 수동으로 관리해야 합니다. Resource API는 이 모든 것을 단일 선언으로 대체합니다. 프레임워크가 요청의 시작, 상태 추적, 자동 취소, 컴포넌트 파괴 시 클린업을 내부적으로 처리합니다. 결과적으로 보일러플레이트 코드가 대폭 감소하고, 메모리 누수의 가능성이 원천적으로 제거되며, 상태 관리가 value(), isLoading(), error(), status() 등 표준화된 시그널을 통해 일관성 있게 제공됩니다.
Angular 시그널과 SSR에 관한 심화 면접 질문은 Angular 19 면접 질문 가이드에서 확인할 수 있습니다.
결론
Angular 20의 Resource API는 Angular 애플리케이션이 비동기 데이터를 다루는 방식에 있어 패러다임의 전환을 의미합니다. 핵심 요점을 정리하면 다음과 같습니다.
- **resource()**는 Promise 기반의 비동기 데이터 페칭을 위한 기본 프리미티브로, AbortSignal을 통한 자동 취소와 리액티브 의존성 추적을 제공합니다
- **httpResource()**는 HttpClient 위에 구축된 고수준 추상화로, GET 요청의 보일러플레이트를 제거하고 인터셉터 및 헤더와 네이티브로 통합됩니다
- **rxResource()**는 debounceTime, retry 등 RxJS 연산자가 필요한 시나리오에서 Observable을 반환하는 방식으로 호환성을 유지합니다
- Zod 유효성 검증은
parse옵션을 통해 프론트엔드와 백엔드 간의 런타임 계약을 수립하여, 잘못된 데이터의 렌더링을 방지합니다 - ResourceStatus의 문자열 리터럴 체계(
'idle','loading','reloading','resolved','error','local')는 최초 로딩과 재로딩을 구분하는 세밀한 UX 패턴을 가능하게 합니다 - 마이그레이션 전략은 기존
ngOnInit+subscribe+takeUntil패턴을 단일 선언으로 대체하며, 각 컴포넌트를 독립적으로 점진 전환할 수 있습니다
Angular 기술 면접을 준비하는 개발자에게 Resource API의 세 가지 변형 간 차이, 자동 취소 메커니즘, 스키마 유효성 검증 통합을 설명하는 능력은 Angular 20의 리액티브 아키텍처에 대한 성숙한 이해를 입증하는 핵심 지표가 됩니다.
연습을 시작하세요!
면접 시뮬레이터와 기술 테스트로 지식을 테스트하세요.
태그
공유
관련 기사

Angular 19 면접 질문: Signals, SSR과 반드시 알아야 할 개념
가장 자주 나오는 Angular 19 면접 질문: Signals, 점진적 하이드레이션, zoneless 변경 감지, 새로운 리액티브 API. 코드 예제와 기대되는 답변까지 함께 정리합니다.

Angular 19 Zoneless: Zone.js 없이 구현하는 성능과 변경 감지
Angular의 zoneless 변경 감지는 Zone.js를 완전히 제거하여 더 작은 번들 크기, 빠른 렌더링, 그리고 시그널을 통한 명시적 반응성을 제공합니다. 이 심층 가이드는 Angular 19의 provideExperimentalZonelessChangeDetection부터 Angular 20+ 안정화 API까지 Zone.js에서 zoneless Angular로의 마이그레이션 경로를 다룹니다.

Angular 면접 질문 TOP 25: 성공을 위한 완벽 가이드
2026년 가장 많이 묻는 Angular 면접 질문 25선. 상세한 답변과 코드 예시, Angular 개발자 포지션을 잡기 위한 팁을 제공합니다.