interfaces 생성 및 auth API 연결

소스코드

이 게시물에는 코드작성이 포함되어 있습니다. 소스코드를 받으신 후 진행해 주세요. MEAN Stack/개발 환경 구축에서 설명된 프로그램들(git, npm, atom editor)이 있어야 아래의 명령어들을 실행할 수 있습니다.

이 게시물의 소스코드는 기본사이트 만들기 / 404 error 페이지 추가에서 이어집니다.

angular-site.git 을 clone 한 적이 있는 경우: 터미널에서 해당 폴더로 이동 후 아래 명령어들을 붙여넣기합니다. 폴더 내 모든 코드가 이 게시물의 코드로 교체됩니다. 이를 원치 않으시면 이 방법을 선택하지 마세요.

git reset --hard
git pull
git reset --hard 7867d26
git reset --soft d0ce71c
npm install
atom .

angular-site.git 을 clone 한 적이 없는 경우: 터미널에서 코드를 다운 받을 폴더로 이동한 후 아래 명령어들을 붙여넣기하여 angular-site.git 을 clone 합니다.

git clone https://github.com/a-mean-blogger/angular-site.git
cd angular-site
git reset --hard 7867d26
git reset --soft d0ce71c
npm install
atom .

- Github에서 소스코드 보기: https://github.com/a-mean-blogger/angular-site/tree/7867d261b5736a1cc0425bb6617fc1d199b4cc7c


Node JS API/JWT(Jason Web Token)로 로그인 REST API 만들기에서 만든 API를 Angular 사이트에 연결해 봅시다.

기본사이트 만들기 시리즈에서는 API 제작에는 관심이 없고 Angular만 공부하는 것이 목적인 사람들을 위해서 API와 Angular 사이트를 합치지 않고 두개의 독립된 app으로 각각 작동하게 하였습니다. 즉 Node JS API/JWT(Jason Web Token)로 로그인 REST API 만들기에서 만든 API를 다운받아 실행시켜 놓은 상태에서 현재 프로젝트도 실행시켜야 합니다. (API와 Angular 사이트를 하나로 합치는 방법은 단편강좌/Angular CLI 사이트에 REST API 서버 추가하기에 설명되어 있습니다.)

이 강의를 진행하기 전에 사용될 로그인 REST API의 스펙을 먼저 확인 하신 후 진행하시기 바랍니다.

이번 강의에서는 로그인 REST API 에서 사용되는 object의 interface를 만들고 API 중 auth 부분의 service를 만들어 봅시다.

폴더 구조


주황색은 변경된 파일, 녹색은 새로 생성된 파일, 회색은 변화가 없는 파일입니다.

api-response.ts, user.ts는 해당 object의 구조를 가지고 있는 interface입니다.

auth.service.ts는 로그인 API에서 auth를 service하는 파일이며, util.service.ts는 사이트 전체적으로 사용되는 함수들을 모은 service입니다.

코드

먼저 interface들을 살펴봅시다.

// src/app/user.ts 

export interface User {
  _id: string;
  username: string;
  password: string;
  name: string;
  email: string;
  passwordConfirmation: string;
  currentPassword: string;
  newPassword: string;
}

user object의 형태를 담고 있습니다.

// src/app/api-response.ts

 export interface ApiResponse {
  success: boolean;
  message: string;
  errors: any;
  data: any;
}

로그인 API에서 return 되는 object의 형태를 담고 있습니다. 이 API는 success: true이면 message과 errors가 null이고, success: false이면 message나 errors 중 하나의 필드에 에러를 return합니다.

다음으로 environment ts들을 살펴봅시다.

// src/app/environments/environment.ts

export const environment = {
  production: false,
  apiBaseUrl:"http://localhost:3000/api",
};

개발 환경의 api base url은 로그인 API의 기본 주소를 취하고 있습니다.

// src/app/environments/environment.prod.ts

export const environment = {
  production: true,
  apiBaseUrl:"api",
};

production 환경의 api base url은 미정입니다. 혹시 production 환경에서 이 사이트를 사용하고 싶으신 분은 여기에 값을 조절해 주면 되겠습니다.

 service들을 살펴봅시다.

// src/app/util.service.ts

import { environment } from '../environments/environment';
import { Injectable } from '@angular/core';
import { ApiResponse } from './api-response';

@Injectable()
export class UtilService {
  public checkSuccess(response: any): Promise<any> { //1
    if(response.success) return Promise.resolve(response);
    else return Promise.reject(response);
  }

  public handleApiError(error: any): Promise<any> { //2
    if(!environment.production) console.error('An error occurred', error);
    return Promise.reject(error);
  }
  
}

1. checkSuccess 함수는 api return field 중 success 항목이 false인 경우 api가 실패한 것과 동일하게 reject를 보내는 함수입니다.

2. handleApiError 함수는 api에 error가 있는 경우에 production 환경이 아닌 경우 브라우저에 error를 표시하는 함수입니다.

위 두 함수 모두 Promise를 다루고 있으므로 Promise에 익숙하지 않으신 분들은 토막글/Javascript Promise 게시물을 먼저 읽어 주시기 바랍니다. 이 다음 auth.service.ts에서도 Promise는 중요합니다.

// src/app/auth.service.ts

import { environment } from '../environments/environment';
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Router } from '@angular/router';

import 'rxjs/add/operator/toPromise';

import { UtilService } from './util.service';
import { ApiResponse } from './api-response';
import { User } from './user';

@Injectable()
export class AuthService {
  private apiBaseUrl = `${environment.apiBaseUrl}/auth`;

  constructor(
    private http: HttpClient,
    private router: Router,
    private utilService: UtilService,
  ) { }

  login(username: string, password: string): Promise<any> { //1
    return this.http.post<ApiResponse>(`${this.apiBaseUrl}/login`,{username:username, password:password})
              .toPromise()
              .then(this.utilService.checkSuccess)
              .then(response => {
    		localStorage.setItem('token', response.data); //1-1
              })
              .catch(this.utilService.handleApiError);
  }

  me(): Promise<User> { //2
    return this.http.get<ApiResponse>(`${this.apiBaseUrl}/me`)
              .toPromise()
              .then(this.utilService.checkSuccess)
              .then(response => {
    		localStorage.setItem('currentUser', JSON.stringify(response.data)); //2-1
                return response.data as User //2-2
              })
              .catch(response =>{
                this.logout(); //2-3
                return this.utilService.handleApiError(response);
              });
  }

  refresh(): Promise<any> { //3
    return this.http.get<ApiResponse>(`${this.apiBaseUrl}/refresh`)
              .toPromise()
              .then(this.utilService.checkSuccess)
              .then(response => {
                localStorage.setItem('token', response.data); //3-1
                if(!this.getCurrentUser()) return this.me(); //3-2
              })
              .catch(response =>{
                this.logout(); //3-3
                return this.utilService.handleApiError(response);
              });
  }

  getToken(): string{ //4
    return localStorage.getItem('token');
  }

  getCurrentUser(): User{ //5
    return JSON.parse(localStorage.getItem('currentUser')) as User;
  }

  isLoggedIn(): boolean { //6
    var token = localStorage.getItem('token');
    if(token) return true;
    else return false;
  }

  logout(): void { //7
    localStorage.removeItem('token');
    localStorage.removeItem('currentUser');
    this.router.navigate(['/']);
  }
}

1. login 함수: api call이 성공하면 data(발급된 token)를 localStorage에 'token'으로 저장(1-1)합니다.

2. me 함수: api call이 성공하면 data(user 정보)를 문자열로 변환한 후 localStorage에 'currentUser'로 저장(2-1)하고 data를 User로 return(2-2)합니다. 만약 api call이 실패하면 logout(2-3)합니다.

3. refresh 함수: api call이 성공하면 data(새로 발급된 token)를 localStorage에 'token'으로 저장(3-1)합니다. 만약 currentUser가 없다면 me함수를 호출(3-2)합니다. api call이 실패하면 logout(3-3)합니다.

4. getToken 함수: localStorage에서 token을 찾아 return합니다.

5. getCurrentUser 함수: localStorage에서 currentUser를 찾아 JSON으로 변경 한 후 User로 return합니다.

6. isLoggedIn 함수: localStorage에 token이 있으면 true를, 아니면 false를 return합니다.

7. logout 함수: localStorage에 token과 currentUser를 지우고 '/'페이지로 이동합니다.

로그인 API 스펙을 잘 읽으신 분들은 여기에 뭔가 이상한 점을 발견하실 수 있을텐데요, 바로 me API와 refresh API에서 token을 header로 전달하는 부분이 없다는 점입니다. token은 대부분의 API에서 요구되기 때문에 각각 개별 API call에 넣기보다는 전체 API call에 token을 담아 보내는 편이 간단합니다. 이 방법은 나중의 강의에서 설명하겠습니다.

Angular 2/Tour of Heroes 강의를 열심히 공부하신 분들은 또 한가지 다른 점을 발견할 수 있는데, 위 코드는 Http service대신에 HttpClient service를 사용한다는 점입니다. 이 부분 역시 API call에 token을 담는 방법과 연관이 있고 나중에 설명하겠습니다.

app.component.ts를 살펴봅시다.

// src/app/app.component.ts

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

import { AuthService } from './auth.service';

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

  constructor(
    public authService: AuthService,
  ) {
    this.authService.refresh().catch(response => null);
  }

}

authService를 사용하여 사이트가 처음 시작될 때 refresh 함수를 호출하게 하였습니다. 사실 지금 사이트는 token을 받는 부분이 없으므로 refresh api는 무조건 실패를 하게 됩니다. 일단 이번 강의에서는 service가 제대로 작동하는지 확인만 하는 것으로 하겠습니다.

마지막으로 app.module.ts입니다.

// src/app/app.module.ts

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { HttpClientModule } from '@angular/common/http'; //1

import { AppRoutingModule }    from './app-routing.module';

import { UtilService } from './util.service'; //2
import { AuthService } from './auth.service'; //3

import { AppComponent } from './app.component';
import { WelcomeComponent } from './welcome/welcome.component';
import { Error404Component } from './error404/error404.component';

@NgModule({
  declarations: [
    AppComponent,
    WelcomeComponent,
    Error404Component,
  ],
  imports: [
    BrowserModule,
    AppRoutingModule,
    HttpClientModule, //1
  ],
  providers: [
    UtilService, //2
    AuthService, //3
  ],
  bootstrap: [AppComponent]
})
export class AppModule { }

1. auth.service.ts에 사용된 httpClient를 사용하기 위해서는 httpClientModule이 필요합니다.

2, 3. 이번 강의에서 생성된 auth, util service가 추가되었습니다.

실행 결과

Angular-CLI 명령어 사용해서 서버를 실행합시다. 

ng serve --open


사이트가 실행된 후 브라우저의 개발자 환경(F12)을 열어보면 console에 에러가 출력되어 있는 것을 볼 수 있습니다. error message는 refresh API에서 온 "token is required!"입니다. token이 없는데 auth service의 refresh함수를 실행하였으니 당연한 결과입니다. API가 정상적으로 작동한다는 뜻입니다.

마치며..

다음 강의에서는 로그인을 만들어 보겠습니다.

댓글

H
Hyunsung Kim 2017.12.02
// sre/app/util.service.ts sre -> src import { environment } from '../environments/environment'; 여기서 ../environ... 이 ./environ... 으로 되어야 하는거 아닌가 싶습니다!
I
Ian H 2017.12.03
@Hyunsung Kim,
sre오타는 수정하였습니다. 감사합니다^^
src/app/util.service.ts에서 src/environments/environment.ts를 찾는 것이기 때문에 '../environments/environment'이 맞습니다~
H
Hyunsung Kim 2017.12.05
@Ian H,
앗... 제가 폴더 위치를 잘못 봤군요!!
H
Hyunsung Kim 2017.12.06
어... 음... 에러가 발생하는데, 자잘한 질문이 많아 죄송합니다.. auth.service.ts(14,39): Property 'apiBaseUrl does not exist on type '{ production: boolean; }' 그리고 environment.ts(2,20): ',' expected
이렇게 나오는데 둘다 확인해본 결과 auth.service.ts 파일은 private apiBaseUrl = `${environment.apiBaseUrl}/auth`;
environment.ts 파일은 export const environment = {   production: false,   apiBaseUrl:"http://localhost:3000/api", };
왜 에러가 발생하는지 모르겠네요;;
I
Ian H 2017.12.06
@Hyunsung Kim,
흠.. 그렇네요 github 에 소스코드를 올려주시면 한번 확인해 보겠습니다
H
Hyunsung Kim 2017.12.07
@Ian H,
I
Ian H 2017.12.08
@Hyunsung Kim,
올려주신 소스코드에는  apiBaseUrl:"http://localhost:3000/api", 부분이 없는데요, 혹시 파일 저장을 안하신 걸까요!? https://github.com/kokily/AngularSite/blob/master/src/environments/environment.ts#L7 https://github.com/kokily/AngularSite/blob/master/src/environments/environment.prod.ts#L2
H
Hyunsung Kim 2017.12.09
@Ian H,
어... 이상하네요; 제꺼 저장된 파일에는 위와 같이 입력되어 있는데 git push를 하면 apiBaseUrl 부분이 없어지네요? 제 로컬에는 적혀 있는데....
I
Ian H 2017.12.11
@Hyunsung Kim,
컴퓨터의 commit ID와 github의 commit ID를 한번 비교해 보세요. 정 안되시면 git reset --hard; git pull -f;로 로컬소스코드를 github 소스코드로 강제로 sync할 수도 있습니다!
H
Hyunsung Kim 2017.12.13
@Ian H,
아우... 감사합니다 제가 직업이 이쪽계열이 아니고 개인적으로 공부하고 싶어서 하다보니 시간이 잘 나질 않네요 ㅜㅜ 알려주신대로 해보겠습니다~!
I
Ian H 2017.12.13
@Hyunsung Kim,
열심히 하시는 모습 부럽습니다! 해보시고 알려주세요^^
H
Hyunsung Kim 2017.12.15
@Ian H,
다시 전체 삭제하고 처음부터 다시 해보았습니다. 잘 되네요!!!!! 오타나 잘못된 부분이 없는데 왜 그랬는지 모르겠어요 ㅜㅜ
I
Ian H 2017.12.18
@Hyunsung Kim,
컴퓨터는 거짓말을 하지 않습니다 ㅋㅋㅋ 어딘가 잘못된 부분이 분명히 있었을 거에요. 어쨌든 지금 잘 되신다니 다행입니다.
댓글쓰기

이 글에 댓글을 다시려면 SNS 계정으로 로그인하세요. 자세히 알아보기

UP