문서의 선택한 두 판 사이의 차이를 보여줍니다.
| angular:httpclient [2023/04/19 13:09] – [Making a DELETE request] taekgu | angular:httpclient [2025/04/15 10:05] (현재) – 바깥 편집 127.0.0.1 | ||
|---|---|---|---|
| 줄 1: | 줄 1: | ||
| + | ====== HttpClient ====== | ||
| + | |||
| + | 관련 POST | ||
| + | * [[angular: | ||
| + | |||
| + | 대부분의 프런트 엔드 응용 프로그램은 HTTP 프로토콜을 통해 백엔드 서비스와 통신합니다. 최신 브라우저는 HTTP 요청을하기 위해 **XMLHttpRequest** 인터페이스와 fetch() API의 두 가지 API를 지원합니다. | ||
| + | |||
| + | // | ||
| + | |||
| + | ===== Setup ===== | ||
| + | HttpClient를 사용하려면 먼저 Angular HttpClientModule을 가져와야합니다. 대부분의 앱은 루트 AppModule에서 그렇게합니다. | ||
| + | |||
| + | // | ||
| + | |||
| + | 앱의 모든 곳에서 // | ||
| + | * open the root AppModule | ||
| + | * import the HttpClientModule symbol from @angular/ | ||
| + | import { HttpClientModule } from ' | ||
| + | * add it to the @NgModule.imports array | ||
| + | |||
| + | HttpClientModule을 AppModule로 가져온 다음 HttpClient를 다음 ConfigService 예제와 같이 응용 프로그램 클래스에 삽입 할 수 있습니다. | ||
| + | <code javascript app/ | ||
| + | import { Injectable } from ' | ||
| + | import { HttpClient } from ' | ||
| + | |||
| + | @Injectable() | ||
| + | export class ConfigService { | ||
| + | constructor(private http: HttpClient) { } | ||
| + | } | ||
| + | </ | ||
| + | |||
| + | ===== JSON data 얻기 ===== | ||
| + | subscribe를 통해서 request를 요구한다. | ||
| + | <code javascript> | ||
| + | this.http.get(this.configUrl) | ||
| + | .subscribe((data: | ||
| + | heroesUrl: data[' | ||
| + | textfile: | ||
| + | }); | ||
| + | </ | ||
| + | service method는 Observable의 구성 데이터를 반환하기 때문에 component는 method의 return value을 구독합니다. subscribe callback은 데이터 필드를 구성 요소의 구성 객체로 복사합니다. 구성 객체는 표시를 위해 구성 요소 템플리트에서 데이터 바인딩됩니다. | ||
| + | |||
| + | ==== 왜 service를 쓰는가 ==== | ||
| + | 이 예제는 너무 간단해서 // | ||
| + | |||
| + | 그러나 데이터 액세스는 거의 불가능합니다. 일반적으로 데이터를 사후 처리하고, | ||
| + | |||
| + | 이 구성 요소는 데이터 액세스 세부 사항으로 인해 복잡해집니다. 이 구성 요소는 이해하기 어렵고 테스트하기가 어려워지며 데이터 액세스 논리를 재사용하거나 표준화 할 수 없습니다. | ||
| + | |||
| + | 그렇기 때문에 데이터 액세스를 별도의 서비스에 캡슐화하고 해당 서비스의 구성 요소에서 위임과 같은 단순한 경우에도 데이터 액세스를 데이터 액세스에서 분리하는 것이 가장 좋은 방법입니다. | ||
| + | |||
| + | ==== Type-checking the response ==== | ||
| + | 위의 subscribe 콜백은 데이터 값을 추출하기 위해 대괄호 표기법(bracket notation)을 필요로합니다. | ||
| + | <code javascript> | ||
| + | .subscribe((data: | ||
| + | heroesUrl: data[' | ||
| + | textfile: | ||
| + | }); | ||
| + | </ | ||
| + | TypeScript가 서비스의 데이터 객체에 heroesUrl 속성이 없다는 오류 메시지가 올바르게 표시되기 때문에 data.heroesUrl을 쓸 수 없습니다. | ||
| + | |||
| + | HttpClient.get () 메소드는 JSON 서버 응답을 익명 객체 유형으로 구문 분석했습니다. 그 물체의 모양이 무엇인지 알지 못합니다. | ||
| + | |||
| + | HttpClient에 응답 형식을 알리면 출력을 더 쉽고 분명하게 사용할 수 있습니다. | ||
| + | |||
| + | 먼저, 올바른 모양의 인터페이스를 정의: | ||
| + | <code javascript> | ||
| + | export interface Config { | ||
| + | heroesUrl: string; | ||
| + | textfile: string; | ||
| + | } | ||
| + | </ | ||
| + | 그런 다음 해당 인터페이스를 서비스에서 // | ||
| + | <code javascript> | ||
| + | getConfig() { | ||
| + | // now returns an Observable of Config | ||
| + | return this.http.get< | ||
| + | } | ||
| + | </ | ||
| + | 업데이트 된 구성 요소 메소드의 콜백은 형식화 된 데이터 객체를 수신하므로 더 쉽고 안전합니다: | ||
| + | <code javascript> | ||
| + | config: Config; | ||
| + | |||
| + | showConfig() { | ||
| + | this.configService.getConfig() | ||
| + | // clone the data object, using its known Config shape | ||
| + | .subscribe((data: | ||
| + | } | ||
| + | </ | ||
| + | |||
| + | ==== Reading the full response ==== | ||
| + | 응답 본문은 필요한 모든 데이터를 반환하지 않습니다. 경우에 따라 서버는 특수 헤더 또는 상태 코드를 반환하여 응용 프로그램 워크 플로에서 중요한 특정 조건을 나타냅니다. | ||
| + | |||
| + | observe option으로 전체 응답을 원한다고 HttpClient에게 알려줍니다. | ||
| + | <code javascript> | ||
| + | getConfigResponse(): | ||
| + | return this.http.get< | ||
| + | this.configUrl, | ||
| + | } | ||
| + | </ | ||
| + | |||
| + | 이제 HttpClient.get()은 JSON 데이터가 아닌 입력 된 HttpResponse의 Observable을 반환합니다. | ||
| + | |||
| + | 구성 요소의 showConfigResponse() 메서드는 응답 헤더와 구성을 표시합니다. | ||
| + | <code javascript> | ||
| + | showConfigResponse() { | ||
| + | this.configService.getConfigResponse() | ||
| + | // resp is of type `HttpResponse< | ||
| + | .subscribe(resp => { | ||
| + | // display its headers | ||
| + | const keys = resp.headers.keys(); | ||
| + | this.headers = keys.map(key => | ||
| + | `${key}: ${resp.headers.get(key)}`); | ||
| + | |||
| + | // access the body directly, which is typed as `Config`. | ||
| + | this.config = { ... resp.body }; | ||
| + | }); | ||
| + | } | ||
| + | </ | ||
| + | 보는 것과같이, | ||
| + | ===== Error Handling ===== | ||
| + | 서버에서 요청이 실패하거나 네트워크 연결 상태가 좋지 않아 서버에 도달하지 못하면 어떻게됩니까? | ||
| + | |||
| + | .subscribe()에 두 번째 콜백을 추가하여 구성 요소를 처리 할 수 있습니다. | ||
| + | <code javascript> | ||
| + | showConfig() { | ||
| + | this.configService.getConfig() | ||
| + | .subscribe( | ||
| + | (data: Config) => this.config = { ...data }, // success path | ||
| + | error => this.error = error // error path | ||
| + | ); | ||
| + | } | ||
| + | </ | ||
| + | 데이터 액세스가 실패 할 경우 사용자에게 어떤 종류의 피드백을 제공하는 것이 좋습니다. 그러나 HttpClient가 반환 한 원시 오류 객체를 표시하는 것은 최선의 방법과 거리가 멀습니다. | ||
| + | |||
| + | ==== Getting error details ==== | ||
| + | 오류가 발생했음을 감지하는 것이 한 가지입니다. 그 오류를 해석하고 사용자에게 친숙한 응답을 작성하는 것은 좀 더 복잡합니다. | ||
| + | |||
| + | 두 가지 유형의 오류가 발생할 수 있습니다. 서버 백엔드는 요청을 거부하여 404 또는 500과 같은 상태 코드와 함께 HTTP 응답을 리턴합니다. 이는 오류 응답입니다. | ||
| + | |||
| + | 또는 요청이 성공적으로 완료되지 못하게하는 네트워크 오류 또는 RxJS 운영자에게 던져지는 예외와 같은 클라이언트 측에서 문제가 발생할 수 있습니다. 이러한 오류는 JavaScript ErrorEvent 객체를 생성합니다. | ||
| + | |||
| + | HttpClient는 HttpErrorResponse에서 두 가지 종류의 오류를 모두 캡처하고 실제로 발생한 상황을 파악하기 위해 해당 응답을 검사 할 수 있습니다. | ||
| + | |||
| + | 오류 검사, 해석 및 해결은 서비스가 아닌 구성 요소에서 수행하려는 작업입니다. | ||
| + | |||
| + | 먼저 다음과 같은 오류 처리기를 고안해야합니다. | ||
| + | <code javascript> | ||
| + | private handleError(error: | ||
| + | if (error.error instanceof ErrorEvent) { | ||
| + | // A client-side or network error occurred. Handle it accordingly. | ||
| + | console.error(' | ||
| + | } else { | ||
| + | // The backend returned an unsuccessful response code. | ||
| + | // The response body may contain clues as to what went wrong, | ||
| + | console.error( | ||
| + | `Backend returned code ${error.status}, | ||
| + | `body was: ${error.error}`); | ||
| + | } | ||
| + | // return an observable with a user-facing error message | ||
| + | return throwError( | ||
| + | ' | ||
| + | }; | ||
| + | </ | ||
| + | 이 핸들러는 사용자 친화적 인 오류 메시지와 함께 RxJS // | ||
| + | |||
| + | 이제 HttpClient 메서드에서 반환 한 Observables를 가져 와서 오류 처리기로 전달합니다. | ||
| + | |||
| + | <code javascript> | ||
| + | getConfig() { | ||
| + | return this.http.get< | ||
| + | .pipe( | ||
| + | catchError(this.handleError) | ||
| + | ); | ||
| + | } | ||
| + | </ | ||
| + | ==== retry() ==== | ||
| + | |||
| + | 때때로 오류는 일시적이며 다시 시도하면 자동으로 사라집니다. 예를 들어 네트워크 중단은 모바일 시나리오에서 흔히 발생하며 다시 시도하면 성공적인 결과를 얻을 수 있습니다. | ||
| + | |||
| + | RxJS 라이브러리는 탐구할만한 몇 가지 재시도 연산자를 제공합니다. 가장 간단한 방법은 retry ()이며 실패한 Observable을 지정된 횟수만큼 자동으로 다시 구독합니다. HttpClient 메서드 호출의 결과에 다시 등록하면 HTTP 요청을 다시 발행하는 효과가 있습니다. | ||
| + | |||
| + | 오류 처리기 바로 전에 HttpClient 메서드 결과로 파이프를 연결하십시오. | ||
| + | <code javascript> | ||
| + | getConfig() { | ||
| + | return this.http.get< | ||
| + | .pipe( | ||
| + | retry(3), // retry a failed request up to 3 times | ||
| + | catchError(this.handleError) // then handle the error | ||
| + | ); | ||
| + | } | ||
| + | </ | ||
| + | |||
| + | ===== Observables and operators ===== | ||
| + | 이 가이드의 이전 섹션에서는 catchError 및 retry와 같은 RxJS Observables 및 연산자에 대해 설명했습니다. 아래에서 계속 진행하면 RxJS 아티팩트가 더 많이 발생합니다. | ||
| + | |||
| + | RxJS는 비동기 및 콜백 기반 코드를 기능적, 반응 적 스타일로 작성하기위한 라이브러리입니다. HttpClient를 포함한 많은 Angular API가 RxJS Observables를 생성하고 사용합니다. | ||
| + | |||
| + | RxJS 자체는이 가이드의 범위를 벗어납니다. 웹에서 많은 학습 자료를 찾을 수 있습니다. 최소한의 RxJS 지식 만 있으면 HttpClient를 효과적으로 사용하기 위해 시간이 지남에 따라 RxJS 기술을 키울 수 있습니다. | ||
| + | |||
| + | 이러한 코드 스니펫을 따르고 있다면 해당 스니펫에 나타나는 RxJS 관측 가능 및 연산자 기호를 가져와야합니다. 이러한 ConfigService 가져 오기는 일반적입니다. | ||
| + | |||
| + | <code javascript> | ||
| + | import { Observable, throwError } from ' | ||
| + | import { catchError, retry } from ' | ||
| + | </ | ||
| + | |||
| + | ===== Requesting non-JSON data ===== | ||
| + | 모든 API가 JSON 데이터를 반환하지는 않습니다. 이 다음 예제에서 DownloaderService 메서드는 서버에서 텍스트 파일을 읽고 파일 내용을 기록한 다음 Observable< | ||
| + | <code javascript> | ||
| + | getTextFile(filename: | ||
| + | // The Observable returned by get() is of type Observable< | ||
| + | // because a text response was specified. | ||
| + | // There' | ||
| + | return this.http.get(filename, | ||
| + | .pipe( | ||
| + | tap( // Log the result or error | ||
| + | data => this.log(filename, | ||
| + | error => this.logError(filename, | ||
| + | ) | ||
| + | ); | ||
| + | } | ||
| + | </ | ||
| + | HttpClient.get()은 responseType 옵션 때문에 기본 JSON 대신 문자열을 반환합니다. | ||
| + | |||
| + | RxJS 탭 연산자 ( " | ||
| + | |||
| + | DownloaderComponent의 download() 메소드는 서비스 메소드에 등록하여 요청을 시작합니다. | ||
| + | <code javascript> | ||
| + | download() { | ||
| + | this.downloaderService.getTextFile(' | ||
| + | .subscribe(results => this.contents = results); | ||
| + | } | ||
| + | </ | ||
| + | |||
| + | ===== Sending data to the server ===== | ||
| + | HttpClient는 서버에서 데이터를 가져 오는 것 외에도 변경 요청, 즉 PUT, POST 및 DELETE와 같은 다른 HTTP 메서드로 서버에 데이터를 보내는 요청을 지원합니다. | ||
| + | |||
| + | 이 가이드의 샘플 앱에는 히어로를 가져와 사용자가 추가, 삭제 및 업데이트 할 수있게 해주는 "Tour of Heroes" | ||
| + | |||
| + | 다음 섹션에서는 샘플 HeroesService의 메소드를 발췌합니다. | ||
| + | |||
| + | ==== Adding headers ==== | ||
| + | 많은 서버는 저장 조작을 위해 여분의 헤더가 필요합니다. 예를 들어 요청 본문의 MIME 형식을 명시 적으로 선언하기 위해 " | ||
| + | |||
| + | HeroesService는 모든 HttpClient 저장 메소드에 전달되는 httpOptions 객체에서 이러한 헤더를 정의합니다. | ||
| + | <code javascript> | ||
| + | import { HttpHeaders } from ' | ||
| + | |||
| + | const httpOptions = { | ||
| + | headers: new HttpHeaders({ | ||
| + | ' | ||
| + | ' | ||
| + | }) | ||
| + | }; | ||
| + | </ | ||
| + | ==== Making a POST request ==== | ||
| + | 애플리케이션은 종종 서버에 데이터를 POST합니다. 양식을 제출할 때 POST됩니다. 다음 예제에서 HeroesService는 영웅을 데이터베이스에 추가 할 때 게시합니다. | ||
| + | <code javascript> | ||
| + | /** POST: add a new hero to the database */ | ||
| + | addHero (hero: Hero): Observable< | ||
| + | return this.http.post< | ||
| + | .pipe( | ||
| + | catchError(this.handleError(' | ||
| + | ); | ||
| + | } | ||
| + | </ | ||
| + | HttpClient.post() 메서드는 서버가 새로운 영웅을 반환 할 것으로 기대하는 형식 매개 변수가 있고 리소스 URL을 사용한다는 점에서 get()과 비슷합니다. | ||
| + | |||
| + | 두 개의 매개 변수가 더 필요합니다. | ||
| + | - hero - 요청 본문에 POST 할 데이터입니다. | ||
| + | - httpOptions -이 경우 필요한 헤더를 지정하는 메소드 옵션. | ||
| + | |||
| + | 물론 그것은 위에서 설명한 것과 거의 같은 방식으로 오류를 포착합니다. | ||
| + | |||
| + | HeroesComponent는이 서비스 메소드에 의해 반환 된 Observable에 가입하여 실제 POST 작업을 시작합니다. | ||
| + | |||
| + | <code javascript> | ||
| + | this.heroesService.addHero(newHero) | ||
| + | .subscribe(hero => this.heroes.push(hero)); | ||
| + | </ | ||
| + | 서버가 새로 추가 된 영웅으로 성공적으로 응답하면 구성 요소는 영웅을 표시된 영웅 목록에 추가합니다. | ||
| + | |||
| + | ==== Making a DELETE request ==== | ||
| + | 이 애플리케이션은 요청 URL에 영웅의 ID를 전달하여 HttpClient.delete 메소드로 영웅을 삭제합니다. | ||
| + | <code javascript> | ||
| + | /** DELETE: delete the hero from the server */ | ||
| + | deleteHero (id: number): Observable< | ||
| + | const url = `${this.heroesUrl}/ | ||
| + | return this.http.delete(url, | ||
| + | .pipe( | ||
| + | catchError(this.handleError(' | ||
| + | ); | ||
| + | } | ||
| + | </ | ||
| + | HeroesComponent는 이 서비스 메소드에 의해 리턴 된 Observable에 등록함으로써 실제 DELETE 조작을 초기화한다. | ||
| + | <code javascript> | ||
| + | this.heroesService.deleteHero(hero.id).subscribe(); | ||
| + | </ | ||
| + | 구성 요소가 삭제 작업의 결과를 기대하지 않으므로 콜백없이 구독합니다. 결과를 사용하지 않더라도 구독해야합니다. subscribe() 메소드를 호출하면 관찰 가능 (observable)이 실행되며, | ||
| + | |||
| + | > subscribe()를 호출해야합니다. 그렇지 않으면 아무 일도 일어나지 않습니다. HeroesService.deleteHero()를 호출하면 DELETE 요청이 시작되지 않습니다. | ||
| + | |||
| + | <code javascript> | ||
| + | // oops ... subscribe() is missing so nothing happens | ||
| + | this.heroesService.deleteHero(hero.id); | ||
| + | </ | ||
| + | |||
| + | === 항상 구독하십시오! === | ||
| + | |||
| + | HttpClient 메서드는 해당 메서드에서 반환 한 관찰 가능 항목에 대해 subscribe ()를 호출 할 때까지 HTTP 요청을 시작하지 않습니다. 이는 모든 HttpClient 메소드에 적용됩니다. | ||
| + | |||
| + | > | ||
| + | |||
| + | HttpClient 메서드에서 반환 된 모든 관찰 가능 항목은 의도적으로 차갑습니다. HTTP 요청의 실행이 지연되므로 실제로 발생하기 전에 관찰 및 관찰 작업을 tap 및 catchError와 같은 추가 작업으로 확장 할 수 있습니다. | ||
| + | |||
| + | subscribe (...)를 호출하면 관찰 가능 객체가 실행되고 HttpClient가 HTTP 요청을 작성하고 서버로 전송합니다. | ||
| + | |||
| + | 이러한 관찰 내용은 실제 HTTP 요청에 대한 청사진이라고 생각할 수 있습니다. | ||
| + | |||
| + | > 사실, 각 subscribe()는 독립적으로 observable을 실행합니다. 두 번 구독하면 두 개의 HTTP 요청이 발생합니다. | ||
| + | > <code javascript> | ||
| + | const req = http.get< | ||
| + | // 0 requests made - .subscribe() not called. | ||
| + | req.subscribe(); | ||
| + | // 1 request made. | ||
| + | req.subscribe(); | ||
| + | // 2 requests made. | ||
| + | </ | ||
| + | |||
| + | ==== Making a PUT request ==== | ||
| + | 앱이 리소스를 업데이트 된 데이터로 완전히 대체하기 위해 PUT 요청을 보냅니다. 다음 HeroesService 예제는 POST 예제와 같습니다. | ||
| + | <code javascript> | ||
| + | /** PUT: update the hero on the server. Returns the updated hero upon success. */ | ||
| + | updateHero (hero: Hero): Observable< | ||
| + | return this.http.put< | ||
| + | .pipe( | ||
| + | catchError(this.handleError(' | ||
| + | ); | ||
| + | } | ||
| + | </ | ||
| + | 위에서 설명한 이유로 호출자 (이 경우 HeroesComponent.update())는 요청을 시작하기 위해 HttpClient.put()에서 반환 된 관찰 가능 객체에 subscribe()해야합니다. | ||
| + | |||
| + | ===== Advanced usage ===== | ||
| + | 우리는 **@angular/ | ||
| + | |||
| + | ==== Configuring the request ==== | ||
| + | 보내는 요청의 다른 측면은 HttpClient 메서드의 마지막 인수로 전달 된 옵션 개체를 통해 구성 할 수 있습니다. | ||
| + | |||
| + | 이전에 HeroesService는 options 객체 (httpOptions)를 save 메소드에 전달하여 기본 헤더를 설정했다. 더 많은 일을 할 수 있습니다. | ||
| + | === Update headers === | ||
| + | HttpHeaders 클래스의 인스턴스는 변경 불가능하기 때문에 이전 옵션 객체에서 기존 헤더를 직접 수정할 수 없습니다. | ||
| + | |||
| + | 대신 set() 메서드를 사용하십시오. 새 변경 내용이 적용된 현재 인스턴스의 복제본을 반환합니다. | ||
| + | |||
| + | 다음 요청을하기 전에 (오래된 토큰이 만료 된 후) 인증 헤더를 업데이트하는 방법은 다음과 같습니다. | ||
| + | <code javascript> | ||
| + | httpOptions.headers = | ||
| + | httpOptions.headers.set(' | ||
| + | </ | ||
| + | === URL Parameters === | ||
| + | URL 검색 매개 변수를 추가하는 것도 비슷한 방식으로 작동합니다. 다음은 검색 단어가 포함 된 영웅을 검색하는 searchHeroes 메소드입니다. | ||
| + | <code javascript> | ||
| + | /* GET heroes whose name contains search term */ | ||
| + | searchHeroes(term: | ||
| + | term = term.trim(); | ||
| + | |||
| + | // Add safe, URL encoded search parameter if there is a search term | ||
| + | const options = term ? | ||
| + | { params: new HttpParams().set(' | ||
| + | |||
| + | return this.http.get< | ||
| + | .pipe( | ||
| + | catchError(this.handleError< | ||
| + | ); | ||
| + | } | ||
| + | </ | ||
| + | 검색어가있는 경우 코드는 HTML URL 인코딩 검색 매개 변수가있는 옵션 개체를 구성합니다. 용어가 " | ||
| + | |||
| + | HttpParams는 변경할 수 없으므로 set() 메서드를 사용하여 옵션을 업데이트해야합니다. | ||
| + | ==== Debouncing requests ==== | ||
| + | 이 샘플에는 npm 패키지 검색 기능이 포함되어 있습니다. | ||
| + | |||
| + | 사용자가 검색 상자에 이름을 입력하면 PackageSearchComponent는 해당 이름의 패키지에 대한 검색 요청을 NPM 웹 API에 전송합니다. | ||
| + | |||
| + | 다음은 템플릿에서 발췌 한 내용입니다. | ||
| + | <code javascript> | ||
| + | <input (keyup)=" | ||
| + | |||
| + | <ul> | ||
| + | <li *ngFor=" | ||
| + | < | ||
| + | < | ||
| + | </li> | ||
| + | </ul> | ||
| + | </ | ||
| + | (keyup) 이벤트 바인딩은 모든 키 입력을 구성 요소의 search() 메서드로 보냅니다. | ||
| + | |||
| + | 모든 키 입력에 대한 요청을 보내는 것은 많은 비용이 듭니다. 사용자가 입력을 중지하고 요청을 보낼 때까지 기다리는 것이 좋습니다. 이 발췌 부분에서 볼 수 있듯이 RxJS 연산자로 구현하기 쉽습니다. | ||
| + | <code javascript> | ||
| + | withRefresh = false; | ||
| + | packages$: Observable< | ||
| + | private searchText$ = new Subject< | ||
| + | |||
| + | search(packageName: | ||
| + | this.searchText$.next(packageName); | ||
| + | } | ||
| + | |||
| + | ngOnInit() { | ||
| + | this.packages$ = this.searchText$.pipe( | ||
| + | debounceTime(500), | ||
| + | distinctUntilChanged(), | ||
| + | switchMap(packageName => | ||
| + | this.searchService.search(packageName, | ||
| + | ); | ||
| + | } | ||
| + | |||
| + | constructor(private searchService: | ||
| + | </ | ||
| + | searchText$는 사용자가 제공하는 검색 창 값의 순서입니다. 이것은 RxJS Subject로 정의됩니다. 즉, search() 메소드에서와 같이 next(value)를 호출하여 값을 생성 할 수있는 멀티 캐스팅 관측자입니다. | ||
| + | |||
| + | 삽입 된 PackageSearchService에 모든 searchText 값을 직접 전달하는 대신 ngOnInit() 파이프의 코드는 세 개의 연산자를 통해 값을 검색합니다. | ||
| + | |||
| + | - debounceTime(500) - 사용자가 입력을 중지 할 때까지 기다립니다 (이 경우 1/2 초). | ||
| + | - distinctUntilChanged() - 검색 텍스트가 변경 될 때까지 대기합니다. | ||
| + | - switchMap() - 서비스에 검색 요청을 보냅니다. | ||
| + | |||
| + | 코드는이 재구성 된 검색 결과의 Observable에 packages$를 설정합니다. 이 템플릿은 AsyncPipe를 사용하여 packages$를 구독하고 도착한 검색 결과를 표시합니다. | ||
| + | |||
| + | 검색 값은 새 값이고 사용자가 입력을 중지 한 경우에만 서비스에 도달합니다. | ||
| + | > withRefresh 옵션은 아래에 설명되어 있습니다. | ||
| + | === switchMap() === | ||
| + | |||
| + | switchMap () 연산자에는 세 가지 중요한 특징이 있습니다. | ||
| + | |||
| + | - Observable을 반환하는 함수 인수를 사용합니다. 다른 데이터 서비스 메소드와 마찬가지로 PackageSearchService.search가 Observable을 반환합니다. | ||
| + | - 이전 검색 요청이 아직 연결 상태가 좋지 않은 상태에서 계속 진행중인 경우 해당 요청을 취소하고 새 요청을 보냅니다. | ||
| + | - 서버가 순서가 바뀌지 않는 경우에도 원래 요청 순서대로 서비스 응답을 반환합니다. | ||
| + | |||
| + | > 이 디 바운싱 로직을 재사용 할 생각이라면 유틸리티 함수 또는 PackageSearchService 자체로 옮기는 것을 고려하십시오. | ||
| + | |||
| + | ==== Intercepting requests and responses ==== | ||
| + | //HTTP Interception// | ||
| + | |||
| + | interceptor는 모든 HTTP request/ | ||
| + | |||
| + | interceptor가 없으면 개발자는 각 HttpClient 메서드 호출에 대해 이러한 작업을 // | ||
| + | |||
| + | === Write an interceptor === | ||
| + | 인터셉터를 구현하려면 HttpInterceptor 인터페이스의 intercept() 메소드를 구현하는 클래스를 선언하십시오. | ||
| + | |||
| + | 아무 것도하지 않고 그냥 요청을 전달하는 무의미한 요격기 요격기가 있습니다. | ||
| + | <code javascript> | ||
| + | import { Injectable } from ' | ||
| + | import { | ||
| + | HttpEvent, HttpInterceptor, | ||
| + | } from ' | ||
| + | |||
| + | import { Observable } from ' | ||
| + | |||
| + | /** Pass untouched request through to the next request handler. */ | ||
| + | @Injectable() | ||
| + | export class NoopInterceptor implements HttpInterceptor { | ||
| + | |||
| + | intercept(req: | ||
| + | Observable< | ||
| + | return next.handle(req); | ||
| + | } | ||
| + | } | ||
| + | </ | ||
| + | |||
| + | intercept 메서드는 request을 Observable로 변환하여 결국 HTTP response을 반환합니다. 이러한 의미에서 각 인터셉터는 완전히 그 자체로 요청을 처리 할 수 있습니다. | ||
| + | |||
| + | 대부분의 인터셉터는 HttpHandler 인터페이스를 구현하는 다음 객체의 handle() 메소드에 대한 (아마도 변경된) request을 전달하고 전달하는 response을 검사합니다. | ||
| + | <code javascript> | ||
| + | export abstract class HttpHandler { | ||
| + | abstract handle(req: HttpRequest< | ||
| + | } | ||
| + | </ | ||
| + | intercept()처럼 handle() 메서드는 HTTP 요청을 서버의 응답을 포함하는 HttpEvents Observable로 변환합니다. intercept() 메소드는 관찰자를 검사하여 호출자에게 반환하기 전에이를 변경할 수 있습니다. | ||
| + | |||
| + | 이 no-op 인터셉터는 단순히 원래 요청으로 next.handle()을 호출하고 일을하지 않고 observable을 반환합니다. | ||
| + | |||
| + | === The next object === | ||
| + | |||
| + | 다음 객체는 인터셉터 체인의 다음 인터셉터를 나타냅니다. 마지막으로 체인에있는 요청은 서버에 요청을 보내고 서버의 응답을받는 HttpClient 백엔드 처리기입니다. | ||
| + | |||
| + | 대부분의 인터셉터는 next.handle ()을 호출하여 요청이 다음 인터셉터와 결국 백엔드 처리기로 전달되도록합니다. 인터셉터는 next.handle () 호출을 건너 뛰고, 체인을 단락시키고, | ||
| + | |||
| + | 이것은 Express.js와 같은 프레임 워크에서 흔히 볼 수있는 미들웨어 패턴입니다. | ||
| + | |||
| + | === Provide the interceptor === | ||
| + | |||
| + | NoopInterceptor는 Angular의 DI (Dependency Injection) 시스템으로 관리되는 서비스입니다. 다른 서비스와 마찬가지로, | ||
| + | |||
| + | 인터셉터는 HttpClient 서비스의 종속성 (선택 사항)이므로 HttpClient를 제공하는 동일한 인젝터 (또는 인젝터의 부모)에 제공해야합니다. DI가 HttpClient를 생성 한 후에 제공되는 인터셉터는 무시됩니다. | ||
| + | |||
| + | 이 앱은 AppModule에서 HttpClientModule을 가져 오는 부작용으로 앱의 루트 인젝터에 HttpClient를 제공합니다. AppModule에서도 인터셉터를 제공해야합니다. | ||
| + | |||
| + | @angular/ | ||
| + | <code javascript> | ||
| + | { provide: HTTP_INTERCEPTORS, | ||
| + | </ | ||
| + | multi : true 옵션을 주목하십시오. 이 필수 설정은 Angle에 HTTP_INTERCEPTORS가 단일 값이 아닌 값 배열을 주입하는 다중 제공자에 대한 토큰임을 나타냅니다. | ||
| + | |||
| + | 이 제공자를 AppModule의 공급자 배열에 직접 추가 할 수 있습니다. 그러나 다소 장황하고 더 많은 인터셉터를 만들어 동일한 방식으로 제공 할 수있는 좋은 기회가 있습니다. 또한 이러한 인터셉터를 제공하는 순서에 세심한주의를 기울여야합니다. | ||
| + | |||
| + | 모든 인터셉터 제공자를 httpInterceptorProviders 배열로 모으는 " | ||
| + | <code javascript> | ||
| + | /* " | ||
| + | import { HTTP_INTERCEPTORS } from ' | ||
| + | |||
| + | import { NoopInterceptor } from ' | ||
| + | |||
| + | /** Http interceptor providers in outside-in order */ | ||
| + | export const httpInterceptorProviders = [ | ||
| + | { provide: HTTP_INTERCEPTORS, | ||
| + | ]; | ||
| + | </ | ||
| + | 그런 다음 가져 와서 다음과 같이 AppModule 공급자 배열에 추가합니다. | ||
| + | <code javascript> | ||
| + | providers: [ | ||
| + | httpInterceptorProviders | ||
| + | ], | ||
| + | </ | ||
| + | 새로운 인터셉터를 만들 때 httpInterceptorProviders 배열에 추가하면 AppModule을 다시 방문 할 필요가 없습니다. | ||
| + | |||
| + | > 전체 샘플 코드에는 더 많은 인터셉터가 있습니다. | ||
| + | |||
| + | === Interceptor order === | ||
| + | |||
| + | Angular는 사용자가 제공 한 순서대로 인터셉터를 적용합니다. 인터셉터 A, B, C를 제공하면 요청은 A-> B-> C로 흐르고 응답은 C-> B-> A로 흐르게됩니다. | ||
| + | |||
| + | 주문을 변경하거나 나중에 인터셉터를 제거 할 수 없습니다. 인터셉터를 동적으로 활성화 및 비활성화해야하는 경우 인터셉터 자체에 해당 기능을 구현해야합니다. | ||
| + | |||
| + | === HttpEvents === | ||
| + | 대부분의 HttpClient 메소드가 수행하는 것처럼 intercept() 및 handle() 메서드가 HttpResponse< | ||
| + | |||
| + | 대신 HttpEvent< | ||
| + | |||
| + | 인터셉터는 HttpClient 메소드보다 낮은 수준에서 작동하기 때문입니다. 단일 HTTP 요청은 업로드 및 다운로드 진행 이벤트를 포함하여 여러 이벤트를 생성 할 수 있습니다. HttpResponse 클래스 자체는 실제로 이벤트 유형이며 HttpEventType.HttpResponseEvent입니다. | ||
| + | |||
| + | 많은 인터셉터는 나가는 요청에만 관심이 있으며 next.handle()에서 이벤트 스트림을 수정하지 않고 반환합니다. | ||
| + | |||
| + | 그러나 next.handle()의 응답을 검사하고 수정하는 인터셉터는 이러한 모든 이벤트를 볼 수 있습니다. 인터셉터는 그렇지 않은 다른 이유가없는 한 손대지 않은 모든 이벤트를 반환해야합니다. | ||
| + | |||
| + | === Immutability === | ||
| + | |||
| + | 인터셉터는 요청과 응답을 변경시킬 수 있지만 HttpRequest 및 HttpResponse 인스턴스 속성은 읽기 전용이므로 거의 변경되지 않습니다. | ||
| + | |||
| + | 좋은 이유는 불변합니다. 앱이 성공하기 전에 요청을 여러 번 다시 시도 할 수 있습니다. 즉, 인터셉터 체인이 동일한 요청을 여러 번 다시 처리 할 수 있습니다. 인터셉터가 원래 요청 객체를 수정할 수있는 경우 재 시도 된 작업은 원본이 아닌 수정 된 요청에서 시작됩니다. Immutability는 인터셉터가 각 시도에 대해 동일한 요청을 볼 수 있도록합니다. | ||
| + | |||
| + | TypeScript는 HttpRequest 읽기 전용 속성을 설정하지 못하게합니다. | ||
| + | <code javascript> | ||
| + | // Typescript disallows the following assignment because req.url is readonly | ||
| + | req.url = req.url.replace(' | ||
| + | </ | ||
| + | 요청을 변경하려면 먼저 복제하고 next.handle()에 전달하기 전에 복제본을 수정하십시오. 이 예와 같이 요청을 단일 단계에서 복제하고 수정할 수 있습니다. | ||
| + | <code javascript> | ||
| + | // clone request and replace ' | ||
| + | const secureReq = req.clone({ | ||
| + | url: req.url.replace(' | ||
| + | }); | ||
| + | // send the cloned, " | ||
| + | return next.handle(secureReq); | ||
| + | </ | ||
| + | clone() 메서드의 해시 인수를 사용하면 요청의 특정 속성을 변경하면서 다른 속성을 복사 할 수 있습니다. | ||
| + | === The request body === | ||
| + | readonly 할당 보호는 딥 업데이트를 방지 할 수 없으며 특히 요청 본문 개체의 속성을 수정하지 못하게 할 수 없습니다. | ||
| + | <code javascript> | ||
| + | req.body.name = req.body.name.trim(); | ||
| + | </ | ||
| + | |||
| + | request body을 변경해야하는, | ||
| + | <code javascript> | ||
| + | // copy the body and trim whitespace from the name property | ||
| + | const newBody = { ...body, name: body.name.trim() }; | ||
| + | // clone request and set its body | ||
| + | const newReq = req.clone({ body: newBody }); | ||
| + | // send the cloned request to the next handler. | ||
| + | return next.handle(newReq); | ||
| + | </ | ||
| + | |||
| + | === Clearing the request body === | ||
| + | |||
| + | 경우에 따라 요청 본문을 대체하지 않고 삭제해야하는 경우도 있습니다. 복제 된 요청 본문을 undefined로 설정하면 Angular는 본문을 그대로 두려고한다고 가정합니다. 그것은 당신이 원하는 것이 아닙니다. 복제 된 요청 본문을 null로 설정하면 Angular는 요청 본문을 지우려는 의도가 있음을 알고 있습니다. | ||
| + | |||
| + | <code javascript> | ||
| + | newReq = req.clone({ ... }); // body not mentioned => preserve original body | ||
| + | newReq = req.clone({ body: undefined }); // preserve original body | ||
| + | newReq = req.clone({ body: null }); // clear the body | ||
| + | </ | ||
| + | |||
| + | === Set default headers === | ||
| + | 앱은 종종 인터셉터를 사용하여 발신 요청에 기본 헤더를 설정합니다. | ||
| + | |||
| + | 샘플 앱에는 인증 토큰을 생성하는 AuthService가 있습니다. 다음은 AuthInceptceptor가 해당 서비스를 주입하여 토큰을 가져오고 해당 토큰과 함께 승인 헤더를 모든 발신 요청에 추가합니다. | ||
| + | <code javascript> | ||
| + | import { AuthService } from ' | ||
| + | |||
| + | @Injectable() | ||
| + | export class AuthInterceptor implements HttpInterceptor { | ||
| + | |||
| + | constructor(private auth: AuthService) {} | ||
| + | |||
| + | intercept(req: | ||
| + | // Get the auth token from the service. | ||
| + | const authToken = this.auth.getAuthorizationToken(); | ||
| + | |||
| + | // Clone the request and replace the original headers with | ||
| + | // cloned headers, updated with the authorization. | ||
| + | const authReq = req.clone({ | ||
| + | headers: req.headers.set(' | ||
| + | }); | ||
| + | |||
| + | // send cloned request with header to the next handler. | ||
| + | return next.handle(authReq); | ||
| + | } | ||
| + | } | ||
| + | </ | ||
| + | 새 헤더를 설정하라는 요청을 복제하는 관행은 너무 일반적이어서 setHeaders 단축키가 있습니다. | ||
| + | <code javascript> | ||
| + | // Clone the request and set the new header in one step. | ||
| + | const authReq = req.clone({ setHeaders: { Authorization: | ||
| + | </ | ||
| + | |||
| + | 헤더를 변경하는 인터셉터는 다음과 같은 여러 가지 다른 작업에 사용할 수 있습니다. | ||
| + | |||
| + | - Authentication/ | ||
| + | - Caching behavior(캐싱동작); | ||
| + | - XSRF 보호 | ||
| + | |||
| + | === Logging === | ||
| + | |||
| + | 인터셉터는 요청과 응답을 함께 처리 할 수 있기 때문에 시간과 같은 작업을 수행하고 전체 HTTP 작업을 기록 할 수 있습니다. | ||
| + | |||
| + | 다음의 LoggingInterceptor를 고려해보십시오.이 LoggingInterceptor는 요청 시간과 응답 시간을 캡처하고 MessageService가 삽입 된 경과 시간으로 결과를 기록합니다. | ||
| + | |||
| + | <code javascript> | ||
| + | import { finalize, tap } from ' | ||
| + | import { MessageService } from ' | ||
| + | |||
| + | @Injectable() | ||
| + | export class LoggingInterceptor implements HttpInterceptor { | ||
| + | constructor(private messenger: MessageService) {} | ||
| + | |||
| + | intercept(req: | ||
| + | const started = Date.now(); | ||
| + | let ok: string; | ||
| + | |||
| + | // extend server response observable with logging | ||
| + | return next.handle(req) | ||
| + | .pipe( | ||
| + | tap( | ||
| + | // Succeeds when there is a response; ignore other events | ||
| + | event => ok = event instanceof HttpResponse ? ' | ||
| + | // Operation failed; error is an HttpErrorResponse | ||
| + | error => ok = ' | ||
| + | ), | ||
| + | // Log when response observable either completes or errors | ||
| + | finalize(() => { | ||
| + | const elapsed = Date.now() - started; | ||
| + | const msg = `${req.method} " | ||
| + | ${ok} in ${elapsed} ms.`; | ||
| + | this.messenger.add(msg); | ||
| + | }) | ||
| + | ); | ||
| + | } | ||
| + | } | ||
| + | </ | ||
| + | RxJS 탭 운영자는 요청의 성공 또는 실패 여부를 캡처합니다. RxJS finalize 연산자는 응답이 관찰되거나 완료 될 때 호출되며 MessageService에 결과를보고합니다. | ||
| + | |||
| + | 탭하거나 파이널 라이즈하지 않으면 발신자에게 반환 된 관찰 가능한 스트림의 값을 터치하지 않습니다. | ||
| + | |||
| + | === Caching === | ||
| + | |||
| + | 인터셉터는 next.handle()에 전달하지 않고 스스로 요청을 처리 할 수 있습니다. | ||
| + | |||
| + | 예를 들어 특정 요청 및 응답을 캐시하여 성능을 향상시킬 수 있습니다. 기존 데이터 서비스를 방해하지 않고 인터셉터에 캐싱을 위임 할 수 있습니다. | ||
| + | |||
| + | CachingInterceptor는 이러한 접근 방식을 보여줍니다. | ||
| + | <code javascript> | ||
| + | @Injectable() | ||
| + | export class CachingInterceptor implements HttpInterceptor { | ||
| + | constructor(private cache: RequestCache) {} | ||
| + | |||
| + | intercept(req: | ||
| + | // continue if not cachable. | ||
| + | if (!isCachable(req)) { return next.handle(req); | ||
| + | |||
| + | const cachedResponse = this.cache.get(req); | ||
| + | return cachedResponse ? | ||
| + | of(cachedResponse) : sendRequest(req, | ||
| + | } | ||
| + | } | ||
| + | </ | ||
| + | isCachable() 함수는 요청이 캐시 가능한지 여부를 결정합니다. 이 샘플에서는 npm 패키지 검색 API에 대한 GET 요청 만 캐시 할 수 있습니다. | ||
| + | |||
| + | 요청이 캐시 가능하지 않은 경우 인터셉터는 요청을 체인의 다음 핸들러로 전달하기 만합니다. | ||
| + | |||
| + | 캐시 가능한 요청이 캐시에서 발견되면 인터셉터는 캐시 된 응답과 함께 of() // | ||
| + | |||
| + | 캐시 가능한 요청이 캐시에 없으면 코드는 sendRequest를 호출합니다. | ||
| + | <code javascript> | ||
| + | /** | ||
| + | * Get server response observable by sending request to `next()`. | ||
| + | * Will add the response to the cache on the way out. | ||
| + | */ | ||
| + | function sendRequest( | ||
| + | req: HttpRequest< | ||
| + | next: HttpHandler, | ||
| + | cache: RequestCache): | ||
| + | |||
| + | // No headers allowed in npm search request | ||
| + | const noHeaderReq = req.clone({ headers: new HttpHeaders() }); | ||
| + | |||
| + | return next.handle(noHeaderReq).pipe( | ||
| + | tap(event => { | ||
| + | // There may be other events besides the response. | ||
| + | if (event instanceof HttpResponse) { | ||
| + | cache.put(req, | ||
| + | } | ||
| + | }) | ||
| + | ); | ||
| + | } | ||
| + | </ | ||
| + | sendRequest 함수는 npm api가 헤더를 사용하지 않으므로 헤더가없는 요청 복제를 만듭니다. | ||
| + | |||
| + | 이 요청은 next.handle()에 전달되어 궁극적으로 서버를 호출하고 서버의 응답을 반환합니다. | ||
| + | |||
| + | sendRequest가 응답을 응용 프로그램으로 가로 채는 것을 확인하십시오. 그것은 call() 함수가 캐시에 응답을 추가하는 tap() 연산자를 통해 응답을 파이프합니다. | ||
| + | |||
| + | 원래 응답은 응용 프로그램 호출자에게 인터셉터 체인을 통해 변경되지 않은 상태로 계속 유지됩니다. | ||
| + | |||
| + | PackageSearchService와 같은 데이터 서비스는 HttpClient 요청 중 일부가 실제로 캐시 된 응답을 반환한다는 것을 알지 못합니다. | ||
| + | === Return a multi-valued Observable === | ||
| + | HttpClient.get () 메서드는 일반적으로 데이터 또는 오류를 내보내는 관찰 가능 항목을 반환합니다. 어떤 사람들은 그것을 "한 번에 끝낸" | ||
| + | |||
| + | 그러나 인터셉터는 이것을 한 번 이상 내보내는 관찰자로 변경할 수 있습니다. | ||
| + | |||
| + | 개정 된 버전의 CachingInterceptor는 관찰 된 결과를 캐시 된 응답을 즉시 내보내고 NPM 웹 API에 요청을 보내고 나중에 업데이트 된 검색 결과와 함께 다시 방출합니다. | ||
| + | <code javascript> | ||
| + | // cache-then-refresh | ||
| + | if (req.headers.get(' | ||
| + | const results$ = sendRequest(req, | ||
| + | return cachedResponse ? | ||
| + | results$.pipe( startWith(cachedResponse) ) : | ||
| + | results$; | ||
| + | } | ||
| + | // cache-or-fetch | ||
| + | return cachedResponse ? | ||
| + | of(cachedResponse) : sendRequest(req, | ||
| + | </ | ||
| + | // | ||
| + | > PackageSearchComponent의 확인란은 PackageSearchService.search ()에 대한 인수 중 하나 인 withRefresh 플래그를 토글합니다. search () 메서드는 사용자 정의 x-refresh 헤더를 만들고 HttpClient.get ()을 호출하기 전에 요청에 추가합니다. | ||
| + | |||
| + | 수정 된 CachingInterceptor는 위에서 설명한 것과 같은 sendRequest () 메서드를 사용하여 캐시 된 값의 존재 여부와 상관없이 서버 요청을 설정합니다. 관찰 가능한 result$는 구독 할 때 요청할 것입니다. | ||
| + | |||
| + | 캐시 된 값이 없으면 인터셉터는 result$를 반환합니다. | ||
| + | |||
| + | 캐시 된 값이 있으면 코드는 캐시 된 응답을 result$로 파이프하여 두 번 내보내는 재구성된 관찰 가능 항목을 생성하고 캐시된 응답을 먼저 (그리고 바로) 응답 한 다음 나중에 서버에서 응답합니다. 구독자는 // | ||
| + | |||
| + | ==== Listening to progress events ==== | ||
| + | 때때로 응용 프로그램은 많은 양의 데이터를 전송하며 이러한 전송에는 오랜 시간이 걸릴 수 있습니다. 파일 업로드가 전형적인 예입니다. 이러한 전송 진행 상황에 대한 피드백을 제공하여 사용자에게 더 나은 경험을 제공하십시오. | ||
| + | |||
| + | 진행 이벤트를 사용하도록 요청하려면 reportProgress 옵션을 true로 설정하여 HttpRequest의 인스턴스를 만들어 진행 이벤트를 추적 할 수 있습니다. | ||
| + | <code javascript> | ||
| + | const req = new HttpRequest(' | ||
| + | reportProgress: | ||
| + | }); | ||
| + | </ | ||
| + | > 모든 진행 이벤트는 변경 감지를 트리거하므로, | ||
| + | > | ||
| + | > HTTP 메소드와 함께 HttpClient# | ||
| + | |||
| + | 그런 다음이 요청 개체를 HttpClient.request () 메서드에 전달합니다.이 메서드는 인터셉터에서 처리 한 것과 동일한 이벤트 인 Observable of HttpEvents를 반환합니다. | ||
| + | <code javascript> | ||
| + | // The `HttpClient.request` API produces a raw event stream | ||
| + | // which includes start (sent), progress, and response events. | ||
| + | return this.http.request(req).pipe( | ||
| + | map(event => this.getEventMessage(event, | ||
| + | tap(message => this.showProgress(message)), | ||
| + | last(), // return last (completed) message to caller | ||
| + | catchError(this.handleError(file)) | ||
| + | ); | ||
| + | </ | ||
| + | getEventMessage 메소드는 이벤트 스트림에서 각 유형의 HttpEvent를 해석합니다. | ||
| + | <code javascript> | ||
| + | /** Return distinct message for sent, upload progress, & response events */ | ||
| + | private getEventMessage(event: | ||
| + | switch (event.type) { | ||
| + | case HttpEventType.Sent: | ||
| + | return `Uploading file " | ||
| + | |||
| + | case HttpEventType.UploadProgress: | ||
| + | // Compute and show the % done: | ||
| + | const percentDone = Math.round(100 * event.loaded / event.total); | ||
| + | return `File " | ||
| + | |||
| + | case HttpEventType.Response: | ||
| + | return `File " | ||
| + | |||
| + | default: | ||
| + | return `File " | ||
| + | } | ||
| + | } | ||
| + | </ | ||
| + | > 이 가이드의 샘플 앱에는 업로드 된 파일을 허용하는 서버가 없습니다. app/ | ||
| + | |||
| + | ===== Security: XSRF Protection ===== | ||
| + | XSRF (Cross-Site Request Forgery)는 공격자가 인증 된 사용자를 속여 웹 사이트에서 무의식적으로 실행하는 공격 기법입니다. HttpClient는 XSRF 공격을 막는 데 사용되는 일반적인 메커니즘을 지원합니다. HTTP 요청을 수행 할 때 인터셉터는 기본적으로 XSRF-TOKEN에 의해 쿠키에서 토큰을 읽고 HTTP 헤더 인 X-XSRF-TOKEN으로 설정합니다. 도메인에서 실행되는 코드 만 쿠키를 읽을 수 있기 때문에 백엔드는 HTTP 요청이 공격자가 아닌 클라이언트 응용 프로그램에서 온 것임을 확신 할 수 있습니다. | ||
| + | |||
| + | 기본적으로 인터셉터는 모든 변이 된 요청 (POST 등)에 대해이 쿠키를 상대 URL로 보내지 만 GET / HEAD 요청이나 절대 URL이있는 요청에는 보내지 않습니다. | ||
| + | |||
| + | 이 기능을 이용하려면 서버가 페이지로드 또는 첫 번째 GET 요청에서 XSRF-TOKEN이라는 자바 스크립트로 읽을 수있는 세션 쿠키로 토큰을 설정해야합니다. 후속 요청에서 서버는 쿠키가 X-XSRF-TOKEN HTTP 헤더와 일치하는지 확인할 수 있으므로 도메인에서 실행중인 코드 만 요청을 보낼 수 있는지 확인하십시오. 토큰은 각 사용자마다 고유해야하며 서버에서 검증 할 수 있어야합니다. 이것은 클라이언트가 자신의 토큰을 만들지 못하게합니다. 토큰을 보안 강화를 위해 소금으로 사이트의 인증 쿠키 다이제스트로 설정하십시오. | ||
| + | |||
| + | 여러 Angular 응용 프로그램이 동일한 도메인 또는 하위 도메인을 공유하는 환경에서 충돌을 방지하려면 각 응용 프로그램에 고유 한 쿠키 이름을 지정하십시오. | ||
| + | |||
| + | > // | ||
| + | |||
| + | ==== Configuring custom cookie/ | ||
| + | 백엔드 서비스가 XSRF 토큰 쿠키 또는 헤더에 대해 다른 이름을 사용하는 경우 HttpClientXsrfModule.withOptions ()를 사용하여 기본값을 대체하십시오. | ||
| + | <code javascript> | ||
| + | imports: [ | ||
| + | HttpClientModule, | ||
| + | HttpClientXsrfModule.withOptions({ | ||
| + | cookieName: ' | ||
| + | headerName: ' | ||
| + | }), | ||
| + | ], | ||
| + | </ | ||
| + | |||
| + | ===== Testing HTTP requests ===== | ||
| + | 외부 종속성과 마찬가지로 HTTP 백엔드가 조롱 받아 테스트가 원격 서버와의 상호 작용을 시뮬레이션 할 수 있어야합니다. @ angular / common / http / testing 라이브러리는 그러한 조롱을 쉽게 설정합니다. | ||
| + | |||
| + | ==== Mocking philosophy ==== | ||
| + | Angular의 HTTP 테스트 라이브러리는 앱이 코드를 실행하고 요청을 먼저 처리하는 테스트 패턴을 위해 설계되었습니다. | ||
| + | |||
| + | 그런 다음 특정 요청이 수행되었거나 수행되지 않았으며 해당 요청에 대한 어설 션을 수행하고 마지막으로 예상되는 요청을 " | ||
| + | |||
| + | 결국 테스트에서 앱이 예기치 않은 요청을하지 않았 음을 확인할 수 있습니다. | ||
| + | |||
| + | > 라이브 코딩 환경에서 이러한 샘플 테스트 / 다운로드 예제를 실행할 수 있습니다. | ||
| + | > | ||
| + | > 이 가이드에서 설명하는 테스트는 src/ | ||
| + | |||
| + | ==== Setup ==== | ||
| + | HttpClient에 대한 호출을 테스트하려면 테스트에 필요한 다른 기호와 함께 HttpClientTestingModule 및 조롱 컨트롤러 HttpTestingController를 가져옵니다. | ||
| + | <code javascript> | ||
| + | // Http testing module and mocking controller | ||
| + | import { HttpClientTestingModule, | ||
| + | |||
| + | // Other imports | ||
| + | import { TestBed } from ' | ||
| + | import { HttpClient, HttpErrorResponse } from ' | ||
| + | </ | ||
| + | |||
| + | 그런 다음 HttpClientTestingModule을 TestBed에 추가하고 // | ||
| + | <code javascript> | ||
| + | describe(' | ||
| + | let httpClient: HttpClient; | ||
| + | let httpTestingController: | ||
| + | |||
| + | beforeEach(() => { | ||
| + | TestBed.configureTestingModule({ | ||
| + | imports: [ HttpClientTestingModule ] | ||
| + | }); | ||
| + | |||
| + | // Inject the http service and test controller for each test | ||
| + | httpClient = TestBed.get(HttpClient); | ||
| + | httpTestingController = TestBed.get(HttpTestingController); | ||
| + | }); | ||
| + | /// Tests begin /// | ||
| + | }); | ||
| + | </ | ||
| + | 이제 테스트 과정에서 요청이 정상 백엔드가 아닌 테스트 백엔드에 부딪칩니다. | ||
| + | |||
| + | 또한이 설정은 TestBed.get ()을 호출하여 테스트 중에 참조 될 수 있도록 HttpClient 서비스 및 조롱 컨트롤러를 주입합니다. | ||
| + | |||
| + | ==== Expecting and answering requests ==== | ||
| + | 이제 GET 요청이 발생할 것으로 예상되는 테스트를 작성하고 모의 응답을 제공 할 수 있습니다. | ||
| + | <code javascript> | ||
| + | it(' | ||
| + | const testData: Data = {name: 'Test Data' | ||
| + | |||
| + | // Make an HTTP GET request | ||
| + | httpClient.get< | ||
| + | .subscribe(data => | ||
| + | // When observable resolves, result should match test data | ||
| + | expect(data).toEqual(testData) | ||
| + | ); | ||
| + | |||
| + | // The following `expectOne()` will match the request' | ||
| + | // If no requests or multiple requests matched that URL | ||
| + | // `expectOne()` would throw. | ||
| + | const req = httpTestingController.expectOne('/ | ||
| + | |||
| + | // Assert that the request is a GET. | ||
| + | expect(req.request.method).toEqual(' | ||
| + | |||
| + | // Respond with mock data, causing Observable to resolve. | ||
| + | // Subscribe callback asserts that correct data was returned. | ||
| + | req.flush(testData); | ||
| + | |||
| + | // Finally, assert that there are no outstanding requests. | ||
| + | httpTestingController.verify(); | ||
| + | }); | ||
| + | </ | ||
| + | 마지막 단계는 요청이 아직 해결되지 않았 음을 확인하는 것으로, | ||
| + | <code javascript> | ||
| + | afterEach(() => { | ||
| + | // After every test, assert that there are no more pending requests. | ||
| + | httpTestingController.verify(); | ||
| + | }); | ||
| + | </ | ||
| + | |||
| + | === Custom request expectations === | ||
| + | URL로 일치하는 것만으로 충분하지 않으면 자체 매칭 기능을 구현할 수 있습니다. 예를 들어, 인증 헤더가있는 발신 요청을 찾을 수 있습니다. | ||
| + | <code javascript> | ||
| + | // Expect one request with an authorization header | ||
| + | const req = httpTestingController.expectOne( | ||
| + | req => req.headers.has(' | ||
| + | ); | ||
| + | </ | ||
| + | 이전 expectOne()과 마찬가지로 0 또는 2 개 이상의 요청이이 조건을 충족하면 테스트가 실패합니다. | ||
| + | |||
| + | === Handling more than one request === | ||
| + | 테스트에서 중복 요청에 응답해야하는 경우 expectOne () 대신 match () API를 사용하십시오. 동일한 인수를 사용하지만 일치하는 요청의 배열을 반환합니다. 일단 반환되면 이러한 요청은 나중에 일치하는 것으로부터 제거되며 사용자는이를 플러시하고 확인해야합니다. | ||
| + | <code javascript> | ||
| + | // get all pending requests that match the given URL | ||
| + | const requests = httpTestingController.match(testUrl); | ||
| + | expect(requests.length).toEqual(3); | ||
| + | |||
| + | // Respond to each request with different results | ||
| + | requests[0].flush([]); | ||
| + | requests[1].flush([testData[0]]); | ||
| + | requests[2].flush(testData); | ||
| + | </ | ||
| + | |||
| + | ==== Testing for errors ==== | ||
| + | 실패한 HTTP 요청에 대해 앱의 방어를 테스트해야합니다. | ||
| + | |||
| + | 다음 예제와 같이 request.flush ()를 호출하여 오류 메시지를 표시합니다. | ||
| + | <code javascript> | ||
| + | it(' | ||
| + | const emsg = ' | ||
| + | |||
| + | httpClient.get< | ||
| + | data => fail(' | ||
| + | (error: HttpErrorResponse) => { | ||
| + | expect(error.status).toEqual(404, | ||
| + | expect(error.error).toEqual(emsg, | ||
| + | } | ||
| + | ); | ||
| + | |||
| + | const req = httpTestingController.expectOne(testUrl); | ||
| + | |||
| + | // Respond with mock error | ||
| + | req.flush(emsg, | ||
| + | }); | ||
| + | </ | ||
| + | |||
| + | 또는 ErrorEvent와 함께 request.error()를 호출 할 수 있습니다. | ||
| + | |||
| + | <code javascript> | ||
| + | it(' | ||
| + | const emsg = ' | ||
| + | |||
| + | httpClient.get< | ||
| + | data => fail(' | ||
| + | (error: HttpErrorResponse) => { | ||
| + | expect(error.error.message).toEqual(emsg, | ||
| + | } | ||
| + | ); | ||
| + | |||
| + | const req = httpTestingController.expectOne(testUrl); | ||
| + | |||
| + | // Create mock ErrorEvent, raised when something goes wrong at the network level. | ||
| + | // Connection timeout, DNS error, offline, etc | ||
| + | const mockError = new ErrorEvent(' | ||
| + | message: emsg, | ||
| + | }); | ||
| + | |||
| + | // Respond with mock error | ||
| + | req.error(mockError); | ||
| + | }); | ||
| + | </ | ||