[angularJS] ng-change/ng-click 내에서 ng-model로 바인딩 된 값을 변경할 시, UI에 변경된 값이 표시되지 않는 경우

2019. 3. 12. 19:35Programming/AngularJS

반응형

angularJS 1.x에서 input이나 select를 사용하여 UI를 구현하다보면, 사용자가 특정 값을 선택했을 때 이전 값으로 되돌려야 하는 경우가 있다. 보통 이런 경우에는 ng-changeng-click을 이용해서 구현하게 된다.

물론 사용자에게 특정 값을 선택할 수 없는 이유를 알려주기 위한 방법은 많다. 툴팁을 이용할수도 있고, 페이지의 눈에 띄는 곳에 문구를 출력하는 방법도 있다. 다만 툴팁이나 문구의 경우 사용자가 동작을 멈추고, 툴팁이나 문구를 찾아야 한다. 사용자가 잘못된 값을 선택했을 때 다이얼로그를 띄우게되면, 사용자가 의도한 동작의 흐름에 따라 자연스럽게 잘못된 값임을 인지할 수 있다.

 

우선 이전 값을 가져오도록 하자. ng-changeng-click에서 Controller내의 함수를 호출할 때, 다음처럼 파라메터를 전달하면 이전 값을 가져올 수 있다. - ng-change get new value and original value, StackOverflow

<select ng-model="selectedValue" ng-change="change(selectedValue, '{{selectedValue}}')">
// change함수에는 DOM에서 가지고 있는 현재 값, 즉 이전 값이 전달된다.

 

Controller 내의 changebreak point를 걸고 값을 확인해보면, 첫번째 파라메터로 현재 값, 두번째 파라메터로 이전 값이 문자열로 전달되는 걸 확인할 수 있다. 이제 현재 값이전 값으로 변경하면, 목표를 달성할 수 없는 걸 확인할 수 있다. 그렇다. 렌더링이 제대로 되지 않는다. 나의 경우는 라디오박스에 동일한 처리를 하려고 했으나, 동일한 name값을 가진 모든 라디오 박스가 선택 해제되는 걸 목격했다.

바인딩 된 값들이 업데이트되면, $digest 함수와 $apply 함수가 호출된다. 이 때 바인딩 된 값들을 업데이트하면 angularJS가 변경사항을 DOM에 반영하지 못한다. $apply 함수를 호출하면 $apply already in progress 출력되는 것을 확인할 수 있다. $apply 함수의 실행이 끝난 뒤 값을 업데이트하면 정상적으로 DOM에 반영되기 때문에, $timeout에서 일정시간을 준 뒤 값을 업데이트하면 정상적으로 DOM에 값이 표시되는 것을 확인할 수 있다.
참고:

 

$timeout(function(){
  $scope.currentValue = previousValue;
}, 100);

$timeout을 이용해서 $appy가 끝났음직한 시간에 값을 업데이트하면 해결되긴 하지만, 시간을 얼마나 줘야 할까? 사실 100ms를 지정해도 정상동작하고, 300ms를 지정해도 정상동작한다. 기능상에 문제가 없기는 하지만, 어쩐지 찜찜하다.

 

$scope.$$phase를 조회하면 현재 $scope의 현재 상태를 조회할 수 있다. 찜찜함을 해소하기 위해서 다음과 같은 코드를 작성했다.

//Call callback function after $apply or $digest.
function _callAfterRendering(param_callback) {
  if (typeof param_callback != "function") {return false;}

  if($scope.$$phase == '$apply' || $scope.$$phase == '$digest') {
    $timeout(function(){
      _callAfterRendering(param_callback)
    }, 100);
  } else {
    param_callback();
  }
}

_callAfterRendering(function_callback)을 호출하게되면, $apply/$digest가 종료된 뒤 함수를 실행하게 된다.


위에서 작성한 _callAfterRendering을 많은 곳에서 사용할 경우에는, 다음처럼 모듈로 작성해놓도록 하자.

'use strict';

angular.module('module.callAfterRendering', [
  ])
  .factory('callAfterRendering', function($timeout) {
    var render = {};

    render.apply = function(param_scope, param_callback) {
      if (typeof param_callback != "function") {return false;}

      //Call callback function after $apply or $digest.
      if(param_scope.$$phase == '$apply' || param_scope.$$phase == '$digest') {
        $timeout(function(){
          render.apply(param_scope, param_callback);
        }, 100);
      } else {
        param_callback();
      }
    }

    return render;
  })

컨트롤러 혹은 다른 모듈에서 사용할 때, 의존성을 설정해준 후 callAfterRendering.apply($scope, callback_function)을 호출하면 된다. 이 때 $scope가 렌더링중이 아닐때는 $$phase가 null값이기 때문에, scope를 전달하지 않을 시에는 단순히 0.1초 뒤에 callback함수를 실행하게 된다.

 

깜빡하고 callAfterRendering.apply를 호출할 시 callback_function만 전달하는 경우, apply의 첫번째 줄에서 param_callback의 type이 undefined가 되기 때문에 문제는 발생하지 않는다. 다만 에러가 발생하기 때문에 디버깅을 하기에는 조금 짜증날지도 모르겠다.

반응형