frontend

transaction을 응용해서 redux 상태 업데이트 최적화하기

하리하링웹 2024. 4. 30. 18:49

개요

웹에서 대용량의 데이터를 보여주는 경우는 어떤 경우가 있을까? 아마 대부분의 경우 긴 리스트 혹은 테이블 형태일것이라고 생각한다.

 

이런 경우에는 보통 virtualization으로 성능을 최적화 할 수 있지만 개인 프로젝트가 아니라 회사 단위의 프로젝트라면 기획, 기존 코드와의 충돌과 같은 문제로 인해 virtualization과 같은 기법을 사용하지 못하는 경우도 있을 수 있다.

 

대용량의 데이터를 웹에서 보여주다보면 별 이슈를 다 겪을 수 있는데 이번에 겪은 이슈도 이로 인해 발생한 이슈이다.

원인은 redux에서 상태 업데이트 시 구독된 middleware의 코드가 실행되는데 이러한 상태 업데이트를 여러 번 실행할 때 n번 middleware가 실행되었으며 약 5000줄의 행과 각 행에서 몇 개의 열에 대한 업데이트로 인해 수만 번의 middleware가 실행되어 심각한 성능 이슈가 발생하였다.

 

물론 최고의 방법은 여러번의 상태 업데이트를 batch처리 하는것이였지만 이러한 방식이 불가능한 상황이였기에 다른 방법을 택해야했다.

 

이 이슈를 해결하기 위해 고안한 방법은 DB의 transation기능에서 아이디어를 얻어 생각해냈으며 여러번의 상태 업데이트를 다른 store에 쌓아뒀다가 상태 업데이트가 완료되었을 때 마지막 단 한 번만 기존 store에 commit하는 방식으로 동작하게 개발하였다.

트랜잭션 기반 상태 관리의 이점

이러한 배경을 고려할 때, Redux를 활용한 트랜잭션 기반 상태 관리 방법은 다음과 같은 이점을 얻을 수 있었다.

  1. 성능 최적화: 트랜잭션 기반의 접근 방식을 사용함으로써, 상태 업데이트 과정에서 발생할 수 있는 성능 저하를 최소화하고, 불필요한 렌더링을 줄일 수 있었으며 단 한 번만 기존 store의 상태가 변경되는것을 보장할 수 있었다.
  2. 에러 처리 : 복잡한 상태 변경을 트랜잭션으로 관리함으로써, 동작 과정 중 에러 발생 시 기존 스토어의 상태를 변경시키지 않고 에러를 해결할 수 있었다.

구현 방식

구현은 아래와 같다. 물론 기존 구현은 에러처리, 후처리, 상태 저장 등 신경 쓰는 것이 훨씬 많지만 구현에 필요한 핵심 코드만 정리해서 간략하게 설명하겠다.

  • 많은 기능이 있는 코드를 최대한 간단하게 정리한 코드이기에 아래의 코드는 로직상의 문제가 있을수도 있음.

[transactionExecutor]

function transactionExecutor(handler: (transactionStore: any) => void) {
	// 임시 트랜잭션 스토어 생성
	const store = configureStore({
		reducer: rootReducer,
		middleware: (getDefaultMiddleware) =>
			getDefaultMiddleware({
				serializableCheck: false,
			}),
		preloadedState: {}, // 기본 상태 또는 초기 상태
	});
	
	handler(store); // 임시 스토어에 상태 업데이트

	const finalState = store.getState();
	
	// 기존 스토어에 적용
	originStore.dispatch({
        type: 'UPDATE_GLOBAL_STATE',
        payload: finalState
    });}

 

transactionExecutor함수:

  • **configureStore**를 사용하여 독립된 트랜잭션 스토어를 생성해준다. 이 스토어는 임시로 상태를 적용할 스토어이다.
  • 핸들러 함수를 통해 주어진 스토어에 상태 업데이트를 수행하고, 최종적으로 기존 스토어에 최종 상태를 반영해주는 함수이다.

[usingTransaction]

// 트랜잭션을 사용하는 함수
function usingTransaction(transactionHandler) {
	try {
		const handleTransaction = (transactionStore) => {
			transactionHandler(transactionStore);
		};

		transactionExecutor(handleTransaction);
	} catch (e) {
		// 에러처리
	} finally {
		// 후처리
	}
}

 

usingTransaction 함수:

  • **transactionExecutor**를 호출하여 트랜잭션 처리를 수행한다. trycatchfinally 구조를 사용하여 예외를 처리하고, 필요한 후처리 작업을 수행할 수 있다. 이 구조는 에러가 발생할 경우에도 애플리케이션의 안정성을 유지하고, 트랜잭션이 성공적으로 완료되었는지 로깅, 후처리 등을 할 수 있는 구조이다.

[사용예시]

// 트랜잭션 사용 예시
usingTransaction((transactionStore) => {
	transactionStore.dispatch({
		type: 'ADD_ITEM',
		payload: { id: 3, name: 'New Item' },
	});

	transactionStore.dispatch({
		type: 'UPDATE_ITEM',
		payload: { id: 1, name: 'Updated Name' },
	});
	
	//... 많은 상태 업데이트
});

• 실제 트랜잭션 로직에서는 **transactionStore**를 사용하여 아이템을 추가하거나 업데이트하는 등의 작업을 수행하며 기존의 store의 상태가 단 한번만 변경되는것을 보장해준다.

 

 

[전체코드]

function transactionExecutor(handler: (transactionStore: any) => void) {
	// 임시 트랜잭션 스토어 생성
	const store = configureStore({
		reducer: rootReducer,
		middleware: (getDefaultMiddleware) =>
			getDefaultMiddleware({
				serializableCheck: false,
			}),
		preloadedState: {}, // 기본 상태 또는 초기 상태
	});
	
	handler(store); // 임시 스토어에 상태 업데이트

	const finalState = store.getState();
	
	// 기존 스토어에 적용
	originStore.dispatch({
        type: 'UPDATE_GLOBAL_STATE',
        payload: finalState
    });}

// 트랜잭션을 사용하는 함수
function usingTransaction(transactionHandler) {
	try {
		const handleTransaction = (transactionStore) => {
			transactionHandler(transactionStore);
		};

		transactionExecutor(handleTransaction);
	} catch (e) {
		// 에러처리
	} finally {
		// 후처리
	}
}

// 트랜잭션 사용 예시
usingTransaction((transactionStore) => {
	transactionStore.dispatch({
		type: 'ADD_ITEM',
		payload: { id: 3, name: 'New Item' },
	});

	transactionStore.dispatch({
		type: 'UPDATE_ITEM',
		payload: { id: 1, name: 'Updated Name' },
	});
	
	//... 많은 상태 업데이트
});

 

결론

물론 위의 방식은 DB에서의 트랜잭션에 비하면 훨씬 부족한 기능이며 단지 아이디어만 가져와 구현한 기능이다. 하지만 대용량 데이터를 성능의 문제 없이 잘 다룬다는 목적을 이뤘다는것이 더 중요하다고 생각한다.

 

위 방식을 통해 상태의 변화 없이 에러 처리, 로깅 등을 할 수 있었으며 기존 약 50초 가까이 걸리던 동작을 5초 정도로 줄이는 효과를 얻을 수 있었다. 위에서도 말했지만 가장 좋은것은 처음부터 batch 처리가 가능한 구조로 만들어 단 한번의 상태 업데이트가 가능한 구조로 만드는 것이지만 현실적으로 모든것을 다 고려하고 구조를 짜는것은 힘들기에 현재 주어진 상황에 맞춰 문제를 해결하는 것도 중요하다고 생각한다.