-
Redux | Reselect 패키지로 선택자 함수 만들기JavaScript/Redux 2020. 5. 15. 10:17
Reselect 패키지로 선택자 함수 만들기
reselect 패키지는 원본 데이터를 다양한 형태로 가공해서 사용할 수 있도록 도와줌
특히 리덕스의 데이터를 리액트 컴포넌트에서 필요한 데이터로 가공하는 용도로 많이 사용
구현 기능
- 친구 목록에 연령 제한 옵션과 개수 제한 옵션 추가
- 연령 제한을 적용한 친구 목록과 연령/개수 제한을 모두 적용한 친구 목록을 보여줌
reselect 패키지 없이 구현해 보기
옵션 선택 컴포넌트 작성
옵션을 선택할 수 있는 기능을 가진 컴포넌트 작성
friend/component 폴더 밑에 NumberSelect.js 파일 작성import React, { Component } from "react"; class NumberSelect extends Component { onChange = (e) => { const value = Number(e.currentTarget.value); // 사용자가 옵션을 선택하면 이를 부모 컴포넌트에 알림 this.props.onChange(value); }; render() { const { value, options, postfix } = this.props; return ( <div> <select onChange={this.onChange} value={value}> {options.map((option) => ( <option key={option} value={option}> {option} </option> ))} </select> {postfix} </div> ); } } export default NumberSelect;
- 부모 컴포넌트가 알려 준 옵션 목록을 화면에 출력
- 주어진 속성값으로 화면을 그리기만 함으로 프레젠테이션 컴포넌트로 작성
상수 관리 파일 작성
연령 제한과 개수 제한의 최댓값을 관리하는 파일
friend 폴더 밑에 common.js 파일 작성export const MAX_AGE_LIMIT = 30; export const MAX_SHOW_LIMIT = 8;
- friend 폴더 밑에 있는 여러 파일에서 공통으로 사용되는 상숫값 관리
친구 목록 릭덕스 코드 리팩터링
연령 제한과 개수 제한 정보를 관리하기 위해 friend/state.js 파일 수정
import createReducer from "../common/createReducer"; import createItemsLogic from "../common/createItemsLogic"; import mergeReducers from "../common/mergeReducers"; import { MAX_AGE_LIMIT, MAX_SHOW_LIMIT } from "./common"; const { add, remove, edit, reducer: friendsReducer } = createItemsLogic( "friends" ); const SET_AGE_LIMIT = "friend/SET_AGE_LIMIT"; const SET_SHOW_LIMIT = "friend/SET_SHOW_LIMIT"; export const addFriend = add; export const removeFriend = remove; export const editFriend = edit; export const setAgeLimit = (ageLimit) => ({ type: SET_AGE_LIMIT, ageLimit }); export const setShowLimit = (showLimit) => ({ type: SET_SHOW_LIMIT, showLimit, }); const INITIAL_STATE = { ageLimit: MAX_AGE_LIMIT, showLimit: MAX_SHOW_LIMIT }; const reducer = createReducer(INITIAL_STATE, { [SET_AGE_LIMIT]: (state, action) => (state.ageLimit = action.ageLimit), [SET_SHOW_LIMIT]: (state, action) => (state.showLimit = action.showLimit), }); const reducers = [reducer, friendsReducer]; export default mergeReducers(reducers);
- 연령/개수 제한 정보를 처리하는 액션 타입, 액션 생성자 함수, 리듀서 함수 작성
- 초기 상탯값으로 연령/개수 최댓값 입력
- 친구 목록을 처리하는 리듀서 함수와 하나로 합침
FriendMain 컴포넌트 리팩터링
FriendMain.js 파일 수정
import React, { Component } from "react"; import { getNextFriend } from "../../common/mockData"; import FriendList from "../component/FriendList"; import { connect } from "react-redux"; import * as actions from "../state"; import NumberSelect from "../component/NumberSelect"; import { MAX_AGE_LIMIT, MAX_SHOW_LIMIT } from "../common"; class FriendMain extends Component { onAdd = () => { const friend = getNextFriend(); this.props.addFriend(friend); }; render() { console.log("FriendMain render"); const { friendsWithAgeLimit, friendsWithAgeShowLimit, ageLimit, showLimit, setAgeLimit, setShowLimit, } = this.props; return ( <div> <button onClick={this.onAdd}>친구 추가</button> // 연령 제한 옵션 <NumberSelect onChange={setAgeLimit} value={ageLimit} options={ageLimitOptions} postfix="세 이하만 보기" /> <FriendList friends={friendsWithAgeLimit} /> // 개수 제한 옵션 <NumberSelect onChange={setShowLimit} value={showLimit} options={showLimitOptions} postfix="명 이하만 보기(연령 제한 적용)" /> <FriendList friends={friendsWithAgeShowLimit} /> </div> ); } } // 연령 & 개수 제한 옵션 const ageLimitOptions = [15, 20, 25, MAX_AGE_LIMIT]; const showLimitOptions = [2, 4, 6, MAX_SHOW_LIMIT]; const mapStateToProps = (state) => { const friends = state.friend.friends; const ageLimit = state.friend.ageLimit; const showLimit = state.friend.showLimit; // 연령 제한이 적용된 친구 목록 const friendsWithAgeLimit = friends.filter( (friend) => friend.age <= ageLimit ); // 연령 & 개수 제한이 적용된 친구 목록 const friendsWithAgeShowLimit = friendsWithAgeLimit.slice(0, showLimit); return { // 컴포넌트의 속성값으로 전달 friendsWithAgeLimit, friendsWithAgeShowLimit, ageLimit, showLimit, }; }; // 액션 생성자 함수 전체를 actions 객체로 전달 export default connect(mapStateToProps, actions)(FriendMain);
- 연령 제한 옵션 선택 시 setAgeLimit 함수가 호출되고, 리덕스 상탯값 변경
- 개수 제한 옵션 선택 시 setShowLimit 함수 호출, 리덕스 상탯값 변경
- mapStateToProps 함수 내부에서 리덕스에 저장된 원본 데이터를 화면에 보여 줄 데이터로 가공
- ※ 문제점: 친구 목록이 변경되지 않았을 때도 데이터를 가공하는 연산이 수행되며,
이는 데이터양이 증가할수록 불필요한 연산도 증가하게 되는 원인이 됨
reselect 패키지 사용하기
지금까지 작성한 코드를 reselect 패키지를 사용하는 코드로 리팩터링
개요
- reselect 패키지를 사용할 때는 선택자(selector) 함수를 작성해야 함
- reselect 패키지는 메모이제이션 기능이 있기 때문에 연산에 사용되는 데이터가 변경된 경우에만 연산을 수행
- 데이터가 변경되지 않았다면 이전 결괏값을 재사용
- 데이터 가공 코드를 컴포넌트 파일에서 분리함으로써 컴포넌트 파일에서는 UI 코드에 집중할 수 있게 됨
설치
$ npm i reselect
친구 목록 데이터의 선택자 함수 만들기
리덕스의 데이터를 컴포넌트에 원하는 방식으로 전달하기 위해 데이터를 가공하는 함수를 선택자라고 할 수 있음
- 상탯값 처리 파일들을 한곳에 모으기 위해 friend 폴더 밑에 state 폴더 생성
- friend/state.js 파일을 friend/state/index.js 경로로 이동
- friend/state 폴더 밑에 선택자 함수를 작성할 selector.js 파일 작성
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( // friends, ageLimit이 변경될 때만 연산 수행 [getFriends, getAgeLimit], (friends, ageLimit) => friends.filter((friend) => friend.age <= ageLimit) ); export const getFriendsWithAgeShowLimit = createSelector( // friends, ageLimit, showLimit이 변경될 때만 연산 수행 [getFriendsWithAgeLimit, getShowLimit], (friendsWithAgeLimit, showLimit) => friendsWithAgeLimit.slice(0, showLimit) );
- createSelector 함수를 이용해서 선택자 함수 생성
- createSelector 로 만든 선택자 함수로 전달되는 인수는 첫 번째 매개변수인 배열 내부의 각 함수들에게 인수로 전달됨
- getFriends, getAgeLimit, getShowLimit 함수들은 상탯값에 있는 데이터를 단순히 전달하는 역할을 하며, 이 함수들도 선택자 함수임
- 아래 두 함수의 첫 번째 인자로 입력된 배열 요소들의 각 반환값은 두 번째 인자로 입력된 함수의 매개변수로 순서에 맞게 전달됨
- getFriendsWithAgeShowLimit 함수는 getFriendsWithAgeLimit 함수 이용
선택자 함수 사용하기
FriendMain.js 파일의 mapStateToProps 함수 수정
// ... import { getAgeLimit, getShowLimit, getFriendsWithAgeLimit, getFriendsWithAgeShowLimit, } from "../state/selector"; // ... const mapStateToProps = (state) => { return { ageLimit: getAgeLimit(state), showLimit: getShowLimit(state), friendsWithAgeLimit: getFriendsWithAgeLimit(state), friendsWithAgeShowLimit: getFriendsWithAgeShowLimit(state), }; }; // ...
- 이전보다 코드가 간결해짐
- 선택자 함수는 다른 컴포넌트에서도 쉽게 가져다 쓸 수 있음
reselect에서 컴포넌트의 속성값 이용하기
선택자 함수는 상탯값 외에도 속성값을 입력받을 수 있음
속성값을 이용하면 컴포넌트의 각 인스턴스에 특화된 값 반환 가능
속성값 넘겨주기
index.js 파일 수정
// ... ReactDOM.render( <Provider store={store}> <div> <FriendMain ageLimit={30} /> <FriendMain ageLimit={15} /> </div> </Provider>, document.getElementById("root") );
- 연령 제한 정보를 속성값으로 전달
- FriendMain 컴포넌트의 인스턴스는 두 개가 됨
속성값을 선택자 함수로 전달하기
FriendMain.js 파일 수정
// ... class FriendMain extends Component { // ... render() { const { friendsWithAgeLimit } = this.props; return ( <div> <button onClick={this.onAdd}>친구 추가</button> <FriendList friends={friendsWithAgeLimit} /> </div> ); } } const mapStateToProps = (state, props) => { return { friendsWithAgeLimit: getFriendsWithAgeLimit(state, props), }; }; export default connect(mapStateToProps, actions)(FriendMain);
- render 메서드는 필요한 코드만 남김
- mapStateToProps 함수는 두 번째 매개변수로 컴포넌트의 속성값을 받을 수 있음
- 선택자 함수의 인수로 상탯값과 속성값을 모두 전달
선택자 함수 수정하기
seletor.js 파일의 getAgeLimit 함수 수정
// ... export const getAgeLimit = (state, props) => props.ageLimit; // ...
- 연령 제한 정보를 속성값으로부터 가져옴
- getFriendsWithAgeLimit으로 전달된 인수는 해당 함수의 첫 번째 매개변수인 배열 요소들에 각각 전달되기 때문에 getAgeLimit 함수의 매개변수를 수정함으로써 연령 제한값을 수정할 수 있음
- 코드를 실행하면 의도한 대로 동작하는 것을 확인 가능
문제점
- reselct에서 제공하는 메모이제이션 기능이 동작하지 않음
- 두 개의 FriendMain 컴포넌트 인스턴스가 서로 다른 연령 제한 속성값을 갖기 때문
- 두 인스턴스는 같은 선택자 함수를 다른 속성값으로 호출하기 때문에,
각 인스턴스 입장에서는 친구 목록과 연령 제한 정보가 변경되지 않더라도 선택자 함수의 입장에서 연령 제한 정보가 변경됨 - 즉, 선택자 함수는 이전의 결괏값을 재사용하지 못하고 매번 반복해서 연산을 수행함
※ getFriendsWithAgeLimit 함수에 콘솔 로그를 추가하고, TimelineMain 컴포넌트를 렌더링한 후 '타임라인 추가'버튼을 눌러보면 친구 목록/연령 제한이 변경되지 않았음에도 콘솔 로그가 출력되는 것을 확인할 수 있음
컴포넌트 인스턴스별로 독립된 메모이제이션 적용하기
컴포넌트 인스턴스별로 독립된 메모이제이션 기능을 제공하기 위해서는 선택자 함수도 여러 인스턴스로 만들어져야 함
선택자 함수 수정하기
selector.js 파일 수정
// ... export const makeGetFriendsWithAgeLimit = () => { return createSelector([getFriends, getAgeLimit], (friends, ageLimit) => friends.filter((friend) => friend.age <= ageLimit) ); };
- 기존 getFriendsWithAgeLimit, getFriendsWithAgeShowLimit 함수는 삭제
- 선택자 함수를 생성하는 함수 정의
- 각 컴포넌트 인스턴스가 makeGetFriendsWithAgeLimit 함수를 호출하면 자신만의 선택자 함수를 가질 수 있음
선택자 함수 호출 컴포넌트 수정하기
FriendMain.js 파일 수정
// ... import { makeGetFriendsWithAgeLimit } from "../state/selector"; // ... const makeMapStateToProps = () => { const getFriendsWithAgeLimit = makeGetFriendsWithAgeLimit(); const mapStateToProps = (state, props) => { return { friendsWithAgeLimit: getFriendsWithAgeLimit(state, props), }; }; return mapStateToProps; }; export default connect(makeMapStateToProps, actions)(FriendMain);
- mapStateToProps를 생성하는 makeMapStateToProps 함수 정의
- connect 함수의 첫 번째 매개변수로 전달된 함수가 새로운 함수를 반환하면,
react-redux는 각 컴포넌트의 인스턴스별로 독립적인 mapStateToProps 함수의 인스턴스를 만들어서 사용 - mapStateToProps 함수가 생성될 때마다 getFriendsWithAgeLimit 선택자 함수도 생성됨
- 생성된 mapStateToProps 함수는 바로 위에서 생성된 선택자 함수를 기억하는 클로저가 됨
- 결과적으로 각 컴포넌트 인스턴스는 각자의 getFriendsWithAgeLimit 함수를 확보하는 셈
※ makeGetFriendsWithAgeLimit 함수에 콘솔 로그를 추가한 뒤 '타임 라인 추가' 버튼을 클릭해보면 이제 해당 함수가 호출되지 않는 것을 확인할 수 있음
참고도서: [ 실전 리액트 프로그래밍 / 저자_ 이재승 / 출판사_ 프로그래밍 인사이트 ]
'JavaScript > Redux' 카테고리의 다른 글
Redux | 리덕스 사가를 이용한 비동기 액션 처리 (0) 2020.05.15 Redux | 리액트 상탯값 리덕스로 관리하기 (0) 2020.05.15 Redux | 데이터 종류별로 상탯값 나누기 (0) 2020.05.14