이번 실습에서는 사용자 데이터를 수집하는 폼을 Angular 앱에 추가하는 방법을 알아봅니다. 이미 동작하는 Angular 앱에 폼을 추가하는 과정으로 진행합니다.
폼에서 수집한 데이터는 앱의 서비스로만 전달하며 서비스는 이 데이터를 브라우저 콘솔에 출력합니다. REST API로 폼 데이터를 주고받는 방법은 이번 실습에서 다루지 않습니다.
IMPORTANT: 이 실습은 로컬 환경에서 진행하는 것을 권장합니다.
학습 목표
- 사용자가 입력한 데이터를 앱의 서비스로 전달하는 폼을 추가합니다.
- 서비스에서 폼 데이터를 브라우저 콘솔에 출력합니다.
-
폼 데이터를 전달하는 메서드 추가하기
이 단계에서는 폼 데이터를 받아 전달할 메서드를 앱의 서비스에 추가합니다. 이 예제에서는 폼 데이터를 브라우저 콘솔에 출력합니다.
IDE의 편집 창에서 다음 작업을 수행하세요:
src/app/housing.service.ts의HousingService클래스 정의 맨 아래에 다음 메서드를 붙여 넣으세요.src/app/housing.service.ts의 제출 메서드
import {Service} from '@angular/core'; import {HousingLocationInfo} from './housinglocation'; @Service() export class HousingService { readonly baseUrl = 'https://angular.dev/assets/images/tutorials/common'; protected housingLocationList: HousingLocationInfo[] = [ { id: 0, name: 'Acme Fresh Start Housing', city: 'Chicago', state: 'IL', photo: `${this.baseUrl}/bernard-hermant-CLKGGwIBTaY-unsplash.jpg`, availableUnits: 4, wifi: true, laundry: true, }, { id: 1, name: 'A113 Transitional Housing', city: 'Santa Monica', state: 'CA', photo: `${this.baseUrl}/brandon-griggs-wR11KBaB86U-unsplash.jpg`, availableUnits: 0, wifi: false, laundry: true, }, { id: 2, name: 'Warm Beds Housing Support', city: 'Juneau', state: 'AK', photo: `${this.baseUrl}/i-do-nothing-but-love-lAyXdl1-Wmc-unsplash.jpg`, availableUnits: 1, wifi: false, laundry: false, }, { id: 3, name: 'Homesteady Housing', city: 'Chicago', state: 'IL', photo: `${this.baseUrl}/ian-macdonald-W8z6aiwfi1E-unsplash.jpg`, availableUnits: 1, wifi: true, laundry: false, }, { id: 4, name: 'Happy Homes Group', city: 'Gary', state: 'IN', photo: `${this.baseUrl}/krzysztof-hepner-978RAXoXnH4-unsplash.jpg`, availableUnits: 1, wifi: true, laundry: false, }, { id: 5, name: 'Hopeful Apartment Group', city: 'Oakland', state: 'CA', photo: `${this.baseUrl}/r-architecture-JvQ0Q5IkeMM-unsplash.jpg`, availableUnits: 2, wifi: true, laundry: true, }, { id: 6, name: 'Seriously Safe Towns', city: 'Oakland', state: 'CA', photo: `${this.baseUrl}/phil-hearing-IYfp2Ixe9nM-unsplash.jpg`, availableUnits: 5, wifi: true, laundry: true, }, { id: 7, name: 'Hopeful Housing Solutions', city: 'Oakland', state: 'CA', photo: `${this.baseUrl}/r-architecture-GGupkreKwxA-unsplash.jpg`, availableUnits: 2, wifi: true, laundry: true, }, { id: 8, name: 'Seriously Safe Towns', city: 'Oakland', state: 'CA', photo: `${this.baseUrl}/saru-robert-9rP3mxf8qWI-unsplash.jpg`, availableUnits: 10, wifi: false, laundry: false, }, { id: 9, name: 'Capital Safe Towns', city: 'Portland', state: 'OR', photo: `${this.baseUrl}/webaliser-_TPTXZd9mOo-unsplash.jpg`, availableUnits: 6, wifi: true, laundry: true, }, ]; getAllHousingLocations(): HousingLocationInfo[] { return this.housingLocationList; } getHousingLocationById(id: number): HousingLocationInfo | undefined { return this.housingLocationList.find((housingLocation) => housingLocation.id === id); } submitApplication(firstName: string, lastName: string, email: string) { console.log( `Homes application received: firstName: ${firstName}, lastName: ${lastName}, email: ${email}.`, ); } }앱이 오류 없이 빌드되는지 확인하세요. 오류가 있다면 수정한 후 다음 단계로 진행하세요.
-
상세 페이지에 폼 기능 추가하기
이 단계에서는 폼과의 상호작용을 처리하는 코드를 상세 페이지에 추가합니다.
IDE의 편집 창에서
src/app/details/details.ts를 수정하세요:- 파일 위쪽의
import구문 뒤에 Angular 폼 클래스를 불러오는 다음 코드를 추가하세요.
src/app/details/details.ts에서 폼 관련 항목 불러오기
import {Component, inject} from '@angular/core'; import {ActivatedRoute} from '@angular/router'; import {HousingService} from '../housing.service'; import {HousingLocationInfo} from '../housinglocation'; import {FormControl, FormGroup, ReactiveFormsModule} from '@angular/forms'; @Component({ selector: 'app-details', imports: [ReactiveFormsModule], template: ` <article> <img class="listing-photo" [src]="housingLocation?.photo" alt="Exterior photo of {{ housingLocation?.name }}" crossorigin /> <section class="listing-description"> <h2 class="listing-heading">{{ housingLocation?.name }}</h2> <p class="listing-location">{{ housingLocation?.city }}, {{ housingLocation?.state }}</p> </section> <section class="listing-features"> <h2 class="section-heading">About this housing location</h2> <ul> <li>Units available: {{ housingLocation?.availableUnits }}</li> <li>Does this location have wifi: {{ housingLocation?.wifi }}</li> <li>Does this location have laundry: {{ housingLocation?.laundry }}</li> </ul> </section> <section class="listing-apply"> <h2 class="section-heading">Apply now to live here</h2> <form [formGroup]="applyForm" (submit)="submitApplication()"> <label for="first-name">First Name</label> <input id="first-name" type="text" formControlName="firstName" /> <label for="last-name">Last Name</label> <input id="last-name" type="text" formControlName="lastName" /> <label for="email">Email</label> <input id="email" type="email" formControlName="email" /> <button type="submit" class="primary">Apply now</button> </form> </section> </article> `, styleUrls: ['./details.css'], }) export class Details { route: ActivatedRoute = inject(ActivatedRoute); housingService = inject(HousingService); housingLocation: HousingLocationInfo | undefined; applyForm = new FormGroup({ firstName: new FormControl(''), lastName: new FormControl(''), email: new FormControl(''), }); constructor() { const housingLocationId = parseInt(this.route.snapshot.params['id'], 10); this.housingLocation = this.housingService.getHousingLocationById(housingLocationId); } submitApplication() { this.housingService.submitApplication( this.applyForm.value.firstName ?? '', this.applyForm.value.lastName ?? '', this.applyForm.value.email ?? '', ); } }Details데코레이터 메타데이터의imports프로퍼티를 다음 코드로 수정하세요:
src/app/details/details.ts의 imports 프로퍼티
import {Component, inject} from '@angular/core'; import {ActivatedRoute} from '@angular/router'; import {HousingService} from '../housing.service'; import {HousingLocationInfo} from '../housinglocation'; import {FormControl, FormGroup, ReactiveFormsModule} from '@angular/forms'; @Component({ selector: 'app-details', imports: [ReactiveFormsModule], template: ` <article> <img class="listing-photo" [src]="housingLocation?.photo" alt="Exterior photo of {{ housingLocation?.name }}" crossorigin /> <section class="listing-description"> <h2 class="listing-heading">{{ housingLocation?.name }}</h2> <p class="listing-location">{{ housingLocation?.city }}, {{ housingLocation?.state }}</p> </section> <section class="listing-features"> <h2 class="section-heading">About this housing location</h2> <ul> <li>Units available: {{ housingLocation?.availableUnits }}</li> <li>Does this location have wifi: {{ housingLocation?.wifi }}</li> <li>Does this location have laundry: {{ housingLocation?.laundry }}</li> </ul> </section> <section class="listing-apply"> <h2 class="section-heading">Apply now to live here</h2> <form [formGroup]="applyForm" (submit)="submitApplication()"> <label for="first-name">First Name</label> <input id="first-name" type="text" formControlName="firstName" /> <label for="last-name">Last Name</label> <input id="last-name" type="text" formControlName="lastName" /> <label for="email">Email</label> <input id="email" type="email" formControlName="email" /> <button type="submit" class="primary">Apply now</button> </form> </section> </article> `, styleUrls: ['./details.css'], }) export class Details { route: ActivatedRoute = inject(ActivatedRoute); housingService = inject(HousingService); housingLocation: HousingLocationInfo | undefined; applyForm = new FormGroup({ firstName: new FormControl(''), lastName: new FormControl(''), email: new FormControl(''), }); constructor() { const housingLocationId = parseInt(this.route.snapshot.params['id'], 10); this.housingLocation = this.housingService.getHousingLocationById(housingLocationId); } submitApplication() { this.housingService.submitApplication( this.applyForm.value.firstName ?? '', this.applyForm.value.lastName ?? '', this.applyForm.value.email ?? '', ); } }Details클래스의constructor()앞에 다음 코드를 추가해 폼 객체를 만드세요.src/app/details/details.ts의 폼 객체
import {Component, inject} from '@angular/core'; import {ActivatedRoute} from '@angular/router'; import {HousingService} from '../housing.service'; import {HousingLocationInfo} from '../housinglocation'; import {FormControl, FormGroup, ReactiveFormsModule} from '@angular/forms'; @Component({ selector: 'app-details', imports: [ReactiveFormsModule], template: ` <article> <img class="listing-photo" [src]="housingLocation?.photo" alt="Exterior photo of {{ housingLocation?.name }}" crossorigin /> <section class="listing-description"> <h2 class="listing-heading">{{ housingLocation?.name }}</h2> <p class="listing-location">{{ housingLocation?.city }}, {{ housingLocation?.state }}</p> </section> <section class="listing-features"> <h2 class="section-heading">About this housing location</h2> <ul> <li>Units available: {{ housingLocation?.availableUnits }}</li> <li>Does this location have wifi: {{ housingLocation?.wifi }}</li> <li>Does this location have laundry: {{ housingLocation?.laundry }}</li> </ul> </section> <section class="listing-apply"> <h2 class="section-heading">Apply now to live here</h2> <form [formGroup]="applyForm" (submit)="submitApplication()"> <label for="first-name">First Name</label> <input id="first-name" type="text" formControlName="firstName" /> <label for="last-name">Last Name</label> <input id="last-name" type="text" formControlName="lastName" /> <label for="email">Email</label> <input id="email" type="email" formControlName="email" /> <button type="submit" class="primary">Apply now</button> </form> </section> </article> `, styleUrls: ['./details.css'], }) export class Details { route: ActivatedRoute = inject(ActivatedRoute); housingService = inject(HousingService); housingLocation: HousingLocationInfo | undefined; applyForm = new FormGroup({ firstName: new FormControl(''), lastName: new FormControl(''), email: new FormControl(''), }); constructor() { const housingLocationId = parseInt(this.route.snapshot.params['id'], 10); this.housingLocation = this.housingService.getHousingLocationById(housingLocationId); } submitApplication() { this.housingService.submitApplication( this.applyForm.value.firstName ?? '', this.applyForm.value.lastName ?? '', this.applyForm.value.email ?? '', ); } }Angular의
FormGroup과FormControl은 폼을 구성하는 타입입니다.FormControl에는 기본값을 지정하고 폼 데이터의 타입을 정의할 수 있습니다. 이 예제에서firstName은 문자열 값을 사용하며 기본값은 빈 문자열입니다.Details클래스의constructor()뒤에 Apply now 버튼 클릭을 처리하는 다음 코드를 추가하세요.src/app/details/details.ts의 제출 처리 메서드
import {Component, inject} from '@angular/core'; import {ActivatedRoute} from '@angular/router'; import {HousingService} from '../housing.service'; import {HousingLocationInfo} from '../housinglocation'; import {FormControl, FormGroup, ReactiveFormsModule} from '@angular/forms'; @Component({ selector: 'app-details', imports: [ReactiveFormsModule], template: ` <article> <img class="listing-photo" [src]="housingLocation?.photo" alt="Exterior photo of {{ housingLocation?.name }}" crossorigin /> <section class="listing-description"> <h2 class="listing-heading">{{ housingLocation?.name }}</h2> <p class="listing-location">{{ housingLocation?.city }}, {{ housingLocation?.state }}</p> </section> <section class="listing-features"> <h2 class="section-heading">About this housing location</h2> <ul> <li>Units available: {{ housingLocation?.availableUnits }}</li> <li>Does this location have wifi: {{ housingLocation?.wifi }}</li> <li>Does this location have laundry: {{ housingLocation?.laundry }}</li> </ul> </section> <section class="listing-apply"> <h2 class="section-heading">Apply now to live here</h2> <form [formGroup]="applyForm" (submit)="submitApplication()"> <label for="first-name">First Name</label> <input id="first-name" type="text" formControlName="firstName" /> <label for="last-name">Last Name</label> <input id="last-name" type="text" formControlName="lastName" /> <label for="email">Email</label> <input id="email" type="email" formControlName="email" /> <button type="submit" class="primary">Apply now</button> </form> </section> </article> `, styleUrls: ['./details.css'], }) export class Details { route: ActivatedRoute = inject(ActivatedRoute); housingService = inject(HousingService); housingLocation: HousingLocationInfo | undefined; applyForm = new FormGroup({ firstName: new FormControl(''), lastName: new FormControl(''), email: new FormControl(''), }); constructor() { const housingLocationId = parseInt(this.route.snapshot.params['id'], 10); this.housingLocation = this.housingService.getHousingLocationById(housingLocationId); } submitApplication() { this.housingService.submitApplication( this.applyForm.value.firstName ?? '', this.applyForm.value.lastName ?? '', this.applyForm.value.email ?? '', ); } }이 버튼은 아직 없으며 다음 단계에서 추가합니다. 위 코드의
FormControl은null을 반환할 수 있습니다. 널 병합 연산자로 값이null일 때 빈 문자열을 사용하도록 합니다.앱이 오류 없이 빌드되는지 확인하세요. 오류가 있다면 수정한 후 다음 단계로 진행하세요.
- 파일 위쪽의
-
상세 페이지에 폼 마크업 추가하기
이 단계에서는 폼을 표시하는 마크업을 상세 페이지에 추가합니다.
IDE의 편집 창에서
src/app/details/details.ts를 수정하세요:Details데코레이터 메타데이터의templateHTML을 다음 코드로 수정해 폼 마크업을 추가하세요.src/app/details/details.ts의 template 프로퍼티
import {Component, inject} from '@angular/core'; import {ActivatedRoute} from '@angular/router'; import {HousingService} from '../housing.service'; import {HousingLocationInfo} from '../housinglocation'; import {FormControl, FormGroup, ReactiveFormsModule} from '@angular/forms'; @Component({ selector: 'app-details', imports: [ReactiveFormsModule], template: ` <article> <img class="listing-photo" [src]="housingLocation?.photo" alt="Exterior photo of {{ housingLocation?.name }}" crossorigin /> <section class="listing-description"> <h2 class="listing-heading">{{ housingLocation?.name }}</h2> <p class="listing-location">{{ housingLocation?.city }}, {{ housingLocation?.state }}</p> </section> <section class="listing-features"> <h2 class="section-heading">About this housing location</h2> <ul> <li>Units available: {{ housingLocation?.availableUnits }}</li> <li>Does this location have wifi: {{ housingLocation?.wifi }}</li> <li>Does this location have laundry: {{ housingLocation?.laundry }}</li> </ul> </section> <section class="listing-apply"> <h2 class="section-heading">Apply now to live here</h2> <form [formGroup]="applyForm" (submit)="submitApplication()"> <label for="first-name">First Name</label> <input id="first-name" type="text" formControlName="firstName" /> <label for="last-name">Last Name</label> <input id="last-name" type="text" formControlName="lastName" /> <label for="email">Email</label> <input id="email" type="email" formControlName="email" /> <button type="submit" class="primary">Apply now</button> </form> </section> </article> `, styleUrls: ['./details.css'], }) export class Details { route: ActivatedRoute = inject(ActivatedRoute); housingService = inject(HousingService); housingLocation: HousingLocationInfo | undefined; applyForm = new FormGroup({ firstName: new FormControl(''), lastName: new FormControl(''), email: new FormControl(''), }); constructor() { const housingLocationId = parseInt(this.route.snapshot.params['id'], 10); this.housingLocation = this.housingService.getHousingLocationById(housingLocationId); } submitApplication() { this.housingService.submitApplication( this.applyForm.value.firstName ?? '', this.applyForm.value.lastName ?? '', this.applyForm.value.email ?? '', ); } }이제 템플릿에
(submit)="submitApplication()"이벤트 핸들러가 포함됩니다. Angular 템플릿에서는 이벤트 이름을 괄호로 감싸 이벤트를 바인딩합니다. 등호 오른쪽 코드는 해당 이벤트가 발생했을 때 실행됩니다. 브라우저 이벤트와 커스텀 이벤트 모두 바인딩할 수 있습니다.앱이 오류 없이 빌드되는지 확인하세요. 오류가 있다면 수정한 후 다음 단계로 진행하세요.

-
새 폼 테스트하기
이 단계에서는 새 폼에서 데이터를 제출했을 때 브라우저 콘솔에 해당 데이터가 표시되는지 테스트합니다.
- IDE의 터미널 창에서
ng serve가 실행 중이 아니라면 실행하세요. - 브라우저에서
http://localhost:4200을 열어 앱에 접속하세요. - 브라우저의 앱 화면에서 마우스 오른쪽 버튼을 클릭하고 메뉴에서 검사를 선택하세요.
- 개발자 도구 창에서 콘솔(Console) 탭을 선택하세요. 다음 단계를 진행하는 동안 개발자 도구 창이 보이도록 두세요.
- 앱에서 다음 작업을 수행하세요:
- 주택을 선택하고 Learn more를 클릭해 상세 정보를 여세요.
- 상세 페이지 아래쪽으로 스크롤해 새 폼을 찾으세요.
- 폼의 입력 필드에 원하는 데이터를 입력하세요.
- Apply now를 클릭해 데이터를 제출하세요.
- 개발자 도구 창의 로그에 폼 데이터가 출력되는지 확인하세요.
- IDE의 터미널 창에서
SUMMARY: 이번 실습에서는 Angular의 폼 기능으로 앱에 폼을 추가하고 이벤트 핸들러를 통해 입력한 데이터를 컴포넌트에 연결했습니다.
이 실습에서 다룬 내용을 더 알아보려면 다음 문서를 참고하세요: