복잡한뇌구조마냥

Angular 공부 ( 개발환경 설정, 기본 구조 및 동작 ) 본문

웹 개발 공부

Angular 공부 ( 개발환경 설정, 기본 구조 및 동작 )

지금해냥 2023. 1. 7. 19:18

1. 개발 환경 설정

1-0. VScode, Node.js 설치

 - 구글 검색창에 검색 후 설치

 - Node.js 는 최신 버전이 아닌 안정된 버전 설치 권장

1-1. Angular CLI 설치 

npm install -g @angular/cli

- 터미널 명령어 실행

npm install -g

- npm 최신 업데이트 오류시 패치

 

- ng 명령어 오류시 컴퓨터 재구동

 

1-2. 기본 어플리케이션 생성

ng new [App-Name]

- ng new 명령어로 생성

 

1-3. 어플리케이션 실행

cd [App-Name] // 경로 이동
ng serve --open // 실행

 - 어플리케이션 경로로 이동

 - ng serve 명령어로 실행 ( --open 옵션으로 실행 후 접속 )

 - 브라우저 실행이 되지 않을 경우  http://localhost:4200/ 주소로 접속

 

2. Agular 컴포넌트 

파일 설명
app.component.ts TypeScript로 작성된 컴포넌트 클래스 코드입니다.
app.component.html HTML로 작성된 컴포넌트 템플릿입니다.
app.component.css 이 컴포넌트에만 적용되는 CSS 스타일 파일입니다.

 

2-1. app.component.ts

import { Component } from '@angular/core';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  title = 'Tour of Heroes';
}

 - 클래스 내부에 값을 지정하여 사용

프로퍼티 설명
selector 컴포넌트의 css 엘리먼트 셀렉터
templateUrl 컴포넌트 템플릿 파일의 위치
styleUrls 컴포넌트 css 스타일 파일의 위치

 

2-2. app.component.html

<h1>{{title}}</h1>

- ts파일의 title 값을 불러와서 사용, 함수도 동작

 

3. 컴포넌트 생성

3-1. 컴포넌트 생성 명령어

ng generate component [컴포넌트 이름]

- ng generate 명령을 실행하면 src/app/[컴포넌트] 폴더를 생성하고 해당 Component를 구성하는 파일을 생성합니다.

 

3-2. OnInit

// src/app/heroes/heroes.component.ts
import { Component, OnInit } from '@angular/core';

@Component({
  selector: 'app-heroes',
  templateUrl: './heroes.component.html',
  styleUrls: ['./heroes.component.css']
})
export class HeroesComponent implements OnInit {

  constructor() { }

  ngOnInit(): void {
  }

}

- OnInit을 import 하여 해당 클래스에 상속

- ngOnInit은 라이프사이클 후킹 함수

- Angular는 컴포넌트를 생성한 직후 ngOnInit을 호출

- 초기화 로직을 ngOnInit에 작성하는 것이 좋음

 

3-2. 생성된 컴포넌트 사용

<!-- app.component.html -->
<h1>{{title}}</h1>
<app-heroes></app-heroes>

- 상위 템플릿에 해당 방법으로 선언해주어 화면에 표시되도록 갱신

 

3-3. 인터페이스 생성 및 사용

// scr/app/hero.ts
export interface Hero {
  id: number;
  name: string;
}

- Angular는 TypeScript 기반 언어이므로 인터페이스를 선언하여 사용

// src/app/heroes/heroes.component.ts
import { Component, OnInit } from '@angular/core';
import { Hero } from '../hero';

@Component({
  selector: 'app-heroes',
  templateUrl: './heroes.component.html',
  styleUrls: ['./heroes.component.css']
})
export class HeroesComponent implements OnInit {
  hero: Hero = {
    id: 1,
    name: 'Windstorm'
  };

  constructor() { }

  ngOnInit(): void {
  }

}

- 다음과 같이 해당 타입을 import 해서 hero 변수에 할당

 

3-4. UppercasePipe로 표시형식 지정

<!-- src/app/heroes/heroes.component.html -->
<h2>{{hero.name | uppercase}} Details</h2>

 - Uppercase는 Angular의 기본 파이프인 UppercasePipe를 가르킴

 - 파이프는 문자열의 형식을 지정하거나, 통화 단위를 변경하고, 날짜나 데이터가 표시되는 형식을 변경할 때 사용

 

4. 양방향 바인딩

4-1. ngModel

<!-- html -->

<div>
  <label for="name">Hero name: </label>
  <input id="name" [(ngModel)]="hero.name" placeholder="name">
</div>

 - [(ngModel)] 은 Angular의 양방향 바인딩 문법

 - hero.name 프로퍼티의 값이 텍스트 박스에 작성되면 값이 전달되는 방식

 - ngModel을 사용하기 위해서는 FormsModule을 로드해야함

 

4-2. FormsModule

// app.module.ts
import { FormsModule } from '@angular/forms'; // <-- NgModel은 이 패키지가 제공합니다.

@NgModule({
  declarations: [
  	AppComponent,
  	HeroesComponent  //새로 추가된 컴포넌트
  ]
  imports: [
    BrowserModule,
    FormsModule  // 해당 부분 추가
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

 - 컴포넌트가 아닌 models.ts에 import해줌으로서 해당 모듈을 로드함

 - ng genelate component로 추가한 컴포넌트도 해당 모듈에 등록해야함

 

5. 외부 데이터 값 사용하기 (데이터베이스 사용 연계?)

5-1. 데이터 파일 생성

// src/app/mock-heroes.ts

import { Hero } from './hero';

export const HEROES: Hero[] = [
  { id: 12, name: 'Dr. Nice' },
  { id: 13, name: 'Bombasto' },
  { id: 14, name: 'Celeritas' },
  { id: 15, name: 'Magneta' },
  { id: 16, name: 'RubberMan' },
  { id: 17, name: 'Dynama' },
  { id: 18, name: 'Dr. IQ' },
  { id: 19, name: 'Magma' },
  { id: 20, name: 'Tornado' }
];

- 다음과 같이 배열 값 데이터 작성

 

5-2. 데이터 로드

// src/app/heroes/heroes.component.ts

import { HEROES } from '../mock-heroes';

export class HeroesComponent implements OnInit {

  heroes = HEROES;
}

- 해당 컴포넌트에 import 하여 변수로 할당

 

<!-- heroes.component.html -->

<h2>My Heroes</h2>
<ul class="heroes">
  <li *ngFor="let hero of heroes">
    <button type="button">
      <span class="badge">{{hero.id}}</span>
      <span class="name">{{hero.name}}</span>
    </button>
  </li>
</ul>

 - 화면출력을 위해서는 *ngFor 을 이용하여 순회

 - *ngFor은 항목을 반복하는 Angular 디렉티브 - 목록에 있는 항목마다 호스트 엘리먼트를 반복

 - for ... of 문 사용하는 것과 비슷하게 사용 ( heroes의 값을 불러와서 hero 변수로 할당하여 사용 )

 

6. 클릭 이벤트

6-1. 클릭 이벤트 작성

<!-- heroes.component.html -->

<li *ngFor="let hero of heroes">
  <button type="button" (click)="onSelect(hero)">
  <!-- ... -->

 - (click) = "함수(파라미터)" 를 작성해주어 해당 버튼 클릭시 동작하도록 지정

 

6-2. 클릭 함수 동작

// src/app/heroes/heroes.component.ts

import { HEROES } from '../mock-heroes';

export class HeroesComponent implements OnInit {

  heroes = HEROES;
  
  selectedHero?: Hero;
  onSelect(hero: Hero): void {
  	this.selectedHero = hero;
  }
}

 - selectedHero? 를 하여 해당 변수가 있을 수 있음을 선언

 - onSelect 함수에 Hero형식의 파라미터를 보내주면 selectedHero 에 값을 넣어주는 함수 작성

 - 버튼 클릭시 해당 함수가 동작하여 해당 변수에 값 할당하는 방식 

 

 

6-3. ngIf

<!-- heroes.component.html -->

<div *ngIf="selectedHero">
  <h2>{{selectedHero.name | uppercase}} Details</h2>
  <div>id: {{selectedHero.id}}</div>
  <div>
    <label for="hero-name">Hero name: </label>
    <input id="hero-name" [(ngModel)]="selectedHero.name" placeholder="name">
  </div>
</div>

 - class에서 옵션처리된 파라미터가 있을 때만 해당 div가 출력되도록 지정

 - *ngIf 디렉티브는 해당 값이 존재할 때만 해당 엘리먼트를 표시하는 역할

 

 

6-4. Angular 제공 클래스 바인딩

<!-- heroes.component.html (히어로 목록) -->

<li *ngFor="let hero of heroes">
  <!-- [class.some-css-class]="some-condition" 문법  -->
  <button [class.selected]="hero === selectedHero" type="button" (click)="onSelect(hero)">
    <span class="badge">{{hero.id}}</span>
    <span class="name">{{hero.name}}</span>
  </button>
</li>

 - [class.some-css-class] = "조건" 문법으로 스타일 지정 

 - [class.selected]="hero === selectedHero" [ selected ]라는 클래스가 hero와 selectedHero와 같을 때 추가됨

 

7. 서비스 (전역상태 저장소?)

7-1. 서비스 생성

ng generate service [서비스 이름]

 - ng generate service 명령어로 해당 서비스 생성

 

7-2. 데이터 가져오기

//src/app/hero.service.ts

import { Injectable } from '@angular/core';
import { Hero } from './hero';
import { HEROES } from './mock-heroes';

@Injectable({
  providedIn: 'root',
})
export class HeroService {
  getHeroes(): Hero[] {
  	return HEROES;
  }
  constructor() { }

}

 - @Injectable() 데코레이터는 서비스를 정의하는 메타데이터 객체를 인자로 받음

 - @Component() 데코레이터에 메타데이터를 사용했던 것과 같은 방식

 - providedIn 클래스가 주입되는 모든 곳에서 같은 인스턴스 공유

 - 서비스에 데이터를 받아와서 해당 함수 호출 시 값을 반환하도록 메소드 추가

 

7-3. 서비스 주입

// src/app/heroes/heroes.component.ts

import { HEROES } from '../mock-heroes';
import { HeroService } from '../hero.service';

export class HeroesComponent implements OnInit {
  heroes: Hero[] = [];
  
  getHeroes(): void {
  	this.heroes = this.heroService.getHeroes();
  }
  constructor(private heroService: HeroService) {}
  
  ngOnInit(): void {
   this.getHeroes();
  }
}

 - 서비스를 import

 - 프로퍼티 할당 값 지정

 - constructor에 서비스타입 인자를 선언 (클래스 프로퍼티로 선언하면서 의존성 객체가 주입)

 - 인스턴스를 찾아서 해당 인자로 전달

 - OnInit에서 데이터를 받아오는 메소드를 동작하도록함

 - 해당 메소드는 동기 방식으로 동작하므로 비동기로 동작하도록 처리 필요

 

7-4. 옵저버블 ( Observable )

 - RxJS라이브러리가 제공하는 클래스 중 가장 중요한 클래스

//src/app/hero.service.ts

import { Injectable } from '@angular/core';
import { Hero } from './hero';
import { HEROES } from './mock-heroes';
import { Observable, of } from 'rxjs';

@Injectable({
  providedIn: 'root',
})
export class HeroService {
  getHeroes(): Observable<Hero[]> {
    const heroes = of(HEROES);
    return heroes;
  }
  constructor() { }

}

  - of(HEROES)는 히어로 데이터를 Observable<Hero[]> 타입으로 반환

  - of 함수는 인자들을 Observable 하게 변환하는 기능

 

7-5. 옵저버블을 사용하는 코드

// 기존 코드
getHeroes(): void {
  this.heroes = this.heroService.getHeroes();
}

======================================================
// 옵저버블을 사용하는 코드
getHeroes(): void {
  this.heroService.getHeroes()
      .subscribe(heroes => this.heroes = heroes);
}

 - 해당 값을 subscribe 를 사용하여 값을 반환하도록 해야 비동기 방식으로 동작

 - subcribe 구을 통해 Observable이 동작하도록함

 

8. 네이게이션

8-1. AppRoutingModule 생성

ng generate module app-routing --flat --module=app

- ng generate module 명령어로 app-routing 모듈을 생성

- app-routing.module.ts 파일이 생성됨

인자 설명
--flat 새로운 폴더를 만들지 않고 src/app폴더에 파일 생성
--module=app ng generate 명령을 실행하면서 AppModule의 imports배열에 자동으로 추가

8-2. app-routing.module.ts

# 기존코드

//src/app/app-routing.module.ts
content_copy
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';

@NgModule({
  imports: [
    CommonModule
  ],
  declarations: []
})
export class AppRoutingModule { }

#수정코드

//src/app/app-routing.module.ts
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { HeroesComponent } from './heroes/heroes.component';

const routes: Routes = [
  { path: '', redirectTo: '/dashboard', pathMatch: 'full' },
  { path: 'dashboard', component: DashboardComponent },
  { path: 'detail/:id', component: HeroDetailComponent },
  { path: 'heroes', component: HeroesComponent }
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule { }

 - RouterModule과 Routes 심볼을 로드

 - 사용할 컴포넌트를 로드

 - path: ' ' 은 root 경로를 의미하며, path값을 지정하여 각 해당되는 컴포넌트가 출력되도록 작성 

 - Routes의 프로퍼티

프로퍼티 설명
path 브라우저 주소표시줄에 있는 URL과 매칭될 문자열을 지정
component 라우터가 생성하고 화면에 표시할 컴포넌트를 지정

- @NgModul에 메타데이터를 지정하면 모듈이 생성될 때 라우터를 초기화 하면서 브라우저 주소 변화를 감지 

- RouterModule.forRoot()에 routes인자를 넣어 실행한 결과를 지정하여 AppRoutingModule에 라우터 초기화

- exports로 RoterModule을 앱에서 사용할 수 있도록 지정

 

8-3. 라우팅 영역(RouterOutlet) 추가

<!-- src/app/app.component.html (router-outlet) -->

<h1>{{title}}</h1>
<router-outlet></router-outlet>

- <router-outlet> 은 라우팅 된 화면이 표시될 위치를 지정하는 엘리먼트

 

8-4. 네이게이션 링크 추가

<!-- src/app/app.component.html (router-outlet) -->

<h1>{{title}}</h1>
<nav>
  <a routerLink="/heroes">Heroes</a>
</nav>
<router-outlet></router-outlet>

 - routerLink는 RouterModule이 제공하는 RouterLink 디렉티브

 - 엘리먼트를 클릭하면 네비게이션은 실행하여 해당 라우팅 경로로 이동

<button type="button" (click)="goBack()">go back</button>
goBack(): void {
  this.location.back();
}

 - 다음과 같이 함수를 만들어 뒤로가기 기능을 제공할 수 있

 

8-5. 라우팅 변수 추출하기

//src/app/app-routing.module.ts

const routes: Routes = [
  { path: '', redirectTo: '/dashboard', pathMatch: 'full' },
  { path: 'dashboard', component: DashboardComponent },
  // id값을 url로 받는 부분
  { path: 'detail/:id', component: HeroDetailComponent },
  { path: 'heroes', component: HeroesComponent }
];

- { path: 'detail/:id', component: HeroDetailComponent }, 

- path값에 :id 를 통해 id라는 값을 할당

 

//ts

getHero(): void {
  const id = Number(this.route.snapshot.paramMap.get('id'));
  this.heroService.getHero(id)
    .subscribe(hero => this.hero = hero);
}

- const id = Number(this.route.snapshot.paramMap.get('id')

- route.snapshot은 컴포넌트가 생성된 직후에 존재하는 라우팅 규칙에 대한 정보를 담고 있는 객체

- paramMap을 사용하면 URL에 존재하는 라우팅 변수를 참조할 수 있음

- id 값에 해당하는 키를 참조 ( 라우팅 변수는 언제나 문자열 타입 )

 

9. 서버에서 데이터 받아오기 ( API )

9-1. HTTP 서비스 추가

// app.module.ts
import { FormsModule } from '@angular/forms'; 
import { HttpClientModule } from '@angular/common/http'; // <-- HTTP통신 제공 서비스

@NgModule({
  declarations: [
  	AppComponent,
  	HeroesComponent  //새로 추가된 컴포넌트
  ]
  imports: [
    BrowserModule,
    FormsModule,
    HttpClientModule, // 해당 부분 추가
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

 - HttpClient는 리모트 서버와 HTTP 통신을 하기 위해 Angular가 제공하는 서비스 

 

9-2. HTTP API

//src/app/hero.service.ts

import { Injectable } from '@angular/core';
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { catchError, map, tap } from 'rxjs/operators';

@Injectable({
  providedIn: 'root',
})
export class HeroService {
  private heroesUrl = 'api/heroes';  // 웹 API 형식의 URL로 사용
  
  // 옵션 예제, 토큰도 이렇게 명시하면 될듯
  httpOptions = {
    headers: new HttpHeaders({ 'Content-Type': 'application/json' })
  };
  
  // GET : 서버에 등록된 데이터 가져오기
  getHeroes(): Observable<Hero[]> {
    return this.http.get<Hero[]>(this.heroesUrl)
      .pipe(
        tap(_ => this.log('fetched heroes')),
        catchError(this.handleError<Hero[]>('getHeroes', []))
      );
  }
  
  // PUT : 수정하기
  updateHero(hero: Hero): Observable<any> {
    return this.http.put(this.heroesUrl, hero, this.httpOptions).pipe(
      tap(_ => this.log(`updated hero id=${hero.id}`)),
      catchError(this.handleError<any>('updateHero'))
    );
  }
  
  // POST: 서버에 새로운 데이터를 추가
  addHero(hero: Hero): Observable<Hero> {
    return this.http.post<Hero>(this.heroesUrl, hero, this.httpOptions).pipe(
      tap((newHero: Hero) => this.log(`added hero w/ id=${newHero.id}`)),
      catchError(this.handleError<Hero>('addHero'))
    );
  }
  
  // DELETE: 서버에서 데이터를 제거
  deleteHero(id: number): Observable<Hero> {
    const url = `${this.heroesUrl}/${id}`;

    return this.http.delete<Hero>(url, this.httpOptions).pipe(
      tap(_ => this.log(`deleted hero id=${id}`)),
      catchError(this.handleError<Hero>('deleteHero'))
    );
  }
  
  // 검색 : 해당되는 데이터 목록을 반환
  searchHeroes(term: string): Observable<Hero[]> {
    if (!term.trim()) {
      // 입력된 내용이 없으면 빈 배열을 반환합니다.
      return of([]);
    }
    return this.http.get<Hero[]>(`${this.heroesUrl}/?name=${term}`).pipe(
      tap(x => x.length ?
         this.log(`found heroes matching "${term}"`) :
         this.log(`no heroes matching "${term}"`)),
      catchError(this.handleError<Hero[]>('searchHeroes', []))
    );
  }
  
  private handleError<T>(operation = 'operation', result?: T) {
    return (error: any): Observable<T> => {

      // TODO: 리모트 서버로 에러 메시지 보내기
      console.error(error); // 지금은 콘솔에 로그를 출력합니다.

      // TODO: 사용자가 이해할 수 있는 형태로 변환하기
      this.log(`${operation} failed: ${error.message}`);

      // 애플리케이션 로직이 끊기지 않도록 기본값으로 받은 객체를 반환합니다.
      return of(result as T);
    };
  }
  
  private log(message) {
    console.log(message)
  }
  
  constructor() {
    private http: HttpClient,
  }
}

 -  http.get 함수로 Observable 값을 반환

 - http응답 body를 반환하는데 타입 지정이 없을 경우 json 객체로 처리

 - catchError() 연산자는 Observable이 실패했을 때 실행되는 연산

 - put 메소드는 [ URL, 수정할 데이터, 옵션 ] 3가지 인자를 받습니다.

 - 옵션은 토큰, 타입 등 헤더에 담을 내용인듯?

 - post 메소드도 [ URL, 수정할 데이터, 옵션 ] 3가지 인자를 받습니다.

 - delete의 경우 제거할 데이터 값에 대한 정보 url 을 제공하며, 옵션적용하여 사용합니다.

 

  9-3. AsyncPipe

<!-- heroes.component.html (히어로 목록) -->

//AsyncPipe
<li *ngFor="let hero of heroes$ | async" >
  <!-- [class.some-css-class]="some-condition" 문법  -->
  <button [class.selected]="hero === selectedHero" type="button" (click)="onSelect(hero)">
    <span class="badge">{{hero.id}}</span>
    <span class="name">{{hero.name}}</span>
  </button>
</li>

 - heroes 배열 대신 heroes$를 사용

 - $는 Observable을 뜻하는 관용적 표현

 - ansync는 Observavle을 자동으로 구독하는 역할할

 

참조

- Angular Tutorial : https://angular.kr/tutorial

 

Angular 가이드

Angular 가이드

angular.kr

 

LIST