Jello's development blog

Jello's development blog

Reselect를 이용하여 React와 Redux 최적화하기

React와 Redux를 같이 사용하면 서로의 관심사를 분리할 수 있는 좋은 조합이 된다. 하지만 어플리케이션이 복잡해질수록 설계를 제대로 해주지 않으면 성능은 떨어지기 마련이다. React에서 가장 시간이 오래 걸리는 작업 중 하나는 바로 렌더링 싸이클이다. 컴포넌트의 State나 Props가 변경되면 렌더링 싸이클이 시작된다. 이 싸이클이 불필요하게 깊숙하게 들어가는 것을 방지하기 위해서 ShouldComponentUpdateImmutable 등 여러 최적화 방법이 제안되었다. 이 글에서는 또 다른 최적화 방법 중 하나인 Reselect를 소개해보려고 한다.

느린 컴포넌트

다음 예제를 보자.

import React from 'react';
import { connect } from 'react-redux';
import { List } from 'Immutable';

const mapStateToProps = (state) => {
	// 페이지 색
	const color = state.UIReducer.get('color');

	// 아이템 합계 계산
	const items = state.itemReducer.get('items', List());
	const totalPrice = items.reduce((acc, i) => {
		return acc + (i.get('price', 0) * i.get('quantity', 0));
	});
	
	// 태스크 계산
	const tasks = state.taskReducer.get('tasks', List());
	const totalWorkingTime = tasks.reduce((acc, i) => {
		return acc + (i.get('workingTime', 0));
	});

	return {
		color,
		items,
		tasks,
		totalPrice,
		totalWorkingTime,
	};
};

class SomeComponent extends React.Component {
	render() {
		...
	}
}

export default connect(mapStateToProps)(SomeComponent);

위의 예제는 현재 페이지의 색을 얻어오고, 아이템과 태스크를 Store로부터 얻어와서 합계를 계산한 뒤에, connect로 SomeComponent라는 컴포넌트에 그 정보를 주입시켜주는 코드이다. itemstasks의 개수가 적다면 웬만큼 빠르게 동작할 것이다. 하지만 몇 천개, 몇 만개라면 상황은 달라진다.

언뜻 보면 잘 짜여진 코드처럼 보이지만, 만약 페이지의 색이 변경된다면 props에서 변경이 일어날 것이고, mapStateToProps 함수가 실행되어 페이지의 색을 다시 가져오는 것은 물론 그것과는 전혀 상관이 없는 totalPrice와 totalWorkingTime를 다시 계산할 것이다.

Reselect

Reselect의 memoized selector를 사용하면 위의 문제를 개선할 수 있다.

import { createSelector } from 'reselect';

export const getColor = (state) => state.UIReducer.get('color');

export const getItems = (state) => state.itemReducer.get('items');

export const getTotalPriceWithItems = createSelector(
	[ getItems ],
	(items) => (
		items.reduce((acc, i) => {
			return acc + (i.get('price', 0) * i.get('quantity', 0));
		});
	)
);

export const getTasks = (state) => state.taskReducer.get('tasks');

export const getTotalWorkingTimeWithTasks = createSelector(
	[ getTasks ],
	(tasks) => (
		tasks.reduce((acc, i) => {
			return acc + (i.get('workingTime', 0));
		});
	)
);

컴포넌트는 이렇게 바꾼다.

import React from 'react';
import { connect } from 'react-redux';
import { List } from 'Immutable';

import * as selectors from '../selectors';

const mapStateToProps = (state) => ({
	color: selectors.getColor(state),
	items: selectors.getItems(state),
	tasks: selectors.getTasks(state),
	totalPrice: selectors.getTotalPriceWithItems(state),
	totalWorkingTime: selctors.getTotalWorkingTimeWithTasks(state),
});

class SomeComponent extends React.Component {
	render() {
		...
	}
}

export default connect(mapStateToProps)(SomeComponent);

Reselect의 createSelector를 이용하여 첫 파라미터에는 간단하게 State로부터 아이템 혹은 태스크를 가져오는 함수를, 마지막 파라미터에는 이들을 변수로 받아 합계를 계산해서 돌려주는 코드를 작성했다. 여기서 Reselect는 첫 번째 파라미터의 배열 안에 있는 함수들이 반환하는 값이 이전과 같으면 마지막 파라미터로 준 함수를 실행하지 않는다. 따라서 color가 바뀐다고 해서 이전과 같은 상태인 item의 가격의 합이나 task의 시간의 합을 계산하지 않는다는 뜻이다. 수 천, 수 만개의 데이터가 item이나 task에 있다고 하더라도 색을 바꾼다고 해서 시간이 그리 오래 걸리지는 않을 것이다.

어플리케이션 개발 초기에 이런 최적화를 하면 후에 어플리케이션이 복잡해지더라도 쓸만한 성능을 낼 수 있다. 또한 컴포넌트와 주입될 데이터를 셀렉터로 분리해놓으면 테스트가 용이해진다는 장점이 있다.