AngularJS와 scope.$apply

AngularJS and scope.$apply를 번역한 글입니다.

만약 여러분들이 Angular를 이용해 수준 있는 코드를 작성해왔다면, $scope.$apply 함수를 이해하고 있을 겁니다.

겉보기에 $apply 함수는 우리가 바인딩한 객체들(bindings)을 업데이트 하기 위해 호출하는 함수처럼 보입니다. 그런데 왜 이게 존재할까요? 그리고 언제 사용해야 할까요?

언제 $apply 함수를 사용해야할지 진짜로 이해하려면 사용해야 하는지 정확하게 아는 게 좋습니다. 한번 깊게 들어가 봅시다~!


자바스크립트는 턴(Turn) 기반 언어입니다

우리가 작성하는 자바스크립트 코드는 한꺼번에 전부 실행되지 않고 조금씩 번갈아가며 실행됩니다.

우리는 ‘조금씩’ 실행되는 부분들을 턴(Turn)이라고 부릅니다. 각 턴들은 실행된 후 끝날 때까지 방해 받지 않습니다.

그리고 턴이 실행중일때는 우리의 브라우저에 어떤 현상도 일어나지 않습니다.

다른 자바스크립트 코드를 실행하지 않을 뿐만 아니라, 웹 페이지 인터페이스는 말 그대로 ‘완전히’ 얼어버립니다. 바로 이때문에 형편없이 작성된 자바스크립트 코드가 웹 페이지를 얼릴 수 있는 겁니다.

그래서 클릭 이벤트를 기다리거나 타이머를 설정하는 등 Ajax 요청 같은 조금의 시간을 요구하는 작업이 있을 때마다, 우리는 콜백 함수를 설정한 후 현재 턴을 끝냅니다.

그 후에, 클릭이 감지되거나 타이머가 완료되는 등 Ajax 요청 완료 신호가 있을 때 새로운 턴이 시작되어 콜백 함수를 실행합니다.

한번 예제를 살펴봅시다.

var button = document.getElementById(‘clickMe’);
function buttonClicked () {
alert(‘the button was clicked’);
}
button.addEventListener(‘click’, buttonClicked);
function timerComplete () {
alert(‘timer complete’);
}
setTimeout(timerComplete, 2000);

예제에서는 하나의 턴안에서 자바스크립트 코드를 읽어들입니다. 버튼을 찾아 클릭 리스너를 등록하고 타이머를 설정합니다

턴이 완료된 후, 필요하다면 브라우저는 웹 페이지를 업데이트 한 뒤 사용자의 행동을 감지하기 시작합니다.

만약 브라우저가 #clickMe 버튼에서 클릭 이벤트를 감지했다면 buttonClicked 함수를 실행할 새로운 턴을 시작합니다. buttonClicked 함수를 다 실행하고 나면 턴은 끝나게 됩니다.

2초 후, 브라우저는 timerComplete 함수를 실행할 새로운 턴을 시작합니다. 역시 timerComplete 함수를 다 실행하고 나면 턴은 끝나게 됩니다.

우리의 자바스크립트 코드는 턴 안에서 실행되고, 턴 사이에는 페이지를 다시 그리거나 사용자의 행동을 감지합니다.


바인딩 객체들을 어떻게 업데이트 할까요?

Angular는 우리가 HTML의 일부분을 자바스크립트 코드의 데이터와 묶을 수 있게 도와줍니다. 그런데 언제 데이터가 변경되고 HTML이 업데이트 되어야 하는지 어떻게 알 수 있을까요?

지금 당장은 특정 객체의 변화를 직접적으로 감지할 수 있는 방법은 없습니다. 그래서 간접적으로 감지하는 두가지 방법이 소개합니다.

첫 번째 방법은 특별한 객체를 사용하는 겁니다.

특별한 객체를 확장해서 데이터를 변경할 때 속성값을 직접 변경하는 것이 아니라 함수를 통해서 변경하는거죠. 그러면 객체의 변화를 감지할 수 있게 되어 화면을 적절한 시점에 업데이트 할 수 있습니다.

이 방법은 반드시 특별한 객체를 확장해야 한다는 단점이 있습니다. 또한 객체 값을 변경할 때 obj.key = ‘value’ 대신 obj.set(‘key’, ‘value’) 형태로 사용해야만 합니다. EmberJS와 KnockoutJS 프레임워크가 이 방법을 사용하죠.

Angular는 다른 방식으로 이 문제를 해결합니다. 바인딩 대상으로 어떤 값이든 허용한 후, 자바스크립트 턴이 끝날 때마다 바인딩한 값들이 변경되었는지 확인을 합니다.

처음에는 비효율적이라는 생각이 들겠지만, 성능 저하를 줄일 수 있는 재치있는 방법이 있기에 괜찮습니다.

가장 큰 이점은 일반 객체를 그대로 사용할 수 있다는 점과 데이터를 원할 때 언제든지 업데이트 할 수 있다는 점, 그리고 값의 변경이 우리가 설정한 바인딩 객체 안에서 이루어 진다는 점입니다.

이 방법이 잘 작동하기 위해서는 데이터가 변경되는 시점이 언제인지 알아야 합니다. 여기서 $scope.$apply가 등장합니다.


$apply와 $digest

$apply 단계에서는 $digest 메소드를 가진 바인딩 객체가 변경되었는지 확인합니다. 실제로 확인하는 단계는 $digest 함수를 통해 진행됩니다.

이 부분이 실제로 마법이 일어나는 곳입니다.

우리는 대부분 $digest 함수를 직접 호출하지는 않고, 대신 $digest함수를 내부적으로 호출하는 $scope.$apply 함수를 호출합니다.

좀 더 들어가봅시다.

$scope.$apply 는 함수나 Angular 표현식을 인수로 받습니다.

그리고 인수를 실행한 후, 바인딩 객체나 watcher를 업데이트 하기 위해 $scope.$digest 함수를 호출합니다.

이때 $digest를 호출하는 과정을 digest loop라고 합니다.

그럼 $apply 함수에 대해 알아보았으니, 언제 호출해야 할지 살펴봅시다. 우리는 언제 $apply 함수를 직접 호출해야 할까요?

그런 경우는 별로 없습니다. 진짜로요.

Angluar 프레임워크가 대부분의 우리 코드들을 $apply 호출 내부에서 실행하기 때문입니다. ng-click같은 이벤트들, 컨트롤러 초기화, $http 콜백들은 모두 $scope.$apply 로 감싸집니다.

그러니 여러분들은 직접 할 필요가 없습니다. 실은 못합니다. $apply 내부에서 $apply를 또 호출하는건 에러를 던지기 때문이죠.

여러분들은 새로운 자바스크립트 턴 안에서 코드를 실행해야 할 때에만 $apply 함수가 필요합니다. 그 때에만 새로운 턴 안에서 코드를 $scope.$apply 함수로 감싸주세요.

물론 그 턴이 Angular 프레임워크가 제공하는 함수로부터 시작되지 않았을 때만을 말합니다. 제공된 함수로 시작된 턴은 이미 Angular가 알고 있으니 $apply 함수로 감싸져서 호출이 되겠죠 :)

여기 예제를 한번 보세요. setTimeout 함수를 사용해서, 약간의 딜레이 후 새로운 턴 안에서 함수를 실행하고 있습니다.

예제에서는 Angular가 새로운 턴에 대해서 알지 못하기 때문에, $scope 변경이 화면에 반영되지 않습니다. Angular가 변경을 감지하지 못했기 때문이죠.

그런데 우리가 $scope.$apply 함수로 코드를 감싼다면, $scope 변경을 Angular가 감지하여 화면을 업데이트 합니다.

그래서 Angular는 편리하게 $timeout을 제공합니다. $timeout은 setTimeout과 비슷하지만 자동으로 코드를 $apply 함수로 감싸주죠.

그러니 setTimeout 대신에 $timeout 을 쓰세요 :)

만약 여러분이 Ajax 코드를 $http 없이 작성한다거나 이벤트를 Angular의 ng-* 리스너를 사용하지 않고 감지하려고 한다거나 타이머를 $timeout 없이 설정한다면, 반드시 코드를 $scope.$apply로 감싸줘야 합니다.


$scope.$apply() vs $scope.$apply(fn)

$apply 함수를 호출할 때 인수없이 호출하는 경우와 함수 인수를 넘기고 호출하는 경우를 살펴보죠.

때때로 저는 데이터가 변경된 후 $scope.$apply 함수를 인수 없이 호출하는 예제를 보곤 합니다.

이 예제들은 바라는 결과를 얻겠지만 몇가지 기회들을 놓치고 있습니다.

여러분들의 코드를 함수로 감싸 $apply 함수에 넘겨주지 않았다고 가정해보죠. 여러분들의 코드에서 에러가 발생한다면 그 에러는 Angular 외부로 던져지게 됩니다.

그말은 즉, 애플리케이션 내부에서 사용하는 에러 처리 메커니즘이 여러분들의 코드가 만들어 낸 에러를 발견하지 못한다는 뜻입니다.

$apply 함수는 여러분들의 코드를 실행할 뿐만 아니라 try/catch 문 안에서 코드를 실행해줍니다. 그래서 여러분들의 에러는 항상 Angular에게 잡힙니다.

그리고 $digest 호출은 finally 구문안에 있기 때문에 에러가 발생하던 안하던 항상 실행됩니다. 나이스~

이제 여러분들은 $apply 함수가 무엇인지 그리고 언제 사용해야 하는지 알게되었습니다.

만약 여러분들이 Angular가 제공하는 기능들만을 사용한다면 보통은 $apply 함수를 사용할 필요가 없을겁니다. 하지만 DOM을 직접 감시하는 디렉티브를 만들기 시작하면 필요해질겁니다 :)


Show your support

Clapping shows how much you appreciated Ian Park’s story.