Angular “expression has changed after it was checked” 예외 — 현재 시간에 의존하는 속성의 근본 원인과 해결책
1. 문제 정의
Angular 2.x 개발 템플릿 모드에서, 컴포넌트 속성이 현재 datetime에 의존할 때 다음과 같은 예외가 발생합니다.
| |
이 예외는 Angular가 템플릿 바인딩의 일관성을 검증하는 동안, 검사 시점과 실제 바인딩 시점 사이에 표현식의 값이 달라졌을 때 던집니다. 프로덕션 빌드에서는 잘 드러나지 않고, 개발 모드(비 프로덕션 빌드)에서 더 쉽게 관찰됩니다.
원본 질문의 코드를 보면 dateNow 프로퍼티가 컴포넌트의 시각 관련 스타일(글꼴 색상) 계산에 쓰이고, 이 값이 계속 바뀔 수 있습니다.
| |
ngAfterViewChecked() 훅에서 this.dateNow = new Date()로 컴포넌트 속성을 바꾸었기 때문에, 템플릿이 이 표현식을 이미 검사한 뒤 값이 변해 예외가 발생한 것입니다.
2. 표면 증상
| 구분 | 증상 | 판단 포인트 |
|---|---|---|
| 발생 시점 | 개발 모드 런타임, 뷰 검사 단계 | Angular 런타임 검증기가 동작 중 |
| 예외 코드 | Expression has changed after it was checked | 바인딩 값을 표현식에서 읽는 동안 값이 변경됨 |
| 관련 함수 | ngAfterViewChecked | 뷰 초기화 후 실행되는 훅 — 여기서 부작용을 만듦 |
| 진짜 원인 | 현재 datetime처럼 계속 변하는 표현식이 템플릿에 바인딩됨 | 값이 매 순간 변해 검사·바인딩 시점이 어긋남 |
증상 fingerprint
- 컴포넌트 속성(또는 뷰 모델 값)이
new Date(), 타임스탬프 계산처럼 외부에서 계속 바뀌는 표현식에 의존한다. - 그 값을 컴포넌트의 어떤 훅(라이프사이클 후반 단계) 안에서 덮어쓴다.
- 개발 모드 빌드에서만 자주 나타나 배포 환경에서는 재현이 잘 안 된다.
이 조건이 하나라도 해당되면, 이 예외가 어느 컴포넌트에서 발생하는지 우선 점검해 둘 가치가 있습니다.
3. 원인 탐구
Angular의 기본 변경 감지(Change Detection)는 템플릿 표현식의 값을 한 번 판단하고, 그 값이 유효한 동안에는 재평가하지 않도록 동작합니다. 그런데 그 표현식이 순수하지 않고 계속 변하는 값에 의존하고 있다면:
- Angular가 템플릿 표현식
this.dateNow를 먼저 검사한다. - 같은 렌더링 사이클 안에서, 개발자 코드가
this.dateNow = new Date()같은 방식으로 그 값을 바꾼다. - Angular가 검사 당시 기억한 값과 현재 실제 값을 비교하다가 값이 달라졌음을 감지하고 예외를 던진다.
질문자가 겪은 시나리오는 전형적인 “검사 이후 값이 반영되는 시점의 어긋남” 패턴입니다.
4. 근본 원인 분석
Expression has changed after it was checked의 본질은 “값을 바꿨다는 사실 자체"가 아니라, 바꾼 것이 Angular 변경 감지 시스템에 통지되지 않았다는 점입니다.
Angular 템플릿 엔진은 미리 검사한 바인딩 값을 두고 다시 재평가하지 않는 가정으로 동작합니다. 그런데 개발자 코드가 Angular의 통지 메커니즘을 거치지 않고 직접 속성을 변경하면, 이후 렌더링 시점에 Angular는 “아까 검증한 값이 이제 맞지 않음"을 인지하게 됩니다.
즉 근본 원인은 “값의 변경을 Angular가 다시 평가하도록 언제 알려야 하는가"라는 시점 문제입니다. 값을 바꾸는 것 자체가 잘못된 것이 아니라, 정합성이 유지되도록 Angular에게 명시적으로 변경을 통지(detectChanges)하는 것이 해결의 방향입니다.
5. 코드 해결책
5.1 ChangeDetectorRef 주입 + detectChanges() 호출
@angular/core의 ChangeDetectorRef를 생성자에 주입하고, 값을 바꾸는 훅(여기서는 ngAfterViewChecked)에서 변경 감지를 명시적으로 호출합니다.
| |
detectChanges() 호출은 Angular에게 “표현식 값이 다시 변동됐으니 재평가하고 바인딩에 반영하라"고 알려줍니다. 이로써 검사된 표현식이 어긋나는 상태가 해소되어 예외로 끝나지 않습니다.
원리 요약
| 시점 | 동작 |
|---|---|
ngAfterViewChecked() | 현재 시각 갱신 + detectChanges() 호출 |
| Angular 변경 감지 | detectChanges() 신호로 표현식 재평가 |
| 이후 렌더링 | 새 값이 바인딩되어 예외 없음 |
일반적인 점검 기준: 이 패턴은 Angular 데이터 개발 템플릿에서 “값을 바꾼 직후 반드시 변경을 통지하자"는 원칙의 일부입니다. 환경에 따라 다를 수 있으므로, 직접 속성을 바꾸는 로직에는 반드시 명시적인 변경 감지 통지가 따라와야 합니다.
5. 향후 예방 조치 — 같은 예외를 피하는 방법
- 동적 값에 의존하는 표현식은 라이프사이클 훅에서 직접 바꾸지 않는다 —
ngAfterViewChecked같은 후훅에서 값을 덮어쓰면 검사 시점과 충돌하기 쉽다. - 값을 변경한다면 반드시
detectChanges()를 같은 흐름 안에서 호출한다 — 여러 곳에서 소비된다면 변경 직후 명시적으로 갱신한다. - 부재 재현(Side Effect)이 있는 표현식을 템플릿에 직접 쓰기보다 컴포넌트 필드로 사전 계산 —
dateNow처럼 값을 한 번 확정해 참조하면 변경 감지와 충돌을 줄인다. - 개발 모드 검증 후 프로덕션 빌드에서도 확인 — 개발 모드에서 발생한 예외는 프로덕션 런타임에서 재현이 어려울 수 있어, 배포 전후로 반복 확인한다.
6. 검증 명령
코드 변경 후 예외가 사라졌는지 아래 순서대로 확인합니다.
| |
환경에 따라 다를 수 있습니다. Angular 프로젝트가 npm 기반이 아닌 다른 빌드 방식을 쓴다면 해당 도구의 검증 명령으로 대신하면 됩니다. 핵심 판단은 변경 후 페이지를 새로 고침하거나 컴포넌트를 다시 그릴 때 예외가 재현되지 않는지입니다.
7. DevTrace 결론
이 문제의 핵심은 “값을 바꾸는 행위"가 아니라, “Angular 변경 감지 시스템에 값을 바꿨다는 사실을 통지하지 않은 것” 입니다.
ChangeDetectorRef.detectChanges()호출로 표현식 재평가를 명시적으로 수행해야 “expression has changed after it was checked” 예외를 근본적으로 해소할 수 있습니다.