본문 바로가기
Front-end Framework/Angular

[실전 어플리케이션 만들기] 4. 사용자 인증(1) - In-memroy service, Authentication service

by kellis 2020. 10. 20.

일반적인 애플리케이션이라면, 로그인 폼에 입력한 계정이 유효한지 체크하기 위해 회원을 담당하는 api서버로 http요청을 할 것입니다.

그러나 이 어플리케이션은 Angular에만 집중하기 위해 api서버를 따로 두지 않고, Angular에서 기본적으로 제공하는 In-memory DB Service를 이용했습니다.

(mockable.io를 사용할 수도 있으나 무료 버전은 유효 기간이 정해져 있으므로 In-memory DB로 결정했습니다.)

이 장에서는 In-memory DB Service를 만들고, 이전 장에서 입력한 Form에서 로그인 버튼을 클릭했을 때 In-memory DB로부터 해당 계정이 유효한지 체크하는 구현부를 살펴보겠습니다.

 

Prerequisite

HttpClientModule, rxjs

 

1. In-memory DB Service구성

1) In-memory Web API를 Npm으로부터 install합니다. 

npm install angular-in-memory-web-api --save

2) InMemoryDbService를 구현한 InMemoryData 클래스를 작성합니다. 이는 API를 호출했을 때, 반환시킬 데이터를 정의하는 클래스입니다.

import { InMemoryDbService } from 'angular-in-memory-web-api';
 
export class InMemoryData implements InMemoryDbService {
  createDb() {
    const users = [
      { id: 1, loginId: 'sks', name: 'Mr. Son', email: 'sks@sys4u.co.kr', passwd: '1234' },
      { id: 2, loginId: 'lsj', name: 'Ms. Su', email: 'lsj@sys4u.co.kr', passwd: '1234' }
    ];
    return { users : users };
  }
}
  • 4라인 : createDb() 메소드는 InMemoryDbService를 구현하기 위해 정의되어야 하는 메서드입니다.
  • 5라인 : 특정 URL을 호출했을 때, 반환되는 배열을 정의하는 변수입니다. 각 object는 id라는 key를 필수로 가지고 있어야 합니다. 
  • 9라인 : api/users라는 URL로 호출하면 users 배열을 반환시키라는 의미입니다. ecma6 문법에 의해, key와 value가 같으므로  다음과 같이 key를 생략할 수 있습니다. 
return { users };

 

3) InMemoryData가 동작하도록 AppModule에 선언해줍니다.

 

[app.module.ts]

..생략..
import { HttpClientInMemoryWebApiModule } from 'angular-in-memory-web-api';
import { InMemoryData } from './core/inmemory/inmemory.data';
 
@NgModule({
  ..생략..
  imports: [
    ..생략..
    HttpClientInMemoryWebApiModule.forRoot(
      InMemoryData, { dataEncapsulation: false }
    )
  ],
  ..생략..
})
export class AppModule {}

위와 같은 작업을 마치면, api/[key]로 요청을 보냈을 때 배열 형태의 value값이 반환됩니다. 여기서는 api/users이 될 것입니다.

api라는 prefix url은 In-Memory api의 url 명명 규칙이므로 변경할 수 없습니다. 

 

In-Memory DB Web api는 실제 애플리케이션에서 쓰기 위한 용도가 아니므로 많은 기능을 제공하지 않습니다. 따라서 조회 기능도 제한적인데, 단 1개의 조회 조건만 허용합니다.
로그인 계정 유효 체크를 하기 위해서는 loginId와 password 2가지로 조회해야 하는데 In-Memory DB는 이 기능을 제공하지 않으므로 loginId에 대해서만 조회 후, 반환되는 데이터에 대해 password를 직접 비교하도록 구성했습니다.

 


2. Authentication(사용자 인증)

In-memory db에 대한 http요청을 하는 것은 어렵지 않아 보입니다. 그럼 과연 어디서 http요청을 해야 보다 좋은 구조가 될지 생각해봅시다.

1) 로그인을 클릭할 때 발생하는 이벤트이므로 LoginFormComponent에서 호출할 수 있습니다. 그러나 Component에서는 로직 처리를 하지 않고 UI 변경에 대한 작업만 수행하기를 권장하므로 이 구조는 좋은 방법이 아닙니다.

 

2) 따라서 Service를 하나 두고, LoginFormComponent에서 해당 Service를 호출하는 방법도 생각할 수 있을 것입니다.

그런데 In-Memory DB는 조회 조건을 1개만 허용한다는 특징 때문에 loginId를 기준으로 조회한 후, 입력한 passwd와 반환된 passwd의 값을 비교하는 로직이 필요합니다.

따라서 login을 수행하는 기능에 login/passwd가 일치하는지 확인하는 기능이 속하게 됩니다. 이는 SRP(Single Response Principle)에 위배되는 구조라고 생각할 수 있습니다.

 

3) 그러므로 login/passwd가 일치하는지 확인하는 기능은 AuthService에서 구현하고, UserService가 AuthService를 호출하는 구조도 생각해 볼 수 있습니다.

만약 In-memory DB를 사용하지 않는다면, 빨간 박스가 서버사이드의 역할이 될 것입니다.

이 애플리케이션에서는 3번처럼 UserService, AuthService를 나누어 구현했습니다. (다만, LoginFormComponent에서 UserService를 호출하지 않고 ngrx/store, ngrx/effect를 이용했습니다. 이에 대한 내용은 다음장에서 확인하실 수 있습니다.)

 

 

1) UserService에서 AuthService를 주입받아 계정 정보가 유효한지에 대한 체크를 하는 authenticate 메서드를 호출합니다.

 

[user.service.ts]

@Injectable({
  providedIn: 'root'
})
export class UserService {
  constructor(
    private authService: AuthService) {
  }
 
  login(user: User): Observable<User> {
    return this.authService.authenticate(user);
  }
}

 

2) AuthService의 authenticate()는 In-memory Web Url을 호출한 뒤 계정이 유효한지에 대해 체크하는 로직을 구현합니다. 

 

[auth.service.ts]

..import 생략..
@Injectable({
  providedIn: 'root'
})
export class AuthService {
  constructor(
    private http: HttpClient) {
  }
 
  authenticate(user: User): Observable<any> {
    return this.http.get<User[]>(`${environment.API_URL}users/?loginId=${user.loginId}`).
      pipe(
        map(users => users[0]),
        switchMap(returnedUser => {
          if (returnedUser === undefined ||
            returnedUser.passwd !== user.passwd
          ) {
            return throwError('No Such loginId or Password.');
          }
          return of(returnedUser);
        }),
        map(returnedUser => {
          return { ...returnedUser, passwd: null };
        })
      )
 
  }
}
  • 11라인 : loginId로 조회한 결과를 User배열의 Observable로 반환받습니다.
  • 13라인 : 배열로 온 User배열 중 첫 번째 User만 필요하므로 변환합니다. 
  • 14~21라인 : loginId로 일치하는 유저가 없거나, 있어도 passwd가 입력된 데이터와 다르다면 에러를 발생시킵니다.
  • 23라인 : 최종적으로 반환된 User에 passwd는 null값으로 변경합니다.

${environment.API_URL}은 environment.ts/environment.prod.ts에 정의합니다. 

export const environment = {
  production: false,
  API_URL: 'api/'
};

 


Conclusions

위에서 잠깐 언급했듯, 이 애플리케이션에서는 LoginFormComponent가 UserService를 직접 사용하지 않고, ngrx/store, ngrx/effect를 이용해 다른 방식으로 구현했습니다.

ngrx/store, ngrx/effect를 써서 애플리케이션의 구조를 어떻게 변화시킬 수 있는지 다음 장에서 살펴보겠습니다. 

 

 

 

[reference]

https://angular.io/tutorial/toh-pt6

댓글