샘플코드를 통해서 react-redux 패키지의 사용 전과 후 코드를 비교해보겠습니다.
App 컴포넌트
Provider 컴포넌트에서는 리액트에서 액션이 처리되었을 때, 이벤트를 받아서 하위에 있는 다른 컴포넌트가 다시 렌더링 될 수 있도록 도와줍니다.
// react-redux 패키지 사용 전
import React from 'react';
import FriendMain from './friend/container/FriendMain';
import TimelineMain from './timeline/container/TimelineMain';
export default function App() {
return (
<div>
<FriendMain />
<TimelineMain />
</div>
)
}
// react-redux 패키지 사용 후
import React from 'react';
import FriendMain from './friend/container/FriendMain';
import TimelineMain from './timeline/container/TimelineMain';
import { Provider } from 'react-redux';
import store from './common/store';
export default function App() {
return (
<Provider store={store}>
<div>
<FriendMain />
<TimelineMain />
</div>
</Provider>
)
}
FriendMain 컴포넌트
기존에 리액트에서 useEffect 훅과 useReducer 훅을 사용하여 상태값을 업데이트 하고 있었습니다. 그 부분을 react-redux를 사용하면서 useSelector 훅으로 friends 데이터를 바로 불러올 수 있습니다.
useSelector 훅은 액션이 처리가 되면, 여기서 반환하는 값의 이전값을 기억했다가 그 값이 변경되었을 때 컴포넌트를 다시 렌더링을 해 줍니다. 따라서, 기존에 prevFriends ! == friends 조건으로 비교하기 위해 작성했던 코드를 제거할 수 있습니다.
그리고 dispatch 함수를 호출하는 경우에 store.dispatch 형식으로 스토어를 가져와서 호출을 했습니다. 하지만 이제는 아래와같이 useDispatch 훅을 이용하면 됩니다. dispatch를 훅으로 가져온다는 것은 dispatch 값이 변할 수도 있다는 이야기입니다. 사실 거의 변할리는 없지만, 중간에 미들웨어를 새로 추가하거나 하면 dispatch가 변경될 수 있습니다. 대부분 그렇게 사용하지는 않기 때문에 거의 변하지 않는다고 생각하셔도 될 것 같습니다.
// react-redux 패키지 사용 전
import React, { useEffect, useReducer } from 'react';
import store from '../../common/store';
import { getNextFriend } from '../../common/mockData'; // 데이터베이스 API 샘플코드
import { addFriend } from '../state';
import FriendList from '../component/FriendList';
export default function FriendMain() {
const [, forceUpdate] = useReducer(v => v+1, 0);
// 스토어 함수로 상태변경을 감지하고 강제 업데이트
useEffect(() => {
let prevFriends = store.getState().friend.friends;
const unsubscribe = store.subscribe(() => {
const friends = store.getState().friend.friends;
if (prevFriends !== friends) {
forceUpdate();
}
prevFriends = friends;
});
return () => unsubscribe();
}, []);
// 이벤트
function onAdd() {
const friend = getNextFriend();
store.dispatch(addFriend(friend));
}
// 렌더링
const friends = store.getState().friend.friends;
return (
<div>
<button onClick={onAdd}>친구 추가</button>
<FriendList friends={friends} />
</div>
);
}
// react-redux 패키지 사용 후
import React from 'react';
import { getNextFriend } from '../../common/mockData'; // 데이터베이스 API 샘플코드
import { addFriend } from '../state';
import FriendList from '../component/FriendList';
import { useSelector, useDispatch } from 'react-redux';
export default function FriendMain() {
// 데이터
const friends = useSelector(state => state.friend.friends);
const dispatch = useDispatch();
// 이벤트
function onAdd() {
const friend = getNextFriend();
dispatch(addFriend(friend));
}
// 렌더링
return (
<div>
<button onClick={onAdd}>친구 추가</button>
<FriendList friends={friends} />
</div>
);
}
만약 여러개의 상태값을 가져오고 싶다면 어떻게 해야할까요? 그 방법은 여러가지가 있는데 한개씩 소개해 보도록 하겠습니다.
첫 번째로는 useSelector 훅을 여러번 사용하는 방법이 있습니다.
const friends1 = useSelector(state => state.friend.friends);
const friends2 = useSelector(state => state.friend.friends);
const friends3 = useSelector(state => state.friend.friends);
두 번째로 배열을 반환하는 방법이 있습니다. 그런데 이 방법은 배열이 계속 새로 생성되기 때문에 계속 렌더링이 발생할 수가 있습니다. 그렇기 때문에 사용하는 것이 두번째 매개변수 입니다. 두번째 매개변수에 함수를 입력해서, 렌더링을 할지 말지 결정할 수가 있습니다.
const [friends1, friends2] = useSelector(state => [state.friend.friends1, state.friend.friends2]);
보통은 react-redux 에서 제공하는 shallowEqual 을 이용하는 방법이 있습니다. 이 함수는 얕은 비교를 사용하기 때문에, 배열을 입력하면 배열의 레퍼런스만 비교하는게 아니라 이 안에 있는 friends1, friends2의 값을 각각 비교하기 때문에 이 두 값이 변경되었을 때만 이 컴포넌트가 렌더링이 됩니다.
하지만 이러한 방식을 자주 사용한다면, 이렇게 매번 shallowEqual 을 사용하는것은 번거롭기 때문에 커스텀 훅을 만들어서 사용하는 방법을 추천합니다.
import { shallowEqual } from 'react-redux';
const [friends1, friends2] = useSelector(
state => [state.friend.friends1, state.friend.friends2],
shallowEqual
);
아래와 같이 커스텀 훅을 만들어서 shallowEqual 을 붙여놓은 후 아래와 같이 사용하면 됩니다. 그런데 한가지 주의할 점은 배열을 입력하지 않고 v3 처럼 값을 가져온다면 그 값의 레퍼런스만 비교하면 될텐데 shallowEqual은 value 하위에 가진 속성값을 모두 다 비교할 것입니다. 그래서 조금 비효율적인 측면이 있기 때문에 이렇게 커스텀 훅을 사용할 때는 값을 하나만 사용을 하더라도 v4 처럼 배열로 사용하는 것이 좋습니다.
import { shallowEqual } from 'react-redux';
function useMySelector(selector) {
return useSelector(selector, shallowEqual);
}
const [v1, v2] = useMySelector(state => [state.v1, state.v2]);
const v3 = useMySelector(state => state.v3);
const [v4] = useMySelector(state => [state.v4]);
세 번째로는 reselect 라는 패키지를 이용하여 메모이제이션을 이용하는 방법이 있습니다. 리덕스에 저장된 데이터를 화면에 보여줄 때에는 다양한 형식으로 가공할 필요가 있습니다. 그럴 때 reselect 라는 라이브러리가 도움이 됩니다.
예를 들어, FriendList 라는 컴포넌트를 그냥 렌더링 하는 것이 아니라 필터를 적용하고 싶을 수도 있습니다. 성별을 관리하는 필터를 적용하고 싶능 경우에 남/여 에 관한 리스트를 따로 관리할 수 있을 것입니다. 그리고 컴포넌트는 리덕스에 있는 데이터를 그대로 가져와서 렌더링 할 수 있습니다. 하지만 이러한 방법은 데이터가 중복하여 들어갈 수 있습니다. 이러면 메모리 측면에서도 비효율적이고, 데이터를 수정할 때에도 다루기가 까다롭습니다. 그리고 필터가 동적으로 변하는 경우에도 문제가 될 수 있습니다.
만약 연령 필터를 작동시킨다고 하는 경우에, 10~15세, 10~30세 이런식으로 옵션이 다양하다고 하면 리덕스에 모든 경우에 수를 저장하기는 힘듭니다. 그래서 아래와 같이 원본 데이터만 스토어에 저장해 두고 필터 연산은 컴포넌트 쪽에서 처리하는 방법을 사용하기도 합니다.
아래의 코드에서 주목해야하는 부분은 useSelector 훅이 액션이 처리될 때마다 현재 상태의 데이터를 반환을 할 것이고, friends 값과 ageLimit 값이 변경되지 않았다면 추가적으로 렌더링을 하지는 않을 것입니다. 하지만 filter 연산은 무조건 수행이 된다는 겁니다. 이 때, reselector 를 이용하면 메모이제이션 기능을 지원하기 때문에 friends 와 ageLimit 값이 변경이 되었을 경우에만 filter 연산이 진행되도록 할 수가 있습니다. 또한 이런 성능 개선 말고도 셀렉터 코드의 분리를 통해서 코드의 개선 효과도 볼 수 있습니다.
const [
ageLimit,
showLimit,
friendsWithAgeLimit,
friendsWithAgeShowLimit,
] = useSelector(state => {
const { ageLimit, showLimit, friends } = state.friend;
const friendWithAgeLimit = friends.filter(item => item.age <= ageLimit);
return [
ageLimit,
showLimit,
friendsWithAgeLimit,
friendsWithAgeLimit.slice(0, showLimit),
]
}, shallowEqual)
우선 셀렉터 파일을 별도로 하나 만들어줍니다. reselect 패키지를 이용하여 아래와 같이 코드를 분리해 두고 사용할 수 있습니다. createSelector 함수로 selector를 만들면 메모이제이션이 기능을 합니다. 따라서 두번째 인자로 전달하는 함수의 매개변수가 변하지 않는다면, 이전 값을 그대로 사용을 합니다. 그래서 필터 연산이 적용되지 않는 것입니다.
import { createSelector } from 'reselect';
const getFriends = state => state.friend.friends;
export const getAgeLimit = state => state.friend.ageLimit;
export const getShowLimit = state => state.friend.showLimit;
export const getFriendsWithAgeLimit = createSelector(
[getFriends, getAgeLimit],
(friends, ageLimit) => friends.filter(item => item.age <= ageLimit)
)
export const getFriendsWithAgeShowLimit = createSelector(
[getFriendsWithAgeLimit, getShowLimit],
(friendsWithAgeLimit, showLimit) => friendsWithAgeLimit.slice(0, showLimit)
)
그래서 기존의 컴포넌트 함수를 아래와 같이 개선할 수 있습니다. 여기서 중요한 것은 데이터를 가공하는 연산이 컴포넌트에 없다는 겁니다. 데이터를 가공하는 작업은 selector 파일로 분리하여 작성을 했습니다.
const [
ageLimit,
showLimit,
friendsWithAgeLimit,
friendsWithAgeShowLimit,
] = useSelector(state => [
getAgeLimit(state),
getShowLimit(state),
getFriendsWithAgeLimit(state),
getFriendsWithAgeShowLimit(state),
],
, shallowEqual)
출처 : 실전 리액트 프로그리맹 - 인프런 (추천합니다 !!)
[Frontend/React] 26. redux-saga를 이용한 비동기 액션 처리 (0) | 2020.10.08 |
---|---|
[Frontend/React] 25. 리덕스 사용 팁 (0) | 2020.10.08 |
[Frontend/React] 23. 리덕스의 구조 - 리듀서, 스토어 (0) | 2020.10.06 |
[Frontend/React] 22. 리덕스의 구조 - 액션, 미들웨어 (0) | 2020.10.06 |
[Frontend/React] 21. 리덕스란 무엇인가? (0) | 2020.10.06 |