1. 들어가며…
- 얼마 전 PrimeReact의 TreeTable 컴포넌트에서 발생한 버그를 수정할 일이 있었다.
(버그 설명: 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을 참고하면 있다!
반응형
'개발 기술 > 사소하지만 놓치기 쉬운 개발 지식' 카테고리의 다른 글
웹사이트를 최적화시키는 3가지 기법: 코드 압축, 경량화, 난독화 (4) | 2024.11.18 |
---|---|
[vue] v-show 선언 위치에 따라 렌더링 버그가 발생한다고? (2) | 2024.11.07 |
모바일에서 연속 입력시 값이 무시되는 이유(feat. click, touch, pointer event) (4) | 2024.10.19 |
iOS 모바일의 보안과 이벤트 유실(feat. 사용자 활성화, 이벤트 루프) (20) | 2024.10.03 |
[CSS/JS] ellipsis 말줄임에서 발생할 수 있는 2가지 인터렉션 문제 (0) | 2024.09.19 |
댓글