AngularJS 성능개선

개요

이 글은 AngularJS의 웹 어플리케이션 성능개선을 진행한 방법을 정리한 글입니다.
- AngularJS v1.4.8 기준으로 작성되었습니다.

AngularJS에서 가장 큰 장점은 양방향 바인딩이라고 생각합니다. 하지만 이것은 성능에 큰 문제를 주는 병목으로 작용하기도 합니다. 양방향 바인딩을 구현하기 위해서 AngularJS는 binding되는 변수들을 모두 $watch하고 있습니다. 그리고 $digest 시에 $watch한 변수 중 수정된 것을 돔에 반영해주는 형태를 띄고 있습니다.

$watch하는 변수가 적을 때에는 개발도 편하고 성능에도 이상없지만 $watch개수가 늘어나면서 성능에 문제가 발생하기 시작합니다. 이번 글도 이 $watch$digest 수를 줄이는 게 주요 내용입니다.

AngularJS 양방향 바인딩 개요도

주요내용

$httpProvider.useApplyAsync

AngularJS의 홈페이지에서는 이 속성에 대해 아래처럼 설명하고 있습니다.

Configure $http service to combine processing of multiple http responses received at around the same time via $rootScope.$applyAsync.
This can result in significant performance improvement for bigger applications that make many HTTP requests concurrently (common during application bootstrap).

간단히 요약하면 $http 서비스에서는 HTTP request의 응답이 올 때에 $digest를 호출합니다. 이 속성을 켜면 동시에 많은 HTTP request를 보낼 때에 묶어서 $digest를 1번만 보내게 한다는 것입니다. 즉, 이 속성을 켜면 $digest 호출 수를 줄여서 성능을 개선할 수 있습니다.

$compileProvider.debugInfoEnabled

런타임 시에 디버깅 모드를 켜고 끌지 정하는 속성입니다. 이 메소드의 디폴트 값이 켜져 있기 때문에 따로 설정하지 않는다면 실제 서비스 환경에서도 디버깅 모드로 동작하게 됩니다. 디버깅 모드에서는 DOM을 조작하는 경우(class 변수 수정 등)가 많이 발생하기 때문에 성능저하를 일으키게 됩니다. 가급적 이 속성을 끄는 것이 좋습니다.

one time binding

AngularJS의 가장 큰 장점 중의 하나인 HTML에서 JS변수를 보여줄 수 있는 Expression{{변수명}}의 형태로 JS의 변수를 추가할 수 있습니다.

아래와 같이 입력시에

  
<div ng-controller="MyCtrl">  
	{{myName}}  
</div>  
  
angular.module('testModule', []).controller('MyCtrl', ['$scope, $timeout', function ($scope, $timeout) {  
	$scope.myName = '김부승';  
	$timeout(function () {  
		$scope.myName = '알투';  
	}, 3000);  
}]);  

아래와 같은 형태로 변합니다.

  
<div ng-controller="MyCtrl">  
	김부승  
</div>  

그리고 3초 후에 아래처럼 변경됩니다.

  
<div ng-controller="MyCtrl">  
	알투  
</div>  

이렇게 변하는 이유는 myName 변수가 변했는지 $watch하고 있으며 3초 후에 $digest될 때에 myName변수가 변경된 것이 DOM에 반영되기 때문입니다. AngularJS는 이렇게 DOM에 $watch 수를 줄이기 위해 one time binding을 제공합니다. one time binding은 한 번만 변수를 DOM에 반영하고 $watch를 해제시켜서 두 번째 $digest 실행할 때는 $watch 수를 줄일 수 있는 방법입니다. one time binding을 적용하면 위의 예제는 아래와 같이 변합니다.

  
<div ng-controller="MyCtrl">  

	{{::myName}}  

</div>  
  
angular.module('testModule', []).controller('MyCtrl', ['$scope, $timeout', function ($scope, $timeout) {  
	$scope.myName = '김부승';  
	$timeout(function () {  
		$scope.myName = '알투';  
	}, 3000);  
}]);  

이렇게 입력하면 똑같이 처음에는 아래처럼 변합니다.

  
<div ng-controller="MyCtrl">  
	김부승  
</div>  

그리고 3초 후에 myName변수가 변한 이후에도 아래처럼 보입니다.

  
<div ng-controller="MyCtrl">  
	김부승  
</div>  

이 방법은 한가지 단점이 있는데 $watch를 완전히 풀어버리기 때문에 해당 Expression을 다시 실행시키고 싶어도 실행시킬 방법이 없다는 것입니다.

ngRepeat track by

ngRepeat

ngRepeat는 간단하게 표한하자면 HTML 소스에서 for문을 실행시켜서 template을 반복적으로 복사하는 것과 비슷합니다. ngRepeat안의 HTML에 들어있는 $watch하는 변수들은 ngRepeat이 반복되는 수만큼 증가하게 됩니다. 즉, ngRepeat이 n번 반복되고 반복하는 곳에 $watch해야 하는 변수가 m개 있다면 1번 $digest를 할 때에 n*m번의 확인을 하게 됩니다. 그리고 이것은 성능의 저하를 일으키는 주된 원인이 됩니다.
예를들어 아래와 같이 ngRepeat을 사용하면(track by의 차이점을 보여주기 위해 one time binding을 사용하겠습니다.)

  
<div ng-controller="MyCtrl">  
	<div ng-repeat="member in memberList">  
		{{::member.name}}
	</div>  
</div>  
  
angular.module('testModule', []).controller('MyCtrl', ['$scope, $timeout', function ($scope, $timeout) {  
	$scope.memberList = [{  
		id: 1,  
		name: '김부승',  
		updatedAt: '2015-01-01',  
		organization: 'NHN Entertainment'  
	}, {  
		id: 2,  
		name: '박현재',  
		updatedAt: '2016-01-01',  
		organization: 'NHN Entertainment'  
	}];  
  
	$timeout(function () {  
		$scope.memberList.splice(0, 1, {id: 1, name: '알투'});  
	}, 3000);  
}]);  

처음에는 아래와 같은 형태로 보이게 되고

  
<div ng-controller="MyCtrl">
	<div ng-repeat="member in memberList">
		김부승
	</div>
	<div ng-repeat="member in memberList">
		박현재
	</div>
</div>

3초 후에는 아래처럼 변하게 됩니다.

  
<div ng-controller="MyCtrl">  
	<div ng-repeat="member in memberList">  
		알투  
	</div>  
	<div ng-repeat="member in memberList">  
		박현재  
	</div>  
</div>  

track by 속성

ngRepeat에서 track by 속성을 주면 성능을 개선할 수 있습니다. track by 속성은 DOM이 변경되었는지 알려주는 힌트와 비슷합니다. 이 값이 변하지 않았으면 자식 DOM들은 변화가 없다고 생각하고 DOM을 다시 그리지 않습니다. 하지만 이 값이 변하면 자식 DOM을 다시 그리게 됩니다.

  
<div ng-controller="MyCtrl">  
	<div ng-repeat="member in memberList track by member.id">  
		{{::member.name}}  
	</div>  
</div>  
  
angular.module('testModule', []).controller('MyCtrl', ['$scope, $timeout', function ($scope, $timeout) {  
	$scope.memberList = [{  
		id: 1,  
		name: '김부승',  
		updatedAt: '2015-01-01',  
		organization: 'NHN Entertainment'  
	}, {   
		id: 2,  
		name: '박현재',  
		updatedAt: '2016-01-01',  
		organization: 'NHN Entertainment'  
	}];  
  
	$timeout(function () {  
		$scope.memberList.splice(0, 1, {id: 1, name: '알투'});  
	}, 3000);  
}]);  

이렇게 수정하면 처음에는 아래와 같은 형태로 보이게 되고

  
<div ng-controller="MyCtrl">  
	<div ng-repeat="member in memberList track by member.id">  
		김부승  
	</div>  
	<div ng-repeat="member in memberList track by member.id">  
		박현재  
	</div>  
</div>  

3초 후에 myName변수가 변한 이후에도 아래처럼 보입니다.

  
<div ng-controller="MyCtrl">  
	<div ng-repeat="member in memberList track by member.id">  
		김부승  
	</div>  
	<div ng-repeat="member in memberList track by member.id">  
		박현재  
	</div>  
</div>  

이렇게 $digest되더라도 ngRepeat에서 track by 속성을 통해 DOM 전체를 다시 그릴지 판단하는데 변화를 감지하는데, member.id가 변하지 않아서 해당 돔 전체를 그대로 사용하기 때문에 이전과 똑같이 보이게 됩니다.

track by 사용 시에 주의할 점

track by를 사용할 때에는 track by속성이 있는 ngRepeat에서 track by의 결과가 중복되서는 안된다는 것입니다. 만약 위의 예에서 아래처럼 member.organization을 사용하면 멤버 2명의 값이 같기 때문에 에러가 발생합니다.

  
<div ng-controller="MyCtrl">  
	<div ng-repeat="member in memberList track by member.organization">  

		{{::member.name}}  

	</div>  
</div>  

one time binding과 ngRepeat track by를 조합하여 성능 개선하기

one time binding과 ngRepeat의 track by 속성을 조합하면 서로의 단점을 보완하여 성능을 개선할 수 있습니다. one time binding은 DOM을 그릴 때에 값을 1번만 binding하고 $watch하지 않게 해제하는 방법을 사용하고, ngRepeat의 track by같은 경우에는 track by 값이 변하면 DOM을 다시 그린다는 특징이 있습니다.

이 2개의 특징을 조합하면 ngRepeat의 track by 값을 변경하게 하면 one time binding한 변수들의 값을 수정할 수 있게 됩니다. 예를 들어서 위의 track by 예제에서 track by를 (member.id + member.updatedAt)으로 하면 member.updatedAt으로 값이 변경되었는지 확인하고 member.id로 track by의 유일성 조건을 맞족하게 됩니다.

이를 코드로 표현하면

  
<div ng-controller="MyCtrl">  
	<div ng-repeat="member in memberList track by (member.id + member.updatedAt)">  
		{{::member.name}}  
	</div>  
</div>  
  
angular.module('testModule', []).controller('MyCtrl', ['$scope, $timeout', function ($scope, $timeout) {  
	$scope.memberList = [{  
		id: 1,  
		name: '김부승',  
		updatedAt: '2015-01-01',  
		organization: 'NHN Entertainment'  
	}, {  
		id: 2,  
		name: '박현재',  
		updatedAt: '2016-01-01',  
		organization: 'NHN Entertainment'  
	}];  

	$timeout(function () {  
		$scope.memberList.splice(0, 1, {id: 1, name: '알투'});  
	}, 3000);  
}]);  

앞 코드를 실행한 결과는 다음처럼 보입니다.

  
<div ng-controller="MyCtrl">  
	<div ng-repeat="member in memberList track by (member.id + member.updatedAt)">  
		김부승  
	</div>  
	<div ng-repeat="member in memberList track by (member.id + member.updatedAt)">  
		박현재  
	</div>  
</div>  

그리고 3초 후에는 다음처럼 값이 변경되게 됩니다.

  
<div ng-controller="MyCtrl">  
	<div ng-repeat="member in memberList track by (member.id + member.updatedAt)">  
		알투  
	</div>  
	<div ng-repeat="member in memberList track by (member.id + member.updatedAt)">  
		박현재  
	</div>  
</div>  

이런 형태로 코드를 작성하게 되면 ngRepeat의 track by 속성의 값을 계산하는데는 기존(1개 속성만 track by할 때)보다 시간이 더 걸리겠지만 ngRepeat 안의 $watch되는 변수들을 one time binding을 사용하여 $watch 수를 줄일 수 있고 이는 1번 $digest할 때의 비용을 줄여줍니다.

그 외 성능개선 Tip

AngularJS Batarang

크롬익스텐션으로 이를 사용하면 DOM의 $watch변수들의 실행 시간을 확인할 수 있습니다.

watch vs watchCollection 성능 분석

성능 개선을 해 보면 $watch$watchCollection의 차이를 알아야합니다. 이 사이트는 이 2개의 차이점을 이해하기 쉽게 해줍니다.

AngularJS 성능개선 Tip 11가지

AngularJS의 성능개선 시에 Tip을 알려줍니다.

참고자료

AngularJs 공식 홈페이지


Comments