앵귤러 기초 강좌 #7 HttpClient로 API 호출
지난 시간에는 Router로 다중 페이지를 구성하는 방법을 배웠습니다. 이제 마지막으로 필요한 도구가 하나 남았습니다. 화면을 띄우고 라우팅까지 했으면, 결국 백엔드 서버에서 데이터를 받아와야 진짜 앱이 됩니다. 이번 시간에는 앵귤러의 표준 통신 도구 HttpClient를 다뤄보겠습니다.
HttpClient 셋업 #
앵귤러는 fetch나 axios 대신 자체 HTTP 클라이언트인 HttpClient를 권장합니다. 인터셉터, 테스트 유틸리티, RxJS 통합이 한 세트로 들어 있고, 앵귤러 SSR(서버 사이드 렌더링)에서도 자연스럽게 동작합니다.
먼저 앱 설정에 한 줄을 추가해야 합니다. Standalone 패턴에서는 app.config.ts에 provideHttpClient()를 등록합니다.
import { ApplicationConfig } from '@angular/core';
import { provideRouter } from '@angular/router';
import { provideHttpClient } from '@angular/common/http';
import { routes } from './app.routes';
export const appConfig: ApplicationConfig = {
providers: [
provideRouter(routes),
provideHttpClient(),
],
};이 한 줄로 앱 전체에서 HttpClient를 주입받아 쓸 수 있게 됩니다.
HttpClientModule을 import하는 방식은 NgModule 시절의 패턴입니다. 새 프로젝트에서는 provideHttpClient()를 사용하세요. Standalone 시대의 표준입니다.GET 요청 #
가장 기본적인 GET 요청부터 봅시다. 사용자 목록을 가져오는 서비스를 만들어보겠습니다.
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
export interface User {
id: number;
name: string;
email: string;
}
@Injectable({ providedIn: 'root' })
export class UserService {
private http = inject(HttpClient);
private apiUrl = 'https://jsonplaceholder.typicode.com/users';
getUsers(): Observable<User[]> {
return this.http.get<User[]>(this.apiUrl);
}
}세 가지 포인트가 있습니다.
inject(HttpClient)로 HttpClient를 주입받습니다. 생성자 주입(constructor(private http: HttpClient))도 여전히 동작하지만, 최근 앵귤러 코드는inject()함수형 주입을 선호합니다.get<User[]>(url)처럼 제네릭으로 응답 타입을 지정합니다. 그러면getUsers()의 반환 타입이Observable<User[]>로 추론되어, 타입 안전하게 다룰 수 있습니다.- HttpClient의 메서드는 즉시 요청을 보내지 않고
Observable을 반환합니다. 누군가 구독(subscribe)할 때 비로소 요청이 나갑니다.
Observable 다루기 #
서비스가 돌려준 Observable을 컴포넌트에서 받아 화면에 그려봅시다. 가장 단순한 방법은 subscribe로 직접 결과를 받는 것입니다.
import { Component, OnInit, inject, signal } from '@angular/core';
import { UserService, User } from './user.service';
@Component({
selector: 'app-user-list',
standalone: true,
template: `
<ul>
@for (user of users(); track user.id) {
<li>{{ user.name }} ({{ user.email }})</li>
}
</ul>
`,
})
export class UserListComponent implements OnInit {
private userService = inject(UserService);
users = signal<User[]>([]);
ngOnInit() {
this.userService.getUsers().subscribe(data => {
this.users.set(data);
});
}
}subscribe 콜백 안에서 받은 데이터를 시그널에 담고, 템플릿은 users()를 호출해 자동으로 갱신됩니다.
다만 subscribe를 직접 쓸 때는 메모리 누수에 주의해야 합니다. 컴포넌트가 사라져도 구독이 살아남으면 더 이상 존재하지 않는 컴포넌트의 상태를 갱신하려다 에러나 누수가 발생할 수 있습니다. 이 문제는 다음 시리즈(중급)에서 본격적으로 다루지만, 지금부터 권장할 수 있는 더 나은 패턴이 하나 있습니다.
toSignal로 Observable → Signal 변환
#
앵귤러 16부터 들어온 toSignal은 Observable을 시그널로 자동 변환해줍니다. 구독 해제를 신경 쓰지 않아도 컴포넌트가 사라지면 알아서 정리되고, 템플릿에서는 그냥 시그널처럼 호출하면 됩니다.
import { Component, inject } from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { UserService } from './user.service';
@Component({
selector: 'app-user-list',
standalone: true,
template: `
@if (users(); as list) {
<ul>
@for (user of list; track user.id) {
<li>{{ user.name }} ({{ user.email }})</li>
}
</ul>
} @else {
<p>로딩 중...</p>
}
`,
})
export class UserListComponent {
private userService = inject(UserService);
users = toSignal(this.userService.getUsers());
}subscribe가 사라졌고, OnInit도 필요 없어졌습니다. toSignal이 내부에서 자동으로 구독,해제까지 처리해 줍니다. 초기에는 undefined를 반환하므로 위처럼 @if (users(); as list)로 로딩 상태를 분기할 수 있습니다.
toSignal을 우선 고려하세요. 직접 subscribe하는 패턴보다 누수 위험이 적고, 시그널 기반 템플릿과도 잘 맞습니다.POST/PUT/DELETE 요청 #
읽기만 하는 앱은 거의 없습니다. 데이터를 만들고 수정하고 지우는 메서드도 비슷한 모양으로 작성합니다.
// 생성
createUser(user: Omit<User, 'id'>): Observable<User> {
return this.http.post<User>(this.apiUrl, user);
}
// 전체 수정
updateUser(user: User): Observable<User> {
return this.http.put<User>(`${this.apiUrl}/${user.id}`, user);
}
// 삭제
deleteUser(id: number): Observable<void> {
return this.http.delete<void>(`${this.apiUrl}/${id}`);
}post와 put의 두 번째 인자가 요청 body입니다. JSON 직렬화는 HttpClient가 알아서 해줍니다. 호출하는 쪽은 GET과 똑같이 Observable을 받아 처리합니다.
this.userService
.createUser({ name: '철수', email: 'cheolsu@example.com' })
.subscribe(created => console.log('생성됨:', created));에러 처리 #
네트워크가 항상 잘 동작한다고 가정하면 안 됩니다. 앵귤러는 RxJS의 catchError 연산자로 에러를 가로채는 방식을 권장합니다.
import { catchError, EMPTY } from 'rxjs';
getUsers(): Observable<User[]> {
return this.http.get<User[]>(this.apiUrl).pipe(
catchError(err => {
console.error('사용자 목록을 가져오지 못했습니다:', err);
return EMPTY;
})
);
}pipe는 Observable에 연산자를 줄 세워 적용하는 통로입니다. catchError는 에러가 발생했을 때 원래 흐름을 갈아 끼울 새 Observable을 반환해야 합니다. 위 예제처럼 EMPTY(아무 값도 보내지 않고 즉시 종료)를 반환하면 구독자에게는 그냥 데이터가 안 온 것처럼 보입니다.
기본값을 흘려주고 싶다면 of([])를 쓰면 됩니다.
import { of } from 'rxjs';
return this.http.get<User[]>(this.apiUrl).pipe(
catchError(() => of([])) // 에러 시 빈 배열로 폴백
);헤더와 옵션 #
API에 인증 토큰을 보내거나 query parameter를 붙여야 할 때가 많습니다. HttpClient의 두(또는 세) 번째 인자에 옵션 객체를 넘기면 됩니다.
import { HttpHeaders, HttpParams } from '@angular/common/http';
getPosts(userId: number, token: string): Observable<Post[]> {
const headers = new HttpHeaders({
Authorization: `Bearer ${token}`,
});
const params = new HttpParams().set('userId', userId);
return this.http.get<Post[]>('/api/posts', { headers, params });
}HttpParams를 쓰지 않고 그냥 객체로 넘기는 짧은 형태도 가능합니다.
this.http.get<Post[]>('/api/posts', {
params: { userId, limit: 10 },
});Interceptor 한 줄 소개 #
매 요청마다 토큰을 직접 붙이는 건 번거롭습니다. 앵귤러는 모든 HTTP 요청을 가로채 공통 처리를 끼워 넣는 Interceptor를 제공합니다.
import { HttpInterceptorFn } from '@angular/common/http';
export const authInterceptor: HttpInterceptorFn = (req, next) => {
const token = localStorage.getItem('token');
if (!token) return next(req);
const cloned = req.clone({
setHeaders: { Authorization: `Bearer ${token}` },
});
return next(cloned);
};app.config.ts에 등록하면 끝입니다.
provideHttpClient(withInterceptors([authInterceptor])),요청 로깅, 토큰 갱신, 공통 에러 핸들링, 캐시 — 이 모든 게 인터셉터 한 곳에서 정리됩니다. 자세한 패턴은 중급 시리즈에서 다룰 예정이니, 지금은 “이런 게 있다” 정도만 기억해두시면 됩니다.
마무리 #
이번 글에서는 HttpClient로 백엔드와 통신하는 방법을 정리했습니다.
provideHttpClient()로 셋업하고inject(HttpClient)로 주입받기get<T>/post<T>/put<T>/delete<T>— 제네릭으로 타입 안전하게- 결과는 Observable. 직접
subscribe하거나toSignal로 변환해 시그널로 다루기 catchError+EMPTY/of로 에러 폴백HttpHeaders,HttpParams또는 객체로 헤더와 query params- Interceptor로 공통 처리(인증,로깅,에러)를 한 곳에 모으기
여기까지가 앵귤러 기초 강좌의 마지막 글입니다. 1편에서 앵귤러가 무엇인지부터 시작해, 컴포넌트와 템플릿 문법, 데이터 바인딩, Directive와 Pipe, Service와 의존성 주입, Router, 그리고 오늘의 HttpClient까지 — 작은 앵귤러 앱을 처음부터 끝까지 만드는 데 필요한 핵심 도구는 모두 한 번씩 다뤘습니다. 이제 여러분은 앵귤러로 “데이터를 받아와 화면에 그리고, 사용자의 입력을 처리하고, 여러 페이지를 오가는” 기본기를 갖춘 셈입니다.
다음 단계는 **“앵귤러 중급 강좌”**입니다. 기초에서는 의도적으로 미뤄둔 주제들을 본격적으로 다룰 예정입니다.
- Reactive Forms — 큰 폼,검증,동적 폼을 깔끔하게 다루는 모델 기반 폼
- RxJS 심화 —
switchMap,debounceTime,combineLatest등 자주 쓰는 연산자와 검색,자동완성,재시도 같은 실전 패턴 - Lifecycle와 Change Detection —
OnInit너머의 라이프사이클, OnPush 전략, 시그널과 Zone.js의 관계 - HTTP Interceptor 패턴 — 토큰 갱신, 캐시, 공통 에러 핸들링
- Standalone Routing 심화 — Lazy loading, Route Guards, Resolver, 라우트 데이터
기초가 “도구의 사용법"이었다면, 중급은 “이 도구로 실제 제품을 어떻게 만드느냐"의 영역입니다. 기초 강좌를 끝까지 따라와주신 여러분, 정말 수고 많으셨습니다. 중급 강좌에서 더 깊은 이야기로 다시 만나뵙겠습니다.