티스토리 뷰

728x90

1. 이 포스트는 Email Client를 작성하는 시리즈의 일부이다

  1-0 이 포스트는 동기, 비동기 검증로직을 모두 생성한다.

    1-0-1 비동기 검증로직은 여기서 처음 작성하는 내용이므로 Angular 섹션에 없다. 

  1-1 여기서는 두 개의 비밀번호가 일치하는지를 확인하는 검증 로직과

  1-2 사용을 원하는 Username이 이미 사용중인지를 확인하는 검증 로직을 작성한다.

  1-3 검증로직은 범용이 가능하도록 분리하여 작성한다. 범용 검증로직에 대한 내용은 아래 링크를 참조한다.

 

 

Angular : Password Validator 구현 사용자 정의 Validator 2

1. 이번 포스트는 지난 포스트의 사용자 정의 검증로직을 좀 더 범용적으로 사용할 수 있도록 변경한다. 1-1 지난 포스트의 연속이므로 이전 포스트를 읽는 게 좋다. 2. 범용로직을 작성한다. 2-0 ��

kogle.tistory.com

 

2. 두 개의 비밀번호가 일치하는지 여부 작성

  2-1 AuthValidators 클래스를 생성한다.

 

0

  2-2 아래처럼 코드를 작성한다. 간단한 동기식 비밀번호 검증 로직이다.

 

import { AbstractControl } from '@angular/forms';

export class AuthValidators {
  static checkPasswordMatch(param1: string, param2: string) {
    return (form: AbstractControl) => {

      const pass1 = form.value[param1]
      const pass2 = form.value[param2]

      if (pass1 !== pass2) {
        return { notMatch: true }
      }
      return null
    }
  }
}

 

  2-3 이제 sign up 컴포넌트에서 사용하도록 하자

    2-3-1 validator를 group의 두 번째 인자로 사용했음을 주의한다.

 

import { Component, OnInit } from '@angular/core';
import { FormGroup, FormBuilder, Validators } from '@angular/forms';
import { AuthValidators } from '../validators/auth-validators';

@Component({
  selector: 'app-sign-up',
  templateUrl: './sign-up.component.html',
  styleUrls: ['./sign-up.component.css'],
})
export class SignUpComponent implements OnInit {
  registrationGroup: FormGroup;

  constructor(
    private formBuilder: FormBuilder
  ) {}

  ngOnInit(): void {
    this.registrationGroup = this.formBuilder.group(
      {
        username: [
          '',
          [
            Validators.required,
            Validators.minLength(4),
            Validators.maxLength(10),
          ],
        ],
        password: [
          '',
          [
            Validators.required,
            Validators.minLength(4),
            Validators.maxLength(10),
          ],
        ],
        passwordConfirmation: [
          '',
          [
            Validators.required,
            Validators.minLength(4),
            Validators.maxLength(10),
          ],
        ],
      },
      {
        validators: AuthValidators.checkPasswordMatch(
          'password',
          'passwordConfirmation'
        ),
      }
    );
  }
}

 

  2-4 결과를 확인해 본다. - 비밀번호가 다를 경우 notMatched가 설정된다.


3. 이번에는 비동기식 검증이다. 지정한 username이 사용중인지를 확인하는 경우다.

  3-0 서비에 사용자확인은 post 방식으로 https://api.angular-email.com/auth/username url로 요청한다.  

  3-1 보낼 데이터는 { username: string } 방식으로 전송한다.

  3-2 auth Service의 코드이다. 중요한 부분은 checkDuplication이 추가된 부분이다.

 

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable, BehaviorSubject } from 'rxjs';
import { tap } from 'rxjs/operators'
import { AuthCredential } from './auth-responses';

@Injectable({
  providedIn: 'root'
})
export class AuthService {

  signedIn$ = new BehaviorSubject(null)
  username: string = ''

  url = 'https://api.angular-email.com/auth'
  constructor(private http: HttpClient) { }

  signedIn(): Observable<SignedInResponse> {
    return this.http.get<SignedInResponse>(`${this.url}/signedIn`).pipe(
      tap((response: SignedInResponse)=> {
        this.username = response.username
        this.signedIn$.next(response.authenticated)
      })
    )
  }

  signUp(crediential: AuthCredential) {
    return this.http.post(`${this.url}/signup`, crediential)
  }

  checkDuplication(username: string) {
    console.log(username);
    return this.http.post(`${this.url}/username`, { username })
  }
}

interface SignedInResponse {
  authenticated: boolean,
  username: string
}

 

  3-3 결과 데이터의 포멧은 일관적이지 않는데,

    3-3-1 실패하면 { username: string } 이 반환되고,

    3-3-2 성공하면 { available: boolean } 이 반환된다.

    3-3-3 좀 더 짜증나는 부분은 성공하면 200, 실패하면 422번 에러를 보낸다.

    3-3-4 따라서 pipe에서도 성공과 실패를 구분해서 가공을 해야 한다.

 

  3-4 AuthValidators에 static 메소드를 하나 더 추가하는데, 이번에는 사용할 서비스를 인자로 받아야 한다.

    3-4-1 서비스를 받지 않으면 자체적으로 구현해야 하는데, 그러면 static으로 구현할 수 없다.
    3-4-2 일반 클래스를 정의하고 생성자에서 서비스를 받아 사용하고 이 클래스 자체도 @injectable로 생성해야 한다.

    3-4-3 코드 재활용과 독립성에 유리하게 보이지만 생각해보면 어차피 서비스 메소드에 따라 검증코드를 바꿔야 한다.

    3-4-4 그래서 나는 static을 선호한다.

 

  3-5 소스코드를 보면 동기식과 비슷하게 작성되어 있다. 함수를 리턴하는 것이고 반환값만 Observable이면 된다.

    3-5-1 사실 guard와 거의 동일하고 할 수 있다.

    3-5-2 결과를 받아 pipe에서 map으로 들어가는 경우는 정상적인 200번 리턴이고 422는 catchError로 넘어간다.

    3-5-3 422일 때 usename이 있는 경우는 정상적인 서버 반환데이터이고 없으면 네트워크 오류로 처리했다.

    3-5-4 of는 RxJS 생성 operator 중에 하나이다. catchError의 경우 데이터를 변환해야 하므로 사용했다.

    3-5-5 정상의 경우 null 반환이 가능한 이유는 반환 타입이 AsyncValidatorFn | AsyncValidatorFn[] | null) 기 때문이다.

  

import { AbstractControl } from '@angular/forms';
import { AuthService } from '../auth.service';
import { map, catchError } from 'rxjs/operators';
import { of, Observable } from 'rxjs';

export class AuthValidators {
  static checkPasswordMatch(param1: string, param2: string) {
    return (form: AbstractControl) => {

      const pass1 = form.value[param1]
      const pass2 = form.value[param2]

      if (pass1 !== pass2) {
        return { notMatch: true }
      }
      return null
    }
  }

  static checkUniqueUsername(authService: AuthService) {
    return (control: AbstractControl): Observable<any> => {
      return authService.checkDuplication(control.value).pipe(
        map(value => {
          return null
        }),
        catchError(err => {
          console.log(err);
          if (err.error.username) {
            return of({ duplication: true })
          }
          return of({noInternet: true})
        })
      )
    }
  }
}

 

  3-6 사용하는 부분에서도 service를 받아 넘겨주도록 코드를 수정해야 한다.

    3-6-1 생성자에서 authSerivce를 주입받아서 username FormControl의 세번째 인자함수의 인자로 넘겨준다.

    3-6-2 이것을 받은 함수는 단순히 러턴 함수를 생성하는 기능을 하고 적절하게 세팅한 후 Observable을 돌려준다. 

 

import { Component, OnInit } from '@angular/core';
import { FormGroup, FormBuilder, Validators } from '@angular/forms';
import { AuthValidators } from '../validators/auth-validators';
import { AuthService } from '../auth.service';

@Component({
  selector: 'app-sign-up',
  templateUrl: './sign-up.component.html',
  styleUrls: ['./sign-up.component.css'],
})
export class SignUpComponent implements OnInit {
  registrationGroup: FormGroup;

  constructor(
    private formBuilder: FormBuilder,
    private authService: AuthService
  ) {}

  ngOnInit(): void {
    this.registrationGroup = this.formBuilder.group(
      {
        username: [
          '',
          [
            Validators.required,
            Validators.minLength(4),
            Validators.maxLength(20),
          ], AuthValidators.checkUniqueUsername(this.authService)
        ],
        password: [
          '',
          [
            Validators.required,
            Validators.minLength(4),
            Validators.maxLength(20),
          ],
        ],
        passwordConfirmation: [
          '',
          [
            Validators.required,
            Validators.minLength(4),
            Validators.maxLength(20),
          ],
        ],
      },
      {
        validators: AuthValidators.checkPasswordMatch(
          'password',
          'passwordConfirmation'
        ),
      }
    );
  }
}

 

  3-7 결과화면

    3-7-1 이전에 piilseong 이라는 username을 만든 적이 있어서 중복이라고 나온다.

 

 


 

4. 이제 form에 대한 검증 처리가 끝났으니 검증이 완료되었을 때만 버튼이 활성화 되도록 변경한다.

  4-1 리셋 버튼도 추가하여 폼에 입력된 데이터도 초기화 한다.

  4-2 가입 화면

 

<h1 class="my-5">Create an Account</h1>
<form class="col-sm-8" [formGroup]="registrationGroup">
  <div class="form-group">
    <label class="form-label" for="username">Username</label>
    <input id="username" class="form-control" type="text" formControlName="username">
  </div>
  <div class="form-group">
    <label class="form-label" for="password">Password</label>
    <input id="password" class="form-control" type="password" formControlName="password">
  </div>
  <div class="form-group">
    <label class="form-label" for="passwordConfirmation">Password Confirmation</label>
    <input id="passwordConfirmation" class="form-control" type="password" formControlName="passwordConfirmation">
  </div>
  <div class="my-4">
    <button type="submit" class="btn btn-secondary mr-2" [disabled]="!registrationGroup.valid">Submit</button>
    <button type="reset" class="btn btn-warning">Reset</button>
  </div>
</form>

<hr>

<div>
  valid : {{ registrationGroup.valid }}<br>
  errors : {{ registrationGroup.errors | json }}<br>
  username errors: {{ registrationGroup.controls.username.errors | json }}
</div>

 

  4-3 로그인 화면

 

<h1 class="my-5">Log In</h1>
<form class="col-sm-8" [formGroup]="loginGroup">
  <div class="form-group">
    <label class="form-label" for="username">Username</label>
    <input id="username" class="form-control" type="text" formControlName="username">
  </div>
  <div class="form-group">
    <label class="form-label" for="password">Password</label>
    <input id="password" class="form-control" type="password" formControlName="password">
  </div>
  <div class="my-3">
    <button type="submit" class="btn btn-secondary mr-2" [disabled]="!loginGroup.valid">Submit</button>
    <button type="reset" class="btn btn-warning">Reset</button>
  </div>
</form>
728x90
댓글