Angular HttpClient와 인터셉터 2026년 가이드: 요청 처리와 면접 질문
Angular HttpClient 함수형 인터셉터 완벽 가이드. 인증, 캐싱, 에러 처리 패턴과 Angular 20 면접에서 자주 나오는 질문을 코드 예제와 함께 설명합니다.

Angular HttpClient 인터셉터는 HTTP 요청 처리 방식을 근본적으로 변화시킵니다. 인증 헤더 추가부터 재시도 로직 구현까지 다양한 용도로 활용할 수 있습니다. Angular 20에서는 기존의 클래스 기반 패턴 대신, 더 예측 가능하고 트리 쉐이킹에 적합한 함수형 인터셉터가 표준 접근 방식으로 권장됩니다.
Angular 20에서는 클래스 기반 인터셉터보다 withInterceptors()를 사용한 함수형 인터셉터를 권장합니다. 함수형 인터셉터는 실행 순서가 더 예측 가능하며 스탠드얼론 컴포넌트와의 통합이 우수합니다.
provideHttpClient를 통한 HttpClient 설정
인터셉터를 작성하기 전에 애플리케이션 부트스트랩에서 HttpClient를 구성해야 합니다. Angular의 스탠드얼론 아키텍처에서는 기존의 HttpClientModule 대신 provideHttpClient()를 사용합니다.
설정은 애플리케이션 구성에서 진행합니다:
import { bootstrapApplication } from '@angular/platform-browser';
import { provideHttpClient, withInterceptors } from '@angular/common/http';
import { AppComponent } from './app/app.component';
import { authInterceptor } from './interceptors/auth.interceptor';
import { loggingInterceptor } from './interceptors/logging.interceptor';
bootstrapApplication(AppComponent, {
providers: [
provideHttpClient(
// Interceptors execute in array order
withInterceptors([authInterceptor, loggingInterceptor])
),
],
});인터셉터는 배열에 지정된 순서대로 실행됩니다. 첫 번째 인터셉터가 발신 요청을 먼저 처리하고, 마지막 인터셉터가 응답을 먼저 수신합니다.
함수형 인터셉터의 구조
함수형 인터셉터는 요청과 next 함수를 받습니다. next(req)를 호출하면 요청이 체인의 다음 인터셉터로 전달되거나, 마지막 인터셉터인 경우 백엔드로 전송됩니다.
기본 구조는 다음과 같습니다:
import { HttpInterceptorFn, HttpRequest, HttpHandlerFn, HttpEvent } from '@angular/common/http';
import { Observable, tap } from 'rxjs';
export const loggingInterceptor: HttpInterceptorFn = (
req: HttpRequest<unknown>,
next: HttpHandlerFn
): Observable<HttpEvent<unknown>> => {
const startTime = performance.now();
console.log(`[HTTP] ${req.method} ${req.url}`);
return next(req).pipe(
tap({
next: () => {
const duration = Math.round(performance.now() - startTime);
console.log(`[HTTP] ${req.method} ${req.url} completed in ${duration}ms`);
},
error: (err) => {
console.error(`[HTTP] ${req.method} ${req.url} failed:`, err.message);
},
})
);
};이 인터셉터는 요청 메서드와 URL을 로그에 기록하고 응답 시간을 측정합니다. tap 연산자는 응답을 수정하지 않고 관찰합니다.
토큰 주입을 포함한 인증 인터셉터
대부분의 애플리케이션은 발신 요청에 인증 토큰을 첨부해야 합니다. HttpRequest 객체는 불변이므로, 인터셉터는 Authorization 헤더가 포함된 요청의 복제본을 생성합니다.
이 패턴은 서비스에서 토큰을 가져와 첨부합니다:
import { HttpInterceptorFn } from '@angular/common/http';
import { inject } from '@angular/core';
import { AuthService } from '../services/auth.service';
export const authInterceptor: HttpInterceptorFn = (req, next) => {
// Inject services using Angular's inject() function
const authService = inject(AuthService);
const token = authService.getAccessToken();
// Skip auth header for public endpoints
if (req.url.includes('/public/') || !token) {
return next(req);
}
// Clone request with Authorization header
const authReq = req.clone({
setHeaders: {
Authorization: `Bearer ${token}`,
},
});
return next(authReq);
};inject() 함수는 인터셉터가 Angular의 주입 컨텍스트 내에서 실행되기 때문에 작동합니다. 이를 통해 클래스 기반 인터셉터의 생성자 주입 보일러플레이트가 불필요해집니다.
에러 처리와 재시도 로직
네트워크 장애는 발생하기 마련입니다. 재시도 인터셉터는 지수 백오프를 사용하여 실패한 요청을 자동으로 재시도하며, 모든 HTTP 호출을 수정하지 않고도 신뢰성을 향상시킵니다.
RxJS 연산자가 재시도 로직을 처리합니다:
import { HttpInterceptorFn, HttpErrorResponse } from '@angular/common/http';
import { retry, timer } from 'rxjs';
export const retryInterceptor: HttpInterceptorFn = (req, next) => {
// Only retry GET requests (idempotent)
if (req.method !== 'GET') {
return next(req);
}
return next(req).pipe(
retry({
count: 3,
delay: (error: HttpErrorResponse, retryCount: number) => {
// Don't retry client errors (4xx)
if (error.status >= 400 && error.status < 500) {
throw error;
}
// Exponential backoff: 1s, 2s, 4s
const delayMs = Math.pow(2, retryCount - 1) * 1000;
console.log(`Retry #${retryCount} in ${delayMs}ms`);
return timer(delayMs);
},
})
);
};POST, PUT, DELETE는 멱등하지 않으므로 이 인터셉터는 GET 요청만 재시도합니다. 클라이언트 에러(4xx)는 서버가 요청을 명시적으로 거부했으므로 재시도를 건너뜁니다.
Angular 면접 준비가 되셨나요?
인터랙티브 시뮬레이터, flashcards, 기술 테스트로 연습하세요.
HttpContext를 사용한 응답 캐싱
HttpClient는 인터셉터와 호출 코드 간에 메타데이터를 전달하기 위한 HttpContext를 제공합니다. 캐싱 인터셉터는 컨텍스트 토큰을 읽어 캐시 동작을 결정할 수 있습니다.
캐시 토큰과 인터셉터는 함께 작동합니다:
import { HttpInterceptorFn, HttpContextToken, HttpResponse } from '@angular/common/http';
import { of, tap } from 'rxjs';
// Context token to control caching per-request
export const CACHE_REQUEST = new HttpContextToken<boolean>(() => false);
const cache = new Map<string, HttpResponse<unknown>>();
export const cacheInterceptor: HttpInterceptorFn = (req, next) => {
// Only cache if explicitly requested via context
if (!req.context.get(CACHE_REQUEST) || req.method !== 'GET') {
return next(req);
}
const cacheKey = req.urlWithParams;
const cachedResponse = cache.get(cacheKey);
if (cachedResponse) {
console.log(`[Cache] HIT: ${cacheKey}`);
return of(cachedResponse.clone());
}
return next(req).pipe(
tap((event) => {
if (event instanceof HttpResponse) {
console.log(`[Cache] STORE: ${cacheKey}`);
cache.set(cacheKey, event.clone());
}
})
);
};호출 코드는 컨텍스트 토큰으로 캐싱을 선택합니다:
import { HttpClient, HttpContext } from '@angular/common/http';
import { CACHE_REQUEST } from '../interceptors/cache.interceptor';
export class UserService {
constructor(private http: HttpClient) {}
getUser(id: string) {
return this.http.get(`/api/users/${id}`, {
context: new HttpContext().set(CACHE_REQUEST, true),
});
}
}이 패턴을 통해 캐싱 동작은 전역 구성에 숨겨지지 않고 호출 지점에서 명시적으로 표현됩니다.
401 처리를 통한 토큰 갱신
액세스 토큰이 만료되면 서버는 401 응답을 반환합니다. 인터셉터는 이를 캐치하고, 토큰을 갱신하고, 원래 요청을 투명하게 재시도할 수 있습니다.
갱신 로직은 여러 동시 요청을 조정합니다:
import { HttpInterceptorFn, HttpErrorResponse } from '@angular/common/http';
import { inject } from '@angular/core';
import { catchError, switchMap, throwError, BehaviorSubject, filter, take } from 'rxjs';
import { AuthService } from '../services/auth.service';
let isRefreshing = false;
const refreshTokenSubject = new BehaviorSubject<string | null>(null);
export const tokenRefreshInterceptor: HttpInterceptorFn = (req, next) => {
const authService = inject(AuthService);
return next(req).pipe(
catchError((error: HttpErrorResponse) => {
if (error.status !== 401 || req.url.includes('/auth/refresh')) {
return throwError(() => error);
}
if (isRefreshing) {
// Wait for the refresh to complete
return refreshTokenSubject.pipe(
filter((token) => token !== null),
take(1),
switchMap((token) => {
const retryReq = req.clone({
setHeaders: { Authorization: `Bearer ${token}` },
});
return next(retryReq);
})
);
}
isRefreshing = true;
refreshTokenSubject.next(null);
return authService.refreshToken().pipe(
switchMap((response) => {
isRefreshing = false;
refreshTokenSubject.next(response.accessToken);
const retryReq = req.clone({
setHeaders: { Authorization: `Bearer ${response.accessToken}` },
});
return next(retryReq);
}),
catchError((refreshError) => {
isRefreshing = false;
authService.logout();
return throwError(() => refreshError);
})
);
})
);
};BehaviorSubject는 여러 요청이 동시에 실패할 때 다중 갱신 호출을 방지합니다. 토큰이 갱신되면 대기 중인 모든 요청이 새 토큰으로 재시도됩니다.
면접 질문: HttpClient와 인터셉터
기술 면접에서는 HTTP 처리 패턴에 대한 이해를 자주 묻습니다. 다음 질문들은 Angular 포지션에서 자주 등장합니다.
Q: 왜 함수형 인터셉터가 클래스 기반 인터셉터보다 선호되나요?
함수형 인터셉터는 더 예측 가능한 실행 순서를 가집니다. Angular 공식 문서에 따르면, withInterceptors()를 사용한 함수형 인터셉터는 엄격한 배열 순서를 유지하지만, DI 기반 인터셉터는 복잡한 모듈 계층에서 순서 문제가 발생할 수 있습니다. 또한 함수형 인터셉터는 트리 쉐이킹이 더 효과적으로 작동하여 번들 크기가 작아집니다.
Q: 특정 요청에서 인터셉터가 실행되지 않도록 하려면 어떻게 해야 하나요?
두 가지 접근 방식이 있습니다. 첫 번째는 인터셉터 내에서 URL 패턴을 확인하고 제외할 경로에 대해 수정 없이 next(req)를 호출하는 것입니다. 두 번째는 호출 코드가 인터셉터 동작을 선택 해제하기 위해 설정하는 HttpContext 토큰을 사용하는 것입니다. 컨텍스트 접근 방식은 제외 결정을 호출 지점에 둡니다.
Q: 인터셉터에서 next()를 호출하는 것을 잊으면 어떻게 되나요?
요청이 서버에 도달하지 않습니다. next()가 반환하는 Observable은 인터셉터 체인의 나머지와 실제 HTTP 호출을 나타냅니다. 이를 호출하지 않으면 하위 인터셉터와 백엔드는 요청을 볼 수 없습니다. 이것은 캐시된 응답을 반환하는 캐싱 인터셉터에서는 의도적인 동작입니다.
Q: 토큰 갱신 중 동시 요청을 어떻게 처리하나요?
BehaviorSubject를 사용하여 조정합니다. 첫 번째 401이 갱신을 트리거하면 플래그를 설정하고 subject에 null을 발행합니다. 이후 401들은 새 토큰이 도착할 때까지 filter를 사용하여 subject를 기다립니다. 이를 통해 다중 갱신 호출을 방지하고 대기 중인 모든 요청이 동일한 새 토큰으로 재시도되도록 보장합니다.
Q: 인터셉터가 응답 본문을 수정할 수 있나요?
네. next()가 반환한 Observable에서 map()을 사용하여 HttpResponse 객체를 변환합니다. 응답은 불변이므로 수정된 본문으로 복제합니다. 이 패턴은 계산 필드 추가, API 엔벨로프 언래핑, 데이터 구조 정규화에 사용할 수 있습니다.
Angular 면접 준비에 대한 자세한 내용은 HttpClient 면접 질문 모듈을 참조하십시오.
함수형 인터셉터 테스트
인터셉터는 모의 HTTP 핸들러로 테스트해야 합니다. Angular의 HttpTestingController는 provideHttpClient()를 통해 등록된 인터셉터와 함께 작동합니다.
테스트 설정은 다음과 같습니다:
import { TestBed } from '@angular/core/testing';
import { HttpClient, provideHttpClient, withInterceptors } from '@angular/common/http';
import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing';
import { authInterceptor } from './auth.interceptor';
import { AuthService } from '../services/auth.service';
describe('authInterceptor', () => {
let httpClient: HttpClient;
let httpTesting: HttpTestingController;
let authServiceSpy: jasmine.SpyObj<AuthService>;
beforeEach(() => {
authServiceSpy = jasmine.createSpyObj('AuthService', ['getAccessToken']);
TestBed.configureTestingModule({
providers: [
provideHttpClient(withInterceptors([authInterceptor])),
provideHttpClientTesting(),
{ provide: AuthService, useValue: authServiceSpy },
],
});
httpClient = TestBed.inject(HttpClient);
httpTesting = TestBed.inject(HttpTestingController);
});
it('should add Authorization header when token exists', () => {
authServiceSpy.getAccessToken.and.returnValue('test-token');
httpClient.get('/api/data').subscribe();
const req = httpTesting.expectOne('/api/data');
expect(req.request.headers.get('Authorization')).toBe('Bearer test-token');
req.flush({ data: 'test' });
});
it('should skip Authorization for public endpoints', () => {
authServiceSpy.getAccessToken.and.returnValue('test-token');
httpClient.get('/public/health').subscribe();
const req = httpTesting.expectOne('/public/health');
expect(req.request.headers.has('Authorization')).toBeFalse();
req.flush({ status: 'ok' });
});
});이 테스트는 실제 네트워크 요청 없이 헤더 주입과 공개 엔드포인트 예외를 모두 검증합니다.
프로덕션 패턴: 인터셉터 조합
실제 애플리케이션은 여러 인터셉터를 스택합니다. 순서가 중요합니다: 인증은 로깅 전에, 캐싱은 재시도 로직 전에 실행되어야 합니다.
프로덕션 구성은 다음과 같습니다:
import { bootstrapApplication } from '@angular/platform-browser';
import { provideHttpClient, withInterceptors } from '@angular/common/http';
import { AppComponent } from './app/app.component';
import { authInterceptor } from './interceptors/auth.interceptor';
import { tokenRefreshInterceptor } from './interceptors/token-refresh.interceptor';
import { cacheInterceptor } from './interceptors/cache.interceptor';
import { retryInterceptor } from './interceptors/retry.interceptor';
import { loggingInterceptor } from './interceptors/logging.interceptor';
bootstrapApplication(AppComponent, {
providers: [
provideHttpClient(
withInterceptors([
// Order: auth -> refresh -> cache -> retry -> logging
authInterceptor,
tokenRefreshInterceptor,
cacheInterceptor,
retryInterceptor,
loggingInterceptor,
])
),
],
});이 순서를 통해 토큰은 갱신 처리 전에 첨부되고, 캐시 확인은 재시도 전에 이루어지며, 로깅은 최종 타이밍을 캡처합니다.
광범위한 Angular 패턴에 대해서는 인터셉터에서 사용되는 연산자를 다루는 RxJS 기초 모듈을 참조하십시오.
연습을 시작하세요!
면접 시뮬레이터와 기술 테스트로 지식을 테스트하세요.
Angular HttpClient 인터셉터 핵심 요약
withInterceptors()를 사용한 함수형 인터셉터는 Angular 20 표준이며, 클래스 기반 대안보다 예측 가능한 순서와 더 나은 트리 쉐이킹을 제공합니다- 생성자 보일러플레이트 없이 서비스에 접근하려면 인터셉터 내에서
inject()를 사용합니다 HttpRequest객체는 불변이므로 헤더를 수정하려면req.clone()으로 요청을 복제합니다HttpContext토큰을 통해 요청별 동작이 전역 구성에 숨겨지지 않고 호출 지점에서 명시적으로 표현됩니다- 토큰 갱신 인터셉터는 다중 갱신 호출을 방지하기 위해
BehaviorSubject로 동시 요청을 조정해야 합니다 - 인터셉터 순서가 중요합니다: 캐싱 전에 인증, 재시도 전에 캐싱, 마지막에 로깅
Angular 코드의 버그를 찾을 수 있나요
실제 코드 한 조각, 숨은 버그 하나, 하루 한 번. 계정 없이 바로 도전할 수 있습니다.

작성자
Anthony Fillion-MailletSharpSkill 창업자
10년 이상 풀스택 개발을 해왔습니다. SharpSkill을 운영하며 이곳에 게시되는 모든 내용에 책임을 집니다.
2026년 8월 22일 업데이트
공유
관련 기사

Angular Signals와 Computed로 구현하는 세밀한 반응성: 2026년 기술 면접 대비
Angular 20 이후 표준이 된 Signals와 Computed, linkedSignal의 작동 원리를 설명합니다. 기술 면접에서 자주 묻는 반응성 설계 패턴과 RxJS와의 사용 구분을 상세히 다룹니다.

2026년 Angular 의존성 주입 완벽 가이드: 프로바이더, 토큰, 면접 대비
Angular의 고급 의존성 주입(DI) 시스템을 심층 분석합니다. InjectionToken, 계층형 인젝터, 프로바이더 전략, 그리고 기술 면접에서 자주 출제되는 질문과 모범 답변을 상세히 다룹니다.

2026년 Angular 제어 흐름 구문 완벽 가이드: @if, @for, @switch와 면접 질문
Angular 17에서 도입된 새로운 제어 흐름 구문(@if, @for, @switch)에 대한 상세 가이드. 기존 구조 디렉티브에서의 마이그레이션 방법, 성능 최적화 전략, 기술 면접 빈출 질문과 답변을 다룹니다.