[AngularJS] 폼(form)과 유효성 검사

Posted in 모바일/Javascript // Posted at 2016. 7. 18. 00:30
728x90

AngularJS는 HTML 폼을 확장하여 입력요소에 자바스크립트 객체를 바인딩하고, 전체 폼 또는 개별 입력요소에 대한 유효성 검사를 추가할 수 있는 방법을 제공한다.

* 폼의 데이터 바인딩
- 폼의 입력요소와 AngularJS의 상호작용은 (뷰와 컨트롤러의 상호작용이 늘 그렇듯이) $scope 객체를 매개로 이뤄진다. 폼 요소와 $scope 객체의 관계를 확인하기 위해 다음과 같이 간단한 코드를 작성해 본다.

<!DOCTYPE html>
<html>
<script src="http://ajax.googleapis.com/ajax/libs/angularjs/1.4.8/angular.min.js"></script>
<script>
var app = angular.module("myApp",[]);
app.controller("myCtrl", function($scope){ 
 $scope.checkScope = function(){  
  console.log($scope); //$scope객체를 console 로그에서 확인해 본다.
 };
});
</script>
<body ng-app="myApp" ng-controller="myCtrl">
<div>
 <p>Name: <input type="text" ng-model="name"></p>
 <p ng-bind="name"></p> 
 <button ng-click="checkScope();">check</button>
</div>
</body>
</html>

input 박스에 ng-model 디렉티브를 선언하고, 바로 밑 <p> 요소에 ng-bind 디렉티브를 동일한 이름으로 선언하였다. (ng-bind 대신 '{{ name }} ' 형태의 표현식으로 해도 동일하게 동작한다)

코드를 실행해 보면, input 요소에 입력한 값들이 자동으로 <p> 요소에 반영되어 데이터가 같이 변경되는 것을 확인할 수 있다.

이와 같은 폼 요소에 대한 자동 바인딩 내부에는 $scope 객체가 존재한다.
코드에서는 명시적으로 $scope 객체에 name이라는 속성을 추가한 적은 없지만, ng-model 디렉티브로 인해 입력요소에 값이 입력되는 순간 $scope에 name라는 속성이 자동으로 추가된다.

콘솔로그를 통해 확인해 보자.
코드가 처음 브라우저에 로딩된 순간에는 $scope객체에 name이 속성을 찾을 수 없다. 그러나 input요소에 값을 입력하는 순간 자동으로 $scope객체에 name속성이 (입력 값과 함께) 자동으로 추가된다. 다음 그림은 콘솔로그를 통해 확인해 본 것이다.

결국 뷰와 컨트롤러의 데이터바인딩은 $scope 객체를 통해 이뤄지며, 이는 폼 요소에 대한 ng-model 디렉티브 설정을 통해 $scope객체의 속성과 연결되어 양방향 바인딩이 자동으로 이뤄지는 메커니즘이다.

* 폼의 입력요소
폼의 입력요소에는 다음과 같은 것들이 있다.
- input 엘리먼트: 텍스트박스, 체크박스, 라디오 버튼 등 입력요소
- select 엘리먼트: 여러개의 아이템 중 하나를 선택할 수 있는 입력요소
- textarea 엘리먼트: 여러 행의 글을 작성할 수 있는 입력요소
- button 엘리먼트: 클릭으로 명령을 전송할 수 있는 입력요소

* CheckBox
- 다음 코드는 체크박스에 대한 AngularJS의 임의구현이다.  첫 번째 체크박스의 ng-model 값을 두 번째 체크박스의 ng-checked 디렉티브로 연결하고 있다. 이렇게 하면 두 번째 체크박스는 첫 번째 체크박스의 선택여부에 동기화되어 선택/해제 된다.
그리고 ng-show 디렉티브는 첫 번째 체크박스의 선택여부에 따라 화면에 표시할 지 여부를 결정한다.

<!DOCTYPE html>
<html>
<script src="http://ajax.googleapis.com/ajax/libs/angularjs/1.4.8/angular.min.js"></script>
<body>
<div ng-app>
  <form>
    checkbox1: <input type="checkbox" ng-model="chk1">
    <br />
    checkbox2: <input type="checkbox" ng-checked=chk1">
  </form>
  <!-- checkbox가 선택된 경우 화면에 표시 -->
  <h1 ng-show="chk1">checkbox1 is checked</h1>
</div>
</body>
</html>

* RadioButton
- 다음 코드는 라이오버튼에 대한 AngularJS의 임의구현이다. 라이브버턴 마다 ng-model을 설정하고 {{xxx}} 표현식으로 선택된 값을 출력한다. 그리고 ng-switch계열 디렉티브를 통해 조건식을 구현할 수 있다.

<!DOCTYPE html>
<html>
<script src="http://ajax.googleapis.com/ajax/libs/angularjs/1.4.8/angular.min.js"></script>
<body ng-app>
<form>
  Pick a topic:
  <input type="radio" ng-model="rdobtn" value="r1">Radio1
  <input type="radio" ng-model="rdobtn" value="r2">Radio2 
</form>
<div ng-switch="rdobtn">
  <!-- 선택된 값 출력 -->
  <h1>checked: {{rdobtn}}</h1>

  <!-- 선택된 값에 대한 조건식 로직 -->
  <div ng-switch-when="r1">
     <h1>radio1 is checked</h1>   
  </div>
  <div ng-switch-when="r2">
     <h1>radio2 is checked</h1>
  </div> 
</div>
</body>
</html>


* SelectBox
- 라이브버튼과 거의 유사한 임의코드이다.

<!DOCTYPE html>
<html>
<script src="http://ajax.googleapis.com/ajax/libs/angularjs/1.4.8/angular.min.js"></script>
<body ng-app>
<form>
  Select a topic:
  <select ng-model="selbox">
    <option value="">
    <option value="v1">value1
    <option value="v2">value2
  </select>
</form>
<div ng-switch="selbox">
  <!-- 선택된 값 출력 -->
  <h1>selected: {{selbox}}</h1>

  <div ng-switch-when="v1">
     <h1>value1 is selected</h1>   
  </div>
  <div ng-switch-when="v2">
     <h1>value2 is selected</h1>   
  </div>
</div>
</body>
</html>

 

* AngularJS의 폼 확장
- AngularJS는 표준 HTML 폼 요소를 확장한 form 디렉티브와 폼의 상태를 추적하기 위한 FormController 컨트롤러의 인스턴스를 생성해 폼을 관리한다.

HTML페이지의 폼의 name 특성을 통해 폼이름을 지정하면 $scope객체에 지정된 이름으로 폼 객체가 추가되며 하위 폼 요소들도 주어진 이름으로 추가된 폼 객체의 하위 프로퍼티로 자동 추가된다. 이후 이들 이름을 기반으로 뷰 및 컨트롤러에서 폼 요소들을 참조하여 특정 작업을 수행할 수 있다.

폼 요소들이 지정된 name값을 기반으로 $scope 객체에 자동추가 된다는 것을 눈으로 확인해보자.
다음과 같은 입력요소를 하나 가지는 간단한 폼을 만든다. 주의할 점은 폼요소가 모두 로딩된 후 $scope 객체를 확인해야 하므로 <script> 요소를 </body> 직전에 위치 시켰다. (일반적으로 스크립트 태그는 이 위치에 두는 것을 권장하는데, 이는 DOM 구성이 스크립트 로딩과 실행으로 지연되지 않도록 하기 위함으로 모든 DOM이 구성되고 난 후 안정적으로 스크립트를 실행시키기 위함도 있다.)

<!DOCTYPE html>
<html lang="en">
<script src="http://ajax.googleapis.com/ajax/libs/angularjs/1.4.8/angular.min.js"></script>
<body>
<div ng-app="myApp" ng-controller="formCtrl">
  <form name="myForm">
    UserName:
    <input type="text" name="myInput" ng-model="myInput"><br>   
  </form>  
</div>
<script>
var app = angular.module('myApp', []);
app.controller('formCtrl', function($scope) { 
 console.log($scope); 
});
</script>
</body>
</html>

지금 상황에서 input 요소에 ng-model 디렉터리가 꼭 필요한 것은 아니지만, AngularJS는 ng-model 디렉티브가 추가된 입력요소에 한해서 $scope 객체에 등록시키기 때문에 추가한 것이다.
결과로 본 콘솔로그는 다음과 같다.

이렇게 폼 요소의 name 값을 기준으로 $scope객체에 자동추가 되기 때문에, 이후 컨트롤러나 뷰에서 해당 폼 요소의 이름으로 접근 가능한 것이다.

* 폼과 입력필드의 상태(폼 유효성 검사의 기반)
- AngularJS는 폼의 유효성 검사를 위해 폼과 입력요소의 상태값을 지속적으로 감시하고 관리한다.
다음은 각각 폼과 입력필드의 상태를 알 수 있는 참조변수를 보여준다.

 

 

 

 

사용자 반응에 따른 폼과 입력요소의 상태값 변화를 눈으로 확인해 보기 위해, 직전의 예제를 다음과 같이 조금 수정해 보자.

<!DOCTYPE html>
<html lang="en">
<script src="http://ajax.googleapis.com/ajax/libs/angularjs/1.4.8/angular.min.js"></script>
<body>
<div ng-app>
  <form name="myForm">
    UserName:
    <input type="text" name="myInput" ng-model="myInput" required><br>   
  </form>  
  <div>
  <h1>Form States</h1>
   Pristine: {{myForm.$pristine}} <br /> 
   Dirty: {{myForm.$dirty}} <br />
   Valid: {{myForm.$valid}} <br />
   Invalid: {{myForm.$invalid}} <br />  
   Submitted: {{myForm.$submitted}} <br />
   Error: {{myForm.$error}} <br />
  
   <h1>Input States</h1>
   Touched: {{myForm.myInput.$touched}} <br />
   Untouched: {{myForm.myInput.$untouched}} <br />
   Pristine: {{myForm.myInput.$pristine}} <br /> 
   Dirty: {{myForm.myInput.$dirty}} <br />
   Valid: {{myForm.myInput.$valid}} <br />
   Invalid: {{myForm.myInput.$invalid}} <br />     
   Error: {{myForm.myInput.$error}} <br />

  </div>
</div>
</body>
</html>

 예제를 브라우저로 실행시키고, 입력요소에 값을 입력하거나 포커스를 이동시켜보자. 그러면 각 행위에 따라 폼의 상태값의 변경을 확인할 수 있을 것이다. 이와같은 폼 상태 변화는 AngularJS가 폼 유효성을 검사하는 기반으로 활용된다.

* 폼 초기화

- 폼을 최초 로딩된 상태로 초기화 시키는 것은, 최초값을 보관해 두었다가 다시 그 값으로 회귀 시켜 주면 된다. 다만 데이터 초기화와 AngularJS의 폼 유효성 검사를 위한 상태값까지 모두 초기화 시켜 주는 것이 좋다. 앞서 살펴본바와 같이 AngularJS는 폼 및 입력요소에 대한 상태값을 지속적으로 관리하는데, 이부분까지 모두 초기화 시켜주면 완벽하다.

다음 코드는 폼 초기화를 위한 코드 샘플인데, 데이터 초기화의 경우 원본을 유지한채 복사본을 사용하는 것으로 처리하고 추가로 $setPristine(), $setUntouched() 함수를 호출하여, 폼 변경에 따른 내부 상태값의 변화 역시 같이 초기화 시켜준다.

<!DOCTYPE html>
<html lang="en">
<script src="http://ajax.googleapis.com/ajax/libs/angularjs/1.4.8/angular.min.js"></script>
<script>
var app = angular.module('myApp', []);
app.controller('formCtrl', function($scope) {
    //폼 초기화를 위한 초기값 저장 
    $scope.origin = {firstName:"OriginFirst", lastName:"OriginLast"};
    //초기값에 기반하여 복사 수행(이 값은 폼과 양방향 바인딩 됨)
    $scope.user = angular.copy($scope.origin);
   
    $scope.resetForm = function() { 
         //폼 요소들의 입력값을 초기 값으로 변경
         $scope.user = angular.copy($scope.origin);   
         //폼 요소를 최초의 깨끗한 상태로 돌리기
         $scope.myForm.$setPristine();
         //폼 요소를 한번도 손대지 않은 상태로 돌리기
         $scope.myForm.$setUntouched();
    };       
});
</script>
<body>
<div ng-app="myApp" ng-controller="formCtrl">
  <form name="myForm" novalidate>
    First Name:<br>
    <input type="text" ng-model="user.firstName"><br>
    Last Name:<br>
    <input type="text" ng-model="user.lastName">
    <br><br>
    <button ng-click="resetForm();">RESET</button>
  </form>
  <p>Pristine: {{myForm.$pristine}}</p>   
</div>
</body>
</html>


* 폼 유효성 검사
- 지금까지 살펴본 것처럼, AngularJS는 폼의 입력요소들의 변경에 따른 상태값을 지속적으로 관리하며 이를 기반으로 폼의 유효성 검사를 지원한다. 폼의 유효성 검사 기준은 HTML5에 새로이 추가된 유효성 관련 특성(Attribute)에 근거하거나 개발자가 직접 자신만의 유효성 검사 기준을 만들 수 있도록 지원한다.

HTML5의 새로운 입력타입과 특성에 기반한 유효성 검사
- 가장 흔한 유효성 검사는 입력필드에 대한 필수입력 여부와 입력필드 타입에 따른 유효값 검사이다.
다음 코드는 HTML5의 required 특성을 부여한 필수 입력필드에 대한 입력여부와 email 타입의 입력필드에 대한 입력값 유효성 검사 샘플이다. 입력필드에 대한 $valid는 입력값 여부에 따라 true/false 값 중 하나를 반환한다.

<form name="myForm">
  <input name="txtName" ng-model="txtName" required
  {{myForm.txtName.$valid}}
  <br />
  <input name="txtEmail" ng-model="txtEmail" type="email" required
  {{myForm.txtEmail.$valid}}
</form>

AngularJS 디렉티브에 기반한 유효성 검사
- AngularJS가 미리 정의해 둔 디렉티브 기반으로 유효성 검사도 가능하다. 다음 코드는 필수입력 지정과 입력문자의 최소/최대 길이를 지정하여 유효성 검사를 수행한다.

<form name="myForm">
   <input name="txtName" ng-model="txtName" ng-required="true" ng-minlength="3"
     ng-maxlength="5"
>
   {{myForm.txtName.$valid}}
</form>

입력폼 검사의 일반적 패턴
- 이번에는 좀 더 실용적인 코드 샘플을 보자. 하나의 필수 입력필드와 이메일 입력필드가 존재한다. 각 입력필드 바로 옆에는 유효성 실패에 대한 안내 메시지를 보여준다. ng-show 디렉티브는 주어진 조건값이 참/거짓에 따라 표시여부를 결정한다. 코드에서 주어진 조건은 입력필드에서 포서커를 잃고($touched) 유효성 검사에 실패한 경우($invalid)에 대한 And 조건으로 부여한다.
그리고 submit 버튼은 ng-disabled 디렉티비를 지정하는데 두 입력필드 중 하나라도 유효하지 않으면 비활성화 되도록 조건을 부여한다.

<!DOCTYPE html>
<html>
<script src="http://ajax.googleapis.com/ajax/libs/angularjs/1.4.8/angular.min.js"></script>
<body ng-app>
<form name="myForm">
  <input name="txtName" ng-model="txtName" required autofocus>
  <span style="color:red" ng-show="myForm.txtName.$touched && myForm.txtName.$invalid">
   <span ng-show="myForm.txtName.$error.required">Username is required.</span>
  </span>
  <br />
  <input type="email" name="txtEmail" ng-model="txtEmail" required>
  <span style="color:red" ng-show="myForm.txtEmail.$touched && myForm.txtEmail.$invalid">
   <span ng-show="myForm.txtEmail.$error.required">Email is required.</span>
   <span ng-show="myForm.txtEmail.$error.email">Invalid email address.</span>
  </span>
  <br />
  <input type="submit" ng-disabled="myForm.txtName.$invalid || myForm.txtEmail.$invalid">
</form>
</body>
</html>


* ngMessages 모듈 이용하기
- ngMessages 서브모듈을 이용하여 유효성 검사에 따른 메시지 표시를 보다 체계적으로 관리할 수 있을 것이다. 이 모듈을 사용하려면 angular-messages.js 파일을 따로 참조해 줘야 한다.
ng-messages 디렉티브에 $error 객체를 바인딩해서 해당 입력요소가 유효한지 여부를 판단할 수 있게 한다. 그리고 ng-message 디렉티브에 검사하고자 하는 유효성 조건의 이름을 지정한다. 예에서는 총 3개(required, minlength, maxlength) 조건을 지정했다.
만일 해당 입력요소에 유효성 오류가 있고, 각 오류가 어떤 검사 기준인지에 따라 서로 다른 메시지를 보여주게 될 것이다.

<!DOCTYPE html>
<html>
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.5.7/angular.min.js"></script>
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.5.7/angular-messages.js"></script>

<script>
(function(angular) {
 angular.module('myApp', ['ngMessages']);
})(window.angular);
</script>

<body ng-app="myApp">
<form name="myForm">
  Name: <input type="text" name="txtName" ng-model="txtName" required ng-minlength="5" ng-maxlength="10">
  <div ng-messages="myForm.txtName.$error">
   <div ng-message="required">You did not enter a field</div>
    <div ng-message="minlength">Your field is too short</div>
    <div ng-message="maxlength">Your field is too long</div>
  </div>

</body>
</html>

 

* 사용자 정의 유효성 검사
- 지금까지는 입력요소의 필수입력여부, 타입 및 길이 등 HTML5의 새로운 특성이나 AngularJS가 내부적으로 지원하는 유효성 검사 기준을 사용하여 체크를 수행하였다.

그러나 실무환경에서는 보다 다양한 유효성 검사 조건이 존재할 수 있다. 예를 들어 입력필드에 들어가는 값은 반드시 숫자여야 하던지, 아님 어떤 정규식에 근거하여 유효성 검사를 수행하기도 한다.

이런 경우, 사용자 정의 유효성 검사기를 새로 만들면 된다. 사용자 정의 유효성 검사를 위해서는 커스텀 디렉티브를 생성하며 기존 디렉티브와의 차이점은 ngModel 모듈을 디렉티브에 추가해야 한다는 점이다.

다음 코드는 정규표현식에 기반한 사용자 정의 유효성 검사 디렉티브 구현을 보여준다. AngularJS 공식문서의 샘플코드이며, 커스텀 디렉티브의 link 함수를 통해 구현한다.

유효성 검사기의 이름은 integer으로 기존 필수입력 조건인 required와 마찬가지로 HTML의 특정 입력요소에 integer라는 이름으로 유효성 기준을 추가할 수 있다.

<!DOCTYPE html>
<html>
<script src="http://ajax.googleapis.com/ajax/libs/angularjs/1.4.8/angular.min.js"></script>
<script>

(function(angular) { 
 var app = angular.module('myApp', []);
 
 var INTEGER_REGEXP = /^\-?\d+$/;
 app.directive("integer", function(){
  return {
   require: 'ngModel',
   link: function(scope, elm, attrs, ctrl){
    ctrl.$validators.integer = function(modelValue, viewValue){
     if(ctrl.$isEmpty(modelValue)){
      //비어 있을 경우 유효한 것으로 간주한다.
      return true;
     };
     if(INTEGER_REGEXP.test(viewValue)){
      //정규식과 일치하므로 유효함
      return true;
     };
     //위 두조건을 모두 만족하지 않으므로 유효하지 않음
     return false;
    };
   }
  };
 });
})(window.angular);
</script>
<body ng-app="myApp">
<form name="myForm">
 <input type="text" ng-model="amount" name="amount" integer /><br />
 <span ng-show="myForm.amount.$error.integer">정수가 아닙니다.</span> 
</form>
</body>
</html>