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

[실전 어플리케이션 만들기] 5. 사용자 인증(2) - ngrx/store, ngrx/effect

by kellis 2020. 10. 20.

 

[실전 애플리케이션 만들기] 4. 사용자 인증(1) - In-memroy service, Authentication service에서 LoginForm에 입력한 계정을 검증하기 위한 서비스를 만들었습니다.

그런데 화면이 전환된다면, 컴포넌트가 교체되면서 검증된 계정 정보가 메모리에서 사라질 것입니다. Angular docs에서는 이렇게 전역적으로 필요한 상태값들은 싱글턴 객체인 서비스에 저장하는 방법을 권고합니다.

Service에서 상태값을 관리한다고 해도 애플리케이션을 개발할 수는 있지만 애플리케이션의 규모가 커지고 구조가 조금이라도 복잡해지면 컴포넌트의 상태를 관리하는 일은 귀찮은 일이 됩니다.

그래서 많은 프로젝트에서 Vue는 Veux로, React는 Redux로, Angular는 ngrx/store로 상태 값을 관리합니다. 이 애플리케이션에서도 상태 값을 관리하기 위해 ngrx/store를 사용하고 부가적으로 ngrx/effects, ngrx-store-localstorage를 사용했습니다.

 

 

1. ngrx/store

ngrx/store의 구성 요소는 크게 3가지 입니다.

  • Store : 애플리케이션에서 공유하고자 하는 상태 값을 저장하는 저장소입니다.
  • Action : Action은 상태를 변경시키기 위한 이벤트로, Action자체는 interface이고 이를 구현한 여러 Action들이 존재합니다. 컴포넌트나 서비스에 의해 이 Action의 구현체가 dispatch 되면 내부적으로 reducer를 호출하여 store에 데이터를 저장합니다.
  • Reducer : Action에 따라 store에 저장할 데이터를 정의해줍니다. 이때, immutable 한 데이터를 반환해주어야 하는데 그 이유는 새로운 데이터가 store에 저장되어야 그 데이터를 구독(subscribe)하는 측이 변화를 감지하기 때문입니다.

위 그림을 보면, ngrx/store가 단순히 상태 값을 관리할 뿐 아니라 store에 값을 저장하는 구조에서 Event-Driven Architecture가 연상된다고 느낄 수 있습니다.

 

1) @ngrx/store를 NPM으로부터 install 합니다.

npm install @ngrx/store@6.1.0 --save

 

2) Store에 저장할 데이터의 구조를 선언합니다. 

 

[app.states.ts]

export interface AppState {
  readonly user: User | null;
}

 

3) LoginSuccess Action을 구현합니다.

로그인 처리 절차를 생각해보면, 인증에 성공할 경우 인증 정보를 store에 저장할 것이라고 예상할 수 있습니다. 

Login이 성공했을 때 store에 데이터를 저장해야 하므로 LoginSuccess Action이 필요합니다.

 

[auth.actions.ts]

import {Action} from '@ngrx/store';
 
export enum AuthActionTypes {
  LOGIN_SUCCESS = '[Auth] Login Success'
}
 
export class LogInSuccess implements Action {
  readonly type = AuthActionTypes.LOGIN_SUCCESS;
  constructor(public payload: any) {}
}
 
export type All =
  | LogInSuccess;
  • 3라인 : Auth와 관련한 액션들을 enum type으로 정의합니다. 이는 Action을 구현한 LoginSuccess에서 type값으로 사용됩니다.
  • 9라인 : LoginSuccess의 생성자에 payload인자를 확인할 수 있는데, 이것은 액션을 dispatch 할 때 경우에 따라 파라미터를 전달할 수 있음을 의미합니다. 
  • 12라인 :  Action구현 클래스들을 type으로 정의합니다. 이것은 reducer에서 사용됩니다. 클래스가 여러 개일 경우 다음과 같이 |(파이프)를 이용해 정의합니다.
export type All =
  | Login
  | LogInSuccess;

 

4) reducer function을 구현합니다. 

 

[auth.reducers.ts]

import { initialUser } from '../../core/model/user.model';
import { AuthActionTypes, All } from '../actions/auth.actions';
 
export function reducer(user = initialUser , action: All): any {
  switch (action.type) {
    case AuthActionTypes.LOGIN_SUCCESS: {
      return {
        loginId: action.payload.loginId,
        name: action.payload.name,
        isAuth: true
      };
    }
    default: {
      return user;
    }
  }
}
  • 4라인 : 호출된 Action이 type으로 정의했던 클래스들 중 하나라면 reducer를 실행합니다.
  • 5라인 : Action 타입에 따라 store에 저장할 데이터를 return 시켜줍니다. 위에서 언급했듯이 immutable 한 데이터가 return 되어야만 이 데이터를 구독하는 측에서 변화를 감지합니다. 
  • 13라인 : 미리 정의된 action이 dispatch 되지 않는 경우에는 기존의 데이터를 그대로 return 하여, 구독하는 측에서 변화 감지를 못하게 합니다. 

 

지금은 User에 대한 reducer 1개뿐이지만, 확장성을 위해 app.states.ts에 다음과 같이 reducers를 정의합니다.

 

[app.states.ts]

export const reducers = {
  user: auth.reducer
};

2라인 : user는 key이고 auth.reducer는 value입니다. store로부터 데이터를 구독하고자 할 때 이 key를 이용합니다.

 

5) appModule에서 storeModule을 import 합니다.

forRoot의 첫 번째 인자로 앞서 정의한 reducers를 넣어줍니다. 

 

[app.module.ts]

StoreModule.forRoot(reducers, {})

 


2. store dev tools

개발자 도구에서 store에 저장된 상태 값들을 확인하기 위해서 크롬의 확장 프로그램 Redux를 설치하고 StoreDevToolsModule을 추가해주어야 합니다.

'Redux는 React의 상태 관리 툴인데?' 라며 의아해하실 수 있습니다.

어떻게 Redux툴이 ngrx/store의 store를 인지할 수 있을까요? 바로 StoreDevToolsModule이 그것을 가능하게 해 줍니다.

 

1) 크롬에서 확장 프로그램 Redux를 설치하고 NPM으로부터 StoreDevToolsModule을 install 합니다.

npm install @ngrx/store-devtools@6.1.0 --save

2) app.module.ts에서 StoreDevtoolsModule을 import 합니다.

 

[app.module.ts]

StoreDevtoolsModule.instrument({
   maxAge: 25
})
  • 2라인 : 개발자 도구에서 보일 기록들의 맥스 값입니다. FIFO(First In, First Out)로 동작하여 최근 25개의 기록만 보입니다. 

 


3. ngrx/effect

위에서 ngrx/store의 동작 구조가 Event-drvien Architecture를 연상시킨다고 언급했는데, ngrx/effect는 Event-driven Architecture와 유사합니다.

Store에 저장할 값에 대한 정의하기 위해 Reducer를 쓴다면, Action이 일어났을 때 실행될 코드를 구현하기 위해 Effect를 사용합니다. Event-driven Architecture의 eventHandler역할과 매우 유사합니다.

또한 Effect에서는 다른 Action을 dispatch 할 수 있습니다.

그래서 이 애플리케이션에서는 로그인 버튼을 클릭하면 Login Action이 dispatch 되고 그에 따라 Login Action에 해당하는 Effect가 실행됩니다. Effect를 어떻게 구현했는지 살펴보겠습니다.

 

1) NPM으로부터 ngrx/effects를 install 합니다.

npm install @ngrx/effects@6.1.0 --save

 

2) Login Action을 구현합니다.

 

[auth.actions.ts]

import {Action} from '@ngrx/store';
 
 
export enum AuthActionTypes {
  LOGIN = '[Auth] Login',
  LOGIN_SUCCESS = '[Auth] Login Success'
}
 
export class LogIn implements Action {
  readonly type = AuthActionTypes.LOGIN;
  constructor(public payload: any) {}
}
 
export class LogInSuccess implements Action {
  readonly type = AuthActionTypes.LOGIN_SUCCESS;
  constructor(public payload: any) {}
}
  
export type All =
  | LogIn
  | LogInSuccess;

 

3) Login Action Effect을 정의합니다.

 

[auth.effects.ts]

...생략...
@Injectable()
export class AuthEffects {
  constructor(
    private actions: Actions,
    private userService: UserService,
    private router: Router
  ) {}
 
  @Effect()
  LogIn: Observable<Action> = this.actions.pipe(
    ofType(AuthActionTypes.LOGIN),
    map((action: LogIn) => action.payload),
    switchMap(payload => {
      return this.userService.login(payload).pipe(
        map((user) => {
          return new LogInSuccess({loginId: user.loginId, name: user.name, refererUrl: payload.refererUrl});
        }),
        catchError((error: string) => {
          return of(new DialogError(error));
        })
      );
    }));
}
  • 13라인 : dispatch 할 때 인자로 넘긴 payload는 action.payload에 있습니다.
  • 15라인 : ngrx/effect를 사용하지 않았다면, LoginFormComponent에서 UserService를 주입받아 login메서드를 호출해야 했을 것입니다. 그러나 LoginFormComponent는 어떤 일들이 일어나는지에 대해서는 관심을 가지지 않고 "Login" Action을 dispatch 하기만 하면 됩니다. 
  • 17라인 : 인증에 성공한다면 LoginSuccess라는 Action을 dispatch 합니다. Effect에서는 다른 Action을 dispatch하기 위해 new 연산자를 사용합니다. 
  • 20라인 : 인증에 실패한다면 DialogError라는 Action을 dispatch합니다. DialogError Action에 대해서는 Wiki에서 다루지 않으니 Gitlab을 확인해주세요. 

 

4) Login Action을 Dispatch 합니다.

LoginFormComponent에서는 로그인 버튼이 클릭되었을 때, Login Action만 dispatch 시켜줍니다. 

 

[login-form.component.ts]

..생략..
export class LoginFormComponent implements OnInit {
  ..생략..
 
  constructor(
    private fb: FormBuilder,
    private route: ActivatedRoute,
    private router: Router,
    private store: Store<AppState>
  ) {}
 
  ..생략..
 
  login() {
    ..생략..
    this.store.dispatch(new LogIn(formValues));
  }
}

16라인 : Login Action을 dispatch 하는데, payload로 formValues를 전달합니다. Login Action에 해당하는 Reducer가 있다면 반환 값이 store에 저장되고, Effect가 있다면 그 구현부가 실행됩니다. 

 


4. store and localstorage sync

인증 정보를 ngrx/store를 이용해 저장했으므로 공유되고, 따라서 화면이 전환되어도 그 데이터를 구독할 수 있습니다.

그러나 새로고침을 하면 어떻게 될까요? Angular에서 로딩한 컴포넌트, 라우터 정보 등이 모두 초기화되므로 store에 저장되었던 데이터도 초기화됩니다.

일반적인 애플리케이션이라면 이를 방지하기 위해 localStorage나 sessionStorage, Cookie 등에 새로고침을 해도 유효해야 할 정보들을 저장해놓습니다.

여기서는 ngrx/store와 localStorage 사이에 동기화를 시켜주는 오픈소스 라이브러리인 ngrx-store-localstorage를 이용해, 자동으로 localStorage에 저장하도록 했습니다.

 

1. NPM으로부터 ngrx-store-localstorage를 install 합니다. 

npm install ngrx-store-localstorage --save

 

2. localStorageSync를 wrapping 합니다. 

 

[app.states.ts]

..생략..
export const reducers = {
  user: auth.reducer
};
 
 
export function localStorageSyncReducer(reducer: ActionReducer<any>): ActionReducer<any> {
  return localStorageSync({keys: ['user'], rehydrate: true})(reducer);
}
 
export const metaReducers: Array<MetaReducer<any, any>> = [localStorageSyncReducer];
  • 6~8라인 : localStorageSync를 wrapping합니다. keys는 reducers에 정의했던 key이고 rehydrate 옵션은 새로고침 시 localstorage에 저장된 데이터를 ngrx/store에 동기화시킬지에 대한 여부입니다.
  • 10라인 : 지금은 동기화할 것이 user 뿐이지만 확장성을 위해 배열로 정의해 export 합니다. 이는 app.modules.ts에서 참조합니다. 

 

3. app.module.ts에서 임포트 하는 StoreModule을 수정합니다. 

forRoot의 두 번째 인자에 metaReducers를 추가합니다. 

StoreModule.forRoot(reducers, {metaReducers}),

 


Conclusions

지금까지와는 사뭇 다른 구조로 코드가 작성되었음을 느낄 수 있습니다. 

LoginFormComponent에서 로그인 버튼을 클릭했을 때, 로그인을 하기 위해 구구절절이 무슨 일을 하는 것이 아니라 "Login" Action만 Dispatch 해줍니다. 이러한 프로그래밍 패러다임도 SRP(Single Response Principle)을 준수하기 위해 나왔다고 생각할 수 있습니다. 

ngrx/store, ngrx/effect를 이용하는 방법은 어렵지 않지만, 어떤 상황에 이용할지 판단하는 것이 중요하며 그것은 개발자의 몫입니다. 

 

아쉽게도 이 장에서는 store로부터 데이터를 구독하는 코드가 없었는데, 다음 장에서 Directive 구현부를 참고하시길 바랍니다. 

 

 

[reference]

https://mherman.org/blog/authentication-in-angular-with-ngrx/#ngrx-store-1

 

댓글