Friday, March 27, 2015

AngularJS: $digest vs $apply vs $timeout vs $evalAsync 차이점 및 설명


 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란 할당된 variableold valuenew value를 비교하여 두 값이 다를 경우 old valuenew value로 업데이트해주는 모듈로 $scope.$watch 함수를 이용하여 이 작업을 합니다. UI쪽에서 발생하는 데이터값의 변화에 Two-way binding이 아무런 추가 작업 없이 정상 동작하는 이유는 AngularUI에서 발생하는 이벤트에 대해 $apply()를 자동으로 실행하기 때문입니다. 예를 들어 ng-click 같은 이벤트가 생성이 된다면 $apply()도 자동으로 실행이 됩니다.

$digest()
$digest()는 위의 동작원리에서 설명한 대로 현재까지 생성된 모든 $scope variable 들의 watcher를 실행하여 값이 변화된 variable의 값을 최신값으로 업데이트해주는 일을 합니다.
사용 예: $scope.digest();

$apply()
간단하게 설명하면 $apply() $rootScope.$digest()랑 같습니다. 하는 일은 $digest()와 같지만 다른 점은 커버하는 scope가 다르다는 점입니다. 다시 말해서 $digest()는 해당 scopevariable의 값들만 업데이트하지만 $apply()는 무조건 rootScoperootScope 밑에 있는 모든 child scope들의 variable들 값을 모두 업데이트하기 때문에 자주 사용하면 제품 전체의 성능/효율성을 저하 시킬 수 있습니다. 기본적으로 Angular$apply()의 사용을 추천하고 있지만 제 개인적인 의견으로는 만약 어떤 scopevariable이 업데이트 되어야 하는지 안다면 $digest()의 사용을 추천합니다.
사용 예: $scope.$apply(); 또는 $scope.$apply(function() { ….. } );

$timeout()
$timeout()은 일반적인 자바스크립의 APIsetTimeout()Angular 버전으로 $timeout()함수 안에서 바뀐 variable 값을 업데이트하기 위해 $apply()를 기본적으로 포함하고 있습니다. $timeout()은 보통 setTimeout()과 마찬가지로 얼마동안의 Delay 이후에 해당 코드를 실행하기 위해 사용되어지고 있는데 이 용도 이외에 ‘$digest already in progress’ 에러 발생에 대한 임시 해결책으로도 사용되어지고 있습니다. (DirectiveController 안에서 여러 번의 $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