Redux 관점으로 RN앱 성능 개선점 찾아보기
안녕하세요, 휴먼스케이프 프론트엔드 개발자 Tasha입니다.
지난번 희귀난치성 질환 환자분들을 위한 서비스 레어노트의 v.1출시 이후, 좀 더 환자 중심의 기획/컨텐츠/UI가 녹여진 대대적인 v.2 업데이트가 이뤄어졌는데요.🙂 업데이트를 진행하며 바쁘게 개발하느라 그동안 기본적인 라이브러리 사용을 제대로 했는지, 성능 부분에서 조금 더 고려할 부분을 놓친 부분이 있진 않았는지 다시 한번 검토하는 시간을 가졌습니다.
지난번에 React 관점에서 React Native 성능 개선에 관해 알아봤다면, 이번에는 React의 렌더와 밀접한 관련이 있는, 상태관리 라이브러리 Redux를 살펴보고 레어노트 앱에 성능 개선을 위해 적용할 부분이 있는지 알아보았습니다.
https://www.wrappixel.com/beginners-guide-to-react-redux-heres-how-to-get-started/
1. 컴포넌트에서 필요한 부분만 선택적으로 Select해오기
레어노트 프로젝트에서는 global store에서 값을 가져올때 useSelector hooks를 이용합니다.
import { userSelector, shallowEqual } from 'react-redux;
function Store() { const selects = useSelector(state => ({ tshirt: state.tshirt, sunglasses: state.sunglasses }), shallowEqual); }
useSelector 훅스 안에 selector 함수를 넣고, 레어노트에서는 보통 값을 복수개로 가져오기 때문에 객체로 값을 가져옵니다. 그러나 이렇게 객체로 가져오게 되면 useSelector로 가져온 값은 엄격한 동등비교(===)를 하기 때문에 매번 다른 값을 가지고 와 계속 렌더가 발생할 수 있습니다. 따라서 진짜 value값에 변경이 일어날 때에만 렌더가 일어나도록 shallowEqual 인자를 넣어줍니다.
이와 같은 방식을 이용하여 실제 다음과 같이 사용합니다. user이름을 가진 state에 있는 diseaseGroupId와 diseaseGroupFullName을 가지고 오고 싶다고 한다면 보통 다음과 같이 가져옵니다.
필요한 부분만 선택적으로 select해 가지고 온 경우
그런데 몇몇 컴포넌트에서는 user의 diseaseGroupId만 필요함에도 불구하고, user state를 통째로 select한 후 필요한 정보만 디스트럭처링을 해서 사용하는 경우가 종종 있었습니다.
state 전체를 select해 필요한 부분만 사용한 경우
이렇게 작성하게 되면 user state의 diseaseGroupId가 아닌 다른 부분이 업데이트되는 경우, 예를들어 user의 환자관계가 변경되었다고 할 때에도 user값 전체를 select해오기 때문에 MyNoteMain 컴포넌트에서 업데이트가 일어납니다. 질환 정보만 필요한 MyNoteMain이 불필요하게 재렌더되는 현상이 발생하므로 select할때는 더 주의해서 필요한 부분만 select할 필요성을 느꼈습니다.
2. selector를 메모아이즈하는 형태로 이용
그동안 서버에서 가져오는 값을 클라이언트에 맞게 정보를 가공할때에는 다음과 같이 reducer에서 값을 리턴하기 전에 데이터를 format하는 형식으로 이용했었습니다.
리듀서 쪽에서 데이터를 가공해서 store에 저장
위에처럼 store에 값을 저장시키기 전에 클라이언트에만 필요한 정보만 가공하여 저장하고, 아래처럼 가공한 정보를 선택해서 가져왔습니다.
이런 식으로 하게 되면 두 가지 문제가 있는데, 1) fetch할 때마다 같은 값이더라도 무조건 map/reduce 로직을 사용한다는 것입니다. 두 번째로는 2)Array.map으로 생성된 값을 저장하기 때문에 무조건 새로운 값으로 인식되어 같은정보여도 render가 불필요하게 일어난다는 점입니다.
따라서 reduce/map 로직이 불필요하게 일어나는 것을 막기 위해, 그리고 값이 달라질때만 업데이트하여 렌더가 이루어지도록 하기 위해 selector를 memoize하는 형태로 이용하는 것이 좋은데, 이때 주로 사용하는 reselect라는 라이브러리가 있습니다.
reselect의 createSelector 를 이용하여 selector들을 넣어주고, select해온 값으로 도출할 값을 만들어 주는 함수를 마지막 인자로 넣어주면됩니다.
이렇게createSelector 를 이용하면 안에 들어간 selector들이 같은 값을 가지고 오는지 비교를 하고, 값이 달라질 때에만 가공하는 함수를 실행시키게 됩니다. 따라서 같은 값을 fetch하게 되면 reselect가 같은 값임을 감지해 map함수를 실행시키지도 않고, 렌더도 일어나지 않게 됩니다.
3. batch 이용
하나의 컴포넌트에서 여러 dispach를 연속적으로 불러와야 하는 경우가 있습니다. 하나의 dispatch는 상태변화를 일으켜 render를 발생시키는데 연속적인 dispatch로 여러번 렌더를 시키는게 맞는지 아니면 이 때 최소한의 렌더를 위해 모든 dispatch를 묶는 dispatch를 만들어서 한번에 업데이트를 시켜야 할지 고민이 있었습니다. 여러 disaptch를 묶게 되면 원하는 방향으로 렌더는 가능하나 디버깅에 용이하지 않고, SRP원칙에도 어긋나기 때문입니다. 따라서 저는 보통 여러 dispatch를 실행시키는 방식으로 작성하였습니다.
import React, { useEffect } from 'react'; import { useDispatch } from 'react-redux';
function App() { const dispatch = useDispatch();
useEffect(() => { dispatch(...); dispatch(...); }, []); }
그런데 이런 고민을 해결해주는, 각각 dispatch를 실행시키되 한 번 업데이트가 일어나도록 하는 react-redux의 batch메소드가 있었습니다.
import React, { useEffect } from 'react'; import { useDispatch, batch } from 'react-redux';
function App() { const dispatch = useDispatch();
useEffect(() => { batch(() => { dispatch(...); dispatch(...); }); }, []); }
이렇게 사용하면 batch로 묶은 액션들이 한번에 업데이트가 되기 때문에 개별적으로 사용하면서도 렌더를 한번만 하도록 조정할 수 있습니다!
4. Normalization 이용
현재까지는 서버에서 받는 값을 약간의 가공을 제외하고는 거의 그대로의 형태로 렌더시켰기 때문에 크게 정규화의 필요성을 느끼지 못했습니다. 그러나 곧 커뮤니티 등 복잡도가 커지는 데이터를 다룰 기능들이 추가될 계획이 있기 때문에 효율적인 데이터 관리를 위해 알아볼 필요성을 느껴 작성하게 되었습니다.
Redux store역시 프론트엔드에서 사용하는 데이터베이스의 일종이므로, 데이터베이스 디자인 원칙이 적용됩니다. store에 값이 반복적으로 들어가있으면 반복된 값을 일일이 변경시키기 까다롭게 됩니다. 또한 변경할 값이 깊이 중첩되어 있으면(nested) 해당 값을 변경시키기에도 까다롭고, 불변성을 유지하는게 어려워 새 object를 할당해 의도하지 않은 렌더를 발생시킬 수도 있게 됩니다. 따라서 이러한 구조를 피하기 위해 Redux는 데이터 정규화를 권장합니다.
Redux에서 소개하는 정규화의 기본 개념은 다음과 같습니다.
각 데이터 타입은 각 테이블을 가지고 있고,
각 테이블은 id값이 key값이고 value값이 아이템 값인 객체의 아이템들을 가지고 있으며,
개개의 아이템을 참조할 수 있도록 아이템 id들을 저장하는 부분이 있어야 하고, 이 부분은 ordering이 되어있어야한다.
{ entities: { authors : { byId : {}, allIds : [] }, books : { byId : {}, allIds : [] }, authorBook : { byId : { 1 : { id : 1, authorId : 5, bookId : 22, }, 2 : { id : 2, authorId : 5, bookId : 15, }, 3 : { id : 3, authorId : 42, bookId : 12, } }, allIds : [1, 2, 3], } } }
정규화된 방식의 구조는 이와 같습니다. 각 아이템들이 객체 형태로 되어있으며, id값으로 아이템 참조가 가능하도록 했습니다. 따라서 하나의 값에 접근할 때 iteration 없이 바로 접근이 가능하며, 하나의 값이 변경되면 해당 부분만 변경하고 나머지는 id값으로 참조하여 데이터 중복을 피한 구조입니다.
이와 같이 데이터를 정규화된 패턴으로 저장하면 커뮤니티 기능을 이용할 때, 작성글/댓글을 수정 및 추가 삭제 할 때, 유저-게시글-덧글 구조를 다룰 때에 좀 더 데이터를 효율적으로 이용할 수 있을 것 같습니다.
지금까지 리덕스를 기준으로 React Native 앱 성능개선할 수 있는 방법에 대해 알아보았습니다. 구현에만 신경쓰다보면 한 두번 렌더가 더 되는 상황에 크게 고려하지 않고 작성하게 될 때가 많은 것 같아요. 시간을 따로 내어 내가 작성한 코드가 정확히 의도대로 이루어지고 있는지 자세히 살펴보면 개선할 부분도 더 찾게 되고 실제 작성할 때에도 좀 더 신경쓰게 되는 것 같아 개인적으로 글을 쓰면서 도움이 많이 되었습니다. 읽어주셔서 감사합니다. 🙂
[출처]
react-redux hooks
reselect 라이브러리
batch
normalization
Get to know us better! Join our official channels below.
Telegram(EN) : t.me/Humanscape KakaoTalk(KR) : open.kakao.com/o/gqbUQEM Website : humanscape.io Medium : medium.com/humanscape-ico Facebook : www.facebook.com/humanscape Twitter : twitter.com/Humanscape_io Reddit : https://www.reddit.com/r/Humanscape_official Bitcointalk announcement : https://bit.ly/2rVsP4T Email : support@humanscape.io