logo

JavaScript Proxy를 활용한 상태 추적 도구 개발기

FrontEnd

1. 레거시가 주는 고통

세일즈서비스팀에서는 사장님의 배달의민족 입점을 도와드리기 위한 입점신청, 계약 검수 및 승인 시스템, 전자계약서’ 등 여러 지면을 개발하고 있습니다. 그중 전자계약서는 업주 정보, 가게 정보, 광고 정보 등을 입력하고 전자 서명을 통해 계약을 완료하는 핵심 지면입니다. 하지만 이 지면은 상태 관리 라이브러리(Redux, Zustand 등) 없이, 거대한 단일 객체에 모든 계약 관련 데이터를 담아 처리하는 구조로 되어 있습니다. 이로 인해, 입점하려는 사용자가 가게 정보를 입력했을 뿐인데, 업주 정보나 광고 정보까지도 영향을 받는 등 예측 불가능한 연쇄 변화가 발생하곤 합니다.

문제는 이 관계들이 코드에 명시적으로 드러나 있지 않고, 문서화도 되어 있지 않다는 점입니다.

따라서, 예상치 못한 값의 변경이 발생할 때마다 아래와 같은 방식으로 확인해야 했습니다.

"어디서 이 필드를 바꾸는 거지?"
→ console.log()를 이곳저곳에 심어서 추적
→ 예상치 못한 값 변경 발견
→ 다시 전체 흐름 재분석

이러한 구조는 개발자에게 지속적인 고통을 주었습니다.

  • 빈번한 버그 핫픽스: 상태 변화의 원인을 명확히 파악하기 어려워, 유사한 이슈가 반복 발생하기도 함.
  • 영향 범위 예측의 어려움: 어떤 값을 바꾸면 어디까지 영향이 갈지 파악하기 어려워, 개발자가 선뜻 손대기 어려운 구조가 됨.
  • 개발자 생산성 저하: 디버깅과 기능 추가에 많은 시간이 소모되며, 생산성이 떨어짐.

문제를 해결하고자 상태 관리 라이브러리 도입을 고려했지만, 현실적인 제약이 있었습니다.

상태 관리 라이브러리 도입이 어려웠던 이유

사실 상태 관리 라이브러리를 도입해 구조적으로 문제를 해결하는 방안은 팀 내에서도 여러 차례 논의됐습니다. 특히 데이터 흐름이 복잡하고 연쇄적인 상태 변화가 많은 상황에서는, 명시적인 액션과 상태 변화를 추적할 수 있는 Redux 같은 도구가 큰 도움이 됩니다.

하지만 여느 회사에서도 그렇듯, 리소스가 부족했습니다. 스프린트마다 우선순위가 높은 업무를 할당하다 보면, 관련 코드 전반을 마이그레이션할 시간을 확보하는 건 거의 불가능했습니다. 설계부터 개발, 테스트까지 전체 마이그레이션 과정을 감당할 여력이 없었습니다. 즉, 근본적인 해결을 하고 싶어도, 지금은 할 수 없는 타이밍이었던 겁니다. 이런 배경 속에서 고민이 시작됐습니다. “상태 관리 라이브러리를 도입하지는 않더라도 최소한 상태의 ‘변화’만이라도 추적할 수 있으면 어떨까?”

라이브러리 도입 없이 상태 변화 추적하기

객체의 상태가 언제, 어떻게 변했는지 가시적으로 추적하는 기능이 절실히 필요했습니다. 특히 복잡하게 얽힌 상태 변화 흐름을 한눈에 파악하지 못하면, 버그 원인 찾기와 수정 작업이 너무 어려웠거든요. Redux가 제공하는 ‘Redux DevTools’의 가장 큰 강점은, 상태 변화의 과정을 시간 순서대로 깔끔하게 보여주고, 변화된 값을 직관적으로 비교할 수 있다는 점입니다. 하지만 Redux를 쓸 수 없는 상황에서, 상태 변화 추적 기능만이라도 갖출 방법을 고민하게 되었습니다.

그렇다면, Redux 같은 별도의 상태 관리 라이브러리를 도입하지 않고도 어떻게 Redux DevTools와 비슷하게 상태 변화를 추적할 수 있을까요? 답은 바로 JavaScript Proxy API에 있었습니다. Proxy를 활용하면 기존 코드에 거의 손대지 않고도 객체의 상태 변화를 가로채서 감시할 수 있기 때문입니다.

2. JavaScript Proxy를 활용한 상태 추적 방식과 구현

JavaScript Proxy란?

JavaScript의 Proxy는 객체의 동작을 가로채서 제어할 수 있는 메커니즘입니다.

예를 들어, 누군가 객체의 name 프로퍼티에 접근하거나 값을 변경할 때 이를 가로채서 로그를 남기거나, 어떤 조건에 따라 다른 동작을 수행하게 만들 수 있습니다.

const user = {
  name: "Baemin",
  age: 15,
};
const proxy = new Proxy(user, {
  get(target, prop) {
    console.log(`GET: ${target[prop]}`);
    return target[prop];
  },
  set(target, prop, value) {
    console.log(`SET: ${prop} = ${value}`);
    target[prop] = value;
    return true;
  },
});
proxy.name;  // GET: Baemin
proxy.age = 16;  // SET: age = 16

이처럼 get, set 등의 트랩 메서드를 정의하면, 객체의 속성 접근이나 변경이 일어날 때마다 우리가 원하는 동작을 삽입할 수 있습니다.

이 아이디어를 확장해서, "객체의 필드가 언제 어떤 값으로 바뀌었는지 로그를 쌓고 추적"하는 도구를 만들 수 있었습니다.

이제 이 도구가 어떤 구조로 만들어졌는지 소개하겠습니다.