Angular 및 RxJS를 사용하여 카드 매칭 게임을 만드는 방법
게시 됨: 2022-03-10오늘은 사용자 인터페이스의 클릭 이벤트로 인해 발생하는 데이터 스트림에 초점을 맞추고 싶습니다. 이러한 클릭스트림의 처리는 많은 이벤트를 처리해야 하는 집중적인 사용자 상호작용이 있는 애플리케이션에 특히 유용합니다. 또한 RxJS를 조금 더 소개하고 싶습니다. 반응형 스타일로 이벤트 처리 루틴을 간결하고 간결하게 표현하는 데 사용할 수 있는 JavaScript 라이브러리입니다.
우리는 무엇을 만들고 있습니까?
학습 게임과 지식 퀴즈는 젊은 사용자와 나이든 사용자 모두에게 인기가 있습니다. 예를 들어 사용자가 이미지 및/또는 텍스트 조각의 혼합에서 관련 쌍을 찾아야 하는 게임 "페어 매칭"이 있습니다.
아래 애니메이션은 게임의 간단한 버전을 보여줍니다. 사용자는 경기장의 왼쪽과 오른쪽에 있는 두 요소를 순서에 상관없이 차례로 선택합니다. 올바르게 일치하는 쌍은 경기장의 별도 영역으로 이동되는 반면 잘못된 할당은 즉시 해제되어 사용자가 새로 선택해야 합니다.

이 튜토리얼에서는 그러한 학습 게임을 단계별로 구축할 것입니다. 첫 번째 부분에서는 게임의 경기장을 보여주는 Angular 구성 요소를 빌드합니다. 우리의 목표는 동물 퀴즈에서 언어 학습 앱의 어휘 트레이너에 이르기까지 다양한 사용 사례와 대상 그룹에 대해 구성 요소를 구성할 수 있다는 것입니다. 이를 위해 Angular는 사용자 정의 가능한 템플릿을 사용하여 콘텐츠 프로젝션 개념을 제공하며 이를 사용할 것입니다. 원리를 설명하기 위해 레이아웃이 다른 두 가지 버전의 게임("game1" 및 "game2")을 빌드합니다.
튜토리얼의 두 번째 부분에서는 반응형 프로그래밍에 중점을 둘 것입니다. 쌍이 일치할 때마다 사용자는 앱에서 일종의 피드백을 받아야 합니다. 라이브러리 RxJS의 도움으로 실현되는 것은 이 이벤트 처리입니다.
- 요구 사항
이 자습서를 따르려면 Angular CLI가 설치되어 있어야 합니다. - 소스 코드
이 튜토리얼의 소스 코드는 여기(14KB)에서 찾을 수 있습니다.
1. 학습 게임을 위한 Angular 구성 요소 구축
기본 프레임워크를 만드는 방법
먼저 "learning-app"이라는 새 프로젝트를 생성해 보겠습니다. Angular CLI를 사용하면 ng new learning-app
명령으로 이를 수행할 수 있습니다. app.component.html 파일에서 미리 생성된 소스 코드를 다음과 같이 바꿉니다.
<div> <h1>Learning is fun!</h1> </div>
다음 단계에서는 학습 게임을 위한 구성 요소가 생성됩니다. 이름을 "matching-game"으로 지정하고 ng generate component matching-game
명령을 사용했습니다. 그러면 필수 HTML, CSS 및 Typescript 파일이 있는 게임 구성 요소에 대한 별도의 하위 폴더가 생성됩니다.
이미 언급했듯이 교육용 게임은 다양한 목적으로 구성할 수 있어야 합니다. 이를 보여주기 위해 동일한 명령을 사용하여 두 개의 추가 구성 요소( game1
및 game2
)를 만듭니다. game1.component.html 또는 game2.component.html 파일에서 미리 생성된 코드를 다음 태그로 교체하여 게임 구성 요소를 자식 구성 요소로 추가합니다.
<app-matching-game></app-matching-game>
처음에는 구성 요소 game1
만 사용합니다. 응용 프로그램을 시작한 직후 게임 1이 표시되도록 하기 위해 이 태그를 app.component.html 파일에 추가합니다.
<app-game1></app-game1>
ng serve --open
으로 응용 프로그램을 시작하면 브라우저에 "matching-game works" 메시지가 표시됩니다. (현재 이것은 matching-game.component.html 의 유일한 콘텐츠입니다.)
이제 데이터를 테스트해야 합니다. /app
폴더에서 pair.ts 라는 파일을 만들고 여기서 Pair
클래스를 정의합니다.
export class Pair { leftpart: string; rightpart: string; id: number; }
쌍 개체는 두 개의 관련 텍스트( leftpart
및 rightpart
)와 ID로 구성됩니다.
첫 번째 게임은 종(예: dog
)이 적절한 동물 등급(예: mammal
)에 할당되어야 하는 종 퀴즈로 되어 있습니다.
Animals.ts 파일에서 테스트 데이터로 배열을 정의합니다.
import { Pair } from './pair'; export const ANIMALS: Pair[] = [ { id: 1, leftpart: 'dog', rightpart: 'mammal'}, { id: 2, leftpart: 'blickbird', rightpart: 'bird'}, { id: 3, leftpart: 'spider', rightpart: 'insect'}, { id: 4, leftpart: 'turtle', rightpart: 'reptile' }, { id: 5, leftpart: 'guppy', rightpart: 'fish'}, ];
구성 요소 game1
은 테스트 데이터에 액세스해야 합니다. 그들은 재산 animals
에 저장됩니다. game1.component.ts 파일에는 이제 다음 내용이 포함됩니다.
import { Component, OnInit } from '@angular/core'; import { ANIMALS } from '../animals'; @Component({ selector: 'app-game1', templateUrl: './game1.component.html', styleUrls: ['./game1.component.css'] }) export class Game1Component implements OnInit { animals = ANIMALS; constructor() { } ngOnInit() { } }
게임 구성 요소의 첫 번째 버전
우리의 다음 목표: 게임 구성 요소 matching-game
은 상위 구성 요소(예: game1
)의 게임 데이터를 입력으로 받아들여야 합니다. 입력은 "쌍" 개체의 배열입니다. 게임의 사용자 인터페이스는 응용 프로그램을 시작할 때 전달된 개체로 초기화되어야 합니다.

이를 위해 다음과 같이 진행해야 합니다.
-
@Input
데코레이터를 사용하여 게임 구성 요소에 속성pairs
을 추가합니다. - 구성 요소의 추가 개인 속성으로
solvedPairs
및unsolvedPairs
배열을 추가합니다. (이미 "해결된" 쌍과 "아직 해결되지 않은" 쌍을 구별할 필요가 있습니다.) - 응용 프로그램이 시작될 때(
ngOnInit
함수 참조) 모든 쌍은 여전히 "해결되지 않은" 상태이므로unsolvedPairs
배열로 이동됩니다.
import { Component, OnInit, Input } from '@angular/core'; import { Pair } from '../pair'; @Component({ selector: 'app-matching-game', templateUrl: './matching-game.component.html', styleUrls: ['./matching-game.component.css'] }) export class MatchingGameComponent implements OnInit { @Input() pairs: Pair[]; private solvedPairs: Pair[] = []; private unsolvedPairs: Pair[] = []; constructor() { } ngOnInit() { for(let i=0; i<this.pairs.length; i++){ this.unsolvedPairs.push(this.pairs[i]); } } }
또한 matching-game
컴포넌트의 HTML 템플릿을 정의합니다. 미해결 쌍과 해결 쌍을 위한 컨테이너가 있습니다. ngIf
지시문은 해결되지 않았거나 해결된 쌍이 하나 이상 존재하는 경우에만 해당 컨테이너가 표시되도록 합니다.
해결되지 않은 쌍의 컨테이너(클래스 container unsolved
)에는 먼저 쌍의 구성 요소가 모두 나열되고(위 GIF의 left
프레임 참조) 다음으로 모두 right
(GIF의 오른쪽 프레임 참조) 구성 요소가 나열됩니다. (저는 쌍을 나열하기 위해 ngFor
지시문을 사용합니다.) 현재로서는 간단한 버튼으로 템플릿으로 충분합니다.
템플릿 표현식 {{{pair.leftpart}}
및 { {{pair.rightpart}}}
를 사용하면 pair
배열을 반복할 때 개별 쌍 객체의 leftpart
및 rightpart
속성 값이 쿼리됩니다. 생성된 버튼의 레이블로 사용됩니다.
할당된 쌍은 두 번째 컨테이너(클래스 container solved
)에 나열됩니다. 녹색 막대(클래스 connector
)는 함께 속해 있음을 나타냅니다.
matching-game.component.css 파일의 해당 CSS 코드는 기사 시작 부분의 소스 코드에서 찾을 수 있습니다.
<div> <div class="container unsolved" *ngIf="unsolvedPairs.length>0"> <div class="pair_items left"> <button *ngFor="let pair of unsolvedPairs" class="item"> {{pair.leftpart}} </button> </div> <div class="pair_items right"> <button *ngFor="let pair of unsolvedPairs" class="item"> {{pair.rightpart}} </button> </div> </div> <div class="container solved" *ngIf="solvedPairs.length>0"> <div *ngFor="let pair of solvedPairs" class="pair"> <button>{{pair.leftpart}}</button> <div class="connector"></div> <button>{{pair.rightpart}}</button> </div> </div> </div>
구성 요소 game1
에서 배열 animals
은 이제 구성 요소 matching-game
의 pairs
속성에 바인딩됩니다(단방향 데이터 바인딩).
<app-matching-game [pairs]="animals"></app-matching-game>
결과는 아래 이미지에 나와 있습니다.

분명히, 우리의 짝짓기 게임은 아직 그렇게 어렵지 않습니다. 왜냐하면 쌍의 왼쪽과 오른쪽 부분이 서로 직접 반대이기 때문입니다. 페어링이 너무 간단하지 않도록 올바른 부품을 혼합해야 합니다. 오른쪽에 있는 unsolvedPairs
배열에 적용하는 자체 정의 파이프 shuffle
문제를 해결합니다(나중에 파이프를 강제로 업데이트하려면 매개변수 test
가 필요함).
... <div class="pair_items right"> <button *ngFor="let pair of unsolvedPairs | shuffle:test" class="item"> {{pair.rightpart}} </button> </div> ...
파이프의 소스 코드는 앱 폴더의 shuffle.pipe.ts 파일에 저장됩니다(문서 시작 부분의 소스 코드 참조). 또한 파이프를 가져와 모듈 선언에 나열해야 하는 app.module.ts 파일도 확인하세요. 이제 원하는 보기가 브라우저에 나타납니다.
확장 버전: 맞춤형 템플릿을 사용하여 게임의 개별 디자인 허용
버튼 대신 임의의 템플릿 스니펫을 지정하여 게임을 사용자 정의할 수 있어야 합니다. matching-game.component.html 파일에서 게임의 왼쪽과 오른쪽에 대한 버튼 템플릿을 ng-template
태그로 바꿉니다. 그런 다음 템플릿 참조의 이름을 ngTemplateOutlet
속성에 할당합니다. 이렇게 하면 보기를 렌더링할 때 해당 템플릿 참조의 내용으로 대체되는 두 개의 자리 표시자가 제공됩니다.
우리는 여기에서 콘텐츠 투영 의 개념을 다루고 있습니다. 구성 요소 템플릿의 특정 부분은 외부에서 제공되고 표시된 위치에서 템플릿으로 "투영"됩니다.
뷰를 생성할 때 Angular는 게임 데이터를 템플릿에 삽입해야 합니다. ngTemplateOutletContext
매개변수를 사용하여 ngFor
지시문에서 pair
변수의 현재 값을 할당해야 하는 템플릿 내에서 contextPair
변수가 사용됨을 Angular에 알립니다.
다음 목록은 unsolved
컨테이너의 대체품을 보여줍니다. solved
된 컨테이너에서 버튼도 ng-template
태그로 교체되어야 합니다.
<div class="container unsolved" *ngIf="unsolvedPairs.length>0"> <div class="pair_items left"> <div *ngFor="let pair of unsolvedPairs" class="item"> <ng-template [ngTemplateOutlet]="leftpart_temp" [ngTemplateOutletContext]="{contextPair: pair}"> </ng-template> </div> </div> <div class="pair_items right"> <div *ngFor="let pair of unsolvedPairs | shuffle:test" class="item"> <ng-template [ngTemplateOutlet]="leftpart_temp" [ngTemplateOutletContext]="{contextPair: pair}"> </ng-template> </div> </div> </div> ...
matching-game.component.ts 파일에서 두 템플릿 참조( leftpart_temp
및 rightpart_temp
)의 변수를 선언해야 합니다. 데코레이터 @ContentChild
는 이것이 콘텐츠 프로젝션임을 나타냅니다. 즉, Angular는 이제 각각의 선택기( leftpart
또는 rightpart
)가 있는 두 개의 템플릿 스니펫이 <app-matching-game></app-matching-game>
호스트 요소의 <app-matching-game></app-matching-game>
( @ViewChild
참조).
@ContentChild('leftpart', {static: false}) leftpart_temp: TemplateRef<any>; @ContentChild('rightpart', {static: false}) rightpart_temp: TemplateRef<any>;
잊지 마세요: ContentChild
및 TemplateRef
유형은 핵심 패키지에서 가져와야 합니다.
부모 구성 요소 game1
에서 선택기 leftpart
및 rightpart
가 있는 두 개의 필수 템플릿 스니펫이 이제 삽입됩니다.
간단하게 하기 위해 여기에서 버튼을 다시 사용하겠습니다.
<app-matching-game [pairs]="animals"> <ng-template #leftpart let-animalPair="contextPair"> <button>{{animalPair.leftpart}}</button> </ng-template> <ng-template #rightpart let-animalPair="contextPair"> <button>{{animalPair.rightpart}}</button> </ng-template> </app-matching-game>
let-animalPair="contextPair"
속성은 컨텍스트 변수인 contextPair
가 이름이 animalPair
인 템플릿 스니펫에서 사용됨을 지정하는 데 사용됩니다.
이제 템플릿 스니펫을 원하는 대로 변경할 수 있습니다. 이것을 보여주기 위해 나는 구성요소 game2
를 사용합니다. game2.component.ts 파일은 game1.component.ts 와 동일한 내용을 가져 옵니다 . game2.component.html 에서는 버튼 대신 개별적으로 디자인된 div
요소를 사용합니다. CSS 클래스는 game2.component.css 파일에 저장됩니다.
<app-matching-game [pairs]="animals"> <ng-template #leftpart let-animalPair="contextPair"> <div class="myAnimal left">{{animalPair.leftpart}}</div> </ng-template> <ng-template #rightpart let-animalPair="contextPair"> <div class="myAnimal right">{{animalPair.rightpart}}</div> </ng-template> </app-matching-game>
홈페이지 app.component.html 에 <app-game2></app-game2>
태그를 추가한 후 애플리케이션을 시작하면 두 번째 버전의 게임이 나타납니다.


game2
에서 게임의 다른 보기 디자인 가능성은 이제 거의 무제한입니다. 예를 들어, 추가 속성을 포함하는 Pair
의 하위 클래스를 정의하는 것이 가능합니다. 예를 들어, 이미지 주소는 왼쪽 및/또는 오른쪽 부분에 대해 저장할 수 있습니다. 이미지는 텍스트와 함께 또는 텍스트 대신 템플릿에 표시될 수 있습니다.
2. RxJS와 사용자 상호 작용 제어
RxJS를 사용한 반응형 프로그래밍의 장점
응용 프로그램을 대화형 게임으로 전환하려면 사용자 인터페이스에서 트리거되는 이벤트(예: 마우스 클릭 이벤트)를 처리해야 합니다. 반응형 프로그래밍에서는 "스트림"이라고 하는 연속적인 이벤트 시퀀스가 고려됩니다. 스트림은 관찰될 수 있습니다("관찰 가능"). 즉, 스트림을 구독하는 하나 이상의 "관찰자" 또는 "구독자"가 있을 수 있습니다. 스트림의 모든 새 값에 대해 (일반적으로 비동기식으로) 알림을 받고 특정 방식으로 이에 반응할 수 있습니다.
이 접근 방식을 사용하면 응용 프로그램 부분 간의 낮은 수준의 결합을 달성할 수 있습니다. 기존 옵저버와 옵저버블은 서로 독립적이며 이들의 결합은 런타임에 변경될 수 있습니다.
JavaScript 라이브러리 RxJS는 Observer 디자인 패턴의 성숙한 구현을 제공합니다. 또한 RxJS에는 스트림을 변환(예: 필터, 맵)하거나 새 스트림으로 결합(예: 병합, 연결)하기 위한 수많은 연산자가 포함되어 있습니다. 연산자는 함수형 프로그래밍의 의미에서 "순수 함수"입니다. 연산자는 부작용을 일으키지 않으며 함수 외부의 상태에 독립적입니다. 순수 함수에 대한 호출로만 구성된 프로그램 논리는 중간 상태를 저장하기 위해 전역 또는 지역 보조 변수가 필요하지 않습니다. 이는 차례로 상태 비저장 및 느슨하게 결합된 코드 블록의 생성을 촉진합니다. 따라서 스트림 연산자의 영리한 조합으로 이벤트 처리의 많은 부분을 실현하는 것이 바람직합니다. 이에 대한 예는 매칭 게임을 기반으로 한 다음 섹션에 나와 있습니다.
Angular 구성 요소의 이벤트 처리에 RxJS 통합
Angular 프레임워크는 RxJS 라이브러리의 클래스와 함께 작동합니다. 따라서 RxJS는 Angular가 설치될 때 자동으로 설치됩니다.
아래 이미지는 고려 사항에서 역할을 하는 주요 클래스와 기능을 보여줍니다.

클래스 이름 | 함수 |
---|---|
관찰 가능(RxJS) | 스트림을 나타내는 기본 클래스. 즉, 연속적인 데이터 시퀀스입니다. 옵저버블을 구독할 수 있습니다. pipe 함수는 관찰 가능한 인스턴스에 하나 이상의 연산자 함수를 적용하는 데 사용됩니다. |
제목(RxJS) | Observable의 하위 클래스는 스트림에 새 데이터를 게시하는 다음 함수를 제공합니다. |
EventEmitter(각도) | 이것은 일반적으로 @Output 데코레이터와 함께 사용하여 구성 요소 출력을 정의하는 각도별 하위 클래스입니다. 다음 함수와 마찬가지로 emit 함수는 구독자에게 데이터를 보내는 데 사용됩니다. |
구독(RxJS) | Observable의 subscribe 함수는 구독 인스턴스를 반환합니다. 구성요소를 사용한 후 구독을 취소해야 합니다. |
이러한 클래스의 도움으로 우리는 게임에서 사용자 상호 작용을 구현하려고 합니다. 첫 번째 단계는 왼쪽 또는 오른쪽에서 사용자가 선택한 요소가 시각적으로 강조 표시되는지 확인하는 것입니다.
요소의 시각적 표현은 상위 구성요소에 있는 두 개의 템플릿 스니펫에 의해 제어됩니다. 따라서 선택한 상태에서 표시되는 방식에 대한 결정은 상위 구성 요소에 맡겨야 합니다. 왼쪽 또는 오른쪽에서 선택을 하거나 선택을 취소하는 즉시 적절한 신호를 수신해야 합니다.
이를 위해 matching-game.component.ts 파일에서 EventEmitter
유형의 네 가지 출력 값을 정의합니다. Output
및 EventEmitter
유형은 코어 패키지에서 가져와야 합니다.
@Output() leftpartSelected = new EventEmitter<number>(); @Output() rightpartSelected = new EventEmitter<number>(); @Output() leftpartUnselected = new EventEmitter(); @Output() rightpartUnselected = new EventEmitter();
matching-game.component.html 템플릿에서 좌우의 mousedown
이벤트에 반응하여 선택된 아이템의 ID를 모든 수신자에게 보낸다.
<div *ngFor="let pair of unsolvedPairs" class="item" (mousedown)="leftpartSelected.emit(pair.id)"> ... <div *ngFor="let pair of unsolvedPairs | shuffle:test" class="item" (mousedown)="rightpartSelected.emit(pair.id)">
우리의 경우 수신기는 구성 요소 game1
및 game2
입니다. 여기에서 이제 leftpartSelected
, rightpartSelected
, leftpartUnselected
및 rightpartUnselected
이벤트에 대한 이벤트 처리를 정의할 수 있습니다. $event
변수는 내보낸 출력 값(이 경우 ID)을 나타냅니다. 다음에서 game1.component.html 에 대한 목록을 볼 수 있으며 game2.component.html 에도 동일한 변경 사항이 적용됩니다.
<app-matching-game [pairs]="animals" (leftpartSelected)="onLeftpartSelected($event)" (rightpartSelected)="onRightpartSelected($event)" (leftpartUnselected)="onLeftpartUnselected()" (rightpartUnselected)="onRightpartUnselected()"> <ng-template #leftpart let-animalPair="contextPair"> <button [class.selected]="leftpartSelectedId==animalPair.id"> {{animalPair.leftpart}} </button> </ng-template> <ng-template #rightpart let-animalPair="contextPair"> <button [class.selected]="rightpartSelectedId==animalPair.id"> {{animalPair.rightpart}} </button> </ng-template> </app-matching-game>
game1.component.ts 에서 (그리고 game2.component.ts 에서도 유사하게) 이제 event
핸들러 기능이 구현되었습니다. 선택한 요소의 ID를 저장합니다. HTML 템플릿(위 참조)에서 이러한 요소에는 selected
클래스가 할당됩니다. CSS 파일 game1.component.css 는 이 클래스가 가져올 시각적 변경(예: 색상 또는 글꼴 변경)을 정의합니다. 선택 항목 재설정(선택 취소)은 쌍 개체가 항상 양수 ID를 갖는다는 가정을 기반으로 합니다.
onLeftpartSelected(id:number):void{ this.leftpartSelectedId = id; } onRightpartSelected(id:number):void{ this.rightpartSelectedId = id; } onLeftpartUnselected():void{ this.leftpartSelectedId = -1; } onRightpartUnselected():void{ this.rightpartSelectedId = -1; }
다음 단계에서는 일치하는 게임 구성 요소에서 이벤트 처리가 필요합니다. 할당이 올바른지, 즉 왼쪽으로 선택한 요소가 오른쪽으로 선택한 요소와 일치하는지 확인해야 합니다. 이 경우 할당된 쌍을 해결된 쌍의 컨테이너로 이동할 수 있습니다.
RxJS 연산자를 사용하여 평가 논리를 공식화하고 싶습니다(다음 섹션 참조). 준비를 위해 matching-game.component.ts 에 주제 assignmentStream
을 생성합니다. 왼쪽 또는 오른쪽에서 사용자가 선택한 요소를 방출해야 합니다. 목표는 RxJS 연산자를 사용하여 스트림을 수정하고 분할하여 두 개의 새 스트림을 얻는 것입니다. 한 스트림은 올바르게 할당된 쌍을 제공하는 solvedStream
스트림과 잘못된 할당을 제공하는 두 번째 스트림 failedStream
입니다. 각각의 경우에 적절한 이벤트 처리를 수행할 수 있도록 구독으로 이 두 스트림을 subscribe
하고 싶습니다.
또한 생성된 구독 개체에 대한 참조가 필요하므로 게임을 종료할 때 "구독 취소"로 구독을 취소할 수 있습니다( ngOnDestroy
참조). Subject
및 Subscription
클래스는 "rxjs" 패키지에서 가져와야 합니다.
private assignmentStream = new Subject<{pair:Pair, side:string}>(); private solvedStream = new Observable<Pair>(); private failedStream = new Observable<string>(); private s_Subscription: Subscription; private f_Subscription: Subscription; ngOnInit(){ ... //TODO: apply stream-operators on //assignmentStream this.s_Subscription = this.solvedStream.subscribe(pair => handleSolvedAssignment(pair)); this.f_Subscription = this.failedStream.subscribe(() => handleFailedAssignment()); } ngOnDestroy() { this.s_Subscription.unsubscribe(); this.f_Subscription.unsubscribe(); }
할당이 정확하면 다음 단계가 수행됩니다.
- 할당된 쌍은 해결된 쌍의 컨테이너로 이동됩니다.
-
leftpartUnselected
및rightpartUnselected
이벤트는 상위 구성 요소로 전송됩니다.
할당이 올바르지 않으면 쌍이 이동되지 않습니다. 왼쪽에서 오른쪽으로 잘못된 할당이 실행된 경우( side1
값이 left
) 왼쪽에 있는 요소에 대한 선택을 취소해야 합니다(기사 시작 부분의 GIF 참조). 오른쪽에서 왼쪽으로 할당하면 오른쪽 요소에 대한 선택이 취소됩니다. 이는 클릭한 마지막 요소가 선택된 상태로 남아 있음을 의미합니다.
두 경우 모두 해당 처리기 함수 handleSolvedAssignment
및 handleFailedAssignment
를 준비합니다(함수 제거: 이 기사 끝에 있는 소스 코드 참조).
private handleSolvedAssignment(pair: Pair):void{ this.solvedPairs.push(pair); this.remove(this.unsolvedPairs, pair); this.leftpartUnselected.emit(); this.rightpartUnselected.emit(); //workaround to force update of the shuffle pipe this.test = Math.random() * 10; } private handleFailedAssignment(side1: string):void{ if(side1=="left"){ this.leftpartUnselected.emit(); }else{ this.rightpartUnselected.emit(); } }
이제 데이터를 구독하는 소비자에서 데이터를 생성하는 생산자로 관점을 변경해야 합니다. matching-game.component.html 파일에서 요소를 클릭할 때 연결된 쌍 개체가 assignmentStream
스트림 스트림으로 푸시되는지 확인합니다. 할당 순서는 중요하지 않기 때문에 왼쪽과 오른쪽에 공통 스트림을 사용하는 것이 좋습니다.
<div *ngFor="let pair of unsolvedPairs" class="item" (mousedown)="leftpartSelected.emit(pair.id)" (click)="assignmentStream.next({pair: pair, side: 'left'})"> ... <div *ngFor="let pair of unsolvedPairs | shuffle:test" class="item" (mousedown)="rightpartSelected.emit(pair.id)" (click)="assignmentStream.next({pair: pair, side: 'right'})">
RxJS 운영자와의 게임 상호 작용 설계
남은 것은 assignmentStream
스트림 스트림을 solvedStream
및 failedStream
스트림으로 변환하는 것뿐입니다. 다음 연산자를 순서대로 적용합니다.
pairwise
할당에는 항상 두 쌍이 있습니다. pairwise
연산자는 스트림에서 데이터를 쌍으로 선택합니다. 현재 값과 이전 값이 쌍으로 결합됩니다.
다음 스트림에서…
„{pair1, left}, {pair3, right}, {pair2, left}, {pair2, right}, {pair1, left}, {pair1, right}“
...이 새 스트림의 결과:
„({pair1, left}, {pair3, right}), ({pair3, right}, {pair2, left}), ({pair2, left}, {pair2, right}), ({pair2, right}, {pair1, left}), ({pair1, left}, {pair1, right})“
예를 들어, 사용자가 왼쪽에서 dog
(id=1)를 선택하고 오른쪽에서 insect
(id=3)을 선택할 때 조합 ({pair1, left}, {pair3, right})
을 얻습니다(배열 ANIMALS
참조). 기사의 시작). 이러한 조합과 다른 조합은 위의 GIF에 표시된 게임 시퀀스의 결과입니다.
filter
({pair1, left}, {pair1, left})
또는 ({pair1, left}, {pair4, left})
와 같이 경기장의 같은 쪽에 만들어진 스트림에서 모든 조합을 제거해야 합니다.
따라서 조합 comb
에 대한 필터 조건은 comb[0].side != comb[1].side
입니다.
partition
이 연산자는 스트림과 조건을 취하여 이로부터 두 개의 스트림을 생성합니다. 첫 번째 스트림에는 조건을 충족하는 데이터가 포함되고 두 번째 스트림에는 나머지 데이터가 포함됩니다. 우리의 경우 스트림에는 정확하거나 잘못된 할당이 포함되어야 합니다. 따라서 조합 comb
의 조건은 comb[0].pair===comb[1].pair
입니다.
예제는 다음을 포함하는 "올바른" 스트림을 생성합니다.
({pair2, left}, {pair2, right}), ({pair1, left}, {pair1, right})
및 "잘못된"스트림
({pair1, left}, {pair3, right}), ({pair3, right}, {pair2, left}), ({pair2, right}, {pair1, left})
map
pair2
와 같은 올바른 할당을 추가 처리하려면 개별 쌍 개체만 필요합니다. 맵 연산자는 조합 comb
이 comb[0].pair
에 매핑되어야 함을 표현하는 데 사용할 수 있습니다. 할당이 올바르지 않으면 선택이 side 로 지정된 side
재설정되어야 하므로 조합 comb
이 문자열 comb[0].side
에 매핑됩니다.
pipe
함수는 위의 연산자를 연결하는 데 사용됩니다. 연산자 pairwise
, filter
, partition
, map
은 rxjs/operators
패키지에서 가져와야 합니다.
ngOnInit() { ... const stream = this.assignmentStream.pipe( pairwise(), filter(comb => comb[0].side != comb[1].side) ); //pipe notation leads to an error message (Angular 8.2.2, RxJS 6.4.0) const [stream1, stream2] = partition(comb => comb[0].pair === comb[1].pair)(stream); this.solvedStream = stream1.pipe( map(comb => comb[0].pair) ); this.failedStream = stream2.pipe( map(comb => comb[0].side) ); this.s_Subscription = this.solvedStream.subscribe(pair => this.handleSolvedAssignment(pair)); this.f_Subscription = this.failedStream.subscribe(side => this.handleFailedAssignment(side)); }
이제 게임이 이미 작동합니다!

연산자를 사용하여 게임 논리를 선언적으로 설명할 수 있습니다. 우리는 두 개의 대상 스트림(쌍으로 결합, 필터링, 분할, 다시 매핑)의 속성만 설명했으며 이러한 작업의 구현에 대해 걱정할 필요가 없었습니다. 직접 구현했다면 구성 요소에 중간 상태도 저장해야 했을 것입니다(예: 왼쪽과 오른쪽에서 마지막으로 클릭한 항목에 대한 참조). 대신, RxJS 연산자는 구현 로직과 우리에게 필요한 상태를 캡슐화하여 프로그래밍을 더 높은 수준의 추상화로 끌어 올립니다.
결론
간단한 학습 게임을 예로 사용하여 Angular 구성 요소에서 RxJS 사용을 테스트했습니다. 반응적 접근 방식은 사용자 인터페이스에서 발생하는 이벤트를 처리하는 데 적합합니다. RxJS를 사용하면 이벤트 처리에 필요한 데이터를 스트림으로 편리하게 정렬할 수 있습니다. filter
, map
또는 partition
과 같은 수많은 연산자를 스트림 변환에 사용할 수 있습니다. 결과 스트림에는 최종 형식으로 준비되고 직접 구독할 수 있는 데이터가 포함됩니다. 각각의 경우에 적합한 연산자를 선택하고 효율적으로 연결하려면 약간의 기술과 경험이 필요합니다. 이 기사는 이에 대한 소개를 제공해야 합니다.
추가 리소스
- Andre Staltz가 쓴 "당신이 놓치고 있는 반응형 프로그래밍 입문서"
SmashingMag 관련 읽기 :
- Angular로 이미지 중단점 관리하기
- 부트스트랩으로 Angular 애플리케이션 스타일링하기
- Angular Material 애플리케이션을 만들고 배포하는 방법