항목 변화 추적과 Rule Engine 기반 상태 처리 최적화

항목 변화 추적과 Rule Engine 기반 상태 처리 최적화

1. 기술 선택 배경

이번 설문 상세 화면을 구현하면서 가장 먼저 부딪힌 문제는, 사용자가 어떤 항목을 바꿨는지 화면이 정확히 알아야 한다는 점이었다. 이 화면은 단순히 값을 입력받는 폼이 아니라, 특정 응답에 따라 다른 문항이 보이거나 숨겨지고, 필수 여부가 바뀌며, 계산 결과가 자동 반영되는 구조였다. 즉, 먼저 “값이 바뀌었는지”를 추적할 수 있어야 그다음 이벤트를 올바르게 실행할 수 있었다.

이를 위해 react-hook-form의 useWatch를 사용해 폼 전체 값을 감시했다. 각 항목이 입력되거나 선택될 때마다 현재 값을 계속 보고, 이전 값과 비교해 실제로 변화가 발생한 항목만 추려내는 방식으로 접근했다. 처음에는 단순히 값 전체를 기준으로 이벤트를 돌리는 방법도 생각했지만, 문항 수와 rule 수가 늘어날수록 성능 부담이 커질 것이 분명했다. 그래서 처음부터 “전체를 다시 계산하는 구조”가 아니라 “변한 것만 반응하는 구조”로 설계 방향을 잡았다. 실제 코드에서도 useWatch로 값을 감시하고, 이후 diff 계산과 rule 해석 단계로 넘기는 흐름으로 구성되어 있다.

2. 기존 접근 방식의 한계

초기에는 값이 하나라도 바뀌면 관련 여부를 따지지 않고 모든 rule을 다시 실행하는 방식에 가까웠다. 구현은 단순했지만, 실제 화면에서는 비효율이 빠르게 드러났다. 사용자가 한 항목만 바꿔도 관련 없는 표시 이벤트와 계산 이벤트까지 함께 다시 평가되었고, 같은 값을 다시 넣는 불필요한 상태 변경도 반복되었다. 계산 rule에 API 호출이 포함된 경우에는 네트워크 요청까지 중복으로 발생할 수 있었다.

특히 동적 설문은 문항 간 의존성이 많아서, 단순한 전체 재실행 방식은 화면이 커질수록 부담이 커진다. 결국 필요한 것은 “현재 어떤 값이 변했는가”와 “그 값이 어떤 rule과 연결되어 있는가”를 분리해서 보는 구조였다. 기존 초안에서도 이 문제의식은 잘 드러나 있었고, 불필요한 반복 실행과 성능 저하를 핵심 한계로 정리하고 있었다.

3. 변화 추적 기반 Rule Engine 실행 구조

값 추적은 단순히 useWatch만 쓰는 것으로 끝나지 않았다. 감시한 현재 값과 이전 스냅샷을 비교해 실제로 달라진 항목만 changedTemplateItemKeySet 형태로 정리하고, 이 집합을 기준으로 다음 실행 대상을 좁혔다. 이렇게 하면 전체 값을 매번 다시 보는 것이 아니라, 이번 입력에서 실제로 영향이 생긴 항목만 후속 처리 대상으로 삼을 수 있다.

그다음 단계에서는 각 rule이 참조하는 항목들을 dependency로 구성했다. 어떤 rule은 특정 질문 하나만 참조하지만, 어떤 rule은 조건값, 계산 변수, aggregation 대상까지 함께 참조한다. 그래서 rule마다 dependency 집합을 만들고, 방금 변경된 항목이 그 안에 포함되어 있을 때만 실행하도록 했다. 코드에서도 changedTemplateItemKeySet을 만들고, dependencyKeySet과 비교해서 isDependencyChanged를 판단하는 흐름으로 구현되어 있다. 이 구조 덕분에 “값 변화 감지”와 “실행 대상 선별”이 분리되었고, 전체 재실행 대신 필요한 rule만 선택적으로 실행할 수 있게 되었다.

4. STATE와 CALC 분리 설계

Rule Engine은 크게 STATE와 CALC 두 축으로 나누어 설계했다. STATE는 화면 상태를 제어하는 역할이다. 문항의 표시와 숨김, 활성화와 비활성화, 읽기 전용 처리, 알림이나 스크롤 같은 UI 반응이 여기에 포함된다. 반면 CALC는 값을 계산하는 역할이다. 수식 계산, 합계 처리, 조건부 계산, API 기반 계산처럼 실제 데이터를 바꾸는 작업은 CALC로 분리했다.

이 둘을 나눈 이유는 실행 비용이 다르기 때문이다. STATE는 비교적 가볍게 자주 반응할 수 있지만, CALC는 잘못 설계하면 동일 계산을 여러 번 실행하거나 오래된 비동기 응답이 최신 값을 덮어쓰는 문제가 생긴다. 그래서 CALC에는 더 엄격한 dependency 비교와 실행 순서 관리가 필요했다. 실제 구현에서도 diff 계산 이후 STATE 평가와 CALC 실행이 분리되어 있고, 각 단계가 독립적으로 동작하도록 구성되어 있다. 이런 분리는 기능을 추가할 때도 유리했다. 화면 상태 규칙과 값 계산 규칙을 같은 방식으로 억지로 처리하지 않아도 되었기 때문이다.

5. 적용 과정과 문제 해결 경험

가장 먼저 정리한 문제는 불필요한 계산 반복이었다. 초기 구조에서는 특정 값 하나가 바뀌면 모든 CALC rule이 다시 실행될 수 있었다. 이를 줄이기 위해 계산식에 사용되는 변수, 조건, aggregation 대상까지 모두 dependency 후보에 포함했고, 실제로 바뀐 항목이 그 안에 있을 때만 계산을 실행하도록 제한했다. 이 과정에서 관련 없는 계산 호출이 많이 줄었고, API 기반 계산의 중복 요청도 함께 줄일 수 있었다. 기존 초안에서도 이 부분은 핵심 개선 사례로 정리되어 있었다.

두 번째 문제는 동일 값 재설정으로 인한 불필요한 리렌더링이었다. rule 실행 결과가 이미 화면에 들어가 있는 값과 같아도 setValue가 반복되면 화면은 다시 렌더링된다. 입력이 많은 화면에서는 이런 누적이 체감 성능 저하로 이어졌다. 그래서 현재 값과 다음 값이 같으면 아예 반영하지 않도록 방어 로직을 넣었다. 단순한 조건처럼 보이지만, 실제로는 UI 깜빡임을 줄이고 상태를 훨씬 안정적으로 유지하는 데 도움이 되었다.

세 번째 문제는 비동기 계산 결과의 순서 충돌이었다. 사용자가 값을 빠르게 바꾸면 먼저 보낸 계산 요청보다 나중 요청이 먼저 끝날 수 있고, 그 상태에서 오래된 응답이 마지막에 도착하면 최신 값을 덮어쓰는 문제가 생긴다. 이를 막기 위해 계산 실행마다 순서를 부여하고, 가장 최신 실행에 해당하는 응답만 반영하도록 제어했다. 초안에서 정리한 latestExecution 비교 방식이 바로 이 문제를 막기 위한 장치였다.

네 번째 문제는 여러 rule이 같은 대상을 동시에 건드릴 때 발생하는 상태 충돌이었다. 예를 들어 어떤 rule은 SHOW를, 다른 rule은 HIDE를 내보내면 최종 상태가 모호해진다. 이 문제를 해결하기 위해 활성화된 rule의 순서를 기록하고, 가장 마지막에 활성화된 rule을 최종 winner로 선택하는 구조를 적용했다. 실제 평가 로직에도 activatedOrder를 기준으로 후보를 비교하는 흐름이 반영되어 있다. 이 덕분에 상태가 뒤섞이지 않고 일관되게 유지되었다.

다섯 번째로는 부수 효과의 중복 실행도 조정했다. 알림 같은 액션은 상태가 같을 때 반복해서 뜨면 사용자 경험을 크게 해칠 수 있다. 그래서 같은 rule에서 같은 값으로 이미 알림을 띄운 경우에는 다시 실행하지 않도록 ref 기반 비교를 넣었다. 화면 입장에서는 작은 차이지만, 실제 운영에서는 이런 방어 코드가 있어야 이벤트 시스템이 과하게 반응하지 않는다.

6. 적용 결과

이 구조를 적용한 뒤 가장 먼저 체감된 변화는 불필요한 실행이 줄었다는 점이었다. 이전에는 한 항목 변경이 전체 재평가로 이어졌다면, 이제는 실제로 연결된 rule만 반응하게 되었다. 그 결과 렌더링 횟수가 줄고, 계산 요청도 필요한 경우에만 발생하게 되었다. 화면이 복잡해질수록 이 차이는 더 크게 느껴졌다.

또 하나의 효과는 확장성이다. 동적 설문 화면은 시간이 지나면 rule이 계속 늘어난다. 새로운 표시 조건, 계산식, 필수 처리, 안내 이벤트가 추가되더라도, 값 변화 추적과 dependency 기반 실행이라는 기준이 이미 정리되어 있으면 기존 구조를 크게 흔들지 않고 붙일 수 있다. 결국 이번 작업은 단순히 이벤트 몇 개를 붙인 것이 아니라, 서식 상세 화면에서 어떤 항목이 왜 변했는지 추적하고, 그 변화에 필요한 반응만 실행하는 기준을 세운 작업이었다고 생각한다.

7. 결론

이번 작업을 통해 느낀 점은, 동적 설문 시스템은 단순한 폼 구현이 아니라 상태와 조건을 다루는 작은 실행 시스템에 가깝다는 것이다. 처음에는 화면에 보이는 동작 하나하나를 개별 처리하려고 했지만, 결국 중요한 것은 “무엇이 변했는지 먼저 정확히 잡고, 그 변화와 관련 있는 rule만 실행하는 구조”를 만드는 것이었다.

그래서 이번 설계의 출발점은 useWatch를 이용한 값 변화 추적이었고, 그 위에 diff 계산, dependency 기반 선별 실행, STATE/CALC 분리, 실행 순서 관리, 중복 방지 로직을 차례로 얹어 갔다. 그 결과 복잡한 동적 설문에서도 성능과 안정성을 함께 확보할 수 있었고, 이후 rule이 더 늘어나더라도 유지보수 가능한 구조를 마련할 수 있었다.

Gabriel