AngularJS (이하 Angular)는 Two-way
binding이라는 편리한 기능을 제공하지만 이 기능을 사용할 때 몇 가지 어려운 점이 있습니다. 그 중에 하나가 Angular Context 외에서 들어오는 데이터를 모델에 업데이트를 할
때 (여기서 모델이란 $scope로 정의된 variable들을 이야기합니다. 예: $scope.data) 개발자가
인위적으로 업데이트를 해야하며 이 때 사용하는 것이 $digest, $apply, $timeout 또는 $evalAsync
입니다. 참고로 Angular Context외 (또는 Angular Scope 외)란 Angular를 사용하지 않고 제작하는 모듈을 가리키며 가장 간단한 예로 서버를 들 수 있습니다. 서버에서
어떤 이벤트가 발생해서 Angular로 제작된 클라이언트 모듈 쪽으로 데이터가 들어오고 그 들어온 데이터를
UI에 반영하기 위해서 $scope.data에 업데이트를 해야 하는데
위에 언급한 함수들을 사용하지 않으면 클라이언트 모듈 쪽에는 데이터가 업데이트 되었지만 UI쪽인 HTML
쪽에는 (보통 {{data}} 형식으로 포함된)
업데이트가 되지 않아서 UI에 반영이 되지 않은채로 남아있습니다.
Two-way binding의
동작 원리
Two-way binding의
동작원리는 개발자가 $scope를 이용해서 variable을 생성하면
생성된 variable 에 watcher가 생성되고 $apply()나 $digest()가 실행되었을 때 현재까지 생성된 모든 watcher들이 실행되어서 변화된 값을 업데이트합니다. 여기서 watcher란 할당된 variable의 old value랑 new
value를 비교하여 두 값이 다를 경우 old value를 new
value로 업데이트해주는 모듈로 $scope.$watch 함수를 이용하여 이 작업을
합니다. UI쪽에서 발생하는 데이터값의 변화에 Two-way binding이 아무런 추가 작업 없이 정상 동작하는 이유는 Angular가 UI에서 발생하는 이벤트에 대해 $apply()를 자동으로 실행하기 때문입니다.
예를 들어 ng-click 같은 이벤트가 생성이 된다면 $apply()도 자동으로 실행이 됩니다.
$digest()
$digest()는 위의 동작원리에서
설명한 대로 현재까지 생성된 모든 $scope variable 들의 watcher를 실행하여 값이 변화된 variable의 값을 최신값으로 업데이트해주는 일을 합니다.
사용 예: $scope.digest();
$apply()
간단하게 설명하면 $apply()는 $rootScope.$digest()랑 같습니다. 하는 일은 $digest()와 같지만 다른
점은 커버하는 scope가 다르다는 점입니다. 다시 말해서
$digest()는 해당 scope의 variable의 값들만 업데이트하지만 $apply()는 무조건 rootScope와 rootScope 밑에 있는 모든 child scope들의 variable들 값을 모두 업데이트하기 때문에 자주 사용하면 제품 전체의 성능/효율성을 저하 시킬 수 있습니다. 기본적으로 Angular는 $apply()의 사용을 추천하고 있지만 제 개인적인 의견으로는 만약 어떤 scope의 variable이 업데이트 되어야 하는지 안다면 $digest()의 사용을 추천합니다.
사용 예: $scope.$apply(); 또는 $scope.$apply(function()
{ ….. } );
$timeout()
$timeout()은 일반적인
자바스크립의 API인 setTimeout()의 Angular
버전으로 $timeout()함수 안에서 바뀐 variable 값을 업데이트하기 위해 $apply()를 기본적으로 포함하고 있습니다.
$timeout()은 보통 setTimeout()과 마찬가지로 얼마동안의 Delay
이후에 해당 코드를 실행하기 위해 사용되어지고 있는데 이 용도 이외에 ‘$digest already in
progress’ 에러 발생에 대한 임시 해결책으로도 사용되어지고 있습니다. (한
Directive나 Controller 안에서 여러 번의 $apply()를 포함한 개발을 해본 개발자라면 최소한 한 번 이상은 저 에러 메세지를 본 경험이 있을겁니다.)
‘$digest already in progress’에러의 발생원인은 Angular는 기본적으로 한 번에 두 개의 $digest() 동시 실행을 금지하고 있어서 이미 $digest()가 실행 중일 때 또 다른 $digest()가 실행이 된다면 이 에러 메세지가 출력되고 나중에
실행된 $digest()는 실행이 되지 않습니다. 그럼 어떻게 $timeout()의 사용이 이 문제의 해결책이 될 수 있는건가요? 그 이유는 $timeout()의 $apply() 실행 타이밍 때문인데 $timeout()은 이미 기존에 실행되고 있는 $digest()가 있다면 그 실행이 끝날 때까지 기다렸다가
$timeout() 내부에 포함된 $scope.$apply()을 실행하는
특징이 있고 이 때문에 위의 에러에 대한 임시 해결책으로 사용되어 지고 있습니다.
사용 예: $timeout(function() {…….}, 0);
$evalAsync()
$evalAsync()는 ‘$digest
already in progress’에러에 대한 Angular팀의 근본적인 해결책으로
이해하시면 편합니다. (위에도 언급했듯이 $timeout()의 기본용도는
이 문제의 해결이 아닌 얼마간의 Delay이후에 실행이기 때문에 근본적인 해결책이 될 수는 없습니다.
또한 약간의 화면 깜빡이는 문제를 발생시킬 수도 있습니다.) $evalAsync()도 기본적으로 $apply() 함수를 포함하고 있으며 현재 진행중인 $digest()가 있을 경우에 이 $digest()의 실행이 끝나기 전에 $evalAsync()에 포함된 variable의 바뀐 데이터도 같이 업데이트할 수 있는 기능을 제공합니다.
$evalAsync()는 AngularJS 1.2 버전부터 제공되어지고 있습니다.
사용 예: $scope.$evalAsync(function() {…….});
추가사항:
AngularJS 1.3 버전부터
$applyAsync() 함수가 추가 되었다고 합니다. 기본적인 기능은 $evalAsync()와 비슷하지만 효율성 면에서 아주 미세한 차이가 있다고 하는 것 같은데 필자는 사용 경험이 없어서 자세한 사항은 잘 모르겠습니다.
References