개발 기술/사소하지만 놓치기 쉬운 개발 지식

[React] 반응형 데이터 연속 연산시 주의할 점(feat. Vue, React의 차이)

by GicoMomg 2024. 12. 9.

1. 들어가며…


(버그 설명: filter 함수를 연속 호출시 마지막 filter만 적용됨)

 

  • 해당 라이브러리의 <TreeTable />은 데이터를 필터링할 수 있도록 filter() 메서드를 제공한다.
  • 이를 이용하면, ref를 사용해 테이블 데이터를 필터링할 수 있다.
  • 예를 들어 아래와 같은 코드가 있다고 가정해보자:
// template 영역
<button onClick={applyDoubleFilter}>Apply Filters</button>

<TreeTable ref={treeTableEl} value={nodes}>
    <Column field="name" header="Name" filter></Column>
    <Column field="size" header="Size" filter></Column>
</TreeTable>
// script 영역
const treeTableEl = useRef();

// treeTableEl.current.filter()를 사용해 필터링 가능
const applyDoubleFilter = () => {
    treeTableEl.current.filter('applications', 'name', 'equals');
    treeTableEl.current.filter('100kb', 'size', 'equals');
};

 

  • 의도대로라면, [Apply Filters] 버튼을 클릭했을 때 name 칼럼은 "applications"와 같고, size 칼럼은 "100kb"인 데이터만 필터링되어야 한다.
  • 하지만, 영상을 보면 알 수 있듯이 마지막으로 호출된 size 칼럼에만 필터가 적용되는 버그가 있었다.

 

  • 그런데 이상한 점이 있다. 바로, 동일한 코드를 공유하는 PrimeVue<TreeTable />에서는 이 문제가 발생하지 않는다는 것이다!
  • 그럼 Vue와 React 두 프레임워크의 어떤 차이 때문에 React에서만 이런 현상이 나타난 것일까?
  • 문제의 원인은 의외로 간단한데, 바로 React에서는 상태 업데이트가 비동기로 동작하기 때문이다.
  • 이번 시간에는 Vue(ref)와 React(setState)의 상태 업데이트 함수의 동작 방식과 React의 비동기 업데이트 문제를 해결하는 법, 총 2가지를 알아보았다.



2. Vue와 React의 상태 업데이트 비교

1) Vue: 종속성을 추적하여 최신 값을 유지한다.

  • Vue는 반응형 시스템을 사용하여 항상 최신 값을 유지한다.
  • Vue의 반응형 데이터는 Proxy를 통해 값 변경을 추적하고, 필요할 때만 업데이트한다.
  • 이 과정은 종속성 추적(Dependency Tracking)과 종속성 트리거(Dependency Triggering)로 작동한다.
  • Vue 3의 ref를 구현한 핵심 클래스 RefImpl을 예로 들어 반응형 동작을 간단히 살펴보겠다.

(1) Getter: 값 접근 시 종속성을 등록한다.

class RefImpl {
  /* (a) */
  get value() {
    if (__DEV__) {
      /* (b) */
      this.dep.track({
        target: this,
        type: TrackOpTypes.GET,
        key: 'value'
      });
    } else {
      /* (b) */
      this.dep.track();
    }
    return this._value;
  }
}
동작 설명
(a) 데이터 접근 시 호출 value 속성에 접근하면 get value()가 호출된다.
(b) 종속성 등록 this.dep.track()은 현재 실행 중인 컨텍스트(예: 컴포넌트 렌더링 함수)를 종속성 리스트에 등록한다.
(b) 렌더링 컨텍스트 추적 데이터가 변경되면 종속성 정보를 기반으로 필요한 컨텍스트를 다시 실행한다.

 

(2) Setter: 값 변경 시 종속성을 트리거한다.

class RefImpl {
  set value(newValue) {
    const oldValue = this._rawValue;
    newValue = this[ReactiveFlags.IS_SHALLOW] ? newValue : toRaw(newValue);

    /* (a) */
    if (hasChanged(newValue, oldValue)) {

      /* (b) */
      this._rawValue = newValue;
      this._value = this[ReactiveFlags.IS_SHALLOW]
        ? newValue
        : toReactive(newValue);

      /* (c) */
      this.dep.trigger();
    }
  }
}
동작 설명
(a) 값 변경 감지 새로운 값과 이전 값을 비교하여 변경 사항이 있으면 내부 상태를 업데이트한다.
(b) 값 변경 적용 _rawValue는 비반응형 원본 데이터, _value는 반응형 데이터이다.
(c) 종속성 트리거 this.dep.trigger()로 이 데이터에 의존하는 모든 컨텍스트를 다시 실행하도록 요청한다.

 

(3) Vue의 최신 값 유지 방식

  • Getter로 데이터 접근 시 렌더링 컨텍스트가 종속성 리스트에 등록된다.
  • 데이터가 변경되면 Setter가 변경 사항을 감지하고 종속성 트리거를 통해 필요한 컴포넌트만 다시 실행한다.
  • 이를 통해 Vue는 항상 최신 값을 보장한다.

 

2) React: 상태 업데이트는 비동기적으로 처리된다.

  • React에서는 상태 업데이트가 setState 또는 useState를 통해 이루어지며,
  • 업데이트 큐렌더링 스케줄링을 기반으로 동작한다.

(1) setState: 상태 변경 요청을 큐에 추가한다.

  • React의 클래스형 컴포넌트에서 setState는 아래와 같이 정의된다. (코드)
Component.prototype.setState = function (partialState, callback) {

  /* (a) */
  if (
    typeof partialState !== 'object' &&
    typeof partialState !== 'function' &&
    partialState != null
  ) {
    throw new Error('Invalid state update argument.');
  }

  /* (b)(c) */
  this.updater.enqueueSetState(this, partialState, callback, 'setState');
};
동작 설명
(a) 상태 값 확인 setState는 객체 또는 함수만 허용하며 유효하지 않은 값은 에러를 발생시킨다.
(b) 업데이트 큐에 추가 enqueueSetState를 호출해 상태 변경 요청을 큐에 추가한다.
(c) 렌더링 작업 예약 업데이트 요청은 이벤트 루프의 다음 틱(next tick)에서 실행되도록 예약된다.

 

(2) enqueueSetState: 업데이트 요청을 저장한다.

function enqueueSetState(inst, payload, callback) {
  /* (a) */
  const fiber = get(inst);

  /* (b) */
  const update = createUpdate(expirationTime, null);
  update.payload = payload;
  enqueueUpdate(fiber, update);

  /* (c) */
  scheduleWork(fiber, expirationTime);
}
동작 설명
(a) Fiber 접근 fiber는 React 컴포넌트를 나타내는 데이터 구조이다.
(b) 업데이트 큐에 추가 enqueueUpdate를 통해 업데이트 객체를 fiber에 추가한다.
(c) 렌더링 작업 예약 scheduleWork로 렌더링 작업을 예약하고, 이벤트 루프의 다음 틱에서 실행된다.

 

(3) scheduleWork: 렌더링 작업을 예약한다.

/* (a) */
function scheduleWork(fiber, expirationTime) {
  scheduleCallback(() => performWork(fiber));
}

/* (b) */
function performWork(fiber) {
  renderRootSync(fiber);
}
동작 설명
(a) 업데이트 요청 예약 scheduleCallback을 통해 작업이 예약된다.
(b) 렌더링 작업 실행 예약된 작업이 이벤트 루프의 다음 틱에서 실행되어 업데이트가 적용된다.

 

(4) React의 상태 업데이트 방식

  • React는 상태 업데이트 요청을 업데이트 큐에 추가하고, 스케줄링된 작업으로 처리한다.
  • 그래서 상태 업데이트는 비동기적으로 실행되며, 즉시 최신 값을 보장하지 않는다.

아하! Vue는 Getter와 Setter를 통해 최신 값을 항상 유지하지만, React는 비동기로 상태를 업데이트하는구나.
그래서 React에서는 상태 업데이트 직후 최신 값을 바로 사용할 수 없어, <TreeTable /> 에서 마지막 값만 업데이트되는 버그가 발생했군 😂

  • 그럼 React의 비동기 문제를 해결하려면 어떻게 해야 할까?

 

3. React의 비동기 업데이트 문제 해결하기

(1) 문제 다시 살펴보기

  • 처음으로 돌아가 보자! PrimeReact의 <TreeTable />에서 filter() 함수를 연속으로 호출했을 때, 마지막 필터만 적용되는 문제를 보았다.
  • 이 문제가 발생하는 이유를 알기 위해, <TreeTable />filter() 구현을 간략히 살펴보겠다.
// TreeTable.jsx

const [filtersState, setFiltersState] = React.useState(props.filters);

const filter = (value, field, mode) => {
    const filters = props.onFilter ? props.filters : filtersState;
    const newFilters = filters ? { ...filters } : {};
    ...
    setFiltersState(newFilters);
};
  • filter()filtersState 값을 가져와 새로운 필터(newFilters)를 생성하고, 이를 setFiltersState()로 반영한다.
  • 로직상으로는 filtersState의 최신 값을 가져와 새로운 필터 값을 적용해야 하지만, filter()를 연속 호출하면 최신 상태 값에 접근하지 못해 마지막에 호출한 필터만 적용된다.
  • 이 문제는 앞서 설명했듯이, react에서는 반응형 데이터를 비동기로 업데이트하기 때문이다.
  • 그럼, 어떻게 하면 비동기 문제를 해결할 수 있을까?

 

값을 업데이트할 때, 함수형 업데이트 방식을 사용하면 된다!

  • 함수형 업데이트를 사용하면 이전 상태 값을 안전하게 참조하여 새 상태를 계산할 수 있다.
  • setState 함수에 콜백을 전달하면 React는 최신 상태 값을 콜백 함수의 인자로 제공한다.

 

(2) 해결 방법: 함수형 업데이트

  • 함수형 업데이트를 이용하면, 최신값에 접근할 수 있다.
  • 그럼, 연속으로 반응형 데이터를 업데이트해도 최신값이 반영되지 않는 문제를 해결할 수 있다 🙂
/* 수정 전 코드 */

const [filtersState, setFiltersState] = React.useState(props.filters);

const filter = (value, field, mode) => {
    const filters = props.onFilter ? props.filters : filtersState;
    const newFilters = filters ? { ...filters } : {};
    ...
    setFiltersState(newFilters);
};
/* 수정 후 코드 */
const [filtersState, setFiltersState] = React.useState(props.filters);

const filter = (value, field, mode) => {
    /* 함수형 업데이트를 사용해 최신값을 안전하게 참조 */
    setFiltersState((prevFilters) => {
        const filters = props.onFilter ? props.filters : prevFilters;
        const newFilters = filters ? { ...filters } : {};
        ...
        return newFilters;
    });
};

 

  • 수정한 코드는 아래 영상에서 확인할 수 있다. 이제 필터가 연속으로 적용되어 기대한 대로 동작한다. 🎉




4. 마치며…

이번 시간에는 React와 Vue의 상태 관리 방식와 React에서 연속 데이터 연산시 발생하는 문제를 살펴보았다.

Vue는 반응형 시스템을 통해 항상 최신 값을 유지하지만, React는 상태 업데이트를 비동기로 처리하기에 연속 호출 시 최신 상태 값을 즉시 참조하지 못하는 문제가 발생한다.

이번 사례에서는 PrimeReact의 <TreeTable />에서 filter()를 연속 호출했을 때 발생한 마지막 호출만 적용되는 문제를 살펴봤다. 이를 해결하기 위해 함수형 업데이트를 사용하여 이전 상태를 참조했다.

React를 사용할 때는 상태 업데이트의 비동기 특성을 염두에 두자. 더 자세한 내용은 primeReact 수정 PR을 참고하면 있다!

 

반응형

댓글