회원가입 만들기

소스코드

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

이 게시물의 소스코드는 기본사이트 만들기 / HTTP Request Interceptor로 REST API Call에 Token 넣기에서 이어집니다.

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

git reset --hard
git pull
git reset --hard 0620bc7
git reset --soft e3b7156
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 0620bc7
git reset --soft e3b7156
npm install
atom .

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


회원가입을 만들어 봅시다. 이제 제법 Angular 2스러운 사이트가 되어가고 있는데요, 이번 강의에는 새로운 내용은 거의 없고 service 생성과 form 생성의 복습이 주된 내용입니다. 새로운 내용이라고 할 만한 것은 custom validation정도입니다.

interfaces 생성 및 auth API 연결에서 auth service를 만든 것 처럼 user service를 생성하고, Reactive Forms Module 로 Login Form 만들기 강의에서 로그인 form을 만든 것 처럼 회원가입 form(user 생성 form)을 만들어 봅시다. 로그인 form에서는 각각 항목을 개별적으로 validation 하는 법을 알아 보았는데, 이번에는 비밀번호와 비밀번호 확인 항목의 값을 비교하는 custom validation을 만들어 보겠습니다.

폴더 구조


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

user.service.ts 파일을 생성합니다. 그리고 아래 명령어로 login component를 생성해 줍시다.

$ ng g component user-new --spec false

--spec false를 options으로 사용하면 .spec.ts를 생성하지 않습니다.

코드

 먼저 user.service.ts 를 살펴봅시다.

// src/app/user.service.ts
import { environment } from '../environments/environment';
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';

import 'rxjs/add/operator/toPromise';

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

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

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

  index(): Promise<User[]> {
    return this.http.get<ApiResponse>(`${this.apiBaseUrl}`)
              .toPromise()
              .then(this.utilService.checkSuccess)
              .then(response => {
                return response.data as User[]
              })
              .catch(this.utilService.handleApiError);
  }

  show(username: string): Promise<User> {
    return this.http.get<ApiResponse>(`${this.apiBaseUrl}/${username}`)
              .toPromise()
              .then(this.utilService.checkSuccess)
              .then(response => {
                return response.data as User
              })
              .catch(this.utilService.handleApiError);
  }

  create(user: User): Promise<User> {
    return this.http.post<ApiResponse>(`${this.apiBaseUrl}`, user)
              .toPromise()
              .then(this.utilService.checkSuccess)
              .then(response => {
                return response.data as User
              })
              .catch(this.utilService.handleApiError);
  }

  update(username: string, user: User): Promise<User> {
    return this.http.put<ApiResponse>(`${this.apiBaseUrl}/${username}`, user)
              .toPromise()
              .then(this.utilService.checkSuccess)
              .then(response => {
                return response.data as User
              })
              .catch(this.utilService.handleApiError);
  }

  destroy(username: string): Promise<User> {
    return this.http.delete<ApiResponse>(`${this.apiBaseUrl}/${username}`)
              .toPromise()
              .then(this.utilService.checkSuccess)
              .then(response => {
                return response.data as User
              })
              .catch(this.utilService.handleApiError);
  }
}

현재 사용중인 로그인 API의 spec에 따른 아주 기본적인 API입니다. interfaces 생성 및 auth API 연결 강의와 비교해서 새로운 내용이 전혀 없기 때문에 설명은 생략합니다.

다음으로 user-new.component의 ts와 html 파일을 살펴봅시다. css는 없습니다.

// src/app/user-new/user-new.component.ts

import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { Router } from '@angular/router';

import { ApiResponse } from '../api-response';

import { UtilService } from '../util.service';
import { UserService } from '../user.service';

@Component({
  selector: 'app-user-new',
  templateUrl: './user-new.component.html',
  styleUrls: ['./user-new.component.css']
})
export class UserNewComponent implements OnInit {
  errorResponse: ApiResponse;
  form: FormGroup;
  formErrors = {
    'username':'',
    'name':'',
    'email':'',
    'password':'',
    'passwordConfirmation':'',
  };
  formErrorMessages = {
    'username': {
      'required': 'Username is required!',
      'pattern': 'Should be 4-12 characters!',
    },
    'name': {
      'required': 'Name is required!',
      'pattern': 'Should be 4-12 characters!',
    },
    'email': {
      'pattern': 'Should be a vaild email address!',
    },
    'password': {
      'required': 'Password is required!',
      'pattern': 'Should be minimum 8 characters of alphabet and number combination!',
    },
    'passwordConfirmation': {
      'required': 'Password Confirmation is required!',
      'match': 'Password Confirmation does not matched!', //4
    },
  };
  buildForm(): void { //1
    this.form = this.formBuilder.group({
      username:["", [Validators.required, Validators.pattern(/^.{4,12}$/)]],
      name:["", [Validators.required, Validators.pattern(/^.{4,12}$/)]],
      email:["", [Validators.pattern(/^[a-zA-Z0-9._%+-][email protected][a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/)]],
      password:["", [Validators.required, Validators.pattern(/^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d]{8,16}$/)]],
      passwordConfirmation:["", [Validators.required]],
    }, {
      validator: this.customValidation, //2
    });

    this.form.valueChanges.subscribe(data => {
      this.utilService.updateFormErrors(this.form, this.formErrors, this.formErrorMessages);
    });
  };

  customValidation(group: FormGroup) { //3
    var password = group.get('password');
    var passwordConfirmation = group.get('passwordConfirmation');
    if(password.dirty && passwordConfirmation.dirty && password.value != passwordConfirmation.value){
        passwordConfirmation.setErrors({'match': true});
    }
  }

  constructor(
    private router: Router,
    private formBuilder: FormBuilder,
    private utilService: UtilService,
    private userService: UserService,
  ) {
    this.buildForm();
  }

  ngOnInit() {
  }

  submit() {
    this.utilService.makeFormDirtyAndUpdateErrors(this.form, this.formErrors, this.formErrorMessages);
    if(this.form.valid){
      this.userService.create(this.form.value)
      .then(data =>{
        this.router.navigate(['/']);
      })
      .catch(response =>{
        this.errorResponse = response;
        this.utilService.handleFormSubmitError(this.errorResponse, this.form, this.formErrors);
      });
    }
  }

}

전체적으로 구조는 login.component.ts와 같습니다. 달라진 부분만 살펴봅시다.

1. 항목이 많아지고 한 항목에 2개 이상의 validation이 들어간 곳도 생겼습니다. Validators.required 뿐만 아니라 Validators.pattern을 사용해 regex를 테스트하는 validation도 생겼습니다. 이 외 사용가능한 전체 validation list는 https://angular.io/api/forms/Validators 에서 확인할 수 있습니다. 여기에 없는 validation을 하고 싶다면 직접 만들어야 합니다.

2. form 단위 validation을 하는 곳입니다. 1번의 validation들은 한 항목의 값에 한정해서 validation을 할 수 있습니다. 비밀번호와 비밀번호 확인의 값을 비교하는 validation을 하려면 form 단위의 validation을 해야합니다. validator에는 validation 함수가 들어가고 form 객체가 파라메터로 들어갑니다.

3. 2번의 validator에 들어가는 custom validation 함수입니다. 파라메터로 form 객체가 들어오고 password항목(control이라고 부릅니다) passwordConfirmation control을 가져와서 값을 비교한 후 이상이 있는 경우 passwordConfirmation에 에러를 지정해 주고 있습니다. 이때 들어가는 'match'라는 이름은 제가 그냥 정한 이름입니다.

4. 3번의 'match' error에 대한 메세지를 설정해 주었습니다.

<!-- src/app/user-new/user-new.component.ts -->

<div class="page page-users">

  <form
    [formGroup]="form"
    (ngSubmit)="submit()"
    class="login-form form-horizontal"
  >
    <div class="contentBox">
      <h3 class="contentBoxTop">New User</h3>

      <fieldset>
        <div class="form-group" [ngClass]="{'has-error': formErrors.username}">
          <label for="username" class="col-sm-3 control-label">Username*</label>
          <div class="col-sm-9">
            <input class="form-control" type="text" formControlName="username" id="username">
            <span *ngIf="formErrors.username" class="help-block">{{formErrors.username}}</span>
          </div>
        </div>
        <div class="form-group" [ngClass]="{'has-error': formErrors.name}">
          <label for="name" class="col-sm-3 control-label">Name*</label>
          <div class="col-sm-9">
            <input class="form-control" type="text" formControlName="name" id="name">
            <span *ngIf="formErrors.name" class="help-block">{{formErrors.name}}</span>
          </div>
        </div>
        <div class="form-group" [ngClass]="{'has-error': formErrors.email}">
          <label for="email" class="col-sm-3 control-label">Email</label>
          <div class="col-sm-9">
            <input class="form-control" type="text" formControlName="email" id="email">
            <span *ngIf="formErrors.email" class="help-block">{{formErrors.email}}</span>
          </div>
        </div>
        <div class="form-group" [ngClass]="{'has-error': formErrors.password}">
          <label for="password" class="col-sm-3 control-label">Password*</label>
          <div class="col-sm-9">
            <input class="form-control" type="password" formControlName="password" id="password">
            <span *ngIf="formErrors.password" class="help-block">{{formErrors.password}}</span>
          </div>
        </div>
        <div class="form-group" [ngClass]="{'has-error': formErrors.passwordConfirmation}">
          <label for="passwordConfirmation" class="col-sm-12 control-label">Password Confirmation*</label>
          <div class="col-sm-9 col-sm-offset-3">
            <input class="form-control" type="password" formControlName="passwordConfirmation" id="passwordConfirmation">
            <span *ngIf="formErrors.passwordConfirmation" class="help-block">{{formErrors.passwordConfirmation}}</span>
          </div>
        </div>
        <p>
          <small>*Required</small>
        </p>
        <div *ngIf="errorResponse?.message" class="alert alert-danger">
          {{errorResponse?.message}}
        </div>
      </fieldset>
    </div>
    <div class="buttons">
      <button type="submit" class="btn btn-default">Submit</button>
    </div>
  </form>

</div>

login.component.html과 비교해서 새로운 부분은 전혀 없으므로 설명은 생략합니다.

user component를 route에 추가해 줍니다.

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

import { NgModule }             from '@angular/core';
import { RouterModule, Routes } from '@angular/router';

import { WelcomeComponent } from './welcome/welcome.component';
import { Error404Component } from './error404/error404.component';
import { LoginComponent } from './login/login.component';
import { UserNewComponent } from './user-new/user-new.component'; //*

const routes: Routes = [
  { path: '',  component: WelcomeComponent },
  { path: 'login', component: LoginComponent },
  { path: 'users/new',  component: UserNewComponent }, //*
  { path: '**', component: Error404Component },
];

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

 user component와 user service를 app.module에 추가해줍니다.

// src/app/app.module.ts

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';

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

import { UtilService } from './util.service';
import { AuthService } from './auth.service';
import { RequestInterceptor } from './request-interceptor.service';
import { UserService } from './user.service'; //*

import { AppComponent } from './app.component';
import { WelcomeComponent } from './welcome/welcome.component';
import { Error404Component } from './error404/error404.component';
import { LoginComponent } from './login/login.component';
import { UserNewComponent } from './user-new/user-new.component'; //*

@NgModule({
  declarations: [
    AppComponent,
    WelcomeComponent,
    Error404Component,
    LoginComponent,
    UserNewComponent, //*
  ],
  imports: [
    BrowserModule,
    AppRoutingModule,
    HttpClientModule,
    FormsModule,
    ReactiveFormsModule,
  ],
  providers: [
    {
      provide: HTTP_INTERCEPTORS,
      useClass: RequestInterceptor,
      multi: true,
    },
    UtilService,
    AuthService,
    UserService, //*
  ],
  bootstrap: [AppComponent]
})
export class AppModule { }

마지막으로 css는 중복된 부분은 생략하고 마지막 줄에 다음을 넣어줍시다.

/* src/app/styles.css */

/* ...생략 */
.page-users {
  max-width: 320px;
}

실행 결과

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

ng serve --open

Sign Up 메뉴를 클릭합니다

각 항목에 값을 바꿔가면서 에러가 제대로 표시되는지 확인해 봅시다.

마치며..

회원가입과 로그인이 완성되었습니다. 다음 강의에서는 특정한 route에 로그인을 한 상태로만 접근이 가능하게 설정하는 법을 알아보겠습니다.

댓글

윤은식 2017.11.28
안녕하세요, 이 블로그를 통해 앵귤러를 잘 배우고 있는 학생입니다. 여기까지 따라오는데에 성공하여 로그인을 시도하면 틀렸을 경우 까진 정상적으로 작동합니다. 허나,  db와 일치하게 맞는 정보로 로그인을 시도시에 핸들링을 통해 undefined [object Object] 를 반환해주네요.. 왜 이럴까요? 콘솔로그를 찍어 Object를 확인해보려 했지만 진짜 오브젝트가 아닌 문자열 'Object' 들이었습니다..
I
Ian H 2017.11.30
@윤은식,
안녕하세요~ github에 소스코드를 올려주시면 한번 확인해 보겠습니다^^
댓글쓰기

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

UP