앱에 검색 기능 추가하기

이번 실습에서는 Angular 앱에 검색 기능을 추가하는 방법을 알아봅니다.

사용자는 앱에서 제공하는 데이터를 검색할 수 있으며, 앱에는 입력한 검색어에 맞는 결과만 표시됩니다.

IMPORTANT: 이 실습은 로컬 환경에서 진행하는 것을 권장합니다.

학습 목표

  • 폼의 입력값으로 조건에 맞는 주택을 검색합니다.
  • 검색 조건에 맞는 주택만 표시합니다.
  1. Home 컴포넌트의 프로퍼티 수정하기

    이 단계에서는 필터링에 사용할 새 배열 프로퍼티를 Home 클래스에 추가합니다.

    1. src/app/home/home.ts의 클래스에 filteredLocationList 프로퍼티를 추가하세요.

      home.ts에 filteredLocationList 프로퍼티 추가하기

      import {ChangeDetectorRef, Component, inject} from '@angular/core';
      import {HousingLocation} from '../housing-location/housing-location';
      import {HousingLocationInfo} from '../housinglocation';
      import {HousingService} from '../housing.service';
      
      @Component({
        selector: 'app-home',
        imports: [HousingLocation],
        template: `
          <section>
            <form>
              <input type="text" placeholder="Filter by city" #filter />
              <button class="primary" type="button" (click)="filterResults(filter.value)">Search</button>
            </form>
          </section>
          <section class="results">
            @for (housingLocation of filteredLocationList; track $index) {
              <app-housing-location [housingLocation]="housingLocation" />
            }
          </section>
        `,
        styleUrls: ['./home.css'],
      })
      export class Home {
        private readonly changeDetectorRef = inject(ChangeDetectorRef);
        housingLocationList: HousingLocationInfo[] = [];
        housingService: HousingService = inject(HousingService);
        filteredLocationList: HousingLocationInfo[] = [];
      
        constructor() {
          this.housingLocationList = this.housingService.getAllHousingLocations();
          this.filteredLocationList = this.housingLocationList;
        }
      
        filterResults(text: string) {
          if (!text) {
            this.filteredLocationList = this.housingLocationList;
            return;
          }
      
          this.filteredLocationList = this.housingLocationList.filter((housingLocation) =>
            housingLocation?.city.toLowerCase().includes(text.toLowerCase()),
          );
        }
      }

      filteredLocationList에는 사용자가 입력한 검색 조건에 맞는 값이 저장됩니다.

    2. 페이지를 처음 불러왔을 때는 filteredLocationList에 전체 주택 목록이 있어야 합니다. Home의 constructor에서 이 값을 설정하세요.

      filteredLocationList 값 설정하기

      import {ChangeDetectorRef, Component, inject} from '@angular/core';
      import {HousingLocation} from '../housing-location/housing-location';
      import {HousingLocationInfo} from '../housinglocation';
      import {HousingService} from '../housing.service';
      
      @Component({
        selector: 'app-home',
        imports: [HousingLocation],
        template: `
          <section>
            <form>
              <input type="text" placeholder="Filter by city" #filter />
              <button class="primary" type="button" (click)="filterResults(filter.value)">Search</button>
            </form>
          </section>
          <section class="results">
            @for (housingLocation of filteredLocationList; track $index) {
              <app-housing-location [housingLocation]="housingLocation" />
            }
          </section>
        `,
        styleUrls: ['./home.css'],
      })
      export class Home {
        private readonly changeDetectorRef = inject(ChangeDetectorRef);
        housingLocationList: HousingLocationInfo[] = [];
        housingService: HousingService = inject(HousingService);
        filteredLocationList: HousingLocationInfo[] = [];
      
        constructor() {
          this.housingLocationList = this.housingService.getAllHousingLocations();
          this.filteredLocationList = this.housingLocationList;
        }
      
        filterResults(text: string) {
          if (!text) {
            this.filteredLocationList = this.housingLocationList;
            return;
          }
      
          this.filteredLocationList = this.housingLocationList.filter((housingLocation) =>
            housingLocation?.city.toLowerCase().includes(text.toLowerCase()),
          );
        }
      }
  2. Home 컴포넌트의 템플릿 수정하기

    Home에는 사용자 입력을 받을 필드가 이미 있습니다. 여기에 입력한 문자열로 결과를 필터링합니다.

    1. Home 템플릿의 input 엘리먼트에 #filter 템플릿 변수를 추가하세요.

      home.ts의 input HTML 엘리먼트에 템플릿 변수 추가하기

      import {ChangeDetectorRef, Component, inject} from '@angular/core';
      import {HousingLocation} from '../housing-location/housing-location';
      import {HousingLocationInfo} from '../housinglocation';
      import {HousingService} from '../housing.service';
      
      @Component({
        selector: 'app-home',
        imports: [HousingLocation],
        template: `
          <section>
            <form>
              <input type="text" placeholder="Filter by city" #filter />
              <button class="primary" type="button" (click)="filterResults(filter.value)">Search</button>
            </form>
          </section>
          <section class="results">
            @for (housingLocation of filteredLocationList; track $index) {
              <app-housing-location [housingLocation]="housingLocation" />
            }
          </section>
        `,
        styleUrls: ['./home.css'],
      })
      export class Home {
        private readonly changeDetectorRef = inject(ChangeDetectorRef);
        housingLocationList: HousingLocationInfo[] = [];
        housingService: HousingService = inject(HousingService);
        filteredLocationList: HousingLocationInfo[] = [];
      
        constructor() {
          this.housingLocationList = this.housingService.getAllHousingLocations();
          this.filteredLocationList = this.housingLocationList;
        }
      
        filterResults(text: string) {
          if (!text) {
            this.filteredLocationList = this.housingLocationList;
            return;
          }
      
          this.filteredLocationList = this.housingLocationList.filter((housingLocation) =>
            housingLocation?.city.toLowerCase().includes(text.toLowerCase()),
          );
        }
      }

      이 예제는 템플릿 참조 변수를 사용해 input 엘리먼트에 접근합니다.

    2. 다음으로 컴포넌트 템플릿의 "Search" 버튼에 이벤트 핸들러를 연결하세요.

      home.ts에서 버튼 클릭 이벤트를 메서드에 바인딩하기

      import {ChangeDetectorRef, Component, inject} from '@angular/core';
      import {HousingLocation} from '../housing-location/housing-location';
      import {HousingLocationInfo} from '../housinglocation';
      import {HousingService} from '../housing.service';
      
      @Component({
        selector: 'app-home',
        imports: [HousingLocation],
        template: `
          <section>
            <form>
              <input type="text" placeholder="Filter by city" #filter />
              <button class="primary" type="button" (click)="filterResults(filter.value)">Search</button>
            </form>
          </section>
          <section class="results">
            @for (housingLocation of filteredLocationList; track $index) {
              <app-housing-location [housingLocation]="housingLocation" />
            }
          </section>
        `,
        styleUrls: ['./home.css'],
      })
      export class Home {
        private readonly changeDetectorRef = inject(ChangeDetectorRef);
        housingLocationList: HousingLocationInfo[] = [];
        housingService: HousingService = inject(HousingService);
        filteredLocationList: HousingLocationInfo[] = [];
      
        constructor() {
          this.housingLocationList = this.housingService.getAllHousingLocations();
          this.filteredLocationList = this.housingLocationList;
        }
      
        filterResults(text: string) {
          if (!text) {
            this.filteredLocationList = this.housingLocationList;
            return;
          }
      
          this.filteredLocationList = this.housingLocationList.filter((housingLocation) =>
            housingLocation?.city.toLowerCase().includes(text.toLowerCase()),
          );
        }
      }

      button 엘리먼트의 click 이벤트를 바인딩하면 filterResults 함수를 호출할 수 있습니다. 함수에는 filter 템플릿 변수의 value 프로퍼티, 즉 input HTML 엘리먼트의 .value를 인자로 전달합니다.

    3. 마지막으로 @for 블록이 filteredLocationList 배열의 값을 순회하도록 수정하세요.

      home.ts의 @for 템플릿 블록 수정하기

      import {ChangeDetectorRef, Component, inject} from '@angular/core';
      import {HousingLocation} from '../housing-location/housing-location';
      import {HousingLocationInfo} from '../housinglocation';
      import {HousingService} from '../housing.service';
      
      @Component({
        selector: 'app-home',
        imports: [HousingLocation],
        template: `
          <section>
            <form>
              <input type="text" placeholder="Filter by city" #filter />
              <button class="primary" type="button" (click)="filterResults(filter.value)">Search</button>
            </form>
          </section>
          <section class="results">
            @for (housingLocation of filteredLocationList; track $index) {
              <app-housing-location [housingLocation]="housingLocation" />
            }
          </section>
        `,
        styleUrls: ['./home.css'],
      })
      export class Home {
        private readonly changeDetectorRef = inject(ChangeDetectorRef);
        housingLocationList: HousingLocationInfo[] = [];
        housingService: HousingService = inject(HousingService);
        filteredLocationList: HousingLocationInfo[] = [];
      
        constructor() {
          this.housingLocationList = this.housingService.getAllHousingLocations();
          this.filteredLocationList = this.housingLocationList;
        }
      
        filterResults(text: string) {
          if (!text) {
            this.filteredLocationList = this.housingLocationList;
            return;
          }
      
          this.filteredLocationList = this.housingLocationList.filter((housingLocation) =>
            housingLocation?.city.toLowerCase().includes(text.toLowerCase()),
          );
        }
      }
  3. 이벤트 핸들러 함수 구현하기

    템플릿의 click 이벤트에 filterResults 함수를 바인딩했습니다. 이제 Home 클래스에 filterResults 함수를 구현합니다.

    1. Home 클래스에 filterResults 함수 구현을 추가하세요.

      filterResults 함수 구현 추가하기

      import {ChangeDetectorRef, Component, inject} from '@angular/core';
      import {HousingLocation} from '../housing-location/housing-location';
      import {HousingLocationInfo} from '../housinglocation';
      import {HousingService} from '../housing.service';
      
      @Component({
        selector: 'app-home',
        imports: [HousingLocation],
        template: `
          <section>
            <form>
              <input type="text" placeholder="Filter by city" #filter />
              <button class="primary" type="button" (click)="filterResults(filter.value)">Search</button>
            </form>
          </section>
          <section class="results">
            @for (housingLocation of filteredLocationList; track $index) {
              <app-housing-location [housingLocation]="housingLocation" />
            }
          </section>
        `,
        styleUrls: ['./home.css'],
      })
      export class Home {
        private readonly changeDetectorRef = inject(ChangeDetectorRef);
        housingLocationList: HousingLocationInfo[] = [];
        housingService: HousingService = inject(HousingService);
        filteredLocationList: HousingLocationInfo[] = [];
      
        constructor() {
          this.housingLocationList = this.housingService.getAllHousingLocations();
          this.filteredLocationList = this.housingLocationList;
        }
      
        filterResults(text: string) {
          if (!text) {
            this.filteredLocationList = this.housingLocationList;
            return;
          }
      
          this.filteredLocationList = this.housingLocationList.filter((housingLocation) =>
            housingLocation?.city.toLowerCase().includes(text.toLowerCase()),
          );
        }
      }

      이 함수는 Array의 filter 메서드로 text 파라미터와 housingLocation.city 프로퍼티를 비교합니다. 추가 연습으로 다른 프로퍼티나 여러 프로퍼티를 비교하도록 수정해 보세요.

    2. 코드를 저장하세요.

    3. 브라우저를 새로고침하고 텍스트를 입력한 뒤 "Search" 버튼을 클릭했을 때 도시 이름으로 주택 정보를 검색할 수 있는지 확인하세요.

      사용자 입력으로 필터링한 검색 결과
  4. SUMMARY: 이번 실습에서는 템플릿 변수로 템플릿의 값에 접근하고, 이벤트 바인딩과 배열 메서드로 검색 기능을 추가했습니다.

    이 실습에서 다룬 내용을 더 알아보려면 다음 문서를 참고하세요: