ExpressionChangedAfterItHasBeenCheckedError 에러에 대해

Jeongkuk Seo
sjk5766
Published in
6 min readDec 25, 2018

Angular를 접한 초반에 ExpressionChangedAfterItHasBeenCheckedError가 발생했는데, 정리하는데 시간이 걸릴 것 같아 ‘나중에 해야지’ 생각만 해두었다가 이제 정리하게 됩니다. 본문에 대한 대부분의 내용은 Max Koretskyi 님이 Angualr-in-depth에 쓴 포스팅을 보고 이해할 수 있었습니다. 번역글은 아니기에 글 마지막에 링크를 남겨두겠습니다.

이 글은 ExpressionChangedAfterItHasBeenCheckedError 에러가 발생할 수 있는 상황과 배경, 그리고 해결 방법에 대해 정리합니다. 또한 이 글은 기본적으로 change detection에 대해 이해를 하고 있어야 읽을 수 있습니다.

먼저 소스부터 보겠습니다.

span 태그의 textContent 프로퍼티에 data를 바인딩 하고 있습니다. 컴포넌트 클래스에서 볼 수 있듯, data는 함수로 시간 값을 리턴하고 있습니다. 이 결과는 크롬에서 다음과 같습니다.

우선 에러의 내용을 살펴보면 Expression has changed after it was checked가 있습니다. 즉 체크된 다음에 변경이 일어났다는 겁니다. 체크가 되었다는 것은 무슨 말이고 변경은 왜 일어났는지 알아보겠습니다.

어플리케이션이 처음 구동 되어 각 컴포넌트의 데이터가 초기화 되었습니다. 초기화 된 데이터는 화면에 렌더링 되고 각 컴포넌트가 표현하는 view의 oldValue 배열에 저장됩니다. 이 상태를 데이터에 대한 체크가 되었다고 볼 수 있습니다.

Angular의 development 모드에선 oldValue 배열에 저장된 레퍼런스와 현재 값이 저장된 레퍼런스가 동일한지 확인하기 위해 확인작업이 발생합니다. 이 확인 작업은 정상적인 change detection 로직은 아닙니다. 그리고 이 때, 값이 다를 경우 ExpressionChangedAfterItHasBeenCheckedError가 발생합니다.

그렇다면 왜 레퍼런스가 동일한지 확인작업이 발생할까요? 만약 Angular의 change detection 동작 중에 상태가 변경되었다고 가정하겠습니다. 이 경우, 다시 Angular가 change detection을 수행해야 할까요? 만약 수행하는 중에 또 다시 상태가 변경될 경우, change detection이 무한 루프에 빠질 수 있습니다. 따라서 Angular는 check가 된 데이터에 대해 확인 작업 시, 데이터가 다를 경우 무한루프에 빠지지 않고 ExpressionChangedAfterItHasBeenCheckedError 에러를 뱉습니다.

그렇다면 또 궁금한 건, 왜 development 모드에서만 발생할까? 확실한 답은 아니지만 제가 참고한 포스팅에 의하면, check 된 데이터가 변경된 상황은 결국 다음 change detection 로직에 의해 반영될 것이며 심각한 문제를 일으키진 않기 때문에 production 모드에서는 확인작업이 발생하지 않지만, development 모드에서는 적어도 인지할 수 있도록 에러를 뱉는 다는 의견이였습니다. ㅎㅎ

여기까지 읽으셨다면, 에러가 왜 발생하는지는 이해했을 겁니다. 그렇다면 에러를 해결하는 방법들을 적용해 보겠습니다.

에러를 피하기위해 _time 변수를 선언하고, 바인딩되는 data 함수내에서 _time을 리턴합니다. 생성장에선 setInterval로 _time을 갱신하고 있습니다. 위 예제는 에러도 발생하지 않고 잘 동작하지만 한 가지 문제가 있습니다. setInterval 함수는 비 동기 함수로 호출 될 경우 change detection 이 발생합니다. 즉 해당 함수 때문에 컴포넌트와 하위 컴포넌트들이 0.001 초마다 change detection이 발생하기 때문에 좋지 않습니다.

setInterval, setTimeout, Ajax 통신과 같은 비 동기 작업들은 zone.js가 감시하여 Angular에게 change detection을 하라고 통지합니다. 이 때, Angular에게 통지하지 않고 변경이 반영될 수 있습니다. 바로 NgZone 서비스가 제공하는 함수 runOutsideAngular 입니다.

NgZone을 사용해 change detection을 피하고 Angular 외부에서 코드를 실행 시키는 것은 일반적으로 사용되는 최적화 기법입니다. 우리는 최종적으로 에러가 발생하지 않고 성능에 문제 없이 변경사항을 화면에 반영하는 코드를 작성할 수 있었습니다.

ExpressionChangedAfterItHasBeenCheckedError가 발생할 수 있는 또 다른 상황에 대해 알아보도록 하겠습니다.

코드는 간단합니다. 부모 컴포넌트인 AppComponent에서는 data라는 변수를 가지고 있고, 자식 컴포넌트인 ChildComponent에선 AfterViewInit 메소드에서 주입된 부모의 data를 변경하고 있습니다. 결과는..? 네..

또 에러가 발생하는 군요. 설명에 들어가기 전 자식 컴포넌트 클래스를 아래와 같이 변경하고 결과를 보겠습니다.

ngOnInit() {
this.parent.data = ‘updated data’ // 추가 됨
}
ngAfterViewInit() {
//this.parent.data = ‘updated data’ // 주석처리 됨
}

변경 된 것이라곤 AfterViewInit 메소드에서 부모의 데이터를 변경한 것을 OnInit 메소드에서 수행했다는 겁니다. 하지만 결과는? 헐..

에러가 발생하지 않습니다. 왜 이럴까요? 이 현상을 이해하기 위해서는 아래 그림을 보셔야 합니다. (참고한 블로그에서 가져왔습니다.)

위 그림에 따르면 OnInit, DoCheck, OnChanges 메소드들은 DOM이 업데이트 되기전에 수행됩니다. 반대로 AfterViewInit 메소드는 DOM이 업데이트 된 후에 호출됩니다. 혹시 이해가 되시나요?

위에서 AfterViewInit 메소드에서 부모의 data를 변경하였을 때, 초기화 된 데이터가 이미 DOM에 반영되고 check 된 후였습니다. 따라서 AfterViewInit 메소드에서 데이터 변경 시 ExpressionChangedAfterItHasBeenCheckedError가 발생합니다. 반대로 OnInit의 경우 아직 부모 컴포넌트에 DOM을 반영하기 전입니다. 따라서 우리가 OnInit에서 변경한 데이터가 DOM에 반영되고 check가 되기 때문에 에러가 발생하지 않습니다.

--

--